这篇文章上次修改于 182 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
这是一个非常好的问题!这也是新手最容易迷糊的地方。
传统的 ASP.NET Core 开发习惯使用 分层架构 (Layered Architecture),即:Controllers、Services、Repositories。
但是,FastEndpoints 提倡的是 垂直切片架构 (Vertical Slice Architecture)。
它的核心理念是:把实现同一个功能的所有代码(API定义、业务逻辑、数据库操作、事件)放在一起,而不是按技术类型分开。
对于你的技术栈 (FastEndpoints + SqlSugar + Event/Command Bus),我推荐以下这种组织方式。这既保持了整洁,又发挥了垂直切片的优势。
推荐的目录结构树
假设你的项目叫 MyShop.Api。
MyProject.Api/
│
├── Program.cs // 程序入口,配置 FastEndpoints 和 SqlSugar
├── appsettings.json
│
├── 📁 Domain/ // 【领域层】存放数据库实体 (SqlSugar Entity)
│ ├── User.cs
│ └── Order.cs
│
├── 📁 Infrastructure/ // 【基础设施层】存放数据库配置、第三方服务
│ ├── 📁 Data/
│ │ └── DbContext.cs // SqlSugar 的配置和注入逻辑
│ └── 📁 Services/
│ └── EmailService.cs // 发邮件的具体实现
│
├── 📁 Features/ // 【核心功能层】按业务模块划分 (垂直切片)
│ │
│ ├── 📁 Users/ // === 用户模块 ===
│ │ │
│ │ ├── 📁 CreateUser/ // 1. 创建用户 (一个完整的切片)
│ │ │ ├── Endpoint.cs // API 定义 (接收 Request, 返回 Response)
│ │ │ ├── Models.cs // Request 和 Response DTO
│ │ │ ├── Mapper.cs // (可选) DTO 转 Entity 的映射
│ │ │ └── Command.cs // (Command Bus) 具体的创建逻辑 + Handler
│ │ │
│ │ ├── 📁 Events/ // 2. 用户模块产生的事件
│ │ │ └── UserCreated.cs // UserCreatedEvent (定义事件)
│ │ │
│ │ └── 📁 Login/ // 3. 用户登录 (另一个切片)
│ │ ├── Endpoint.cs
│ │ └── ...
│ │
│ └── 📁 Orders/ // === 订单模块 ===
│ │
│ ├── 📁 CreateOrder/
│ │ ├── Endpoint.cs
│ │ └── ...
│ │
│ └── 📁 EventHandlers/ // 订单模块需要监听别人的事件
│ └── UserCreatedHandler.cs // 监听用户创建事件 (比如送优惠券)
│
└── 📁 Shared/ // 【共享层】通用的东西
├── Enums/
└── Utils/详细解构:为什么要这样分?
1. Domain/ (数据库实体)
这里只放 SqlSugar 的实体类。因为实体可能会被多个 Feature 共享(比如用户模块要读 User 表,订单模块也要读 User 表),所以把它们提出来放在根目录下。
// Domain/User.cs
[SugarTable("Users")]
public class User { ... }2. Infrastructure/ (基础设施)
这里放那些“脏活累活”,比如 SqlSugar 的初始化代码。这样你的 Feature 代码就很干净,不需要关心数据库是怎么连接的。
3. Features/ (核心 - 垂直切片)
这是 FastEndpoints 的精髓。按业务功能(Feature)分文件夹,而不是按 Controller/Service 分。
以 Features/Users/CreateUser/ 为例,这个文件夹里包含了实现“创建用户”所需的一切:
Endpoint.cs:- 它是“前台”。它不知道数据库,也不知道复杂的逻辑。
- 它负责:验证参数 ->
command.ExecuteAsync()->event.PublishAsync()-> 返回结果。
Models.cs:- 存放
CreateUserRequest和CreateUserResponse。 - 不用单独建个 DTO 文件夹,因为这些 DTO 只给这个 Endpoint 用,放一起最好找。
- 存放
Command.cs:- 它是“逻辑核心”。里面包含
CreateUserCommand(参数) 和CreateUserHandler(执行者)。 - 在这里面写 SqlSugar 的
_db.Insertable(...)代码。 - 新手提示:如果逻辑很简单,其实可以直接写在 Endpoint 里。但既然你要用 Command Bus,就放在这里。
- 它是“逻辑核心”。里面包含
4. 关于 Events 的位置
事件(Event)稍微特殊一点,因为它具有“广播”性质。
定义事件 (
UserCreated.cs):- 建议放在产生事件的模块下,例如
Features/Users/Events/。 - 因为这代表“用户模块发生了这件事”。
- 建议放在产生事件的模块下,例如
处理事件 (
UserCreatedHandler.cs):- 放在消费该事件的模块里。
- 比如,“用户注册后要送一张优惠券”。这个逻辑属于“订单/营销模块”,所以 Handler 应该放在
Features/Orders/EventHandlers/里。 - 这样通过目录结构就能看出来:Order 模块 依赖于 User 模块的事件。
代码示例:如何把它们串起来
1. 基础设施 (Infrastructure/Data/DbSetup.cs)
namespace MyShop.Api.Infrastructure.Data;
public static class DbSetup
{
public static void AddSqlSugar(this IServiceCollection services)
{
// 你的 SqlSugar 初始化代码
// ...
}
}2. 垂直切片 (Features/Users/CreateUser/...)
Command.cs (逻辑)
namespace MyShop.Api.Features.Users.CreateUser; // 注意命名空间
// Command 定义
public class CreateUserCommand : ICommand<int>
{
public string Name { get; set; }
}
// Handler 实现 (这里写 SqlSugar)
public class CreateUserHandler : ICommandHandler<CreateUserCommand, int>
{
private readonly ISqlSugarClient _db;
public CreateUserHandler(ISqlSugarClient db) => _db = db;
public async Task<int> ExecuteAsync(CreateUserCommand cmd, CancellationToken ct)
{
var entity = new Domain.User { Name = cmd.Name }; // 使用 Domain 中的实体
return await _db.Insertable(entity).ExecuteReturnIdentityAsync();
}
}Endpoint.cs (接口)
namespace MyShop.Api.Features.Users.CreateUser;
public class Endpoint : Endpoint<CreateUserRequest, int>
{
public override void Configure()
{
Post("/api/users");
AllowAnonymous();
}
public override async Task HandleAsync(CreateUserRequest req, CancellationToken ct)
{
// 1. 调用同文件夹下的 Command
var userId = await new CreateUserCommand { Name = req.Name }.ExecuteAsync(ct);
// 2. 发布事件 (定义在 Features/Users/Events 下)
await new Features.Users.Events.UserCreatedEvent { UserId = userId }.PublishAsync();
await SendAsync(userId);
}
}总结
这种架构的好处是:
- 高内聚:修改“创建用户”的功能时,你只需要打开
Features/Users/CreateUser这个文件夹,所有相关代码都在这,不用在 Controller、Service、Mapper 文件夹之间跳来跳去。 - 清晰的依赖:Command 属于该功能内部逻辑,Event 属于跨模块通信。
- 适合团队:你要做“订单功能”,我就专心写
Features/Orders里的代码,不会跟写“用户功能”的同事冲突。
希望这套架构树能帮你理清思路!作为新手,先严格按照这个文件夹结构去写几个接口,写着写着你就会发现“垂直切片”真香了。
没有评论