You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD中如何让仓储重构聚合时绕过业务逻辑校验?

解决方案:分离数据加载与业务操作的入口

要解决仓储加载历史数据时绕过业务校验,同时强制终端用户遵守规则的问题,核心思路是为聚合提供两套创建/修改入口:一套专供仓储重建历史数据使用(跳过业务规则),另一套面向终端用户(严格执行校验)。以下是两种落地方案:


方案一:内部方法+程序集可见性控制

通过C#的internal访问修饰符,给Project添加仅允许仓储程序集调用的内部方法,直接添加历史Disbursement;终端用户只能调用带校验的公开方法。

步骤1:修改Project类,添加内部方法

public class Project {
   public Budget Budget { get; }
   private List<Disbursement> Disbursements { get; }

   // 公开构造函数:用于创建新项目
   public Project(Budget budget) {
      Budget = budget ?? throw new ArgumentNullException(nameof(budget));
      Disbursements = new List<Disbursement>();
   }

   // 终端用户操作的公开方法:带业务规则校验
   public void AddDisbursement(Disbursement newDisbursement) {
      if (newDisbursement == null)
         throw new ArgumentNullException(nameof(newDisbursement));

      var source = Budget.GetSource(newDisbursement.SourceNo);
      if (source == null)
         throw new Exception("无效的资金来源编号");
      if (source.AvailableAmount < newDisbursement.Amount)
         throw new Exception("可用资金不足");

      var use = Budget.GetUse(newDisbursement.UseNo);
      if (use == null)
         throw new Exception("无效的资金用途编号");
      if (use.AvailableAmount < newDisbursement.Amount)
         throw new Exception("该用途已超支");

      Disbursements.Add(newDisbursement);
      // 更新资金来源和用途的已使用金额
      source.UpdateUsedAmount(newDisbursement.Amount);
      use.UpdateUsedAmount(newDisbursement.Amount);
   }

   // 内部方法:专供仓储加载历史数据,跳过校验
   internal void AddDisbursementForRehydration(Disbursement disbursement) {
      if (disbursement == null)
         throw new ArgumentNullException(nameof(disbursement));
      Disbursements.Add(disbursement);
   }
}

步骤2:允许仓储程序集访问内部方法

在领域模型项目的AssemblyInfo.cs中添加:

[assembly: InternalsVisibleTo("YourWarehouseAssemblyName")]

这样仓储所在的程序集就能调用AddDisbursementForRehydration方法,而外部用户无法访问。

步骤3:完善Source和Use类的金额更新逻辑

public class Source {
   public int SourceNo { get; }
   public decimal BudgetedAmount { get; set; }
   public decimal UsedAmount { get; private set; }
   public decimal AvailableAmount => BudgetedAmount - UsedAmount;

   // 供重建时重置已使用金额
   internal void ResetUsedAmount() {
      UsedAmount = 0;
   }

   // 供添加支出时更新已使用金额
   internal void UpdateUsedAmount(decimal amount) {
      UsedAmount += amount;
   }
}

// Use类逻辑与Source一致,此处省略

步骤4:仓储加载数据时的逻辑

仓储从数据库读取所有数据后,先创建Project实例,再调用内部方法批量添加历史Disbursement,最后重新计算资金来源和用途的已使用金额:

public Project GetProject(Guid projectId) {
   // 从数据库读取项目、预算、所有支出数据
   var budgetData = _dbContext.GetBudget(projectId);
   var budget = MapToBudget(budgetData);
   var disbursementDatas = _dbContext.GetDisbursements(projectId);
   var disbursements = disbursementDatas.Select(MapToDisbursement).ToList();

   // 创建Project实例
   var project = new Project(budget);

   // 批量添加历史支出
   foreach (var disbursement in disbursements) {
      project.AddDisbursementForRehydration(disbursement);
   }

   // 重新计算已使用金额,确保数据一致性
   ResetAndRecalculateUsedAmounts(project);

   return project;
}

private void ResetAndRecalculateUsedAmounts(Project project) {
   // 重置所有资金来源和用途的已使用金额
   foreach (var source in project.Budget.Sources)
      source.ResetUsedAmount();
   foreach (var use in project.Budget.Uses)
      use.ResetUsedAmount();

   // 遍历支出,累加已使用金额
   foreach (var disbursement in project.GetDisbursements()) { // 需添加GetDisbursements()公开方法返回只读集合
      var source = project.Budget.GetSource(disbursement.SourceNo);
      var use = project.Budget.GetUse(disbursement.UseNo);
      source.UpdateUsedAmount(disbursement.Amount);
      use.UpdateUsedAmount(disbursement.Amount);
   }
}

方案二:工厂类负责聚合重建

创建专门的工厂类,封装历史数据的重建逻辑,通过私有构造函数确保只有工厂能直接初始化聚合的内部集合。

步骤1:创建ProjectFactory

public class ProjectFactory {
   public static Project Reconstruct(Budget budget, List<Disbursement> disbursements) {
      if (budget == null)
         throw new ArgumentNullException(nameof(budget));
      if (disbursements == null)
         disbursements = new List<Disbursement>();

      // 直接调用Project的私有构造函数
      var project = new Project(budget, disbursements);

      // 校验关联关系(确保支出对应的资金来源/用途存在)
      ValidateDisbursementAssociations(project);

      // 重新计算已使用金额
      ResetAndRecalculateUsedAmounts(project);

      return project;
   }

   private static void ValidateDisbursementAssociations(Project project) {
      foreach (var disbursement in project.GetDisbursements()) {
         if (project.Budget.GetSource(disbursement.SourceNo) == null)
            throw new InvalidOperationException($"支出引用了无效的资金来源编号:{disbursement.SourceNo}");
         if (project.Budget.GetUse(disbursement.UseNo) == null)
            throw new InvalidOperationException($"支出引用了无效的资金用途编号:{disbursement.UseNo}");
      }
   }

   private static void ResetAndRecalculateUsedAmounts(Project project) {
      foreach (var source in project.Budget.Sources)
         source.ResetUsedAmount();
      foreach (var use in project.Budget.Uses)
         use.ResetUsedAmount();

      foreach (var disbursement in project.GetDisbursements()) {
         var source = project.Budget.GetSource(disbursement.SourceNo);
         var use = project.Budget.GetUse(disbursement.UseNo);
         source.UpdateUsedAmount(disbursement.Amount);
         use.UpdateUsedAmount(disbursement.Amount);
      }
   }
}

步骤2:修改Project类,添加私有构造函数

public class Project {
   public Budget Budget { get; }
   private List<Disbursement> Disbursements { get; }

   // 私有构造函数:专供工厂重建历史数据
   private Project(Budget budget, List<Disbursement> disbursements) {
      Budget = budget;
      Disbursements = disbursements;
   }

   // 公开构造函数:用于创建新项目
   public Project(Budget budget) {
      Budget = budget ?? throw new ArgumentNullException(nameof(budget));
      Disbursements = new List<Disbursement>();
   }

   // 终端用户操作的公开方法:带业务规则校验
   public void AddDisbursement(Disbursement newDisbursement) {
      // 同方案一的校验逻辑
   }

   // 公开方法:返回只读的支出集合
   public IReadOnlyList<Disbursement> GetDisbursements() {
      return Disbursements.AsReadOnly();
   }
}

步骤3:仓储加载数据时调用工厂

public Project GetProject(Guid projectId) {
   // 从数据库读取数据并映射为领域对象
   var budget = MapToBudget(_dbContext.GetBudget(projectId));
   var disbursements = _dbContext.GetDisbursements(projectId)
                                 .Select(MapToDisbursement)
                                 .ToList();

   // 调用工厂重建Project
   return ProjectFactory.Reconstruct(budget, disbursements);
}

方案对比

  • 方案一:实现简单,依赖C#的程序集可见性控制,适合中小型项目。
  • 方案二:更符合DDD的单一职责原则,将重建逻辑从聚合中剥离,适合复杂领域模型或需要多维度重建的场景。

两种方案都能确保:

  1. 终端用户只能通过带校验的公开方法修改聚合,严格遵守业务规则;
  2. 仓储加载历史数据时绕过业务校验,但仍会做必要的一致性校验(如关联关系、金额计算),避免数据无效。

内容的提问来源于stack exchange,提问作者Jason 'Bug' Fenter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 15:47:06