Skip to content
bitzorcas
中EN

Tutorial

构建第一个垂直切片:律所案件立案与建档实战

通过真实的民商事案件立案场景,掌握 BitzOrcas.Modern 垂直切片架构:继承 TenantAggregateRoot 统一聚合根、[BitzTable] 元数据、[GenerateEndpoint] 路由装配、IAuthorizedRequest 声明式鉴权与 ICommandRepository 数据落盘。

Last updated

在传统分层架构(Layered Architecture)中,团队经常陷入“样板代码地狱”:为系统新增一个“诉讼立案”功能,开发者必须在 MatterController、IMatterService、MatterServiceImpl、CreateMatterDto、MatterMapper、MatterEntity、IMatterRepository、MatterRepositoryImpl 等 6~8 个分散的工程与目录间来回穿梭。每一次微小的字段变更,都会引发全链路文件的同步修改,不仅认知负荷极高,且多人协同极易引发 Git 合并冲突。

BitzOrcas.Modern 彻底打破这种按技术职责横向切分的陈旧范式,全面拥抱“垂直切片架构(Vertical Slice Architecture)”。我们遵循“一用例一文件(One Use Case, One File)”原则:将 Minimal API 路由契约、声明式授权元数据、命令契约、4 阶不变量规则与业务 Handler 紧凑聚合于单一内聚单元中。

本教程将以真实的**律所民商事诉讼立案(Matter Intake)**业务为背景,带你从零实现一个生产级的垂直切片。

垂直切片全生命周期时序图

"SQL Server (LegalMatterIntake 表)""ICommandRepository<MatterIntake, string>""MatterIntake (TenantAggregateRoot)""CreateMatterIntakeCommandHandler""Mediator 10 级应用管道""Minimal API ([GenerateEndpoint])""SQL Server (LegalMatterIntake 表)""ICommandRepository<MatterIntake, string>""MatterIntake (TenantAggregateRoot)""CreateMatterIntakeCommandHandler""Mediator 10 级应用管道""Minimal API ([GenerateEndpoint])"管道阶段:全自动审计、鉴权、校验与幂等锁定管道收尾阶段:原子事务提交与事件出箱"律所业务端 / 前端 SPA"1. POST /api/legal/matters (携带 JWT 令牌与请求 Payload)12. 分派 CreateMatterIntakeCommand23. 关联 traceparent 日志上下文 (LoggingBehavior)34. 检验 IAuthorizedRequest 资源权限 (AuthorizationBehavior)45. 执行 Tier 1 不变量静态校验 (IRequestRule)56. 争抢 Redis 分布式防重放锁 (IdempotencyBehavior)67. 校验通过,移交清洗后的强类型 Command78. 读取 ICurrentTenant 校验多租户上下文可用性89. 调用 MatterIntake.Create(...) 执行领域不变量检查910. 产生聚合根实例并产出 MatterIntakeCreatedEvent1011. 调用 repository.SaveAsync(matter, ct) 暂存变更1112. 用例编排完成,返回 Result<string>.Success(matter.Id)1213. 自动开启数据库事务并提交物理变更1314. 领域事件自动写入 CAP 事务发件箱 (同事务原子提交)1415. 输出 HTTP 200 OK 并返回立案唯一流水号15"律所业务端 / 前端 SPA"

第一步:构建领域统一聚合根(Domain Aggregate)

在 BitzOrcas.Modern 中,业务领域模型直接继承自框架基类 TenantAggregateRoot<TId>。 基类已经内置了:

  • Id:实体全局唯一主键;
  • TenantId:多租户物理/逻辑隔离分区键(来自 ITenantEntity);
  • 审计追踪信封:CreateTime、CreateBy、UpdateTime、UpdateBy(实现 IAuditableEntity);
  • 软删除与乐观并发:IsDeleted、DeleteTime、RowVersion(实现 ISoftDelete 与 IConcurrencyTracked);
  • 领域事件收集:基类内置 Raise(IDomainEvent) 方法。

在业务领域目录创建领域错误契约与聚合根模型,并使用中立的 [BitzTable] 与 [BitzColumn] 特性标注持久化元数据:

src/Modules/Business/Legal/Contracts/LegalErrors.cs
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>
/// 案件立案领域稳定错误契约
/// </summary>
public static class LegalErrors
{
/// <summary>
/// 当事人与相对方为同一主体冲突
/// </summary>
public static readonly Error AdversaryConflict =
Error.Conflict("Legal.AdversaryConflict", "委托当事人与相对方主体相同,无法建立诉讼委托档案。");
/// <summary>
/// 争议标的金额非法
/// </summary>
public static readonly Error InvalidClaimAmount =
Error.Validation("Legal.InvalidClaimAmount", "涉案标的金额必须为大于零的有效数值。");
/// <summary>
/// 租户上下文缺失
/// </summary>
public static readonly Error TenantRequired =
Error.Unauthorized("Tenant.Required", "当前租户上下文缺失或未通过租户识别链解析。");
/// <summary>
/// 案件标的名称非法
/// </summary>
public static readonly Error InvalidTitle =
Error.Validation("Legal.Validation.InvalidTitle", "案件标的名称不能为空且长度不得超过 200 个字符。");
/// <summary>
/// 委托当事人与相对方名称必填
/// </summary>
public static readonly Error PartiesRequired =
Error.Validation("Legal.Validation.PartiesRequired", "案件委托当事人与争议相对方名称均属于必填项。");
}
src/Modules/Business/Legal/Domain/MatterIntake.cs
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.Legal.Domain;
/// <summary>
/// 案件立案统一聚合根
/// </summary>
/// <remarks>
/// 继承自 TenantAggregateRoot,自动获得租户隔离、审计追踪、软删除与乐观并发版本号。
/// 编译期由 Roslyn 增量源生成器产出双 ORM (SqlSugar / EF Core) 生产级映射元数据。
/// </remarks>
[BitzTable("LegalMatterIntake", IsTenant = true, IsSoftDelete = true, Description = "民商事诉讼立案表")]
public sealed class MatterIntake : TenantAggregateRoot<string>
{
private const int CodeMaxLength = 32;
private const int TitleMaxLength = 200;
private const int PartyNameMaxLength = 100;
/// <summary>
/// 案件业务外部流水号(例如 MAT-20260923-0001)
/// </summary>
[BitzColumn(Length = CodeMaxLength, IsRequired = true, IsUnique = true, Description = "案件业务流水号")]
public string MatterCode { get; private set; } = string.Empty;
/// <summary>
/// 案件标的名称
/// </summary>
[BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "案件标的名称")]
public string MatterTitle { get; private set; } = string.Empty;
/// <summary>
/// 委托当事人名称
/// </summary>
[BitzColumn(Length = PartyNameMaxLength, IsRequired = true, Description = "委托当事人名称")]
public string ClientName { get; private set; } = string.Empty;
/// <summary>
/// 争议对方当事人名称(用于后续利益冲突检索)
/// </summary>
[BitzColumn(Length = PartyNameMaxLength, IsRequired = true, Description = "争议对方当事人名称")]
public string OpposingParty { get; private set; } = string.Empty;
/// <summary>
/// 涉案争议标的金额(单位:元)
/// </summary>
[BitzColumn(Precision = 18, Scale = 2, IsRequired = true, Description = "涉案争议标的金额")]
public decimal ClaimAmount { get; private set; }
/// <summary>
/// 仅供 ORM 持久化物化使用的空构造函数
/// </summary>
[Obsolete("仅供 ORM 持久化物化使用。请使用 Create 工厂方法。", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public MatterIntake() : base("0") { }
private MatterIntake(
string id,
string matterCode,
string matterTitle,
string clientName,
string opposingParty,
decimal claimAmount,
string tenantId) : base(id)
{
MatterCode = matterCode;
MatterTitle = matterTitle;
ClientName = clientName;
OpposingParty = opposingParty;
ClaimAmount = claimAmount;
TenantId = tenantId;
}
/// <summary>
/// 工厂方法:执行立案前领域不变量守门
/// </summary>
public static Result<MatterIntake> Create(
string id,
string matterCode,
string matterTitle,
string clientName,
string opposingParty,
decimal claimAmount,
string tenantId)
{
// 领域规则 1:当事人与争议对方不能为同一主体(利冲硬性冲突)
if (string.Equals(clientName.Trim(), opposingParty.Trim(), StringComparison.OrdinalIgnoreCase))
{
return Result<MatterIntake>.Failure(LegalErrors.AdversaryConflict);
}
// 领域规则 2:争议标的金额必须大于 0
if (claimAmount <= 0)
{
return Result<MatterIntake>.Failure(LegalErrors.InvalidClaimAmount);
}
var intake = new MatterIntake(
id,
matterCode.Trim(),
matterTitle.Trim(),
clientName.Trim(),
opposingParty.Trim(),
claimAmount,
tenantId);
return Result<MatterIntake>.Success(intake);
}
}

第二步:编写垂直切片命令与 Handler(One File Use Case)

在 BitzOrcas.Modern 中,我们将 Minimal API 端点路由装配、声明式授权标记、命令模型与业务 Handler 组织在同一个切片文件中。

在 src/Modules/Business/Legal/Application/Commands/CreateMatterIntake/ 目录下创建 CreateMatterIntakeCommand.cs:

src/Modules/Business/Legal/Application/Commands/CreateMatterIntake/CreateMatterIntakeCommand.cs
using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.Application.Authorization;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
using BitzOrcas.Endpoint.Attributes;
using BitzOrcas.Modules.Legal.Domain;
using Mediator;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
/// <summary>
/// 案件立案命令对象(写模型)
/// </summary>
/// <remarks>
/// 标记 [GenerateEndpoint] 特性,由 Roslyn 生成器在编译期直接挂载 Minimal API 路由,消除手工 Controller 注册。
/// 实现 IAuthorizedRequest 接口,管道自动接管 RBAC 权限裁决。
/// </remarks>
[GenerateEndpoint(HttpRoute.Post, "/api/legal/matters", Tag = "LegalMatter")]
public sealed record CreateMatterIntakeCommand(
string MatterTitle,
string ClientName,
string OpposingParty,
decimal ClaimAmount) : ICommand<Result<string>>, IAuthorizedRequest
{
/// <summary>
/// 声明受保护资源:Legal 模块的 Matter 资源
/// </summary>
public ResourceDescriptor Resource { get; } = new("Legal", "Matter");
/// <summary>
/// 声明执行动作:创建动作
/// </summary>
public AuthorizationAction Action { get; } = AuthorizationAction.Create;
}
/// <summary>
/// 案件立案编排处理器
/// </summary>
/// <remarks>
/// 注入轻量泛型命令仓储 ICommandRepository,不泄漏具体 ORM 类型。
/// 事务开启与提交由 10 级应用管道自动完成,Handler 内部聚焦纯粹领域逻辑编排。
/// </remarks>
public sealed class CreateMatterIntakeCommandHandler(
ICommandRepository<MatterIntake, string> repository,
ICurrentTenant currentTenant) : ICommandHandler<CreateMatterIntakeCommand, Result<string>>
{
/// <summary>
/// 处理立案逻辑
/// </summary>
public async ValueTask<Result<string>> Handle(
CreateMatterIntakeCommand request,
CancellationToken cancellationToken)
{
// 1. 验证当前多租户上下文有效性
var tenant = currentTenant.Tenant;
if (!tenant.IsAvailable)
{
return Result<string>.Failure(LegalErrors.TenantRequired);
}
// 2. 生成确定性主键与业务外部流水号
var matterId = Guid.NewGuid().ToString("N");
var matterCode = $"MAT-{DateTime.UtcNow:yyyyMMdd}-{matterId[..6].ToUpperInvariant()}";
// 3. 调用领域聚合根工厂方法,守护核心业务不变量
var createResult = MatterIntake.Create(
matterId,
matterCode,
request.MatterTitle,
request.ClientName,
request.OpposingParty,
request.ClaimAmount,
tenant.EffectiveTenantId);
if (createResult.IsFailure)
{
return Result<string>.Failure(createResult.Error);
}
var matter = createResult.GetValueOrThrow();
// 4. 提交至 ICommandRepository,由外层 TransactionPipelineBehavior 在事务提交时统一落库
var saveResult = await repository.SaveAsync(matter, cancellationToken);
if (saveResult.IsFailure)
{
return Result<string>.Failure(saveResult.Error);
}
// 5. 返回新创建的案件唯一标识
return Result<string>.Success(matter.Id);
}
}

第三步:注入 Tier 1 静态参数校验规则(IRequestRule)

BitzOrcas.Modern 贯彻 4 阶业务校验范式。对于非空、字符串长度、数值边界等通用不变量,通过实现 IRequestRule<TRequest> 在进入 Handler 之前自动由管道执行:

src/Modules/Business/Legal/Application/Commands/CreateMatterIntake/CreateMatterIntakeCommandRule.cs
using BitzOrcas.Application.Validation;
using BitzOrcas.Domain.Results;
using BitzOrcas.Modules.Legal.Domain;
namespace BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
/// <summary>
/// 案件立案 Tier 1 基础不变量规则校验器
/// </summary>
public sealed class CreateMatterIntakeCommandRule : IRequestRule<CreateMatterIntakeCommand>
{
public ValueTask<Result> ValidateAsync(CreateMatterIntakeCommand request, CancellationToken cancellationToken)
{
// 校验案件标题必填且长度合规
if (string.IsNullOrWhiteSpace(request.MatterTitle) || request.MatterTitle.Length > 200)
{
return ValueTask.FromResult(Result.Failure(LegalErrors.InvalidTitle));
}
// 校验委托方与相对方名称非空
if (string.IsNullOrWhiteSpace(request.ClientName) || string.IsNullOrWhiteSpace(request.OpposingParty))
{
return ValueTask.FromResult(Result.Failure(LegalErrors.PartiesRequired));
}
// 校验金额必须大于零
if (request.ClaimAmount <= 0)
{
return ValueTask.FromResult(Result.Failure(LegalErrors.InvalidClaimAmount));
}
return ValueTask.FromResult(Result.Success());
}
}

第四步:编写端到端集成测试(Machine Verification)

在 tests/BitzOrcas.Integration.Tests/Legal/ 目录下添加集成测试,验证垂直切片在真实 HTTP 管道下的执行表现:

tests/BitzOrcas.Integration.Tests/Legal/MatterIntakeSliceTests.cs
using System.Net;
using System.Net.Http.Json;
using BitzOrcas.Domain.Results;
using BitzOrcas.Modules.Legal.Application.Commands.CreateMatterIntake;
using BitzOrcas.Modules.Legal.Domain;
using Shouldly;
using Xunit;
public sealed class MatterIntakeSliceTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public MatterIntakeSliceTests(CustomWebApplicationFactory factory)
{
// 获取预配置好测试 JWT 令牌的 HTTP 客户端实例
_client = factory.CreateAuthenticatedClient(tenantId: "1000001", role: "SuperAdmin");
}
[Fact]
public async Task CreateMatter_WithValidPayload_ShouldReturnSuccessWithGeneratedId()
{
// 1. 准备真实的民商事案件立案 Payload
var command = new CreateMatterIntakeCommand(
MatterTitle: "某科技公司跨国技术秘密侵权诉讼案",
ClientName: "某全球半导体创新科技有限公司",
OpposingParty: "某离职研发高管及其实控第三方公司",
ClaimAmount: 15_000_000.00m);
// 2. 发起 Minimal API POST 请求
var response = await _client.PostAsJsonAsync("/api/legal/matters", command);
// 3. 机器断言:验证 HTTP 状态码为 200 OK
response.StatusCode.ShouldBe(HttpStatusCode.OK);
// 4. 解析统一 Result<string> 响应模型并断言返回主键有效
var result = await response.Content.ReadFromJsonAsync<Result<string>>();
result.ShouldNotBeNull();
result.IsSuccess.ShouldBeTrue();
result.Value.ShouldNotBeNullOrWhiteSpace();
}
[Fact]
public async Task CreateMatter_WithSameClientAndOpponent_ShouldFailWithAdversaryConflictError()
{
// 1. 构造委托当事人与争议相对方重叠的冲突 Payload
var conflictCommand = new CreateMatterIntakeCommand(
MatterTitle: "内部股东决议效力确认争议",
ClientName: "某合伙企业控股有限公司",
OpposingParty: "某合伙企业控股有限公司",
ClaimAmount: 1_000_000.00m);
// 2. 发起请求
var response = await _client.PostAsJsonAsync("/api/legal/matters", conflictCommand);
// 3. 断言失败:管道自动拦截并返回 RFC 9457 规范的错误详情
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
var result = await response.Content.ReadFromJsonAsync<Result<string>>();
result.ShouldNotBeNull();
result.IsFailure.ShouldBeTrue();
result.Error.Code.ShouldBe(LegalErrors.AdversaryConflict.Code);
}
}

架构小结与收益回顾

通过上述三步,你已经完成了一个具备工业级质量的垂直切片:

  1. 零样板代码:没有传统分层的 Controller、Service、Repository 胶水包装,所有逻辑高度聚焦在用例自身;
  2. 零反射装配:[GenerateEndpoint] 与 [BitzTable] 在编译期静态生成,无缝支持 .NET 10 Native AOT 裁切与极速冷启动;
  3. 安全透明托管:日志跟踪、RBAC 授权、三阶校验、数据库事务与 CAP 领域事件出箱全部由 10 级应用管道隐式接管,业务代码纯净自洽。

100%

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