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

Lay*_*ldK 1 c# domain-driven-design repository-pattern automapper asp.net-core

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

有一些DDD元素与我的项目不能很好地工作,我希望有人可以告诉我整个过程有什么问题.

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

  • 当进程涉及多个聚合根相互交互时,应使用域服务
  • 您应该将聚合根ID传递给域服务,然后使用存储库加载它们
  • 存储库应该返回它从映射的持久性模型构造的域模型(在这种情况下,我使用的是AutoMapper)

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

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();
            }
Run Code Online (Sandbox Code Playgroud)

Aggregate1Repo:

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

FirstService:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

SecondService:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这里的问题是我基本上正在为三个不同的aggregate1副本工作,因为每次我尝试加载时,AutoMapper都会在存储库中创建一个新对象.我可以在两个服务中单独调用aggregate1Repo.Update,但我仍然在处理三个不同的对象,它们都代表相同的东西.我觉得我的思维中必须有一个根本的缺陷,但我不知道它是什么.

Tse*_*eng 7

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

永远不应该使用AutoMapper映射持久性模型或域模型,这几乎不会起作用.

其原因在于,大多数/多个ORM将通过引用(即EntityFramework)跟踪实体及其更改.因此,如果您使用automapper并获取新实例,则会破坏ORM的工作方式并遇到此类问题.

这可能是一个有趣的读物:为什么使用AutoMapper和EntityFramework将DTO映射到实体是可怕的

虽然它处理DTO - > Entity,但它适用于Domain Model - > Entity.

此外,Jimmy Bogard(AutoMapper的作者)曾评论过一篇博客文章(现在不可用,但disqus评论仍然存在)

吉米博加德评论说:

绝对有使用AutoMapper的地方,并且使用它.但是,我认为这篇文章错过了它们:

  1. 配置验证负责处理未映射的目标类型上的成员.这很简单.

  2. 依赖注入需要直接依赖于其他程序集.例如,你在Core中有IRepository,在另一个程序集中有一个引用System.Data的实现

  3. AutoMapper是永远都不会打算映射成一个行为模型.AutoMapper旨在构建DTO,而不是重新映射
  4. AutoMapper还使用Reflection.Emit和表达式树编译,缓存一次.如果您使用自动投影,它比您自己编写的任何服务器端代码都快.

你提出的观点是常见的抱怨,但大多数人都不明白如何正确使用AutoMapper.但是,有些地方我绝对不会使用AutoMapper:

  1. 目标类型不是源类型的投影.看起来很明显,如果AutoMapper不是Auto,那么就没有意义了.它应该摆脱你将被迫写的脑死代码.
  2. 映射到复杂模型.我只使用AutoMapper来展平/投影,永远不会回到行为模型.我非常关注这一点,并且每当我看到它时都会阻止这种使用.
  3. 无论如何,您不想删除代码的任何地方.
  4. 你更喜欢明确的约定.这是一个完整的其他主题,两种方法的优点和缺点.
  5. 你不想理解魔法.我构建了许多基于会议的帮助程序,涵盖了各种各样的场景,但我确保我的团队了解幕后实际发生的事情.

您的选择基本上归结为

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

要么

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

后者将导致一些持久性细节泄漏到您的域模型中.这可能是也可能不适用于您的用例.它通常适用于事件采购超出范围的小型项目.

至于你的例子的其余部分,它与实际用例有点远,并且很难说为什么你的服务是以这种方式创建的.

可能是一个糟糕的选择聚合根,错误/错误的关注分离.很难从抽象的术语中分辨出来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);
Run Code Online (Sandbox Code Playgroud)

您不能(也不应该)依赖于某些ORM可以缓存实体的事实,因此不依赖于对您的存储库的多次调用将返回实体的完全相同的实例.