这篇文章上次修改于 182 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

这是一个非常好的问题!这也是新手最容易迷糊的地方。

传统的 ASP.NET Core 开发习惯使用 分层架构 (Layered Architecture),即:ControllersServicesRepositories

但是,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:

    • 存放 CreateUserRequestCreateUserResponse
    • 不用单独建个 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);
    }
}

总结

这种架构的好处是:

  1. 高内聚:修改“创建用户”的功能时,你只需要打开 Features/Users/CreateUser 这个文件夹,所有相关代码都在这,不用在 Controller、Service、Mapper 文件夹之间跳来跳去。
  2. 清晰的依赖:Command 属于该功能内部逻辑,Event 属于跨模块通信。
  3. 适合团队:你要做“订单功能”,我就专心写 Features/Orders 里的代码,不会跟写“用户功能”的同事冲突。

希望这套架构树能帮你理清思路!作为新手,先严格按照这个文件夹结构去写几个接口,写着写着你就会发现“垂直切片”真香了。