项目概述这个项目是一个典型的企业级管理系统包含了用户、角色、部门等基础功能模块。在技术选型上采用了目前比较主流的技术栈后端方面使用.NET 10作为主要框架配合EF Core做数据访问FastEndpoints替代传统的ControllerMediatR实现CQRS模式。数据存储支持MySQL、PostgreSQL和SQL Server消息队列选择了RabbitMQ通过CAP框架集成缓存用Redis还集成了.NET Aspire来做云原生的基础设施管理。前端部分基于Vben Admin这是一个非常优秀的Vue 3 TypeScript Vite的管理后台模板UI组件用的是Ant Design Vue整体体验不错。架构设计分层架构整个项目采用了经典的三层架构这个结构应该很多做DDD的朋友都比较熟悉。三层之间的依赖关系是单向的Web层依赖Infrastructure层Infrastructure层依赖Domain层Domain层作为核心不依赖任何其他层。Ncp.Admin ├── Domain领域层 │ ├── AggregatesModel聚合模型 │ └── DomainEvents领域事件 ├── Infrastructure基础设施层 │ ├── EntityConfigurations实体配置 │ └── Repositories仓储实现 └── Web表现层 ├── Application应用服务层 │ ├── Commands命令 │ ├── Queries查询 │ └── DomainEventHandlers领域事件处理器 └── EndpointsAPI端点这种分层的好处是职责清晰Domain层只关注业务逻辑Infrastructure层负责技术实现Web层处理HTTP请求和响应。核心设计模式1. 领域驱动设计DDD在这个项目中DDD主要体现在聚合根的设计上。每个聚合根都有自己的业务边界状态只能通过业务方法来修改。就拿部门这个聚合根来说吧/// summary /// 部门ID强类型ID /// /summary public partial record DeptId : IInt64StronglyTypedId; /// summary /// 部门聚合根 /// /summary public class Dept : EntityDeptId, IAggregateRoot { public string Name { get; private set; } string.Empty; public string Remark { get; private set; } string.Empty; public DeptId ParentId { get; private set; } default!; public int Status { get; private set; } 1; protected Dept() { } // 业务方法更新部门信息 public void UpdateInfo(string name, string remark, DeptId parentId, int status) { Name name; Remark remark; ParentId parentId; Status status; UpdateTime new UpdateTime(DateTimeOffset.UtcNow); // 发布领域事件 AddDomainEvent(new DeptInfoChangedDomainEvent(this)); } // 软删除 public void SoftDelete() { if (IsDeleted) { throw new KnownException(部门已经被删除); } IsDeleted true; UpdateTime new UpdateTime(DateTimeOffset.UtcNow); } }这里有几个设计点我觉得值得说一下。首先是强类型ID比如DeptId这样可以避免把部门ID和用户ID搞混编译器就能帮你检查出来。其次是属性都用private set外面不能直接修改必须通过业务方法这样就保证了业务规则的一致性。另外当部门信息变更时会发布领域事件这样可以通知其他需要同步更新的地方比如用户表中的部门名称。2. CQRS模式命令查询职责分离CQRS在这个项目中主要体现在读写分离上。写操作通过命令Command来处理读操作通过查询Query来处理。这样做的好处是职责清晰而且可以针对不同的场景做优化。写操作这边命令的定义很简单就是一个record。每个命令都有对应的验证器和处理器。看一个创建部门的例子/// summary /// 创建部门命令 /// /summary public record CreateDeptCommand(string Name, string Remark, DeptId? ParentId, int Status) : ICommandDeptId; /// summary /// 命令验证器 /// /summary public class CreateDeptCommandValidator : AbstractValidatorCreateDeptCommand { public CreateDeptCommandValidator(DeptQuery deptQuery) { RuleFor(d d.Name).NotEmpty().WithMessage(部门名称不能为空); RuleFor(d d.Name) .MustAsync(async (n, ct) !await deptQuery.DoesDeptExist(n, ct)) .WithMessage(d $该部门已存在Name{d.Name}); RuleFor(d d.Status).InclusiveBetween(0, 1).WithMessage(状态值必须为0或1); } } /// summary /// 命令处理器 /// /summary public class CreateDeptCommandHandler(IDeptRepository deptRepository) : ICommandHandlerCreateDeptCommand, DeptId { public async TaskDeptId Handle(CreateDeptCommand request, CancellationToken cancellationToken) { var parentId request.ParentId ?? new DeptId(0); var dept new Dept(request.Name, request.Remark, parentId, request.Status); await deptRepository.AddAsync(dept, cancellationToken); // 注意不需要手动调用SaveChanges框架会自动处理 return dept.Id; } }验证器这里用了FluentValidation支持同步和异步验证。比如检查部门名称是否已存在这种需要查数据库的验证就可以用异步的MustAsync。读操作这边直接使用DbContext而且可以用投影来优化性能。比如获取部门树的时候只选择需要的字段/// summary /// 部门查询服务 /// /summary public class DeptQuery(ApplicationDbContext applicationDbContext) : IQuery { private DbSetDept DeptSet { get; } applicationDbContext.Depts; /// summary /// 获取部门树使用投影优化性能 /// /summary public async TaskIEnumerableDeptTreeDto GetDeptTreeAsync( bool includeInactive false, CancellationToken cancellationToken default) { // 使用投影只选择需要的字段减少内存占用 var allDepts await DeptSet.AsNoTracking() .WhereIf(!includeInactive, d d.Status ! 0) .Select(d new DeptTreeNode { Id d.Id, Name d.Name, Remark d.Remark, ParentId d.ParentId, Status d.Status, CreatedAt d.CreatedAt }) .ToListAsync(cancellationToken); // 在内存中构建树形结构 return BuildTreeStructure(allDepts); } }这样读写分离的好处是查询这边可以针对不同的查询场景做优化比如用投影减少内存占用或者将来可以加缓存、用读库等而不会影响写操作的逻辑。3. 事件驱动架构事件驱动这块项目实现了领域事件和集成事件两种机制。领域事件主要用于聚合内部的同步操作集成事件用于跨服务通信。比如说当部门信息变更的时候需要同步更新用户表中的部门名称。这个过程就可以通过领域事件来实现/// summary /// 部门信息变更领域事件 /// /summary public record DeptInfoChangedDomainEvent(Dept Dept) : IDomainEvent;然后在事件处理器中处理这个逻辑/// summary /// 部门信息变更领域事件处理器 - 用于更新用户部门名称 /// /summary public class DeptInfoChangedDomainEventHandlerForUpdateUserDeptName( IMediator mediator, UserQuery userQuery) : IDomainEventHandlerDeptInfoChangedDomainEvent { public async Task Handle(DeptInfoChangedDomainEvent domainEvent, CancellationToken cancellationToken) { var dept domainEvent.Dept; var deptId dept.Id; var newDeptName dept.Name; // 查询所有属于该部门的用户ID var userIds await userQuery.GetUserIdsByDeptIdAsync(deptId, cancellationToken); // 通过Command更新每个用户的部门名称而不是直接操作数据库 foreach (var userId in userIds) { var command new UpdateUserDeptNameCommand(userId, newDeptName); await mediator.Send(command, cancellationToken); } } }这样设计的好处是部门聚合和用户聚合之间没有直接依赖通过事件来通信。如果将来需要增加新的业务逻辑比如部门变更时要发送通知只需要再加一个事件处理器就行了不需要改现有的代码。4. FastEndpoints轻量级API框架在API设计这块项目选择了FastEndpoints而不是传统的Controller。主要是觉得FastEndpoints的代码更简洁性能也更好。一个端点就是一个类职责清晰。看一个创建部门的例子/// summary /// 创建部门的API端点 /// /summary [Tags(Depts)] public class CreateDeptEndpoint(IMediator mediator) : EndpointCreateDeptRequest, ResponseDataCreateDeptResponse { public override void Configure() { Post(/api/admin/dept); AuthSchemes(JwtBearerDefaults.AuthenticationScheme); Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptCreate); } public override async Task HandleAsync(CreateDeptRequest req, CancellationToken ct) { var cmd new CreateDeptCommand(req.Name, req.Remark, req.ParentId, req.Status); var deptId await mediator.Send(cmd, ct); var response new CreateDeptResponse(deptId, req.Name, req.Remark); await Send.OkAsync(response.AsResponseData(), cancellation: ct); } }代码很简洁一个类就把路由、认证、权限都配置好了。请求和响应都是强类型的类型安全有保障。而且测试起来也很方便不需要启动HTTP服务器直接测端点就行了。几个核心特性1. 强类型ID这个项目里所有聚合根都用强类型ID而不是直接用long或int。比如部门ID是DeptId用户ID是UserId。这样做的好处是编译器能帮你检查类型错误不会把部门ID和用户ID搞混。使用起来也很简单// 定义强类型ID public partial record DeptId : IInt64StronglyTypedId; // 使用强类型ID var deptId new DeptId(123); var parentId request.ParentId ?? new DeptId(0);框架会自动处理序列化和类型转换用起来很顺手。2. 仓储模式仓储这块写操作通过仓储来处理查询操作直接使用DbContext。仓储的实现很简单/// summary /// 部门仓储接口 /// /summary public interface IDeptRepository : IRepositoryDept, DeptId { } /// summary /// 部门仓储实现 /// /summary public class DeptRepository(ApplicationDbContext context) : RepositoryBaseDept, DeptId, ApplicationDbContext(context), IDeptRepository { }框架会自动管理事务和SaveChanges命令处理器里不需要手动调用这样代码更简洁也不容易出错。3. 验证机制验证用的是FluentValidation支持同步和异步验证。比如创建部门的时候需要检查部门名称是否已存在就可以用异步验证public class CreateDeptCommandValidator : AbstractValidatorCreateDeptCommand { public CreateDeptCommandValidator(DeptQuery deptQuery) { RuleFor(d d.Name).NotEmpty().WithMessage(部门名称不能为空); // 异步验证检查部门名称是否已存在 RuleFor(d d.Name) .MustAsync(async (n, ct) !await deptQuery.DoesDeptExist(n, ct)) .WithMessage(d $该部门已存在Name{d.Name}); } }4. 异常处理业务异常用KnownException来处理框架会自动转换成合适的HTTP状态码。比如在聚合根里// 在聚合根中 public void SoftDelete() { if (IsDeleted) { throw new KnownException(部门已经被删除); } // ... } // 在命令处理器中 var dept await deptRepository.GetAsync(request.DeptId, cancellationToken) ?? throw new KnownException($未找到部门DeptId {request.DeptId});这样前端收到的错误信息就很清晰不需要再做额外的转换。测试策略测试这块项目用的是xUnit集成测试用了Aspire来自动管理测试环境。这样做的好处是不用手动搭建测试数据库、Redis这些基础设施Aspire会自动启动和管理。看一个部门创建接口的测试例子[Collection(WebAppTestCollection.Name)] public class DeptTests(WebAppFixture app) : AuthenticatedTestBaseWebAppFixture(app) { [Fact] public async Task CreateDept_WithValidData_ShouldSucceed() { // Arrange var client await GetAuthenticatedClientAsync(); var deptName $测试部门_{Guid.NewGuid():N}; try { // Act var request new CreateDeptRequest(deptName, 测试备注, null, 1); var (response, result) await client.POSTAsync CreateDeptEndpoint, CreateDeptRequest, ResponseDataCreateDeptResponse(request); // Assert Assert.True(response.IsSuccessStatusCode); Assert.NotNull(result?.Data); Assert.Equal(deptName, result.Data.Name); } finally { await CleanupTestDataAsync(); } } }这种测试方式很接近真实的场景测试的是完整的HTTP请求流程而且会自动清理测试数据保证测试之间的独立性。另外还支持身份认证测试可以模拟登录用户的各种操作。前端架构前端用的是Vben Admin这个模板这是一个基于Vue 3的管理后台框架。技术栈也比较主流Vue 3 Composition API、TypeScript、Vite、Ant Design Vue状态管理用Pinia路由用Vue Router。Vben Admin这个框架做得很完善开箱即用的功能很多。比如权限控制支持路由权限和按钮权限用起来很方便。还有国际化支持可以多语言切换。主题和布局也可以定制基本的管理后台需求都能满足。最重要的是类型安全前后端都用了TypeScript接口定义好之后类型检查能帮你发现很多问题。开发规范为了让代码质量更统一项目里制定了一些开发规范。比如文件的组织方式聚合根放在Domain/AggregatesModel/{AggregateName}Aggregate/领域事件放在Domain/DomainEvents/仓储放在Infrastructure/Repositories/命令放在Web/Application/Commands/{Module}Commands/查询放在Web/Application/Queries/端点放在Web/Endpoints/{Module}Endpoints/还有一些强制性的要求比如所有聚合根都用强类型ID而且不手动赋值ID依赖EF的值生成器。所有命令都要有对应的验证器。领域事件要在聚合发生改变时发布。命令处理器不能调用SaveChanges框架会自动处理。仓储必须用异步方法。业务异常用KnownException处理。另外项目还提供了很多代码片段可以快速生成常用代码。比如ncpcmd可以生成命令及其验证器和处理器ncpar可以生成聚合根ncprepo可以生成仓储接口和实现epp可以生成FastEndpoint的完整实现。这样开发效率会高不少。云原生支持项目集成了.NET Aspire这个功能真的很方便。启动开发环境只需要运行AppHost项目Aspire会自动管理所有依赖服务不需要手动启动数据库、Redis、RabbitMQ这些。# 仅需确保Docker环境运行 docker version # 直接运行AppHost项目Aspire会自动管理所有依赖服务 cd src/Ncp.Admin.AppHost dotnet runAspire会自动启动和管理数据库容器MySQL、PostgreSQL等、消息队列容器RabbitMQ等、Redis容器还会提供统一的Aspire Dashboard界面可以查看所有服务的状态。服务之间的连接字符串也会自动配置省了很多麻烦。代码分析可视化框架还提供了代码流分析和可视化功能这个对理解架构很有帮助。可以通过命令行工具生成HTML文件# 安装全局工具 dotnet tool install -g NetCorePal.Extensions.CodeAnalysis.Tools # 生成可视化文件 cd src/Ncp.Admin.Web netcorepal-codeanalysis generate --output architecture.html支持生成架构流程图、命令链路图、事件流程图、类图等可以直观地看到代码之间的关系和数据流向。总结这个项目算是一个DDD架构的实践案例展示了如何在.NET 10生态中应用DDD、CQRS、事件驱动这些架构思想。整体架构清晰职责分明代码组织得也比较规范。技术栈上后端用.NET 10 EF Core FastEndpoints MediatR前端用Vue 3 TypeScript Vite都是目前比较主流的技术。开发体验上有代码片段、自动化工具还有完善的开发规范开发效率还可以。可维护性这块代码分层清晰测试支持也比较完善还有代码可视化工具方便新人理解架构。云原生支持也很到位Aspire让基础设施管理变得简单。