简体   繁体   English

EF Core:更新对象图复制子实体

[英]EF Core: Update object graph duplicates child entities

We have a quite complex domain model and we are using Entityframework Core as ORM.我们有一个非常复杂的域模型,我们使用 Entityframework Core 作为 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!类似于文档的这一部分: https : //docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix-of-new-and-existing-entities我们使用 GUID 作为 Id实体和 Id 由数据库在插入时生成!

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我想向 StructureTemplate 类型的根实体添加一个新元素(GeneralElementTemplate 类型)
  • I load the StructureTemplate entity from the DB with all sub entities (there is already one Element in the root entity --> see screenshot #1)我从数据库中加载 StructureTemplate 实体和所有子实体(根实体中已经有一个 Element --> 见截图 #1)
  • I create the new element (named elementTemplate)我创建了新元素(名为 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)我将新元素添加到根实体中的 Elements 集合中(现在 Elements 集合中有两个实体 --> 参见屏幕截图 #2)
  • I invoke SaveChanges on the DBContext我在 DBContext 上调用 SaveChanges
  • Everything is saved fine一切都保存得很好
  • But there are now THREE entities in the Elements collection of the root entity!但是现在根实体的 Elements 集合中有三个实体! The new added entity is twice in the collection (see screenshot #3)!?新添加的实体在集合中出现了两次(见截图 #3)!?
  • In the database (SQL Server) everything is insert/updated as expected.在数据库 (SQL Server) 中,一切都按预期插入/更新。 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: GeneralElementTemplate 实现:

public class GeneralElementTemplate : Entity { private HashSet _attributes = new HashSet();公共类GeneralElementTemplate:实体{私有HashSet _attributes = new HashSet(); private HashSet _groups = new HashSet();私有 HashSet _groups = 新 HashSet(); private HashSet _materials = new HashSet();私有 HashSet _materials = new HashSet(); private HashSet _points = new HashSet();私有 HashSet _points = new HashSet(); private HashSet _sections = new HashSet();私有 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: 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: 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:调试会话的屏幕截图: 从数据库加载根实体后 --> 一个元素在根实体中 将新元素添加到根实体的 Elements 集合后 在 DbContext.SaveChanges() 之后,新元素在 Elements 集合中出现了两次!

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.发生这种情况的原因是使用 HashSet 作为子实体的集合类型以及我们在 Entity 基类中自定义的 GetHashCode() 实现。 GetHashCode() returns different values for an entity which has no id set and the same entity with the id set. GetHashCode() 为没有设置 id 的实体和设置了 id 的同一实体返回不同的值。

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.当我们现在向 HashSet 添加一个新的子实体(id 未设置)时,将调用 GetHashCode() 并将该实体与此哈希一起存储在 HashSet 中。 Now EF Core saved the entity and sets the id (GetHashCode will now return a different value).现在 EF Core 保存了实体并设置了 id(GetHashCode 现在将返回不同的值)。 Then EF Core checks if the entity is already in the HashSet.然后 EF Core 检查实体是否已经在 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.由于哈希码已更改,HashSet 的 contains 方法将返回 false,EF Core 将再次将实体添加到集合中。

Our solution was to use Lists for the child entities!我们的解决方案是对子实体使用列表!

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. update 方法将从 DB 中获取加载的实体,以及可能来自 API 层的传递的实体。

Internally the method will update the root Entity "The aggregate" and the all eagerly loaded entities that related to that entity "The included navigations"在内部,该方法将更新根实体“聚合”和与该实体“包含的导航”相关的所有急切加载的实体

eg例如

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 这是 Nuget 包

Please don't hesitate to contribute or advise请不要犹豫贡献或建议

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM