3
votes

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!

2
DbContext.StructureTemplates for given key should exits how many times. if you modify structureTemplate statement to ToList() instead of SingleOrDefault() how many should you get. what im seeing is you have an entity with the same key.... if you trying to update it, you should not be using ADD or well i cant see the code for ADD so have no idea what its doing. aka i can not see how GeneralElementTemplate maps to the entity. I want to add a new element (of Type GeneralElementTemplate) to the root entity of type StructureTemplate sounds fishy looking what what code i can seeSeabizkit
Did you load the entries for the models on which you are adding elementTemplate, by adding the AddElementTemplate code it would be easier to determine this.Seabizkit
@Seabizkit i don't get your point. I'm loading a single root entity (StructureTemplate) from the DB. The root entity is attached to the DbContext and has ONE child of type GeneralElementTemplate. Then I add one more child element of type GeneralElementTemplate. Now I save all changes. In the DB are now two childs of type GeneralElementTemplate for this root entity --> ok. But in the structureTemplate (the root entity) I have THREE childs of type GeneralElementTemplate in the Elements collection and the second and third child element are the same objects. I will add infos about the config.goflo
I need to re-read everything but to clarify i am wanting to see,... structureTemplate.AddElementTemplate(elementTemplate); i want to see what AddElementTemplate is doing. also add the code for class, StructureTemplate, it will help in trying to understand what is going on. The other code you added is cool but haven't gone over it yet as for now it not what i need to see. Add code for StructureTemplate and AddElementTemplate, as those are the key parts in scenario.Seabizkit
@Seabizkit added implementation of StructureTemplate and GeneralElementTemplate. The AddElementTemplate method just adds the new item to the private HashSet.goflo

2 Answers

2
votes

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!

2
votes

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