【问题标题】:Problem with EF Core write operations with mapped entitiesEF Core 写入操作与映射实体的问题
【发布时间】:2021-02-13 08:39:19
【问题描述】:

上下文

我正在尝试自己的 DDD 架构。与我见过的其他项目的主要区别在于,我没有使用我的域模型作为数据实体,而是我有单独的模型,我称之为商店,它们从域模型映射并表示数据库的状态。

如果您不熟悉DDD,其想法是将核心业务逻辑与应用程序的其他元素(例如数据库)完全解耦。为了实现这一点,我定义了包含业务逻辑和验证的域模型,然后定义了实体模型,它们表示与域模型相同的状态(业务和验证逻辑条带化),但也表示 EF 特定的关系属性。

问题

EF 操作适用于更简单的操作。假设我们有一个竞赛,它可以包含多个试验。

伪代码示例:

contest = new Contest
contest.Add(new Trial(1))
contest.Add(new Trial(2))

data.Save(contest) // performs mapping to ContestEntity and calls dbContext.Add
// So far so good

contestWithTrials = data.Get() // contest comes with 2 Included Trials
contestWithTrials.Add(new Trial(3))
data.Save(contestWithTrials) // performs mapping, calls dbContext.Update and tries to save but fails.

错误是:

无法跟踪实体类型“Trial”的实例,因为已在跟踪另一个具有键值“{Id: 1}”的实例

试图更新或删除商店中不存在的实体

由于某种原因,映射混淆了 EF,它试图重新创建已经存在的 Trial,但我不明白为什么 - 我可以看到在调用 SaveChanges 之前在 DbSet.Local 中正确添加了实体,但是它仍然会抛出。

我已经建立了一个 PoC 分支here。根据 Progrman 的建议,它是一个控制台应用程序,具有最少的可重现示例。由于设置需要几个包,我认为最好在一个 repo 中而不是单个文件中。

【问题讨论】:

  • 不清楚问题是什么或您要解决什么问题。请edit您的问题包含您拥有的完整源代码作为minimal reproducible example,其他人可以编译和测试。 StackOverflow 上有很多问题涉及诸如“实体类型的实例......无法跟踪,因为另一个具有键值的实例”之类的错误。
  • @Progman 感谢您的提示,同时将代码库简化为可能的最简单配置,我已经缩小了问题的范围。我仍然无法弄清楚,但我完全重写了我的问题并提供了代码示例,尽管是在一个 repo 中。
  • 请不要将 MCVE 添加到外部站点,将其添加到您的问题本身。还要保持“最小”,这表明您遇到的问题。
  • @Progman 我不明白。为什么你更喜欢有一个大的复制粘贴,而不是简单地克隆一个 repo?复制粘贴也不起作用,因为您需要安装 Nuget 包。我最好走这条路。正如我之前所说 - 它已经是“最小的”。我从纯控制台应用程序归零并添加了组件,直到我设法模拟了同样的问题。
  • 该问题与 AutoMapper 有关,您可能需要查看其他问题,例如 stackoverflow.com/questions/41482484/…stackoverflow.com/questions/50459427/…stackoverflow.com/questions/48359363/…,这些问题解释了更新子实体的问题。

标签: c# asp.net-core entity-framework-core domain-driven-design


【解决方案1】:

最好将包含业务逻辑的域模型类与基础架构依赖项分开,以解决您的数据库问题。但是,当您使用 EF Core 时,您可以完全取消您的 实体模型,因为 EF Core 的设计方式已经允许您分离域和数据库问题。

让我们看一个来自 Microsoft 支持的 EShopOnWeb project 的示例。 领域模型类 Order(Ordering 上下文的聚合根)包含领域逻辑,并且其结构使得可以以最佳方式遵守业务不变量。

当您查看 Order 类时,您会发现它没有数据库或其他基础架构依赖项。领域模型类也位于

https://github.com/dotnet-architecture/eShopOnWeb/blob/master/src/ApplicationCore/Entities/OrderAggregate/Order.cs

解决方案。

public class Order : BaseEntity, IAggregateRoot
{
    private Order()
    {
        // required by EF
    }

    public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
    {
        Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
        Guard.Against.Null(shipToAddress, nameof(shipToAddress));
        Guard.Against.Null(items, nameof(items));

        BuyerId = buyerId;
        ShipToAddress = shipToAddress;
        _orderItems = items;
    }

    public string BuyerId { get; private set; }
    public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
    public Address ShipToAddress { get; private set; }

    private readonly List<OrderItem> _orderItems = new List<OrderItem>();

    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    public decimal Total()
    {
        var total = 0m;
        foreach (var item in _orderItems)
        {
            total += item.UnitPrice * item.Units;
        }
        return total;
    }
}

为了将业务模型映射到数据库以持久化来自 EF Core 的数据内置功能,可以通过简单地定义相应的配置类来使用,如下所示。为了将其与业务层分开,它还位于项目的infrastructure layer(或数据层)中。

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));

        navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

        builder.OwnsOne(o => o.ShipToAddress, a =>
        {
            a.WithOwner();
            
            a.Property(a => a.ZipCode)
                .HasMaxLength(18)
                .IsRequired();

            a.Property(a => a.Street)
                .HasMaxLength(180)
                .IsRequired();

            a.Property(a => a.State)
                .HasMaxLength(60);

            a.Property(a => a.Country)
                .HasMaxLength(90)
                .IsRequired();

            a.Property(a => a.City)
                .HasMaxLength(100)
                .IsRequired();
        });
    }
}

EF Core 唯一需要的是 Order 域模型类中的私有无参数构造函数,从我的角度来看,考虑到您可以节省编写数据库映射类的工作量,这是一个可以接受的折衷方案。

如果我受到不提供此类功能的其他框架的限制,我通常也会采用与您现在类似的方式,但如果手头有 EF Core 的功能,我建议您重新考虑您的方法EF Core 配置功能一试。

我知道这不是您所面临的技术问题的确切答案,但我想向您展示另一种方法。

【讨论】:

  • 感谢您的输入,我知道实体配置,但我不知道它们有那么大的能力。虽然出于其他原因我不会最终采用这种方法,但它肯定是一个可行的选择,因此竖起大拇指:)
【解决方案2】:

您的问题是,当您从数据库加载实体时,EF Core 开始在其更改跟踪器中跟踪它们,以便在调用 SaveChanges() 后立即识别您对加载的实体所做的更改。只要您修改 EF 加载的实际对象,此行为就可以正常工作。

您正在做的是:加载 DatabaseTrial(假设它具有 id 1),然后将其映射到 DomainTrial,可能对其进行修改,然后将其映射到也具有 id 1 的 DatabaseTrial 的新实例并将其添加到语境。这让 EF 感到困惑,因为它现在有两个不同的对象(通过引用),它们的 id 都为 1。这是不允许的,因为 id 必须是唯一的(如果 EF 没有抛出这个异常,应该使用哪个 DatabaseTrial 对象来更新数据库条目?) .

解决方案非常简单:从数据库加载实体时只需使用 AsNoTracking() 即可。这将阻止更改跟踪器跟踪最初加载的对象,并且一旦调用 Update(),将仅跟踪处于“已修改”状态的新实体并用于更新数据库条目。正如文档所述:

对于具有生成键的实体类型,如果一个实体设置了它的主键值,那么它将在修改状态下被跟踪。如果未设置主键值,则将在已添加状态下对其进行跟踪。这有助于确保插入新实体,同时更新现有实体。如果主键属性设置为属性类型的 CLR 默认值以外的任何值,则认为实体已设置其主键值。

这也适用于正在添加到竞赛中的试用版,因为它的主键在创建后设置为默认值,并且 EF 会知道必须插入它。

【讨论】:

  • 感谢您的回答。您很接近,但修改单个实体有效。当我尝试更新集合时会出现问题 - 例如,如果我在 Contest.Trials 集合中添加新的 Trial。然后在从 Domain->Entity 映射期间,集合被重新创建,这就是导致错误的原因。我找到了 AutoMapper.Collections.EntityFrameworkCore 的潜在解决方案,但我还没有测试过,因此我没有在这里写答案。如果您可以确认它有效并更改您的答案,我会接受它。 (如果你愿意,欢迎使用我的 repo 作为 PoC)。
  • @Alex 我个人不喜欢使用 AutoMapper 有几个原因,所以我不打算深入研究这个解决方案。您是否尝试过我的解决方案,但它对您不起作用?因为我们正在使用这种方法,并且我们也有添加对象然后保存的集合,它工作正常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-16
  • 1970-01-01
相关资源
最近更新 更多