Entity Framework 和 DDD - 在将实体传递到业务层之前加载所需的相关数据

Entity Framework and DDD - Load required related data before passing entity to business layer

假设您有一个域对象:

class ArgumentEntity
{
    public int Id { get; set; }
    public List<AnotherEntity> AnotherEntities { get; set; }
}

并且您有 ASP.NET Web API 控制器来处理它:

[HttpPost("{id}")]
public IActionResult DoSomethingWithArgumentEntity(int id)
{
    ArgumentEntity entity = this.Repository.GetById(id);
    this.DomainService.DoDomething(entity);
    ...
}

它接收实体标识符,通过 id 加载实体并使用域服务在其上执行一些业务逻辑。

问题: 这里的问题是相关数据。 ArgumentEntity 具有 AnotherEntities 集合,仅当您通过 Include/Load 方法明确要求时,EF 才会加载该集合。 DomainService是业务层的一部分,应该对持久化、相关数据等EF概念一无所知。

DoDomething 服务方法期望接收 ArgumentEntity 实例并加载 AnotherEntities 集合。 您会说 - 这很简单,只需在 Repository.GetById 中包含所需的数据,然后使用相关集合加载整个对象。

现在让我们从简化的示例回到大型应用程序的实际情况:

  1. ArgumentEntity 要复杂得多。它包含多个相关集合,并且相关实体也有其相关数据。

  2. 您有多个 DomainService 方法。每种方法都需要加载不同的相关数据组合。

我能想到可能的解决方案,但所有这些都远非理想:

  1. 始终加载整个实体 -> 但效率低下且通常无法加载。

  2. 添加几个存储库方法:GetByIdOnlyHeader、GetByIdWithAnotherEntities、GetByIdFullData 以在控制器中加载特定数据子集 -> 但控制器知道要加载和传递哪些数据每种服务方式。

  3. 添加几个存储库方法:GetByIdOnlyHeader、GetByIdWithAnotherEntities、GetByIdFullData在每个服务方法中加载特定的数据子集->效率低下,sql查询每个服务方法调用。如果您为一个控制器操作调用 10 个服务方法会怎么样?

  4. 每个域方法调用存储库方法来加载额外的所需数据(例如:EnsureAnotherEntitiesLoaded)-> 它很难看,因为我的业务逻辑开始意识到 EF 概念相关数据。

问题: 在将实体传递给业务层之前,您将如何解决加载实体所需相关数据的问题?

好问题:)

我认为 "related data" 本身并不是一个严格的 EF 概念。相关数据对于 NHibernate、Dapper 或即使您使用文件进行存储也是一个有效的概念。

不过,我基本同意其他观点。所以这就是我通常做的事情:我有一个存储库方法,在你的例子中是 GetById,它有两个参数:id 和 params Expression<Func<T,object>>[]。然后, 我做的存储库中包含。这样您的业务逻辑就不会依赖于 EF(如果需要,可以为另一种类型的数据存储框架手动解析表达式),并且每个 BLL 方法都可以自己决定它们实际需要哪些相关数据。

public async Task<ArgumentEntity> GetByIdAsync(int id, params Expression<Func<ArgumentEntity,object>>[] includes)
{
    var baseQuery = ctx.ArgumentEntities; // ctx is a reference to your context
    foreach (var inlcude in inlcudes)
    {
       baseQuery = baseQuery.Include(include);
    }
    return await baseQuery.SingleAsync(a=>a.Id==id); 
}

在 DDD 的上下文中,您似乎错过了项目中导致您出现此问题的某些建模方面。您所写的实体看起来没有高度凝聚力。如果不同的流程(服务方法)需要不同的相关数据,那么您似乎还没有找到合适的聚合。考虑将您的实体拆分为多个具有高内聚性的聚合。然后与特定聚合相关的所有进程将需要该聚合包含的全部或大部分数据。

所以我不知道你问题的答案,但如果你能退后几步重构你的模型,我相信你不会遇到这样的问题。

在您的示例中,我可以看到显然属于应用层的方法 DoSomethingWithArgumentEntity。此方法调用了属于数据访问层的 Repository。我认为这种情况不符合经典的分层架构——你不应该直接从应用层调用 DAL。

所以你的代码可以用另一种方式重写:

[HttpPost("{id}")]
public IActionResult DoSomethingWithArgumentEntity(int id)
{
    this.DomainService.DoDomething(id);
    ...
}

DomainService 实现中,您可以从 repo 读取此特定操作所需的任何内容。这避免了您在应用层的麻烦。在业务层中,您将有更多的自由来实现读取:使用多个存储库方法读取半完整实体,或使用 EnsureXXX 方法或其他方法。运维需要阅读的知识会放到运维的代码里,应用层就不需要这些知识了。

每次出现这种情况都是一个强烈的信号,表明您的实体设计不当。正如 krzys 所说,该实体没有凝聚力。换句话说,如果您经常单独需要一个实体的各个部分,您应该拆分这个实体。