使用 DDD 和 AutoMapper,您如何在单个工作单元内处理多个服务中的同一个聚合根?

Using DDD and AutoMapper, how do you do work on the same aggregate root in multiple services within a single unit of work?

我正在尝试在非基于 Web 的项目中学习和实施域驱动设计。我有一个主循环,它将在一个工作单元中对许多实体执行多个过程。除非整个循环的工作成功,否则我不希望保留任何更改。我正在使用 AutoMapper 将持久性模型转换为存储库中的域模型,并且我的服务正在使用存储库在工作前检索数据。

DDD 的某些元素不适用于我的项目,我希望有人能告诉我整个过程中我的错误之处。

以下是我正在努力解决的 DDD 想法:

这是我正在尝试做的一个例子。

using (var scope = serviceProvider.CreateScope())
            {
                var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();
                var aggregate1Repo = scope.ServiceProvider.GetService<IAggregate1Repository>();
                var aggregate2Repo = scope.ServiceProvider.GetService<IAggregate2Repository>();
                var aggregate3Repo = scope.ServiceProvider.GetService<IAggregate3Repository>();
                var firstService = scope.ServiceProvider.GetService<IFirstService>();
                var secondService = scope.ServiceProvider.GetService<ISecondService>();

                var aggregate1 = aggregate1Repo.Find(1); //First copy of aggregate1
                var aggregate2 = aggregate2Repo.Find(1000); 
                var aggregate3 = aggregate3Repo.Find(123); 

                aggregate1.DoSomeInternalWork();

                firstService.DoWork(aggregate1.Id,aggregate2.Id); 
                secondService.DoWork(aggregate1.Id,aggregate3.Id);

                aggregate1Repo.Update(aggregate1);
                unitOfWork.Commit();
            }

聚合 1 回购:

public class Aggregate1Repository
{
    private readonly AppDBContext _dbContext;
    private IMapper _mapper;

    public Aggregate1Repository(AppDBContext context, IMapper mapper)
    {
        _dbContext = context;
        _mapper = mapper;
    }   

    public Aggregate1 Find(int id)
    {
        return _mapper.Map<Aggregate1>(_dbContext
            .SomeDBSet.AsNoTracking()
            .Find(id));
    }
}

第一服务:

public class FirstService : IFirstService
{
    private readonly IAggregate1Repository _agg1Repo;
    private readonly IAggregate2Repository _agg2Repo;

    public FirstService(IAggregate1Repository agg1Repo, IAggregate2Repository agg2Repo)
    {
        _agg1Repo = agg1Repo;
        _agg2Repo = agg2Repo;
    }

    public void DoWork(int aggregate1Id, int aggregate2Id)
    {
        var aggregate1 = _agg1Repo.Find(aggregate1Id); //second copy of aggregate1
        var aggregate2 = _agg2Repo.Find(aggregate2Id);
        //do some calculations and modify aggregate1 in some fashion
        //I could update aggregate1 in the repository here,
        // but this copy of aggregate1 doesn't have the changes made prior to this point
    }
}

第二服务:

public class SecondService : ISecondService
{
    private readonly IAggregate1Repository _agg1Repo;
    private readonly IAggregate3Repository _agg3Repo;

    public FirstService(IAggregate1Repository agg1Repo, IAggregate3Repository agg3Repo)
    {
        _agg1Repo = agg1Repo;
        _agg3Repo = agg3Repo;
    }

    public void DoWork(int aggregate1Id, int aggregate3Id)
    {
        var aggregate1 = _agg1Repo.Find(aggregate1Id); //third copy of aggregate1
        var aggregate3 = _agg2Repo.Find(aggregate3Id);
        //do some calculations and modify aggregate1 in some fashion
        //I could update aggregate1 in the repository here,
        // but this copy of aggregate1 doesn't have the changes made prior to this point
    }
}

这里的问题是,我实际上是在处理 aggregate1 的三个不同副本,因为每次我尝试加载它时,AutoMapper 都会在存储库中创建一个新对象。我可以在这两个服务中分别调用 aggregate1Repo.Update,但我仍然要处理三个不同的对象,它们都代表相同的事物。我感觉我的思路一定是有根本性的缺陷,但是我不知道是什么。

首先,您的问题实际上与 DDD 无关。这只是一个典型的 ORM/AutoMapper 问题。

您应该从不使用 AutoMapper 将映射到持久性模型或域模型,这将几乎从不 工作。

原因在于,most/many ORM 将通过引用跟踪实体及其更改(即 EntityFramework)。因此,如果您使用 automapper 并获取新实例,就会破坏 ORM 的工作方式,并且 运行 会陷入此类问题。

这对您来说可能是一本有趣的读物:Why mapping DTOs to Entities using AutoMapper and EntityFramework is horrible

虽然它处理 DTO -> 实体,但它同样适用于领域模型 -> 实体。

还有Jimmy Bogard(AutoMapper的作者)曾经在博客上评论过post(现在不可用,但是disqus评论是still there

Jimmy Bogard 评论道:

There are definitely places to use AutoMapper, and places not to use it. However, I think this post misses them:

  1. Configuration validation takes care of members that exist on the destination type that aren't mapped. That's easy.

  2. Dependency injection takes care of depending directly on other assemblies. For example, you'd have IRepository in Core and the implementation that references System.Data in another assembly

  3. AutoMapper was never, ever intended to map back into a behavioral model. AutoMapper is intended to build DTOs, not map back in
  4. AutoMapper also uses Reflection.Emit and expression tree compilation, cached once. If you use autoprojection, it's faster than any server-side code you could write yourself.

The points you raised are common complaints, but mostly it's people not understand how to use AutoMapper correctly. However, there are places I absolutely wouldn't use AutoMapper:

  1. When the destination type isn't a projection of the source type. Seems obvious, if AutoMapper isn't Auto then there's no point. It's supposed to get rid of the brain-dead code you would be forced to write anyway.
  2. Mapping to complex models. I only use AutoMapper to flatten/project, never back to behavioral models. I'm very up front about this and discourage this use whenever I see it.
  3. Anywhere that you're not trying to delete code you would have written anyway.
  4. You prefer explicit over convention. This is a whole other topic, with pros and cons of both approaches.
  5. You prefer not to understand the magic. I build lots of convention-based helpers covering a wide array of scenarios, but I make sure that my team understands what is actually happening underneath the covers.

你的选择基本上可以归结为

  1. 为您的域模型使用事件源(并将其构建为存储库中的一系列事件,因此为了持久化,您只保存新模型)

  1. 直接使用您的域模型作为持久性模型。

后一个将导致一些持久性细节泄漏到您的域模型中。对于您的用例,这可能会或可能不会被接受。它通常适用于事件溯源超出范围的小型项目。

至于你的示例的其余部分,它与实际用例有点相去甚远,很难说为什么你的服务是这样创建的。

可能是一个错误的聚合根选择,wrong/bad关注点分离。很难从 抽象的 术语中分辨出来,如 SecondService

一个聚合根可以看作是一个事务边界。该根中的所有实体都需要同时更新。

事实上,您只将 id 传递给 DoWork 方法表明它们是不同的操作(因此,交易本身)或者只应分配 id。

如果它们应该在外部作用域中使用,您应该将聚合根引用传递给它,而不是只传递 ID。

firstService.DoWork(aggregate1,aggregate2); 
secondService.DoWork(aggregate1,aggregate3);

// instead of 
firstService.DoWork(aggregate1.Id,aggregate2.Id); 
secondService.DoWork(aggregate1.Id,aggregate3.Id);

你不能(也不应该)依赖这样一个事实,即某些 ORM 可能会缓存一个实体,因此不要依赖对你的存储库的多次调用将 return 完全相同的实体实例.