vertical slice architecture 是一种靠项目结构和命名空间约束落地的组织约定,非语言特性或框架;需按功能建目录(如features/orders/createorder)、统一命名空间、禁止跨切片引用,依赖通过shared项目或接口隔离。

Vertical Slice Architecture 不是 C# 语言特性,也不是 .NET 内置功能,它是一种靠项目结构和命名空间约束来落地的组织约定。你不需要安装新 SDK 或升级到 .NET 9 才能用——只要项目能建文件夹、能设引用、能写 IRequest 和 IRequestHandler,今天就能开始。
怎么建第一个切片:从 Controllers 文件夹里搬出来
常见错误是先写好一个 OrdersController,再慢慢“拆”成切片。这几乎注定失败:控制器里混着 DTO、验证逻辑、仓储调用,边界早已模糊。
正确做法是从零新建一个功能目录:
在解决方案根目录下创建 Features/Orders/CreateOrder/ 文件夹
所有类型都放在 Features.Orders.CreateOrder 命名空间下
只允许该目录内互相引用;禁止 Features.Customers.GetCustomerQuery 出现在这个文件夹里的任何类中
接口定义(如 IOrderRepository)必须放在 Application 或 Shared 项目中,且仅被依赖,不反向引用实现
示例入口点只需三样东西:
public record CreateOrderCommand(string CustomerId, List<orderitemdto> Items) : IRequest<result>>;
public class CreateOrderEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder routes) =>
routes.MapPost("orders", async (CreateOrderCommand cmd, ISender sender) =>
await sender.Send(cmd));
}
public class CreateOrderHandler : IRequestHandler<createordercommand result>>
{
private readonly IOrderRepository _repo;
public CreateOrderHandler(IOrderRepository repo) => _repo = repo;
<pre class="brush:php;toolbar:false;">public async Task<Result<Guid>> Handle(CreateOrderCommand req, CancellationToken ct)
{
var order = new Order(req.CustomerId);
foreach (var item in req.Items)
order.AddItem(item.ProductId, item.Quantity);
await _repo.AddAsync(order, ct);
return Result.Success(order.Id);
}
}
MediatR 是最顺手的载体,但不是唯一选项
MediatR 能自然对齐「一个请求 → 一个处理者」的切片语义,但它只是工具。如果你不想引入额外依赖,完全可以用:
原生委托:Func<createordercommand cancellationtoken task>>></createordercommand>
自定义接口:IHandle<trequest tresponse></trequest> + 手动注册
甚至直接在 MapPost 里写业务逻辑(仅限极简原型,不推荐长期使用)
关键判断点在于:是否容易阻止跨切片调用?MediatR 的 ISender 是抽象的,天然不暴露 Handler 实现,比直接 new 一个 CreateOrderHandler 更利于隔离。
注意:不要把 MediatR 当作垂直切片的“开关”。禁用它的注册或换掉它,只要切片目录结构、命名空间、引用方向没变,架构就还在。
Shared 项目不是“放公共代码的地方”,而是“防污染的隔离墙”
很多人建一个 Shared 项目,然后往里塞 Result<t></t>、AppException、ValidationFailure,再顺手加个 DateTimeProvider —— 这已经越界了。
Shared 只应包含两类东西:
跨切片通信契约:如 Result<t></t>、IRequest<tresponse></tresponse>、IQuery<tresponse></tresponse>
基础抽象:如 IUnitOfWork、IEmailSender,但它们的实现(SqlUnitOfWork、SmtpEmailSender)必须在具体切片或 Infrastructure 中
一旦你在 Shared 里放了具体实现、DTO 映射逻辑、或某个切片专用的枚举,就等于给所有切片开了后门。后续没人敢动它,因为它“好像哪都在用”。
切片之间真的一点都不能通信?那关联查询怎么办
严格来说,切片之间不能直接引用彼此的 Handler、Query 或 Command 类型。但业务上肯定有依赖,比如「创建订单后发邮件通知客户」。
可行解法只有两种:
事件驱动:在 CreateOrderHandler 末尾发布 OrderCreatedEvent,由另一个切片(如 Notifications/SendOrderConfirmation/)订阅处理
通过共享接口协作:定义 ICustomerService.GetCustomerById(Guid id),实现放在 Customers 切片,但接口声明在 Shared;CreateOrderHandler 只依赖接口,不碰具体实现
别用静态方法、Service Locator 或直接 new 一个跨切片类——这些都会让边界瞬间失效,而且后期根本没法做单元测试隔离。
真正的难点不在技术实现,而在于每次新增一个跨功能调用时,你得停下来问一句:这个依赖是不是真的不可绕过?能不能用事件延迟耦合?有没有更小粒度的接口可以抽出来?这种纪律性,比写十个 IRequestHandler 都难。











