基于NetCorePal Cloud Framework的DDD架构管理系统实践
前段时间在做一个管理系统的项目,想尝试一下DDD架构在实际项目中的应用。经过一番调研,最终选择了NetCorePal Cloud Framework作为基础框架,结合.NET 10和Vue 3搭建了一套完整的前后端分离架构。今天就想和大家分享一下这个项目的架构设计和技术选型,希望能给正在做类似项目的朋友一些参考。
项目源码地址:https://github.com/zhouda1fu/Ncp.Admin
项目概述
这个项目是一个典型的企业级管理系统,包含了用户、角色、部门等基础功能模块。在技术选型上,采用了目前比较主流的技术栈:
后端方面,使用.NET 10作为主要框架,配合EF Core做数据访问,FastEndpoints替代传统的Controller,MediatR实现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(领域事件处理器)
- └── Endpoints(API端点)
复制代码 这种分层的好处是职责清晰,Domain层只关注业务逻辑,Infrastructure层负责技术实现,Web层处理HTTP请求和响应。
核心设计模式
1. 领域驱动设计(DDD)
在这个项目中,DDD主要体现在聚合根的设计上。每个聚合根都有自己的业务边界,状态只能通过业务方法来修改。就拿部门这个聚合根来说吧:- /// <summary>
- /// 部门ID(强类型ID)
- /// </summary>
- public partial record DeptId : IInt64StronglyTypedId;
- /// <summary>
- /// 部门聚合根
- /// </summary>
- public class Dept : Entity<DeptId>, 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)
- : ICommand<DeptId>;
- /// <summary>
- /// 命令验证器
- /// </summary>
- public class CreateDeptCommandValidator : AbstractValidator<CreateDeptCommand>
- {
- 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)
- : ICommandHandler<CreateDeptCommand, DeptId>
- {
- public async Task<DeptId> 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 DbSet<Dept> DeptSet { get; } = applicationDbContext.Depts;
-
- /// <summary>
- /// 获取部门树(使用投影优化性能)
- /// </summary>
- public async Task<IEnumerable<DeptTreeDto>> 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)
- : IDomainEventHandler<DeptInfoChangedDomainEvent>
- {
- 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)
- : Endpoint<CreateDeptRequest, ResponseData<CreateDeptResponse>>
- {
- 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 : IRepository<Dept, DeptId> { }
- /// <summary>
- /// 部门仓储实现
- /// </summary>
- public class DeptRepository(ApplicationDbContext context)
- : RepositoryBase<Dept, DeptId, ApplicationDbContext>(context),
- IDeptRepository { }
复制代码 框架会自动管理事务和SaveChanges,命令处理器里不需要手动调用,这样代码更简洁,也不容易出错。
3. 验证机制
验证用的是FluentValidation,支持同步和异步验证。比如创建部门的时候,需要检查部门名称是否已存在,就可以用异步验证:- public class CreateDeptCommandValidator : AbstractValidator<CreateDeptCommand>
- {
- 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) : AuthenticatedTestBase<WebAppFixture>(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,
- ResponseData<CreateDeptResponse>>(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 run
复制代码 Aspire会自动启动和管理数据库容器(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让基础设施管理变得简单。
如果你也在做类似的管理系统,或者想了解DDD在实际项目中的应用,可以看看这个项目的代码,应该能有一些参考价值。项目地址在https://github.com/zhouda1fu/Ncp.Admin,欢迎交流讨论。
参考资料
最后附上一些相关的参考资料,有兴趣的朋友可以深入了解一下:
- NetCorePal Cloud Framework - 项目使用的基础框架
- FastEndpoints - 轻量级API框架
- Vben Admin - 前端管理后台模板
- .NET Aspire - 云原生应用开发平台
项目源码地址:https://github.com/zhouda1fu/Ncp.Admin
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |