Search code examples
c#entity-frameworkmany-to-manyentity-framework-6

Many to many relationship in Entity Framework - results in “An entity object cannot be referenced by multiple instances of IEntityChangeTracker”


I am trying to setup a classic "Article and Categories" scenario in Entity Framework. To do this, I have these classes:

public class RichContent : BaseIdEntity
{
    public string Headline { get; set; }

    public virtual List<RichContentCategory.RichContentCategory> Categories { get; set; }


    public RichContent()
    {
        Categories = new List<RichContentCategory.RichContentCategory>();
    }
}

And my categories:

public class RichContentCategory : BaseIdEntity
{
    public string Name { get; set; }
    public virtual List<RichContent.RichContent> RichContents { get; set; }

    public RichContentCategory()
    {
        this.RichContents = new List<RichContent.RichContent>();
    }
}

Which is setup with this:

   modelBuilder.Entity<RichContent>()
                   .HasMany<RichContentCategory>(s => s.Categories)
                   .WithMany(c => c.RichContents)
                   .Map(cs =>
                   {
                       cs.MapLeftKey("RichContentRefId");
                       cs.MapRightKey("RichContentCategoryId");
                       cs.ToTable("RichContentCategoryPlacement");
                   });

Now, when I run this and add a category like the following and saves:

   item.Categories.Add(richContentCategory);

I get the following error at _db.RichContent.Add(content); inside my CREATE method:

An unhandled exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll

Additional information: An entity object cannot be referenced by multiple instances of IEntityChangeTracker.

I could understand the underlying problem is how I need to use same Context. However, I inject the context using Ninject, and my service that creates looks like this:

 public class RichContentService : IRichContentService
    {
        private readonly Context _db;

        public RichContentService(Context db)
        {
            _db = db;
        }

        public RichContent Create(RichContent content)
        {
            _db.RichContent.Add(content);

            _db.SaveChanges();

            return GetById(content.Id);
        }
}

So my question is basically - how do you add categories to an article like in this case?

EDIT:

My code that runs is an importer that has this:

 finally
                {
                    if (needToCreate)
                    {
                        var result = Create(richContent);
                        logger.Info("Created a new rich-content page");
                    }
                }

And the Create() method:

 public RichContent Create(RichContent content)
        {
            PerformValidation(content);

            _db.RichContent.Add(content);
            _db.SaveChanges();

            return GetById(content.Id);
        }

And the input to the CREATE method:

 var item = new RichContent()
            {
                WordpressId = dto.id,
                DateCreated = dto.date,
                Slug = dto.slug,
                Content = dto.content,
                Headline = dto.title,
                DateModified = dto.modified,

            };

            if (dto.custom_fields.category_type != null && dto.custom_fields.category_type.Any() &&
               !string.IsNullOrEmpty(dto.custom_fields.category_type[0]))
            {
                var categories = _richContentCategoryService.GetBySpecification(new RichContentCategorySpecification()
                {
                    Take = Int32.MaxValue
                });

                var list = dto.custom_fields.category_type[0];
                foreach (var richContentCategory in categories)
                {

                    if (list.Contains(richContentCategory.WordpressName))
                    {
                        item.Categories.Add(richContentCategory);
                    }
                }
            }

            if (dto.custom_fields.featured != null)
            {
                item.Featured = dto.custom_fields.featured.Any(c => c == "1");
            }

            if (dto.custom_fields.area != null)
            {
                item.Area = dto.custom_fields.area.Any(c => c == "1");

                if (item.Area)
                {
                    var children = ConvertWordpressDtos(dto.children);
                    foreach (var child in children.Where(c=>c.Area))
                    {

                        child.Slug = string.Format("{0}/{1}", item.Slug, child.Slug);
                        item.Children.Add(child);
                    }
                }


            }

Solution

  • The problem

    The exception clearly indicates that when you save a new RichContent an entity associated with the RichContent is tracked by another Entity Framework context. In your particular case the entity is RichContentCategory that is returned from _richContentCategoryService.GetBySpecification:

    var categories = _richContentCategoryService.GetBySpecification(new RichContentCategorySpecification()
    {
        Take = Int32.MaxValue
    });
    
    var list = dto.custom_fields.category_type[0];
    foreach (var richContentCategory in categories)
    {
        if (list.Contains(richContentCategory.WordpressName))
        {
            item.Categories.Add(richContentCategory);
        }
    }
    

    How to fix

    The best option here is to manage to use the same context for all object that are used in this scenario. To achieve this you can add the item to categories and than use _richContentCategoryService to save categories:

    var categories = _richContentCategoryService.GetBySpecification(new RichContentCategorySpecification()
    {
        Take = Int32.MaxValue
    });
    
    var list = dto.custom_fields.category_type[0];
    foreach (var richContentCategory in categories)
    {
        if (list.Contains(richContentCategory.WordpressName))
        {
            richContentCategory.RichContents.Add(item);
        }
    }
    
    // Save updated categories somehow, using "_richContentCategoryService" object
    _richContentCategoryService.Save(categories);
    

    If you are not able to do it you can try doing smth like this:

    foreach (var richContentCategory in categories)
    {
        if (list.Contains(richContentCategory.WordpressName))
        {
            item.Categories.Add(new RichContentCategory { Id = richContentCategory.Id});
        }
    }