Skip to content
bitzorcas
中EN

Recipe

实战:如何编写一个写操作切片(Command Slice)

遵循“一用例一文件”原则,从零编写一个企业级写操作垂直切片:包含 Minimal API 路由契约、RBAC 动作鉴权、IRequestRule 纯函数校验规则、ICommandRepository 窄端口恢复与领域聚合根状态流转。

Last updated

在传统的架构中,增加一个简单的业务写操作(如“民商事案件结案归档”)需要修改 Controller、Service、DTO、Mapper、Entity 多个分散文件。代码散落各处不仅导致极高的认知负担,更使得横切关注点(如权限校验、幂等防重、事务提交、审计记录)在各个服务中被随意复制或遗漏。

在 BitzOrcas.Modern 中,我们坚决贯彻 “一用例一文件(One Use Case, One File)” 原则:将 Minimal API 路由生成契约、参数校验规则、RBAC 鉴权声明与命令处理器 Handler 组织在同一个自包含的 .cs 文件中,并由 10 级管道统一守卫。

本篇指南将以**律所案件结案归档(CloseMatterCommand)**为标准金样板,带你掌握编写写操作切片的完整范式。

写操作切片单文件结构与执行流转

10 级线性执行管道单个垂直切片文件: CloseMatterCommand.cs

1. [GenerateEndpoint(RequirePermission = 'legal.matters.close')] 端点契约

2. CloseMatterCommand (入站 Command Record)

3. CloseMatterCommandRule (IRequestRule 纯函数校验)

4. CloseMatterCommandHandler (用例编排与 ICommandRepository)

Logging (第 1 级) -> License -> Impersonation -> Auth (第 4 级)

Validation (第 5 级执行 CloseMatterCommandRule) -> Idempotency (第 6 级)

Transaction (第 7 级) -> Events Outbox -> Audit -> ReadModel


完整单文件写操作切片金样板

在 src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/ 目录下创建 CloseMatterCommand.cs:

src/Modules/Legal/BitzOrcas.Modules.Legal/Application/Matters/CloseMatterCommand.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions;
using BitzOrcas.Application.Attributes;
using BitzOrcas.Application.Security;
using BitzOrcas.Application.Validation;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
using BitzOrcas.Domain.Tenancy;
using BitzOrcas.Modules.Legal.Domain;
namespace BitzOrcas.Modules.Legal.Application.Matters;
/// <summary>
/// 案件结案归档命令契约
/// </summary>
/// <remarks>
/// <para>承载案件业务主键、结案意见说明与卷宗物理归档柜号。</para>
/// <para>由 Roslyn 增量生成器在编译期自动生成 Minimal API 端点并挂载细粒度 RBAC 动作权限。</para>
/// </remarks>
[GenerateEndpoint(
Route = "api/legal/matters/{matterId}/close",
Method = EndpointMethod.Post,
RequirePermission = "legal.matters.close", // 声明式 RBAC 动作权限码
Summary = "案件结案与卷宗归档",
Description = "将处于在审状态的民商事案件正式结案并归档卷宗物理柜位")]
public sealed record CloseMatterCommand(
string MatterId,
string ClosingRemarks,
string ArchiveBoxNumber) : ICommand<Result>;
/// <summary>
/// 案件业务强类型错误字典
/// </summary>
public static class MatterErrors
{
public static readonly Error IdRequired = Error.Validation("Matter.IdRequired", "案件标识不能为空。");
public static readonly Error ClosingRemarksInvalid = Error.Validation("Matter.ClosingRemarksInvalid", "结案意见不能为空且不能超过 1000 个字符。");
public static readonly Error ArchiveBoxRequired = Error.Validation("Matter.ArchiveBoxRequired", "卷宗归档柜位号不能为空且不能超过 64 个字符。");
}
/// <summary>
/// 案件结案前置一阶纯函数校验规则
/// </summary>
/// <remarks>
/// <para>实现 <see cref="IRequestRule{TRequest}"/> 接口,属于 10 级管道第 5 级(ValidationBehavior)。</para>
/// <para>在进入业务 Handler 之前由框架自动拦截并校验入参合法性,保障领域层不受脏数据污染。</para>
/// </remarks>
public sealed class CloseMatterCommandRule : IRequestRule<CloseMatterCommand>
{
/// <summary>
/// 异步执行纯函数参数校验
/// </summary>
/// <param name="command">结案命令入参。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>校验通过或失败结果。</returns>
public ValueTask<Result> ValidateAsync(CloseMatterCommand command, CancellationToken cancellationToken)
{
// 1. 案件唯一标识非空
if (string.IsNullOrWhiteSpace(command.MatterId))
{
return ValueTask.FromResult(Result.Failure(MatterErrors.IdRequired));
}
// 2. 结案意见非空与长度限制
if (string.IsNullOrWhiteSpace(command.ClosingRemarks) || command.ClosingRemarks.Length > 1000)
{
return ValueTask.FromResult(Result.Failure(MatterErrors.ClosingRemarksInvalid));
}
// 3. 归档卷宗编号格式校验
if (string.IsNullOrWhiteSpace(command.ArchiveBoxNumber) || command.ArchiveBoxNumber.Length > 64)
{
return ValueTask.FromResult(Result.Failure(MatterErrors.ArchiveBoxRequired));
}
return ValueTask.FromResult(Result.Success());
}
}
/// <summary>
/// 案件结案用例执行处理器
/// </summary>
/// <remarks>
/// <para>聚焦纯粹的领域编排,事务提交、CAP 领域事件分发与变更审计均由 10 级管道全自动治理。</para>
/// <para>依赖规范:写侧仅依赖 <see cref="ICommandRepository{TAggregateRoot, TId}"/> 窄端口。</para>
/// </remarks>
public sealed class CloseMatterCommandHandler : ICommandHandler<CloseMatterCommand, Result>
{
private readonly ICommandRepository<MatterIntake, string> _repository;
private readonly ICurrentUser _currentUser;
/// <summary>
/// 初始化案件结案处理器
/// </summary>
/// <param name="repository">命令仓储窄端口。</param>
/// <param name="currentUser">当前登录用户安全上下文。</param>
public CloseMatterCommandHandler(
ICommandRepository<MatterIntake, string> repository,
ICurrentUser currentUser)
{
_repository = repository;
_currentUser = currentUser;
}
/// <summary>
/// 执行案件结案领域编排
/// </summary>
/// <param name="request">结案命令契约。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>操作执行结果。</returns>
public async Task<Result> Handle(CloseMatterCommand request, CancellationToken cancellationToken)
{
// 1. 从命令仓储恢复聚合根 (管道与仓储自动叠加当前租户隔离边界)
var matterResult = await _repository.FindAsync(request.MatterId, cancellationToken);
if (matterResult.IsFailure)
{
return Result.Failure(matterResult.Error);
}
var matter = matterResult.GetValueOrThrow();
// 2. 调用聚合根领域行为流转状态 (内部守护在审状态、封锁不可变字段并登记 CloseDomainEvent)
var closeResult = matter.CloseMatter(
closingRemarks: request.ClosingRemarks,
archiveBoxNumber: request.ArchiveBoxNumber,
operatorId: _currentUser.Id);
if (closeResult.IsFailure)
{
return closeResult;
}
// 3. 提交聚合更新 (事务提交、集成事件分发与审计快照由管道自动接管)
return await _repository.SaveAsync(matter, cancellationToken);
}
}

编写写操作切片的 4 项核心铁律

  1. 一用例一文件(One Use Case, One File):Command 契约、[GenerateEndpoint]、IRequestRule 校验与 Handler 必须声明在同一个自包含文件中,严禁为了拆解而拆解;
  2. 严禁在 Handler 中编写横切关注点:严禁手动开启/提交数据库事务、手动调用 _dbContext.SaveChangesAsync() 或手工记录审计日志,所有横切逻辑由 10 级管道统一处理;
  3. 聚合根全权负责状态不变量:所有状态流转(如 matter.CloseMatter(remarks, boxNumber, operatorId))必须封装在聚合根内部方法中,严禁在 Handler 中直接对聚合根属性执行 setter 赋值;
  4. 写侧仅依赖 ICommandRepository 窄端口:命令切片禁止直接注入泛型 ORM 上下文执行复杂宽表查询,读逻辑与报表导出必须分流至只读模型 Store。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%