Skip to content
bitzorcas
EN

Concept

垂直切片架构

每个模块的内部结构——一个特性、一个文件夹、一个 PR;端点 + 命令 + 处理器 + 规则 + 测试放在一起,配合 Result\<T\> 和 Mediator 管道。

Last updated

在 BitzOrcas.Modern 的每个模块内部,垂直切片架构(VSA)决定了组织方式。一个特性——创建备忘录、获取问候、发送聊天消息——生活在一个特性文件夹中:端点、处理器和验证规则放在一起。没有分层往返。没有 Domain → Application → Infrastructure → API 的跳转。一个特性,一个文件夹,一个 PR。

切片结构

一个切片分布在模块的各个项目中。CreateNote Sandbox 特性看起来像这样:

BitzOrcas.Application/Sandbox/Notes/
├── CreateNoteCommand.cs ← ICommand<Note>(record)
├── CreateNoteCommandHandler.cs ← ICommandHandler<CreateNoteCommand, Note>
└── GetNoteByIdQuery.cs ← IQuery<Note?>(record)
BitzOrcas.Application/Sandbox/Notes/(处理器内联或分离)
├── CreateNoteCommandHandler.cs ← 处理器实现
└── GetNoteByIdQueryHandler.cs ← 处理器实现
BitzOrcas.Api/Endpoints/Sandbox/
├── NoteEndpoints.cs ← Minimal API 端点映射

完整切片 — CreateNote

学习 VSA 最干净的方式是端到端阅读一个切片。以下是 Sandbox 模块的 CreateNote

CreateNoteCommand.cs
public sealed record CreateNoteCommand(string Title, string Content) : ICommand<Result<Note>>;
CreateNoteCommandHandler.cs
public sealed class CreateNoteCommandHandler : ICommandHandler<CreateNoteCommand, Result<Note>>
{
private readonly ISandboxDataPort _dataPort;
public CreateNoteCommandHandler(ISandboxDataPort dataPort) => _dataPort = dataPort;
public async ValueTask<Result<Note>> Handle(
CreateNoteCommand command, CancellationToken cancellationToken)
{
var note = Note.Create(command.Title, command.Content);
await _dataPort.AddAsync(note, cancellationToken).ConfigureAwait(false);
return Result<Note>.Success(note);
}
}
NoteEndpoints.cs
public static class NoteEndpoints
{
internal static RouteHandlerBuilder MapNoteEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/v1/sandbox/notes");
group.MapPost("/", async (
CreateNoteCommand command,
IMediator mediator,
CancellationToken cancellationToken) =>
{
var result = await mediator.Send(command, cancellationToken);
return result.IsFailure
? TypedResults.BadRequest(result.Error)
: TypedResults.Created($"/api/v1/sandbox/notes/{result.Value.Id}", result.Value);
})
.WithName("CreateNote")
.WithSummary("Create a sandbox note");
// GET by ID, GET all, etc.
return group;
}
}

这就是整个特性。小文件。一个切片。一个 PR。

Result<T> — 业务失败绝不抛异常

BitzOrcas 强制使用 BitzOrcas.DomainResult<T> 模式。业务失败返回 Result.Failure(Error),永不抛异常:

// 成功路径
return Result<Note>.Success(note);
// 业务失败路径
return Result<Note>.Failure(new Error(
ErrorType.Validation,
"Note title cannot be empty"));
// 未找到路径
return Result<Note>.Failure(Error.NotFound("Note", noteId));

Error 类型使用 {模块}.{场景}.{原因} 编码并携带结构化详情。端点将 Result<T> 转换为适当的 HTTP 响应。

验证 — IRequestRule

BitzOrcas 使用 IRequestRule 进行验证(非 FluentValidation)。规则附加到命令上:

CreateGreetingCommandRule.cs
public sealed class CreateGreetingCommandRule : IRequestRule<CreateGreetingCommand>
{
public ValueTask<Error?> ValidateAsync(
CreateGreetingCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Message))
return ValueTask.FromResult<Error?>(
new Error(ErrorType.Validation, "Message is required."));
if (request.Message.Length > 500)
return ValueTask.FromResult<Error?>(
new Error(ErrorType.Validation, "Message must not exceed 500 characters."));
return ValueTask.FromResult<Error?>((Error?)null);
}
}

ValidationPipelineBehavior 在处理器之前运行所有已注册的 IRequestRule<T> 实现。规则与处理器一起在 DI 中注册。

Mediator 管道

每个请求在到达处理器之前经过管道:

LoggingPipelineBehavior → 计时 + 异常兜底
→ AuthorizationPipelineBehavior → 默认拒绝授权
→ ValidationPipelineBehavior → IRequestRule 验证
→ IdempotencyPipelineBehavior → IIdempotentRequest 去重
→ TransactionPipelineBehavior → UoW 提交/回滚(仅命令)
→ ActivityAuditPipelineBehavior → 活动审计
→ DomainEventDispatchPipelineBehavior → 分发领域事件
→ [处理器]

查看 Mediator 管道图了解可视化流程。

约定

  • 处理器使用 public sealed Mediator 的源代码生成器需要具体类型。
  • 处理器返回 ValueTask<T> 对同步快速路径分配友好。
  • 每个 await 使用 .ConfigureAwait(false) 这是库代码;永远不捕获同步上下文。
  • 命令和查询是 record 首选不可变性和相等性。
  • 端点是 IEndpointRouteBuilder 上的静态扩展方法。 无控制器。只有 Minimal APIs。
  • 每个切片一个端点。 一个切片是一个特性;一个特性有一个 HTTP 入口。
  • 规则与处理器放在一起。 在 DI 中自动注册。
  • 权限定义在 Contracts 中。 端点通过 .RequirePermission(...) 引用它们。

CQRS 无仪式感

VSA 与 CQRS 自然配合。区分在于两个标记接口:

public sealed record CreateNoteCommand(string Title, string Content) : ICommand<Result<Note>>;
public sealed record GetNoteByIdQuery(Guid Id) : IQuery<Result<Note>>;

TransactionPipelineBehavior 仅对 ICommand 消息运行(不对 IQuery),因此查询处理器跳过 UoW 提交。IdempotencyPipelineBehavior 仅处理实现 IIdempotentRequest 的消息。

VSA 不做的事情

  • 它不替代你的领域模型。 聚合仍然执行不变量。VSA 是关于如何组织特性。
  • 它不强制数据库隔离。 每个模块的适配器共享同一数据库,通过 TenantEntityBase 实现租户隔离。
  • 它不使架构测试变为可选。 ArchUnitNET 强制命名空间边界——切片分散逻辑,架构测试防止漂移。

相关