EF6 Child entities not updating in many-to-many relationship

c# entity-framework-6

Question

I have two entities in a many-to-many relationship: A Map which can have many Tags (and a Tag in turn can be used by many Maps).

I'm trying to update a parent Map entity, including removing items from its child Tags collection. While the Map entity is honoring changes in the database changes to the Tags collection are never honored (apart from the initial creation of them). Any ideas what I'm doing wrong?

In the database are 3 tables:

  • Map
  • Tag
  • MapTags

The entity classes:

public class Map
{
    public Map()
    {
        Tags = new List<Tag>();
    }

    public string Id { get; set; }
    ...
    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public Tag()
    {
        Maps = new List<Map>();
    }

    public int Id { get; set; }
    public string Text { get; set; }
    public ICollection<Map> Maps { get; set; }
}

And the EF6 mappings:

public class MapMap : EntityTypeConfiguration<Map>
{
    public MapMap()
    {
        // Primary Key
        this.HasKey(t => new { t.Id });

        // Properties

        this.Property(t => t.Id)
            .IsRequired()
            .HasMaxLength(32);

        ...

        this.ToTable("Map");
        this.Property(t => t.Id).HasColumnName("Id");
        ...

        // Relationships

        this.HasMany(m => m.Tags)
            .WithMany(t => t.Maps)
            .Map(m =>
            {
                m.MapLeftKey("MapId");
                m.MapRightKey("MapTagId");
                m.ToTable("MapTags");
            });
    }
}

public class TagMap : EntityTypeConfiguration<Tag>
{
    public TagMap()
    {
        // Primary Key
        this.HasKey(t => new { t.Id });

        // Properties

        this.Property(t => t.Id)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        this.Property(t => t.Text)
            .IsRequired()
            .HasMaxLength(256);

        // Table & Column Mappings

        this.ToTable("Tag");
        this.Property(t => t.Id).HasColumnName("Id");
        this.Property(t => t.Text).HasColumnName("Text");

        // Relationships

        this.HasMany(t => t.Maps)
            .WithMany(m => m.Tags)
            .Map(m =>
            {
                m.MapLeftKey("TagId");
                m.MapRightKey("MapId");
                m.ToTable("MapTags");
            });
    }
}

Code to update a Map's Tags:

map.Tags = new List<Tag>();
foreach (string item in data.tags)
{
    Tag tag = MapRepository.FindTagByText(item);
    if (tag == null)
    {
        try
        {
            tag = WebMapRepository.CreateTag(new Tag()
                {
                    Text = item
                });
        }
        catch (DbEntityValidationException ex)
        {
            DisplayValidationErrors(ex, "Tag [" + item + "] validation errors:");
            throw; // Abort
        }
    }
    map.Tags.Add(tag);
}

And the DAL code updating the Map:

public static Map UpdateMap(Map map)
{
    using (MapContext context = new MapContext())
    {
        context.Maps.Attach(map);
        context.Entry(map).State = EntityState.Modified;
        context.SaveChanges();
        return GetMap(map.Id);
    }
}

Workaround While I'd prefer a more elegant solution, for now I'm just running SQL directly to refresh my relationships manually.

1
2
7/17/2014 1:26:58 AM

Accepted Answer

Looking at your code within UpdateMap it looks like you are working in a disconnected scenario?

If so, reattaching entities in a disconnected scenario has two steps:

  1. Reattach the entity graph to the context so that it is tracked
  2. Set the state for each entity in the graph

Setting the EntityState to Modified on the root Map entity will attach all entities in the object graph to the context. However, it will mark all child Tag entities with the Unchanged state. This would correspond to your comment that changes to the Map entity are being persisted, but changes to the Tag collection are not.

The solution to this will very much depend on the architecture of you application. One potential solution would be to get the corresponding Map entity from the database and then walk its object graph against the updated map and paint its state accordingly.

The following example shows tags in the collection of the queried map entity that are not in the tag collection of the disconnected map having their relationship with the map removed:

public static Map UpdateMap(Map map)
{
    // get map in its current state
    var previousMap = context.Maps
       .Where(m => m.Id == map.Id)
       .Include(m => m.Tags)
       .Single();

    // work out tags deleted in the updated map
    var deletedTags = previousMap.Tags.Except(map.Tags).ToList();

    // remove the references to removed tags
    deletedTags.ForEach(t => previousMap.Tags.Remove(t));

    // .. deal with added tags
    // very similar code to deleted so not showing

    context.SaveChanges();
}

For this to work, your Tag type will need to implement IEquatable<Tag> to allow the Except operation on the set to work correctly (as it is a reference type).

NOTE Ive used HashSets instead of as in the question where Lists but thats just an implementation detail.

E.g.

   public class Tag : IEquatable<Tag>
    {
        public Tag()
        {
            Maps = new HashSet<Map>();
        }

        public int Id { get; set; }
        public string Text { get; set; }
        public virtual ISet<Map> Maps { get; private set; }

        public bool Equals(Tag other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return Id == other.Id;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != GetType()) return false;
            return Equals((Tag) obj);
        }

        public override int GetHashCode()
        {
            return Id;
        }

        public static bool operator ==(Tag left, Tag right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Tag left, Tag right)
        {
            return !Equals(left, right);
        }
    }

I'll get the test project up on GitHub.

2
7/20/2014 9:05:07 PM

Popular Answer

You issue is you are managing the collection manually and detached from the context, this is a side effect of the repository pattern and it is why many say it is an anti-pattern.

  1. Try to remove the new list and the constructor and make the ICollection virtual.
  2. since tag has a id match the tag on the map by id and not by text
  3. your update map is attaching the map see if attaching the tags solves the issue.
  4. I don't know because I make the configurations on the context but you should only need to map the many to many relation on one of the entities.

I think all your issues are because you are using the repository pattern and I always avoid it because it creates more problems that it solves, this is just my opinion, and many specialists disagree.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow