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

EF Core: Update object graph duplicates child entities

我们有一个非常复杂的域模型,我们使用 Entityframework 核心作为 ORM。更新始终在根实体上执行。如果我们需要添加或更新子对象,我们加载根实体,修改子实体,然后保存根实体。类似于文档的这一部分:https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix-of-new-and-existing-entities 我们使用 GUID 作为实体的 ID,这些 ID 由数据库在插入时生成!

效果很好,但有一个问题我无法解决:

我已经尝试构建一个小型示例项目来演示该行为,但示例项目一切正常。 有人能解释一下这是怎么回事吗?

结构模板实现:

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 实现:

public class 通用元素模板:实体 { 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);
    }
}

结构模板的实体类型配置:

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);
    }
}

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);
    }
}

调试会话的屏幕截图:

问题已解决:)

经过长时间的调试,我们发现并解决了问题。发生这种情况的原因是使用 HashSet 作为子实体的集合类型以及我们在实体基 class 中自定义实现 GetHashCode()。 GetHashCode() return 未设置 ID 的实体和设置了 ID 的同一实体的不同值。

当我们现在将新的子实体(未设置 id)添加到 HashSet 时,将调用 GetHashCode() 并将该实体与此哈希一起存储在 HashSet 中。现在 EF Core 保存了实体并设置了 id(GetHashCode 现在将 return 一个不同的值)。然后 EF Core 检查实体是否已经在 HashSet 中。由于哈希码已更改,因此 HashSet 的 contains 方法将 return false 并且 EF Core 将再次将该实体添加到集合中。

我们的解决方案是为子实体使用列表!

我知道我的回答晚了。但是我创建了一个扩展方法来执行 Generic Graph Update

更新方法将从数据库中获取加载的实体,并从 API 层获取传递的实体。

在内部,该方法将更新根实体“The aggregate”以及与该实体“The included navigations”相关的所有急切加载的实体

例如

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

请不要犹豫,贡献或建议