dotnet-core-expert 技能实战:基于 CQRS 的分层清洁架构(Clean Architecture)完整落地指南
dotnet-core-expert 技能实战基于 CQRS 的分层清洁架构Clean Architecture完整落地指南【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills在构建 .NET 8 应用时如何同时保证业务核心独立、读写路径清晰、横切关注点可控是架构设计的核心命题。本文以 dotnet-core-expert 技能的清洁架构参考文档为主线系统讲解 Domain、Application、Infrastructure、WebApi 四层结构的职责边界与依赖方向并通过 MediatR 实现 CQRS 命令/查询分离、FluentValidation 校验与管道行为Pipeline Behavior的完整落地。读完本文你将获得一套可复制的分层模板以及支撑这套模板的依赖注入、API 集成与仓库级最佳实践依据。为什么在 .NET 8 中需要清洁架构 CQRS清洁架构的核心思想是业务逻辑不依赖任何外部框架、数据库或 UI 技术。在 dotnet-core-expert 的定义中该技能面向 .NET 8、C# 12、Minimal API、Entity Framework Core 与 CQRS/MediatR 场景且明确要求遵循清洁架构原则Follow clean architecture principles并将其写入 MUST DO 约束。将 CQRS命令查询职责分离叠加在清洁架构之上带来两层收益读写分离命令Commands处理状态变更查询Queries只做读取互不干扰各自拥有独立的模型与校验路径依赖反转Application 层只依赖接口如IApplicationDbContext具体实现由 Infrastructure 注入业务用例可以在不触碰数据库的情况下被独立测试。从仓库的渐进式披露结构看该主题对应技能路由表中的 Clean Architecture 条目明确标注Load When: CQRS, MediatR, layers, DI patterns即当任务涉及 CQRS、MediatR、分层或依赖注入模式时Agent 会加载本参考文档作为深度指引。解决方案结构四层分离的目录骨架原文档给出的标准项目结构如下这是整个架构的蓝图Solution.sln ├── src/ │ ├── Domain/ # Core business logic │ │ ├── Entities/ │ │ ├── ValueObjects/ │ │ ├── Exceptions/ │ │ └── Interfaces/ │ ├── Application/ # Use cases, CQRS handlers │ │ ├── Common/ │ │ ├── Products/ │ │ │ ├── Commands/ │ │ │ └── Queries/ │ │ └── DependencyInjection.cs │ ├── Infrastructure/ # External concerns │ │ ├── Persistence/ │ │ ├── Identity/ │ │ └── DependencyInjection.cs │ └── WebApi/ # API layer │ ├── Endpoints/ │ ├── Filters/ │ └── Program.cs └── tests/各层职责与依赖方向可归纳为层职责依赖方向Domain实体、值对象、领域异常、领域接口承载业务逻辑与不变量不依赖任何层Application用例编排命令/查询 Handler、DTO、校验、管道行为依赖 DomainInfrastructure持久化EF Core、身份认证、外部服务依赖 Application 接口WebApi端点映射、过滤器、程序入口依赖 Applicationtests单元测试与集成测试可引用全部 src 项目依赖箭头一律指向内层WebApi → Application → DomainInfrastructure 实现 Application 声明的接口。这与 dotnet-core-expert 中 MUST NOT DO: Mix concerns across architectural layers禁止跨层混用关注点的约束直接对应。领域层用实体封装业务不变量领域层是架构的核心也是唯一存放业务规则的地方。原文档以Product实体演示了贫血模型 vs 富模型的差距// Domain/Entities/Product.cs namespace Domain.Entities; public class Product { public int Id { get; private set; } public string Name { get; private set; } string.Empty; public string Description { get; private set; } string.Empty; public decimal Price { get; private set; } public int CategoryId { get; private set; } public Category Category { get; private set; } null!; public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } private Product() { } // EF Core public static Product Create(string name, string description, decimal price, int categoryId) { if (string.IsNullOrWhiteSpace(name)) throw new DomainException(Product name is required); if (price 0) throw new DomainException(Product price must be greater than zero); return new Product { Name name, Description description, Price price, CategoryId categoryId, CreatedAt DateTime.UtcNow }; } public void Update(string name, string description, decimal price) { if (string.IsNullOrWhiteSpace(name)) throw new DomainException(Product name is required); if (price 0) throw new DomainException(Product price must be greater than zero); Name name; Description description; Price price; UpdatedAt DateTime.UtcNow; } }几个关键设计点值得深入属性私有 setterId、Price、CreatedAt均不允许外部直接修改状态变更必须经由Create/Update工厂方法与业务方法完成从而保证名称非空、价格大于零这类业务不变量无法被绕过私有无参构造函数private Product() { }这是专为 EF Core 实体物化materialization准备的——EF Core 在从数据库还原实体时会调用该构造器同时仍对业务代码封锁直接new Product()的能力Create静态工厂 参数校验创建与更新时重复校验相同的不变量避免新建合法、更新非法的漏洞领域异常业务规则违反时抛出自定义DomainException而非通用Exception便于上层在管道中统一识别与处理// Domain/Exceptions/DomainException.cs namespace Domain.Exceptions; public class DomainException : Exception { public DomainException(string message) : base(message) { } }从实现层面看这一设计也呼应了技能约束中的 Use record types for DTOs 与 Enable nullable reference types 等工程要求——实体使用 string.Empty/null!显式处理空值配合Nullableenable/Nullable编译开关可获得完整的空安全分析。应用层命令写路径的用例编排命令Command表达用户想要系统做什么是写路径的入口。原文档以创建商品为例给出了命令三件套Command 记录、Handler 与 Validator。命令定义不可变的请求记录// Application/Products/Commands/CreateProduct/CreateProductCommand.cs using MediatR; namespace Application.Products.Commands.CreateProduct; public record CreateProductCommand( string Name, string Description, decimal Price, int CategoryId ) : IRequestProductDto;record类型天然具备不可变性与值语义与 C# 12 面向数据的编程风格一致实现IRequestProductDto表明该命令的执行结果是ProductDto。命令处理器用例编排// Application/Products/Commands/CreateProduct/CreateProductCommandHandler.cs using Domain.Entities; using Domain.Interfaces; using MediatR; namespace Application.Products.Commands.CreateProduct; public class CreateProductCommandHandler : IRequestHandlerCreateProductCommand, ProductDto { private readonly IApplicationDbContext _context; public CreateProductCommandHandler(IApplicationDbContext context) { _context context; } public async TaskProductDto Handle( CreateProductCommand request, CancellationToken cancellationToken) { var product Product.Create( request.Name, request.Description, request.Price, request.CategoryId ); _context.Products.Add(product); await _context.SaveChangesAsync(cancellationToken); return new ProductDto( product.Id, product.Name, product.Description, product.Price, product.Category.Name ); } }注意 Handler 中没有if/else业务规则——名称与价格的合法性已由领域层的Product.Create保证。Handler 只负责实例化实体 → 落库 → 返回结果的编排这正是 Application 层用例编排Use cases and orchestration定位的体现。同时await SaveChangesAsync(cancellationToken)贯彻了技能 MUST DO 中所有 I/O 使用 async/await的要求。命令校验器管道的守门员// Application/Products/Commands/CreateProduct/CreateProductCommandValidator.cs using FluentValidation; namespace Application.Products.Commands.CreateProduct; public class CreateProductCommandValidator : AbstractValidatorCreateProductCommand { public CreateProductCommandValidator() { RuleFor(x x.Name) .NotEmpty() .MaximumLength(100); RuleFor(x x.Description) .MaximumLength(500); RuleFor(x x.Price) .GreaterThan(0) .LessThan(1000000); RuleFor(x x.CategoryId) .GreaterThan(0); } }FluentValidation 在这里处理的是接口层输入校验输入完整性、长度、数值范围而领域层处理的是业务不变量名称非空、价格为正。二者职责互补Validator 在管道前置阶段拦截非法请求避免无效数据进入领域层。这对应技能 MUST DO 中的 Skip input validation 为禁止项。应用层查询读路径的分页与投影查询Query只读不改允许针对读场景做更激进的优化。原文档的GetProductsQuery演示了搜索、分页与投影三个高频诉求// Application/Products/Queries/GetProducts/GetProductsQuery.cs using MediatR; namespace Application.Products.Queries.GetProducts; public record GetProductsQuery( int Page 1, int PageSize 10, string? SearchTerm null ) : IRequestPagedResultProductDto;// Application/Products/Queries/GetProducts/GetProductsQueryHandler.cs using Application.Common.Models; using Domain.Interfaces; using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Products.Queries.GetProducts; public class GetProductsQueryHandler : IRequestHandlerGetProductsQuery, PagedResultProductDto { private readonly IApplicationDbContext _context; public GetProductsQueryHandler(IApplicationDbContext context) { _context context; } public async TaskPagedResultProductDto Handle( GetProductsQuery request, CancellationToken cancellationToken) { var query _context.Products .Include(p p.Category) .AsQueryable(); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) { query query.Where(p p.Name.Contains(request.SearchTerm) || p.Description.Contains(request.SearchTerm)); } var totalCount await query.CountAsync(cancellationToken); var products await query .OrderBy(p p.Name) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .Select(p new ProductDto( p.Id, p.Name, p.Description, p.Price, p.Category.Name )) .ToListAsync(cancellationToken); return new PagedResultProductDto( products, totalCount, request.Page, request.PageSize ); } }值得注意的实践细节先CountAsync后ToListAsync分页需要总记录数计算TotalPages但仅Include导航属性、不做投影时对整表计数而真正取数时通过Select投影到 DTO避免把整个Category实体加载进内存延迟执行IncludeWhereOrderBy仅构造表达式树两次数据库往返count list都发生在异步方法内部全程不阻塞线程空搜索词兜底string.IsNullOrWhiteSpace过滤空白搜索词避免Contains()产生无意义的全表条件。这一查询模式与技能中 entity-framework.md 的 Query Patterns 一脉相承——该参考文档进一步给出了AsNoTracking()只读查询、AsSplitQuery()防笛卡尔爆炸、EF.CompileAsyncQuery编译查询等性能优化手段可视为读路径的进阶补充。DTO 与公共模型层间传输的定型载体DTO 是跨层传输的标准载体原文档强调不要将实体直接暴露给 API对应技能 MUST NOT DO 中的 Expose entities directly in API responses。// Application/Products/ProductDto.cs namespace Application.Products; public record ProductDto( int Id, string Name, string Description, decimal Price, string CategoryName );// Application/Common/Models/PagedResult.cs namespace Application.Common.Models; public record PagedResultT( ListT Items, int TotalCount, int Page, int PageSize ) { public int TotalPages (int)Math.Ceiling(TotalCount / (double)PageSize); public bool HasPreviousPage Page 1; public bool HasNextPage Page TotalPages; }PagedResultT是泛型分页容器四个只读计算属性TotalPages、HasPreviousPage、HasNextPage让 WebApi 层可以直接序列化返回无需再自行换算分页元数据。使用record类型承载 DTO 也正是技能 MUST DO 的明确要求Use record types for DTOs其带来的副产品是值相等比较与解构支持对测试断言尤为友好。应用层接口依赖反转的支点清洁架构的内聚靠的是接口抽象。原文档给出的IApplicationDbContext是 Application 层与持久化之间的契约// Application/Common/Interfaces/IApplicationDbContext.cs using Domain.Entities; using Microsoft.EntityFrameworkCore; namespace Application.Common.Interfaces; public interface IApplicationDbContext { DbSetProduct Products { get; } DbSetCategory Categories { get; } Taskint SaveChangesAsync(CancellationToken cancellationToken default); }这里的价值在于Handler 只面向IApplicationDbContext编程并不知道底层是 SQL Server、PostgreSQL 还是内存数据库。而真实实现ApplicationDbContext位于 Infrastructure 层——参考 entity-framework.md 的 DI 片段可见Infrastructure 通过AddScopedIApplicationDbContext(provider provider.GetRequiredServiceApplicationDbContext())将具体 DbContext 注册为接口实现从而完成依赖反转。这也为测试提供了关键便利集成测试可以通过WebApplicationFactoryProgram替换为内存提供程序。依赖注入装配按层组织扩展方法清洁架构的 DI 装配惯例是每个层提供自己的DependencyInjection静态类由 WebApi 统一调用。Application 层的装配如下// Application/DependencyInjection.cs using System.Reflection; using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; namespace Application; public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { services.AddMediatR(cfg cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IPipelineBehavior,), typeof(ValidationBehavior,)); services.AddTransient(typeof(IPipelineBehavior,), typeof(LoggingBehavior,)); return services; } }三个核心注册行为RegisterServicesFromAssembly扫描当前程序集自动注册所有IRequestHandler,无需为每个 Handler 手写注册AddValidatorsFromAssemblyFluentValidation 的批量扫描所有AbstractValidatorT自动进入容器供IValidatorT集合注入使用AddTransient(typeof(IPipelineBehavior,), ...)按泛型注册管道行为ValidationBehavior与LoggingBehavior会在每次请求处理时按注册顺序依次执行后文详述。对应的 Infrastructure 层装配可参考 entity-framework.md 中的AddInfrastructure扩展方法——它注册AddDbContextApplicationDbContext使用连接字符串DefaultConnection并指定迁移程序集再注册仓储与服务而 WebApi 的Program.cs只需依次调用builder.Services.AddApplication()与AddInfrastructure()即可完成整条依赖链的接通。MediatR 管道行为横切关注点的统一出口管道行为IPipelineBehavior,是 MediatR 最强大的扩展点它让校验、日志、事务、性能统计等横切关注点从业务 Handler 中剥离按声明顺序包在 Handler 外层执行。原文档给出校验与日志两个标准实现。ValidationBehavior自动拦截非法请求// Application/Common/Behaviors/ValidationBehavior.cs using FluentValidation; using MediatR; namespace Application.Common.Behaviors; public class ValidationBehaviorTRequest, TResponse : IPipelineBehaviorTRequest, TResponse where TRequest : IRequestTResponse { private readonly IEnumerableIValidatorTRequest _validators; public ValidationBehavior(IEnumerableIValidatorTRequest validators) { _validators validators; } public async TaskTResponse Handle( TRequest request, RequestHandlerDelegateTResponse next, CancellationToken cancellationToken) { if (!_validators.Any()) { return await next(); } var context new ValidationContextTRequest(request); var validationResults await Task.WhenAll( _validators.Select(v v.ValidateAsync(context, cancellationToken))); var failures validationResults .SelectMany(r r.Errors) .Where(f f ! null) .ToList(); if (failures.Count ! 0) { throw new ValidationException(failures); } return await next(); } }行为要点通过构造函数注入IEnumerableIValidatorTRequest所有适用于该请求类型的校验器会被自动收集即使未来新增校验器也无需改动此行为Where(f f ! null)过滤空错误Task.WhenAll并发执行全部校验器校验失败抛出ValidationException(failures)由全局异常处理统一转换为 400 响应_validators.Any()为空时直接放行保证未配置校验器的命令不受影响。LoggingBehavior自动记录请求轨迹// Application/Common/Behaviors/LoggingBehavior.cs using MediatR; using Microsoft.Extensions.Logging; namespace Application.Common.Behaviors; public class LoggingBehaviorTRequest, TResponse : IPipelineBehaviorTRequest, TResponse where TRequest : IRequestTResponse { private readonly ILoggerLoggingBehaviorTRequest, TResponse _logger; public LoggingBehavior(ILoggerLoggingBehaviorTRequest, TResponse logger) { _logger logger; } public async TaskTResponse Handle( TRequest request, RequestHandlerDelegateTResponse next, CancellationToken cancellationToken) { var requestName typeof(TRequest).Name; _logger.LogInformation(Handling {RequestName}, requestName); var response await next(); _logger.LogInformation(Handled {RequestName}, requestName); return response; } }该行为使用结构化日志模板{RequestName}命名参数记录请求名天然兼容 Serilog 等结构化日志系统——参考技能中 cloud-native.md 的 Structured Logging 一节可知结构化日志是生产环境聚合检索Seq、ELK的基础。执行顺序由于注册顺序为 Validation 在前、Logging 在后一次命令的执行链是ValidationBehavior → LoggingBehavior → Handler即先校验后记录。若需统计耗时只需在LoggingBehavior中包裹Stopwatch并记录ElapsedMilliseconds完全无需改动业务代码。API 集成Minimal API 与 ISender清洁架构的 WebApi 层保持极薄——端点只做三件事绑定参数、发消息、返回结果。原文档使用 Minimal API 的MapGroup组织商品端点// WebApi/Endpoints/ProductEndpoints.cs using Application.Products.Commands.CreateProduct; using Application.Products.Queries.GetProducts; using MediatR; namespace WebApi.Endpoints; public static class ProductEndpoints { public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder app) { var group app.MapGroup(/api/products) .WithTags(Products) .WithOpenApi(); group.MapGet(/, async ( [AsParameters] GetProductsQuery query, ISender sender) { var result await sender.Send(query); return Results.Ok(result); }); group.MapPost(/, async ( CreateProductCommand command, ISender sender) { var product await sender.Send(command); return Results.Created($/api/products/{product.Id}, product); }); return app; } }这里有两个值得展开的细节ISender与IMediator的选择ISender是 MediatR 专门用于Send命令/查询的最小接口只暴露发送语义相比之下IMediator还包含发布Publish事件等能力。在端点层只使用ISender更符合最小暴露原则[AsParameters]将GetProductsQuery的Page、PageSize、SearchTerm属性直接映射为查询字符串参数如/api/products?Page2PageSize20SearchTermapple无需手动BindAsyncHTTP 语义GET 用Results.Ok200POST 用Results.Created201并返回 Location 头/api/products/{id}。端点通过MapProductEndpoints(this IEndpointRouteBuilder app)扩展方法挂载在Program.cs中调用app.MapProductEndpoints()即可。技能中 minimal-apis.md 还补充了路由组鉴权.RequireAuthorization()、端点过滤器IEndpointFilter校验、ProducesT响应文档等进阶组合可与本模板无缝叠加。测试与验证从 build 到 test 的闭环技能 SKILL.md 的核心工作流将验证作为强制步骤dotnet build验证编译dotnet test验证测试全部通过再用curl或 REST 客户端验证端点。结合本模板# 1. 编译验证 dotnet build Solution.sln # 2. 单元/集成测试 dotnet test # 3. 端点验证启动 WebApi 后 curl http://localhost:5000/api/products?Page1PageSize10 curl -X POST http://localhost:5000/api/products \ -H Content-Type: application/json \ -d {name:Gaming Laptop,description:RTX 4090,price:19999,categoryId:1}测试层tests/建议优先采用集成测试路线技能 MUST DO 明确要求使用WebApplicationFactoryProgram编写集成测试配合IApplicationDbContext接口替换内存数据库即可在真实请求管线中验证端点 → 管道行为 → Handler → 领域实体 → 持久化的全链路行为而无需任何外部数据库依赖。快速参考表PatternPurposeIRequestTMediatR command/query interfaceIRequestHandlerTReq, TResHandler implementationIPipelineBehavior,Cross-cutting concernsIValidatorTFluentValidation interfaceISenderMediatR sender for endpointsDomain entitiesBusiness logic and invariantsApplication layerUse cases and orchestrationInfrastructureExternal dependencies与仓库其它参考文档的衔接本模板并非孤立存在在 dotnet-core-expert 技能内它与另外四份参考文档构成完整的能力矩阵minimal-apis.md端点模式、路由组、端点过滤器与错误处理负责 WebApi 层的进阶补充entity-framework.mdDbContext 配置、实体映射、查询优化与迁移负责 Infrastructure 持久化层authentication.mdJWT、密码哈希、授权策略可无缝挂载到本模板的 WebApi 与 Application 层cloud-native.mdDocker 多阶段构建、健康检查、Redis 缓存与 Kubernetes 部署将本模板推向生产环境。当你在实际项目中遇到 Implement CQRS、Refactor to clean architecture、Set up MediatR pipelines 这类任务时即可按技能的路由表加载本参考文档直接复用其中的分层骨架与代码模板开始落地。【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考