news 2026/5/6 18:03:28

.NET整洁架构实战:领域驱动设计与CQRS模式落地指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET整洁架构实战:领域驱动设计与CQRS模式落地指南

1. 项目概述:一个面向实战的.NET整洁架构技能库

最近在.NET社区里,一个名为“dotnet-clean-architecture-skills”的项目引起了我的注意。这个由RonnyTheDev维护的仓库,名字起得相当直白,直指当下企业级应用开发中的一个核心痛点:如何在一个真实的、复杂的业务系统中,系统性地应用整洁架构(Clean Architecture)原则,而不仅仅是停留在“洋葱模型”的图示层面。

我见过太多团队,在引入整洁架构时,初期热情高涨,画出了漂亮的依赖关系图,定义了清晰的层(Domain, Application, Infrastructure, Presentation)。但一到具体编码,问题就来了:实体(Entity)和值对象(Value Object)到底怎么区分?领域服务(Domain Service)和应用服务(Application Service)的职责边界在哪里?仓储(Repository)的实现如何避免基础设施细节污染领域层?跨聚合的事务和一致性怎么处理?这些问题往往让项目在中期陷入混乱,最终要么架构形同虚设,要么因为过度设计而拖慢进度。

RonnyTheDev的这个项目,在我看来,就是一个针对这些“落地难题”的答案库。它不是一个简单的“Hello World”示例,而是一个旨在展示如何在接近真实业务复杂度的场景下,运用一系列架构技能(Skills)来构建可维护、可测试、可演进的系统。它聚焦于“技能”二字,意味着这里提供的不是一成不变的框架,而是可组合、可借鉴的模式与实践。对于任何一位正在或即将在.NET生态中实践领域驱动设计(DDD)和整洁架构的中高级开发者、架构师来说,这个项目都值得深入研读,它能帮你避开很多我亲自踩过的坑。

2. 架构核心思想与项目设计解析

2.1 超越分层:依赖倒置与领域核心

很多初学者容易把整洁架构简单地理解为“分四层”。这个项目的价值在于,它清晰地展示了分层背后的核心驱动力:依赖倒置原则(Dependency Inversion Principle, DIP)。所有依赖关系都指向内层,外层(如Web API、数据库)依赖于内层定义的抽象(接口)。

在这个项目中,你会看到Domain层是绝对的核心,它不引用任何其他项目。这里定义了业务实体、聚合根、领域事件、领域服务接口以及仓储接口。例如,一个Order聚合根,它包含OrderItems集合,并封装了AddItemConfirm等保证自身一致性的方法。它的存在不关心数据是存在SQL Server还是Redis里,也不关心是通过REST API还是gRPC被调用。

Application层则包含了应用服务、DTOs、以及对外部服务的抽象(如IEmailService)。应用服务协调领域对象和基础设施来完成一个具体的用例(如“下订单”)。它依赖于Domain层定义的接口,但不依赖于具体的实现。这种设计使得业务逻辑的测试可以完全脱离数据库和网络。

2.2 技能集(Skills)的模块化体现

项目命名为“skills”非常贴切。它不是一个大一统的框架,而是通过模块化的设计,展示了多种架构技能的集成。你可能会看到以下这些“技能”被具体实现:

  1. 领域建模技能:如何设计富有行为的聚合根,而非贫血模型。如何利用值对象(如MoneyAddress)来封装概念和验证逻辑。
  2. 仓储模式技能:如何定义纯粹的领域仓储接口(如IOrderRepository),并在Infrastructure层通过Entity Framework Core或Dapper实现。这里会涉及工作单元(Unit of Work)模式的应用,以管理事务。
  3. 领域事件技能:如何发布和处领域事件,以实现聚合间的最终一致性。项目可能会集成MediatR库来作为进程内的中介者,解耦事件发布和处理。
  4. 验证技能:输入验证(DTO级别)和业务规则验证(领域模型级别)的区分与实现。可能会使用FluentValidation库。
  5. 异常处理技能:定义自定义的领域异常(如OrderAlreadyConfirmedException),并设计全局异常过滤器,将其转换为合适的HTTP状态码和响应体。
  6. API设计技能:如何设计RESTful端点,如何利用ApiController特性、模型绑定、以及像ProblemDetails这样的标准响应格式。

这些技能被有机地整合在一个连贯的项目结构中,让你看到它们是如何协同工作的,而不是孤立地存在。

2.3 技术栈选型与考量

基于.NET生态,项目通常会选择一套成熟、稳定且社区支持度高的技术组合。例如:

  • .NET 8+:作为基础运行时,利用其长期支持(LTS)特性和高性能。
  • Entity Framework Core:作为ORM的首选,用于实现仓储模式。项目会展示如何配置DbContext、定义实体映射、以及进行高效的查询(可能包括Specification模式)。
  • MediatR:用于实现CQRS(命令查询职责分离)模式中的命令/查询分发,以及领域事件的进程内处理。它能极大地简化应用服务的结构。
  • FluentValidation:用于提供强大且易读的验证规则定义。
  • Swagger/OpenAPI:用于自动生成API文档,是现代API开发的标配。
  • xUnit/NUnit & Moq:用于编写单元测试和集成测试,演示如何对领域逻辑和应用服务进行有效测试。

注意:技术栈是技能的载体,而非技能本身。这个项目的重点不是教你用EF Core,而是教你如何在整洁架构下正确、优雅地使用EF Core。例如,它会强调在Infrastructure层实现仓储,并确保领域层对EF Core完全无感知。

3. 核心模块与代码结构深度拆解

3.1 领域层:业务逻辑的纯净家园

领域层是项目的灵魂。我们以一个典型的电商“订单”领域为例,看看这个项目可能如何构建。

实体与聚合根设计

// Domain/Entities/Order.cs public class Order : AggregateRoot<Guid> // 继承一个自定义的AggregateRoot基类,可能包含ID和领域事件列表 { private readonly List<OrderItem> _items = new(); public CustomerId CustomerId { get; private set; } // 使用强类型ID public OrderStatus Status { get; private set; } public Address ShippingAddress { get; private set; } // 值对象 public Money TotalAmount { get; private set; } // 值对象 public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); private Order() { } // EF Core 需要 public Order(CustomerId customerId, Address shippingAddress) { Id = OrderId.New(); CustomerId = customerId; ShippingAddress = shippingAddress; Status = OrderStatus.Pending; TotalAmount = Money.Zero; AddDomainEvent(new OrderCreatedDomainEvent(Id)); } public void AddItem(ProductId productId, int quantity, Money unitPrice) { // 业务规则校验 if (Status != OrderStatus.Pending) throw new OrderCannotBeModifiedException(Id); var existingItem = _items.FirstOrDefault(i => i.ProductId == productId); if (existingItem != null) { existingItem.IncreaseQuantity(quantity); } else { _items.Add(new OrderItem(Id, productId, quantity, unitPrice)); } RecalculateTotal(); } public void Confirm() { if (Status != OrderStatus.Pending) throw new InvalidOrderOperationException("只能确认待处理的订单。"); if (_items.Count == 0) throw new InvalidOrderOperationException("订单不能没有商品。"); Status = OrderStatus.Confirmed; AddDomainEvent(new OrderConfirmedDomainEvent(Id)); } private void RecalculateTotal() { TotalAmount = new Money(_items.Sum(i => i.SubTotal.Amount), TotalAmount.Currency); } }

这段代码展示了几个关键技能:1)使用私有构造函数和属性保护器封装状态;2)使用值对象(Money,Address)和强类型ID(OrderId,CustomerId)增强类型安全和表达力;3)在聚合根内部封装业务规则(如确认订单的条件);4)在状态变更时发布领域事件。

领域服务与仓储接口

// Domain/Services/IOrderDomainService.cs public interface IOrderDomainService { Task<bool> CanCustomerPlaceOrderAsync(CustomerId customerId, CancellationToken cancellationToken); } // Domain/Interfaces/IOrderRepository.cs public interface IOrderRepository : IRepository<Order> { Task<Order?> GetByIdAsync(OrderId id, CancellationToken cancellationToken); Task<IEnumerable<Order>> GetPendingOrdersOlderThanAsync(DateTime cutoffDate, CancellationToken cancellationToken); // 注意:这里只有领域相关的查询方法,分页、复杂过滤等通常放在应用层的查询中处理。 }

领域服务用于处理跨多个聚合的业务逻辑,而仓储接口则定义了领域层需要的数据持久化契约,没有任何具体技术细节。

3.2 应用层:用例的协调者

应用层负责协调领域对象和基础设施来完成一个具体的用户操作(用例)。

命令与命令处理器(CQRS模式)

// Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs public record CreateOrderCommand : IRequest<Result<OrderDto>> { public Guid CustomerId { get; init; } public AddressDto ShippingAddress { get; init; } = null!; public List<OrderItemDto> Items { get; init; } = new(); } // Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Result<OrderDto>> { private readonly IOrderRepository _orderRepository; private readonly IOrderDomainService _orderDomainService; private readonly IMapper _mapper; // 使用AutoMapper进行对象映射 public CreateOrderCommandHandler(IOrderRepository orderRepository, IOrderDomainService orderDomainService, IMapper mapper) { _orderRepository = orderRepository; _orderDomainService = orderDomainService; _mapper = mapper; } public async Task<Result<OrderDto>> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { // 1. 使用领域服务校验业务规则 var canPlaceOrder = await _orderDomainService.CanCustomerPlaceOrderAsync(new CustomerId(request.CustomerId), cancellationToken); if (!canPlaceOrder) { return Result.Failure<OrderDto>(OrderErrors.CustomerCannotPlaceOrder); } // 2. 创建领域实体 var shippingAddress = _mapper.Map<Address>(request.ShippingAddress); var order = new Order(new CustomerId(request.CustomerId), shippingAddress); // 3. 处理命令中的订单项 foreach (var itemDto in request.Items) { order.AddItem(new ProductId(itemDto.ProductId), itemDto.Quantity, new Money(itemDto.UnitPrice, "USD")); } // 4. 持久化 await _orderRepository.AddAsync(order, cancellationToken); // 注意:工作单元(Unit of Work)的提交(SaveChangesAsync)通常由管道行为(如MediatR的Pipeline Behavior)或控制器层统一控制。 // 5. 返回DTO return Result.Success(_mapper.Map<OrderDto>(order)); } }

应用服务(这里体现为CommandHandler)变得非常清晰:校验、协调领域对象、调用仓储。它不包含具体的业务规则,业务规则属于领域层。Result<T>模式是一种常见的用于处理操作成功/失败并携带信息的方式,比直接抛出异常在某些场景下更具表现力。

3.3 基础设施层:细节的实现者

这一层是实现细节的所在,它依赖于应用层和领域层定义的抽象。

仓储实现

// Infrastructure/Persistence/Repositories/OrderRepository.cs public class OrderRepository : IOrderRepository { private readonly ApplicationDbContext _dbContext; public OrderRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken cancellationToken) { // 使用Include加载必要的关联数据,避免N+1查询 return await _dbContext.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task AddAsync(Order order, CancellationToken cancellationToken) { await _dbContext.Orders.AddAsync(order, cancellationToken); } // ... 其他接口方法实现 }

DbContext配置

// Infrastructure/Persistence/ApplicationDbContext.cs public class ApplicationDbContext : DbContext, IUnitOfWork { public DbSet<Order> Orders => Set<Order>(); // ... 其他DbSet protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); // 应用所有IEntityTypeConfiguration base.OnModelCreating(modelBuilder); } // 实现IUnitOfWork接口 public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default) { // 在保存更改前,可以派发领域事件 await _mediator?.DispatchDomainEventsAsync(this); // 假设通过MediatR派发 var result = await base.SaveChangesAsync(cancellationToken); return result > 0; } } // Infrastructure/Persistence/EntityConfigurations/OrderEntityTypeConfiguration.cs public class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order> { public void Configure(EntityTypeBuilder<Order> builder) { builder.ToTable("Orders"); builder.HasKey(o => o.Id); builder.Property(o => o.Id).HasConversion(id => id.Value, value => new OrderId(value)); // 强类型ID转换 builder.OwnsOne(o => o.TotalAmount, money => { money.Property(m => m.Amount).HasColumnName("TotalAmount").HasPrecision(18, 2); money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3); }); // 值对象作为自有实体 builder.OwnsOne(o => o.ShippingAddress); // 地址值对象 builder.HasMany(o => o.Items).WithOne().HasForeignKey("OrderId"); // 配置聚合内的集合 builder.Ignore(o => o.DomainEvents); // 忽略领域事件列表,它不持久化 } }

这里展示了EF Core的高级映射技巧,如强类型ID的值转换、值对象的拥有实体配置,这些都是实现整洁架构时必备的“基础设施技能”。

3.4 表现层:系统的入口点

表现层(通常是Web API)非常薄,其主要职责是接收HTTP请求,将请求模型映射为应用层的命令/查询,调用MediatR发送,然后处理响应。

// WebAPI/Controllers/OrdersController.cs [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly ISender _sender; private readonly IMapper _mapper; public OrdersController(ISender sender, IMapper mapper) { _sender = sender; _mapper = mapper; } [HttpPost] [ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request, CancellationToken cancellationToken) { var command = _mapper.Map<CreateOrderCommand>(request); var result = await _sender.Send(command, cancellationToken); if (result.IsSuccess) { return CreatedAtAction(nameof(GetOrderById), new { id = result.Value.Id }, result.Value); } // 根据Result中的错误类型,返回不同的ProblemDetails return HandleFailure(result); } // 其他动作:GetOrderById, GetOrders, CancelOrder等 }

控制器变得极其简洁,业务逻辑完全委托给了应用层。ISender接口通常由MediatR提供。

4. 关键架构技能与设计模式实战

4.1 CQRS与MediatR的优雅结合

CQRS(命令查询职责分离)在这个项目中不是指独立的读写数据库,而是指在代码层面将修改状态的操作(命令)和查询数据的操作(查询)分离。这带来了更清晰的职责划分。

命令侧:如上所述,使用IRequest<T>IRequestHandler。命令通常会导致状态变更,并返回一个结果(成功或失败+数据)。

查询侧:查询通常不改变状态,它们只返回数据。可以使用专门的查询对象和处理器。

// Application/Orders/Queries/GetOrderDetails/GetOrderDetailsQuery.cs public record GetOrderDetailsQuery : IRequest<OrderDetailsDto> { public Guid OrderId { get; init; } } // Application/Orders/Queries/GetOrderDetails/GetOrderDetailsQueryHandler.cs public class GetOrderDetailsQueryHandler : IRequestHandler<GetOrderDetailsQuery, OrderDetailsDto> { private readonly IApplicationDbContext _dbContext; // 可能是一个为查询优化的只读DbContext或Dapper连接 private readonly IMapper _mapper; public async Task<OrderDetailsDto> Handle(GetOrderDetailsQuery request, CancellationToken cancellationToken) { // 直接使用DbContext或Dapper执行高效的查询,返回DTO,不经过领域模型 var order = await _dbContext.Orders .AsNoTracking() // 查询不需要变更跟踪 .Include(o => o.Items) .ProjectTo<OrderDetailsDto>(_mapper.ConfigurationProvider) // 使用AutoMapper的ProjectTo进行高效投影 .FirstOrDefaultAsync(o => o.Id == new OrderId(request.OrderId), cancellationToken); return order ?? throw new NotFoundException(nameof(Order), request.OrderId); } }

查询处理器可以直接访问数据库,返回为前端量身定制的DTO,避免了领域模型的复杂加载和转换,性能更优。MediatR作为中介者,统一了命令和查询的发送入口。

4.2 领域事件与最终一致性

领域事件是聚合间通信的重要手段。当Order被确认时,它发布了OrderConfirmedDomainEvent。这个事件可能需要触发其他操作,比如通知用户、更新库存、触发物流流程。

领域事件定义与发布

// Domain/Events/OrderConfirmedDomainEvent.cs public sealed record OrderConfirmedDomainEvent(Guid OrderId) : IDomainEvent; // 在AggregateRoot基类中 public abstract class AggregateRoot<TId> : Entity<TId>, IHasDomainEvents { private readonly List<IDomainEvent> _domainEvents = new(); public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly(); protected void AddDomainEvent(IDomainEvent eventItem) => _domainEvents.Add(eventItem); public void ClearDomainEvents() => _domainEvents.Clear(); }

领域事件处理

// Application/Orders/EventHandlers/OrderConfirmedEventHandler.cs public class OrderConfirmedEventHandler : INotificationHandler<OrderConfirmedDomainEvent> { private readonly IInventoryService _inventoryService; // 领域服务接口,可能在另一个限界上下文中 public async Task Handle(OrderConfirmedDomainEvent notification, CancellationToken cancellationToken) { // 这里可以调用外部库存系统的接口,扣减库存 // 这是一个跨限界上下文的集成,可能需要使用异步消息或HTTP调用,并考虑幂等性 await _inventoryService.ReserveItemsForOrderAsync(notification.OrderId, cancellationToken); } }

事件的派发通常在基础设施层(如DbContext的SaveChangesAsync中)完成,通过MediatR将事件传递给相应的处理器。这实现了聚合间的解耦和最终一致性。

4.3 验证策略:从输入到业务规则

验证是分层的:

  1. 请求模型验证:在API端点,使用FluentValidation或Data Annotations对CreateOrderRequest进行基本格式和必填项校验。这属于“防御性编程”,防止垃圾数据进入系统。
  2. 命令/查询验证:可以在MediatR的Pipeline Behavior中添加一个验证行为,在命令/查询到达处理器之前,使用FluentValidation进行更复杂的业务规则校验(例如,订单金额必须大于0)。
  3. 领域模型验证:最重要的验证在领域实体内部。如Order.Confirm()方法中的检查。这是保证业务逻辑一致性的最后一道防线,也是最重要的防线。
// Application/Common/Behaviors/ValidationBehavior.cs public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) { _validators = validators; } public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (_validators.Any()) { var context = new ValidationContext<TRequest>(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); // 或返回一个ValidationResult } return await next(); } }

这种分层验证确保了关注点分离,也让错误信息能够精准地反馈给调用方。

5. 测试策略:保障架构健康度

整洁架构的一个巨大优势就是可测试性。项目应该展示如何针对各层进行有效测试。

5.1 领域层单元测试

领域层是纯业务逻辑,没有任何外部依赖,最适合单元测试。

// Domain.UnitTests/Entities/OrderTests.cs public class OrderTests { [Fact] public void AddItem_Should_IncreaseTotalAmount() { // Arrange var order = new Order(new CustomerId(Guid.NewGuid()), new Address("Street", "City", "Zip")); var initialTotal = order.TotalAmount; // Act order.AddItem(new ProductId(Guid.NewGuid()), 2, new Money(10.0m, "USD")); // Assert order.TotalAmount.Amount.Should().Be(20.0m); // 使用FluentAssertions order.Items.Should().HaveCount(1); } [Fact] public void Confirm_WhenOrderIsEmpty_ShouldThrowException() { // Arrange var order = new Order(new CustomerId(Guid.NewGuid()), new Address("Street", "City", "Zip")); // Act & Assert var act = () => order.Confirm(); act.Should().Throw<InvalidOrderOperationException>().WithMessage("*不能没有商品*"); } }

测试完全在内存中运行,速度快,可以覆盖所有业务规则分支。

5.2 应用层集成测试

应用服务(Command/Query Handler)有外部依赖(如仓储、领域服务)。我们需要使用Mock框架(如Moq)来模拟这些依赖。

// Application.UnitTests/Orders/Commands/CreateOrderCommandHandlerTests.cs public class CreateOrderCommandHandlerTests { private readonly Mock<IOrderRepository> _mockOrderRepository; private readonly Mock<IOrderDomainService> _mockDomainService; private readonly IMapper _mapper; private readonly CreateOrderCommandHandler _handler; public CreateOrderCommandHandlerTests() { _mockOrderRepository = new Mock<IOrderRepository>(); _mockDomainService = new Mock<IOrderDomainService>(); var config = new MapperConfiguration(cfg => cfg.AddProfile<OrderMappingProfile>()); _mapper = config.CreateMapper(); _handler = new CreateOrderCommandHandler(_mockOrderRepository.Object, _mockDomainService.Object, _mapper); } [Fact] public async Task Handle_ValidCommand_ShouldCreateAndSaveOrder() { // Arrange var command = new CreateOrderCommand { CustomerId = Guid.NewGuid(), ... }; _mockDomainService.Setup(s => s.CanCustomerPlaceOrderAsync(It.IsAny<CustomerId>(), It.IsAny<CancellationToken>())) .ReturnsAsync(true); _mockOrderRepository.Setup(r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>())) .Returns(Task.CompletedTask); // Act var result = await _handler.Handle(command, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); _mockOrderRepository.Verify(r => r.AddAsync(It.Is<Order>(o => o.CustomerId.Value == command.CustomerId), It.IsAny<CancellationToken>()), Times.Once); _mockDomainService.VerifyAll(); } }

测试验证了Handler是否正确协调了领域服务和仓储,并调用了预期的方法。

5.3 端到端(E2E)测试

使用WebApplicationFactory或TestServer来启动一个内存中的API实例,进行完整的API调用测试。这可以验证从控制器到数据库的整个流程,包括路由、模型绑定、中间件、依赖注入等。

// WebAPI.FunctionalTests/OrdersControllerTests.cs public class OrdersControllerTests : IClassFixture<CustomWebApplicationFactory> { private readonly HttpClient _client; public OrdersControllerTests(CustomWebApplicationFactory factory) { _client = factory.CreateClient(); } [Fact] public async Task PostOrder_ReturnsCreatedResponse() { // Arrange var request = new CreateOrderRequest { ... }; var content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); // Act var response = await _client.PostAsync("/api/orders", content); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); response.Headers.Location.Should().NotBeNull(); var responseString = await response.Content.ReadAsStringAsync(); var createdOrder = JsonSerializer.Deserialize<OrderDto>(responseString); createdOrder.Should().NotBeNull(); createdOrder.Id.Should().NotBe(Guid.Empty); } }

E2E测试成本最高,但信心也最强,通常用于测试关键的用户旅程。

6. 部署、配置与运维考量

6.1 依赖注入与模块化配置

整洁架构的项目通常有清晰的分层,依赖注入(DI)的配置也需要与之对应。在WebAPI项目的Program.cs或启动类中,会分层注册服务。

// 在WebAPI Program.cs中 builder.Services.AddApplication(); // 扩展方法,注册Application层的服务(MediatR, AutoMapper, FluentValidation等) builder.Services.AddInfrastructure(builder.Configuration); // 扩展方法,注册Infrastructure层服务(DbContext, 仓储实现,外部服务客户端等) builder.Services.AddWebApi(); // 扩展方法,注册API相关服务(控制器,健康检查,Swagger等) // 在Application层 DependencyInjection.cs public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // ... 其他应用层服务 return services; } }

这种模块化的配置使得各层的职责更加清晰,也便于在测试时替换实现(例如,用内存数据库替换真实数据库)。

6.2 数据库迁移与种子数据

使用EF Core的迁移(Migrations)来管理数据库 schema 的演变。项目通常会包含迁移脚本和种子数据逻辑。

# 在Infrastructure项目目录下 dotnet ef migrations add InitialCreate --startup-project ../WebAPI --output-dir Persistence/Migrations dotnet ef database update --startup-project ../WebAPI

种子数据可以在应用启动时(IHostedService)或通过一个单独的DbSeeder类来插入,确保开发、测试环境有可用的基础数据。

6.3 日志、监控与健康检查

对于生产环境,项目会集成标准的可观测性工具。

  • 日志:使用ILogger<T>接口,并配置像Serilog这样的强大日志库,将日志输出到控制台、文件、Elasticsearch等。
  • 健康检查:ASP.NET Core内置了健康检查中间件。可以添加对数据库、外部API(如库存服务)的健康检查端点。
    builder.Services.AddHealthChecks() .AddDbContextCheck<ApplicationDbContext>("database") .AddUrlGroup(new Uri("https://inventory-service/api/health"), "inventory-service"); app.MapHealthChecks("/health");
  • 监控:集成Application Insights或OpenTelemetry来收集指标、追踪和异常信息。

6.4 容器化与部署

项目结构天然适合容器化。一个典型的Dockerfile会以多阶段构建来优化镜像大小。

# 构建阶段 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["WebAPI/WebAPI.csproj", "WebAPI/"] COPY ["Application/Application.csproj", "Application/"] COPY ["Domain/Domain.csproj", "Domain/"] COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] RUN dotnet restore "WebAPI/WebAPI.csproj" COPY . . RUN dotnet publish "WebAPI/WebAPI.csproj" -c Release -o /app/publish # 运行阶段 FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "WebAPI.dll"]

然后使用docker-compose.yml或Kubernetes清单文件来定义服务、数据库、缓存等组件,实现一键部署。

7. 常见陷阱、优化建议与个人心得

在实践这个项目所展示的架构过程中,我遇到过不少坑,也总结出一些优化点。

7.1 过度设计与性能权衡

整洁架构有时会让人陷入“过度抽象”的陷阱。不是每个实体都需要一个仓储接口,不是每个操作都需要一个领域事件。对于简单的CRUD操作,如果业务逻辑极其简单,直接使用应用服务调用仓储可能更清晰。引入CQRS、领域事件等模式,一定要有明确的业务复杂度作为支撑。否则,反而会增加理解和维护成本。

性能注意点

  • N+1查询:在领域层通过仓储获取聚合时,如果聚合包含集合,务必使用Include或投影(Select)一次性加载所需数据。在查询侧,应使用为查询优化的方式(如AsNoTracking()ProjectTo)。
  • 对象映射开销:AutoMapper在映射复杂对象图时可能有开销。对于高性能场景,可以考虑手动映射或使用像Mapster这样更快的库,或者在查询侧直接使用EF Core的投影到DTO,避免映射。
  • 领域事件处理:如果事件处理器执行耗时操作(如调用外部HTTP API),务必将其设计为异步且具有重试和幂等性。考虑使用后台任务(如IHostedService)或消息队列来解耦,避免阻塞主业务流程。

7.2 聚合设计过大或过小

聚合是事务和一致性的边界。设计过大的聚合(如把整个用户购物车和所有订单都放在一个聚合里)会导致并发冲突严重,性能低下。设计过小(如每个订单项都是一个聚合)则会导致业务规则难以维护,需要引入复杂的事件溯源或最终一致性。我的经验是:从业务用例出发,思考“哪些对象必须作为一个整体被修改,以保证业务规则不被破坏?”通常,一个用例只修改一个聚合。

7.3 工作单元(Unit of Work)的提交时机

谁应该调用DbContext.SaveChangesAsync()?常见做法是在MediatR的Pipeline Behavior中,在所有Handler执行成功后统一提交。或者,在Web API的Action Filter中提交。这确保了单个HTTP请求对应一个事务。但需要小心处理异常回滚。项目中需要明确约定并统一实现。

7.4 版本控制与演化

领域模型会随着业务演化而改变。EF Core迁移可以处理数据库schema的变更。但对于已发布的API和领域事件,变更需要谨慎。考虑使用API版本控制(如Microsoft.AspNetCore.Mvc.Versioning)和事件版本化(在事件中添加版本号,处理器能够处理多个版本)。对于破坏性变更,需要设计兼容性策略。

7.5 从“项目”到“解决方案”的扩展

这个“dotnet-clean-architecture-skills”项目通常展示的是一个单一的、模块化的应用程序(一个解决方案包含多个项目)。在微服务架构下,每个限界上下文(Bounded Context)可能就是一个独立的服务(一个解决方案)。此时,DomainApplication层可能被包装成NuGet包,供多个服务引用,或者每个服务有自己的领域模型。跨服务通信则通过API网关、消息队列和领域事件(升级为集成事件)来实现。这是架构演进的下一步,但核心的整洁架构思想——依赖倒置、领域核心——依然适用。

这个项目就像一个精心打磨的工具箱,里面的每一样工具(技能)都有其适用场景。真正的技能在于,你能够根据自己项目的实际规模、团队经验和业务复杂度,从这个工具箱中挑选出合适的工具,组合使用,而不是生搬硬套。它提供的是方向和最佳实践,而不是银弹。花时间理解每个模式背后的“为什么”,比复制粘贴代码更重要。当你真正理解并内化了这些技能,你就能设计出经得起时间考验的软件系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 18:02:35

暗黑破坏神2重制版自动化:Botty如何通过视觉智能改变游戏体验

暗黑破坏神2重制版自动化&#xff1a;Botty如何通过视觉智能改变游戏体验 【免费下载链接】botty D2R Pixel Bot 项目地址: https://gitcode.com/gh_mirrors/bo/botty 厌倦了在《暗黑破坏神2重制版》中重复刷怪、手动拾取装备的枯燥过程&#xff1f;Botty是一款基于像素…

作者头像 李华
网站建设 2026/5/6 18:01:30

英雄联盟LCU工具箱:League Akari 全面使用指南与实战技巧

英雄联盟LCU工具箱&#xff1a;League Akari 全面使用指南与实战技巧 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power &#x1f680;. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit League Akari是一款基于英…

作者头像 李华
网站建设 2026/5/6 17:58:39

3倍推理加速!Ultralytics YOLO模型OpenVINO终极部署实战指南

3倍推理加速&#xff01;Ultralytics YOLO模型OpenVINO终极部署实战指南 【免费下载链接】ultralytics Ultralytics YOLO &#x1f680; 项目地址: https://gitcode.com/GitHub_Trending/ul/ultralytics 在计算机视觉部署的战场上&#xff0c;性能瓶颈往往是阻碍AI应用落…

作者头像 李华