在传统分层架构(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)**业务为背景,带你从零实现一个生产级的垂直切片。
垂直切片全生命周期时序图
第一步:构建领域统一聚合根(Domain Aggregate)
在 BitzOrcas.Modern 中,业务领域模型直接继承自框架基类 TenantAggregateRoot<TId>。
基类已经内置了:
Id:实体全局唯一主键;TenantId:多租户物理/逻辑隔离分区键(来自ITenantEntity);- 审计追踪信封:
CreateTime、CreateBy、UpdateTime、UpdateBy(实现IAuditableEntity); - 软删除与乐观并发:
IsDeleted、DeleteTime、RowVersion(实现ISoftDelete与IConcurrencyTracked); - 领域事件收集:基类内置
Raise(IDomainEvent)方法。
在业务领域目录创建领域错误契约与聚合根模型,并使用中立的 [BitzTable] 与 [BitzColumn] 特性标注持久化元数据:
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", "案件委托当事人与争议相对方名称均属于必填项。");}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:
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 之前自动由管道执行:
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 管道下的执行表现:
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); }}架构小结与收益回顾
通过上述三步,你已经完成了一个具备工业级质量的垂直切片:
- 零样板代码:没有传统分层的
Controller、Service、Repository胶水包装,所有逻辑高度聚焦在用例自身; - 零反射装配:
[GenerateEndpoint]与[BitzTable]在编译期静态生成,无缝支持 .NET 10 Native AOT 裁切与极速冷启动; - 安全透明托管:日志跟踪、RBAC 授权、三阶校验、数据库事务与 CAP 领域事件出箱全部由 10 级应用管道隐式接管,业务代码纯净自洽。