We have a quite complex domain model and we are using Entityframework Core as ORM. Updates are always performed on the root entities. If we need to add or update an child object, we load the root entity, modify the childs and then save the root entity. Similar to this part of the docu: https://learn.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix-of-new-and-existing-entities We are using GUIDs as Ids for the entities and the Ids are generated by the database on inserts!
That works quite well but there is a problem which I can't resolve:
In the database (SQL Server) everything is insert/updated as expected. After the operation the root object has two elements (and not three)...
GeneralElementTemplate elementTemplate = new GeneralElementTemplate(ElementTemplateType.Line);
StructureTemplate structureTemplate = DbContext.StructureTemplates
.Include(x => x.Elements).ThenInclude(e => e.Attributes)
.Include(x => x.Elements).ThenInclude(e => e.Groups)
.Include(x => x.Elements).ThenInclude(e => e.Materials)
.Include(x => x.Elements).ThenInclude(e => e.Points)
.Include(x => x.Elements).ThenInclude(e => e.Sections)
.Where(b => b.Id == structureTemplateId)
.SingleOrDefault();
if (structureTemplate == null)
{
return NotFound();
}
structureTemplate.AddElementTemplate(elementTemplate);
DbContext.SaveChanges();
I tried already to build a small sample project to demonstrate that behavior but with the sample project everything is working fine. Can somebody explain what's going on?
StructureTemplate implementation:
public class StructureTemplate : Document<StructureTemplate>
{
private HashSet<GeneralElementTemplate> _elements = new HashSet<GeneralElementTemplate>();
private HashSet<StructureTemplateTag> _structureTemplateTags = new HashSet<StructureTemplateTag>();
public StructureTemplate(
DocumentHeader header,
uint versionNumber = InitialLabel,
IEnumerable<GeneralElementTemplate> elements = null)
: base(header, versionNumber)
{
_elements = (elements != null) ? new HashSet<GeneralElementTemplate>(elements) : new HashSet<GeneralElementTemplate>();
}
/// <summary>
/// EF Core ctor
/// </summary>
protected StructureTemplate()
{
}
public IReadOnlyCollection<GeneralElementTemplate> Elements => _elements;
public IReadOnlyCollection<StructureTemplateTag> StructureTemplateTags => _structureTemplateTags;
public override IReadOnlyCollection<Tag> Tags => _structureTemplateTags.Select(x => x.Tag).ToList();
public void AddElementTemplate(GeneralElementTemplate elementTemplate)
{
CheckUnlocked();
_elements.Add(elementTemplate);
}
public override void AddTag(Tag tag) => _structureTemplateTags.Add(new StructureTemplateTag(this, tag));
public void RemoveElementTemplate(Guid elementTemplateId)
{
CheckUnlocked();
var elementTemplate = Elements.FirstOrDefault(x => x.Id == elementTemplateId);
_elements.Remove(elementTemplate);
}
public override void RemoveTag(Tag tag)
{
var existingEntity = _structureTemplateTags.SingleOrDefault(x => x.TagId == tag.Id);
_structureTemplateTags.Remove(existingEntity);
}
public void SetPartTemplateId(Guid? partTemplateId)
{
CheckUnlocked();
PartTemplateId = partTemplateId;
}
}
GeneralElementTemplate implementation:
public class GeneralElementTemplate : Entity { private HashSet _attributes = new HashSet(); private HashSet _groups = new HashSet(); private HashSet _materials = new HashSet(); private HashSet _points = new HashSet(); private HashSet _sections = new HashSet();
public GeneralElementTemplate(
ElementTemplateType type,
IEnumerable<NamedPointReference> points = null,
IEnumerable<NamedSectionReference> sections = null,
IEnumerable<NamedMaterialReference> materials = null,
IEnumerable<NamedGroupReference> groups = null,
IEnumerable<NamedAttributeReference> attributes = null)
: base()
{
Type = type;
_points = points != null ? new HashSet<NamedPointReference>(points) : new HashSet<NamedPointReference>();
_sections = sections != null ? new HashSet<NamedSectionReference>(sections) : new HashSet<NamedSectionReference>();
_materials = materials != null ? new HashSet<NamedMaterialReference>(materials) : new HashSet<NamedMaterialReference>();
_groups = groups != null ? new HashSet<NamedGroupReference>(groups) : new HashSet<NamedGroupReference>();
_attributes = attributes != null ? new HashSet<NamedAttributeReference>(attributes) : new HashSet<NamedAttributeReference>();
}
/// <summary>
/// EF Core ctor
/// </summary>
protected GeneralElementTemplate()
{
}
public IReadOnlyCollection<NamedAttributeReference> Attributes => _attributes;
public IReadOnlyCollection<NamedGroupReference> Groups => _groups;
public IReadOnlyCollection<NamedMaterialReference> Materials => _materials;
public IReadOnlyCollection<NamedPointReference> Points => _points;
public IReadOnlyCollection<NamedSectionReference> Sections => _sections;
public ElementTemplateType Type { get; private set; }
public virtual GeneralElementTemplate Reincarnate()
{
return new GeneralElementTemplate(
Type,
Points,
Sections,
Materials,
Groups,
Attributes);
}
}
Entity Type Configuration for StructureTemplate:
public class StructureTemplateTypeConfiguration : IEntityTypeConfiguration<StructureTemplate>
{
public void Configure(EntityTypeBuilder<StructureTemplate> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder
.Property(e => e.Id)
.ValueGeneratedOnAdd();
builder
.OwnsOne(e => e.Header, headerBuilder =>
{
headerBuilder
.Property(e => e.Name)
.HasConversion<string>(x => x, x => EntityName.ToEntityName(x))
.HasMaxLength(EntityName.NameMaxLength)
.IsUnicode(false);
headerBuilder
.Property(e => e.Descriptions)
.HasConversion(
d => JsonConvert.SerializeObject(d.ToStringDictionary()),
d => d == null
? TranslationDictionary.Empty
: JsonConvert.DeserializeObject<Dictionary<EntityLang, string>>(d).ToTranslationDictionary())
.HasMaxLength((int)TranslatedEntry.EntryMaxLength * (Enum.GetValues(typeof(EntityLang)).Length + 1));
});
builder
.Property(e => e.VersionNumber);
builder
.HasMany(e => e.Elements)
.WithOne();
builder.Metadata.FindNavigation(nameof(StructureTemplate.Elements)).SetPropertyAccessMode(PropertyAccessMode.Field);
// TAGS
builder
.Ignore(e => e.Tags);
builder
.HasMany(e => e.StructureTemplateTags);
builder.Metadata
.FindNavigation(nameof(StructureTemplate.StructureTemplateTags))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
Entity Type Configuration for StructureTemplateElement:
public class StructureElementTemplateTypeConfiguration : IEntityTypeConfiguration<GeneralElementTemplate>
{
public void Configure(EntityTypeBuilder<GeneralElementTemplate> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ToTable("StructureTemplateElements");
builder
.Property(e => e.Id)
.ValueGeneratedOnAdd();
builder
.Property(e => e.Type);
builder
.HasMany(e => e.Attributes)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Attributes)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Groups)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Groups)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Materials)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Materials)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Points)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Points)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Sections)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Sections)).SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
Screenshot of debugging session:

I know my answer is late. But I've created an extension method to do Generic Graph Update
The update method will take the loaded entity from the DB and the passed one that may come from the API layer.
Internally the method will update the root Entity "The aggregate" and the all eagerly loaded entities that related to that entity "The included navigations"
e.g.
var updatedSchool = mapper.Map<School>(apiModel);
var dbSchool = dbContext.Schools
.Include(s => s.Classes)
.ThenInclude(s => s.Students)
.FirstOrDefault();
dbContext.InsertUpdateOrDeleteGraph(updatedSchool, dbSchool);
dbContext.SaveChanges();
The project is here
And here is the Nuget package
Please don't hesitate to contribute or advise
Problem solved :)
After some long debugging sessions we found and solved the problem. The reason why that occurs is the using of HashSet as collection type for the child entities and our custom implementation of GetHashCode() in the Entity base class. GetHashCode() returns different values for an entity which has no id set and the same entity with the id set.
When we now add a new child entity (id not set) to the HashSet GetHashCode() will be invoked and the entity is stored with this hash in the HashSet. Now EF Core saved the entity and sets the id (GetHashCode will now return a different value). Then EF Core checks if the entity is already in the HashSet. Because the hash code has changed, the contains method of the HashSet will return false and EF Core will add the entity again to the set.
Our solution was to use Lists for the child entities!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With