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:
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.
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:
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.
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.
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.