EF Core: Update object graph duplicates child entities

c# entity-framework-core entity-framework-core-2.2

Question

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

  • I want to add a new element (of Type GeneralElementTemplate) to the root entity of type StructureTemplate
  • I load the StructureTemplate entity from the DB with all sub entities (there is already one Element in the root entity --> see screenshot #1)
  • I create the new element (named elementTemplate)
  • I add the new element to the Elements collection in the root entity (now two entities are in the Elements collection --> see screenshot #2)
  • I invoke SaveChanges on the DBContext
  • Everything is saved fine
  • But there are now THREE entities in the Elements collection of the root entity! The new added entity is twice in the collection (see screenshot #3)!?
  • 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: After loading root entity from DB --> one Element is in the root entity After adding the new element to the Elements collection of the root entity After DbContext.SaveChanges() the new element is in the Elements collection twice!

1
2
1/8/2020 11:28:50 AM

Accepted Answer

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!

1
1/20/2020 8:19:35 AM


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