在.NET生态系统中,大多数初学者关注的是具体框架的使用——ASP.NET MVC如何写Controller、Entity Framework如何映射数据库、Web API如何设计端点。但实际的企业级开发远比这些复杂:当系统需要支撑多个团队协作、处理复杂的业务逻辑、应对不断变化的需求时,缺乏架构指导的代码很快就会陷入混乱。
这本书填补了一个关键空白:将通用软件架构理论(分层架构、领域驱动设计、CQRS、事件溯源等)与.NET具体技术栈结合起来,为.NET开发者提供了一条从编码到架构的进阶路径。
书籍围绕四个核心架构主题展开:
| 主题 | 核心问题 | 解决思路 |
|---|---|---|
| 分层架构 | 如何分离关注点,保持代码整洁 | 经典三层 + 领域层增强 |
| 领域驱动设计 | 如何用代码表达复杂业务逻辑 | 聚合、实体、值对象、领域服务 |
| CQRS | 如何优化读写分离的复杂场景 | 命令与查询的职责分离 |
| 事件溯源 | 如何记录和回溯业务状态变化 | 事件存储 + 投影重建 |
传统的.NET应用常采用三层架构:
┌─────────────────────┐
│ Presentation │ ASP.NET MVC / Web API / Blazor
├─────────────────────┤
│ Business Logic │ Service Layer / Business Logic
├─────────────────────┤
│ Data Access │ Entity Framework / ADO.NET
└─────────────────────┘
┌─────────────────────┐
│ Database │ SQL Server / PostgreSQL
└─────────────────────┘
这个模式在中小型项目中工作良好,但在企业级应用中暴露出几个问题:
书中提出的改进方案是引入领域层(Domain Layer)和应用层(Application Layer):
┌─────────────────────────┐
│ Presentation Layer │ ASP.NET Core Controllers
├─────────────────────────┤
│ Application Layer │ Use Cases / Application Services
├─────────────────────────┤
│ Domain Layer │ Entities, Aggregates, Value Objects
├─────────────────────────┤
│ Infrastructure Layer │ EF Core, Repositories, External Services
└─────────────────────────┘
┌─────────────────────────┐
│ Database / Storage │
└─────────────────────────┘
Presentation Layer(表现层)
Application Layer(应用层)
Domain Layer(领域层)
Infrastructure Layer(基础设施层)
实现上述分层架构的关键是依赖反转(Dependency Inversion Principle)。在.NET中,通过依赖注入实现:
// Domain Layer 定义接口
public interface IOrderRepository
{
Task<Order> GetByIdAsync(Guid id);
Task SaveAsync(Order order);
}
// Infrastructure Layer 实现接口
public class EfOrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public EfOrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(Guid id)
{
return await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
}
public async Task SaveAsync(Order order)
{
// EF Core 的变更追踪会自动处理
await _context.SaveChangesAsync();
}
}
// Application Layer 使用接口
public class PlaceOrderHandler : IRequestHandler<PlaceOrderCommand, OrderDto>
{
private readonly IOrderRepository _orderRepo;
private readonly IPaymentService _paymentService;
public PlaceOrderHandler(
IOrderRepository orderRepo,
IPaymentService paymentService)
{
_orderRepo = orderRepo;
_paymentService = paymentService;
}
public async Task<OrderDto> Handle(PlaceOrderCommand command, CancellationToken ct)
{
// 应用层编排业务逻辑,不实现业务规则
var order = await _orderRepo.GetByIdAsync(command.OrderId);
var payment = await _paymentService.ChargeAsync(order.TotalAmount);
order.MarkAsPaid(payment.TransactionId);
await _orderRepo.SaveAsync(order);
return MapToDto(order);
}
}
// 在 Startup / Program 中注册
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddScoped<IPaymentService, StripePaymentService>();
这样,Application Layer 只依赖于 Domain Layer 定义的接口,不依赖 Infrastructure Layer 的具体实现。更换数据库或支付服务提供商时,只需要修改注册代码。
虽然书中主要基于 .NET Core/5,但 .NET 6/8 带来了新的架构选项:
在架构设计中,这些新特性可以与传统分层架构共存——Minimal API 可以作为表现层的另一选择,与 MVC Controller 配合使用。
领域驱动设计(Domain-Driven Design)由Eric Evans在2003年提出,核心思想是让代码模型反映业务模型,而不是让业务去适应技术实现。
有唯一标识且会改变状态的对象。在.NET中:
public class Order : Entity<Guid>
{
public Guid Id { get; private set; }
public string OrderNumber { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
private List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public Order(string orderNumber)
{
Id = Guid.NewGuid();
OrderNumber = orderNumber;
Status = OrderStatus.Pending;
CreatedAt = DateTime.UtcNow;
}
public void AddItem(Product product, int quantity, decimal unitPrice)
{
var item = new OrderItem(product.Id, product.Name, quantity, unitPrice);
_items.Add(item);
RecalculateTotal();
}
public void MarkAsPaid(string transactionId)
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be paid");
Status = OrderStatus.Paid;
AddDomainEvent(new OrderPaidEvent(Id, transactionId));
}
private void RecalculateTotal()
{
TotalAmount = _items.Sum(i => i.Subtotal);
}
}
没有唯一标识,由属性值决定相等性的对象:
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string ZipCode { get; }
public string Country { get; }
public Address(string street, string city, string zipCode, string country)
{
Street = street;
City = city;
ZipCode = zipCode;
Country = country;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Street;
yield return City;
yield return ZipCode;
yield return Country;
}
}
// EF Core 值对象映射(Owned Entity)
modelBuilder.Entity<Order>().OwnsOne(
o => o.ShippingAddress, a => {
a.Property(p => p.Street).HasColumnName("ShippingStreet");
a.Property(p => p.City).HasColumnName("ShippingCity");
// ...
});
将多个实体和值对象组合为一个一致性边界。聚合根(Aggregate Root)是外部访问的唯一入口:
// Order 是聚合根,OrderItem 只能在 Order 内部修改
public class OrderItem : Entity<Guid>
{
public Guid Id { get; private set; }
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal Subtotal => Quantity * UnitPrice;
internal OrderItem(Guid productId, string productName, int quantity, decimal unitPrice)
{
Id = Guid.NewGuid();
ProductId = productId;
ProductName = productName;
Quantity = quantity;
UnitPrice = unitPrice;
}
}
仓库(Repository)模式是聚合的持久化接口。需要注意:EF Core 的 DbContext 本身就是一个工作单元 + 仓库,不需要对每个实体都创建一个仓库接口。
// 通用仓库基类
public class RepositoryBase<T> where T : class
{
protected readonly AppDbContext Context;
public RepositoryBase(AppDbContext context)
{
Context = context;
}
public async Task<T> GetByIdAsync(params object[] id)
{
return await Context.Set<T>().FindAsync(id);
}
public void Add(T entity)
{
Context.Set<T>().Add(entity);
}
public void Update(T entity)
{
Context.Set<T>().Update(entity);
}
public void Remove(T entity)
{
Context.Set<T>().Remove(entity);
}
}
领域事件用于在聚合之间进行通信,而不引入耦合。在 .NET 8 中,可以使用 MediatR 或 .NET 内置的 Channel<T> 来实现:
public record OrderPaidEvent(Guid OrderId, string TransactionId) : IDomainEvent;
// 领域事件处理
public class OrderPaidHandler : INotificationHandler<OrderPaidEvent>
{
private readonly IEmailService _emailService;
private readonly IInventoryService _inventoryService;
public OrderPaidHandler(IEmailService emailService, IInventoryService inventoryService)
{
_emailService = emailService;
_inventoryService = inventoryService;
}
public async Task Handle(OrderPaidEvent notification, CancellationToken cancellationToken)
{
// 发送确认邮件
await _emailService.SendOrderConfirmationAsync(notification.OrderId);
// 更新库存
await _inventoryService.ReserveItemsAsync(notification.OrderId);
}
}
命令查询职责分离(Command Query Responsibility Segregation)由Greg Young提出,核心思想是:读操作和写操作使用不同的模型。
┌──────────────────────────────────────┐
│ Client │
└────┬────────────────────┬─────────────┘
│ Command │ Query
▼ ▼
┌──────────┐ ┌──────────────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
│ (Write) │ │ (Read) │
└────┬─────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Domain │ │ Read-only │
│ Database │ │ Denormalized │
│ (Write) │ │ Database │
└──────────┘ └──────────────┘
│
│ Sync (Eventual Consistency)
└────────────────────────►
传统分层架构中,读写使用同一模型,在复杂业务场景下会产生矛盾:
典型场景:订单列表页面需要显示订单号、用户姓名、商品名称、价格、状态等,但Order聚合可能只包含ID引用。如果通过Order聚合来查询,需要 Join 多个表,性能差且复杂度高。
使用 MediatR 作为命令/查询分发器:
// === Command 定义 ===
public record PlaceOrderCommand(
Guid CustomerId,
List<OrderItemDto> Items,
Address ShippingAddress
) : IRequest<OrderDto>;
// === Command Handler ===
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, OrderDto>
{
private readonly ICustomerRepository _customerRepo;
private readonly IUnitOfWork _unitOfWork;
public PlaceOrderCommandHandler(
ICustomerRepository customerRepo,
IUnitOfWork unitOfWork)
{
_customerRepo = customerRepo;
_unitOfWork = unitOfWork;
}
public async Task<OrderDto> Handle(PlaceOrderCommand command, CancellationToken ct)
{
var customer = await _customerRepo.GetByIdAsync(command.CustomerId);
var order = customer.PlaceOrder(command.Items, command.ShippingAddress);
await _unitOfWork.SaveChangesAsync();
return MapOrderToDto(order);
}
}
// === Query 定义 ===
public record GetOrdersQuery(
Guid CustomerId,
int Page,
int PageSize
) : IRequest<PagedResult<OrderListDto>>;
// === Query Handler ===
// 直接查询读模型(可能是内存快照、视图表、或者独立的读数据库)
public class GetOrdersQueryHandler : IRequestHandler<GetOrdersQuery, PagedResult<OrderListDto>>
{
private readonly OrderReadDbContext _readDb;
public GetOrdersQueryHandler(OrderReadDbContext readDb)
{
_readDb = readDb;
}
public async Task<PagedResult<OrderListDto>> Handle(GetOrdersQuery query, CancellationToken ct)
{
var orders = await _readDb.OrderViews
.Where(o => o.CustomerId == query.CustomerId)
.OrderByDescending(o => o.CreatedAt)
.Skip((query.Page - 1) * query.PageSize)
.Take(query.PageSize)
.ToListAsync(ct);
return new PagedResult<OrderListDto>(orders, query.Page, query.PageSize);
}
}
// === Program.cs 注册 ===
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();
builder.Services.AddDbContext<OrderDbContext>(options => ...);
builder.Services.AddDbContext<OrderReadDbContext>(options => ...);
使用 SQL View、Materialized View 或触发器将写模型同步到读模型。
优点:实时一致,无需额外组件
缺点:无法解耦,数据库成为瓶颈
在写操作完成后发布事件,异步更新读模型:
// 写操作完成后
await _mediator.Publish(new OrderPlacedEvent(order.Id));
// 事件处理器更新读模型
public class OrderPlacedHandler : INotificationHandler<OrderPlacedEvent>
{
private readonly OrderReadDbContext _readDb;
public async Task Handle(OrderPlacedEvent evt, CancellationToken ct)
{
var view = new OrderView
{
OrderId = evt.OrderId,
Status = "Pending",
CreatedAt = DateTime.UtcNow,
// 预计算显示字段
};
_readDb.OrderViews.Add(view);
await _readDb.SaveChangesAsync(ct);
}
}
优点:解耦,读模型可以灵活优化
缺点:最终一致性,需要处理消息丢失和重复
使用 RabbitMQ、Azure Service Bus 或 Kafka 作为事件传输:
写操作 → 发布事件到消息队列 → 消费者更新读模型
这是最推荐的方案,适合需要高可用、可扩展的.NET企业应用。
事件溯源(Event Sourcing)将状态变化存储为一系列事件,而不是存储当前状态:
| 传统持久化 | 事件溯源 |
|---|---|
UPDATE Orders SET Status='Paid' |
INSERT INTO OrderEvents (OrderId, EventType, Data) VALUES (...) |
| 只保留最终状态 | 保留所有更改历史 |
| 状态变更不可追溯 | 可回溯到任意时间点 |
| 审计需要额外实现 | 天然审计日志 |
// 领域事件基类
public abstract class DomainEvent
{
public Guid EventId { get; } = Guid.NewGuid();
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public int Version { get; set; }
}
// 聚合根基类
public abstract class EventSourcedAggregate : Entity<Guid>
{
private readonly List<DomainEvent> _uncommittedEvents = new();
public IReadOnlyCollection<DomainEvent> UncommittedEvents => _uncommittedEvents;
public int Version { get; protected set; }
protected void ApplyEvent(DomainEvent @event)
{
Apply(@event);
_uncommittedEvents.Add(@event);
}
protected abstract void Apply(DomainEvent @event);
public void LoadFromHistory(IEnumerable<DomainEvent> history)
{
foreach (var @event in history)
{
Apply(@event);
Version = @event.Version;
}
}
public void ClearUncommittedEvents()
{
_uncommittedEvents.Clear();
}
}
// 具体聚合
public class Order : EventSourcedAggregate
{
public string OrderNumber { get; private set; }
public OrderStatus Status { get; private set; }
public Order(IEnumerable<DomainEvent> history) : base()
{
LoadFromHistory(history);
}
public static Order Create(string orderNumber)
{
var order = new Order(Enumerable.Empty<DomainEvent>());
order.ApplyEvent(new OrderCreatedEvent(order.Id, orderNumber));
return order;
}
public void MarkAsPaid(string transactionId)
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Already paid");
ApplyEvent(new OrderPaidEvent(Id, transactionId));
}
protected override void Apply(DomainEvent @event)
{
switch (@event)
{
case OrderCreatedEvent e:
Id = e.OrderId;
OrderNumber = e.OrderNumber;
Status = OrderStatus.Pending;
Version = e.Version;
break;
case OrderPaidEvent e:
Status = OrderStatus.Paid;
Version = e.Version;
break;
}
}
}
事件溯源的读模型通过投影(Projection)构建:
// 订单列表投影
public class OrderListProjection : IProjection
{
private readonly OrderReadDbContext _db;
public OrderListProjection(OrderReadDbContext db)
{
_db = db;
}
public async Task ProjectAsync(DomainEvent @event)
{
switch (@event)
{
case OrderCreatedEvent e:
_db.OrderViews.Add(new OrderView
{
Id = e.OrderId,
OrderNumber = e.OrderNumber,
Status = "Pending",
TotalAmount = 0
});
break;
case OrderPaidEvent e:
var order = await _db.OrderViews.FindAsync(e.OrderId);
if (order != null) order.Status = "Paid";
break;
}
await _db.SaveChangesAsync();
}
}
适合场景:
不适合场景:
| 存储方案 | 适用场景 | .NET 生态 |
|---|---|---|
| SQL Server(事件表) | 小规模,已有SQL基础设施 | EF Core / Dapper |
| EventStoreDB | 专注事件溯源的数据库 | 有官方.NET客户端 |
| Azure Cosmos DB | 云原生,自动扩展 | Cosmos SDK |
| Marten | .NET开发者友好 | PostgreSQL + 文档存储 |
| Kafka | 高吞吐事件流 | Confluent Kafka .NET |
推荐:对于大多数.NET项目,从 Marten 或 SQL Server事件表 开始足够。EventStoreDB 适合对事件溯源有深度需求的场景。
书中强调了从模块化单体开始的理念。与其一开始就拆分为微服务,不如先保持单体但做好模块化:
┌──────────────────────────────────────────┐
│ Monolith App │
│ ┌─────────┐ ┌─────────┐ ┌───────────┐ │
│ │ Orders │ │ Catalog │ │ Billing │ │
│ │ Module │ │ Module │ │ Module │ │
│ └────┬────┘ └────┬────┘ └─────┬─────┘ │
│ │ │ │ │
│ ┌────┴───────────┴────────────┴─────┐ │
│ │ Shared Kernel / API │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
模块边界规则:
// 每个模块是一个独立的类库
// OrderModule.csproj
// CatalogModule.csproj
// BillingModule.csproj
// 模块注册
public static class OrderModuleRegistration
{
public static IServiceCollection AddOrderModule(this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<OrderDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("OrdersDb")));
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(OrderModuleRegistration).Assembly));
return services;
}
}
// 在Program.cs中
builder.Services
.AddOrderModule(builder.Configuration)
.AddCatalogModule(builder.Configuration)
.AddBillingModule(builder.Configuration);
当模块化单体无法满足独立部署和扩展需求时,再按模块边界拆分为微服务,这个迁移路径会非常平滑。
阶段1: 模块化单体
→ 所有模块在一个进程中
→ 共享数据库(不同Schema)
→ 模块间方法调用
阶段2: 进程内拆分
→ 模块拆分为独立进程
→ 通过gRPC/HTTP通信
→ 独立数据库
阶段3: 正式微服务
→ 独立部署流水线
→ 独立数据库
→ 事件驱动通信
→ 服务发现、API网关
检查清单
┌────────────────────────────────────────────┐
│ 🔴 危险信号 │
│ - Controllers 中直接调用 EF Core │
│ - Service 层超过500行的方法 │
│ - 没有接口层,直接引用具体实现 │
│ - 数据库迁移脚本混乱,缺乏版本管理 │
│ - 单元测试需要启动数据库 │
│ │
│ 🟢 健康信号 │
│ - 依赖注入统一管理 │
│ - 业务逻辑在领域模型中 │
│ - 模块之间有清晰的接口定义 │
│ - 数据库变更通过Migration管理 │
│ - 有合理的错误处理和日志策略 │
└────────────────────────────────────────────┘
项目规模?
├── 小型 / 原型
│ └── Minimal API + EF Core + 简单的三层
├── 中型 / B2B SaaS
│ ├── 传统三层 → ASP.NET Core + EF Core + AutoMapper
│ └── DDD → 加上领域层 + MediatR
└── 大型 / 企业级
├── DDD + CQRS → 事件驱动架构
├── 事件溯源 → 需要完整的审计追踪
└── 微服务 → 多团队协作,独立部署需求
"架构不是设计出来的,是演进出来的"
在实践中,过度设计是比设计不足更常见的问题。合理的做法是:
这本书是.NET开发者从"会用框架"到"理解架构"的必读进阶之作。它的最大价值在于:
📚 读书笔记 | 架构师书架系列
本文是对《.NET企业级应用架构设计》(Dino Esposito, Andrea Saltarello 著)的读书笔记,在保留原书核心框架的基础上进行了扩展和完善,补充了基于 .NET 6/8 的实践示例和架构决策分析。