本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、集成事件
领域事件解决同一微服务/界限上下文 内的通信,而集成事件 负责跨微服务、跨服务器的业务协同,是实现分布式事务最终一致性的核心,必须依托消息中间件实现,主流选型:RabbitMQ(中小型项目)、Kafka(高吞吐场景)、Redis(轻量场景)。
1.RabbitMQ核心概念
- 信道(Channel):TCP连接之上的虚拟通信通道,TCP创建销毁开销大,信道轻量化、用完即关,实现单TCP连接多信道复用,提升性能。
- 队列(Queue):消息暂存容器,生产者投递消息至队列,消费者从队列拉取消息,支持持久化、独占、自动删除等配置。
- 交换机(Exchange):消息路由核心,根据RoutingKey将消息转发至对应队列,常用类型:direct(精准路由)、fanout(广播)、topic(模糊匹配),DDD集成事件优先用direct。
- 消息确认机制:生产者确认、消费者手动ACK,避免消息丢失,保证分布式数据一致性。
2.代码示例
消息生产者(发布集成事件)
cs
using RabbitMQ.Client;
using System.Text;
// 模拟订单微服务:发布订单支付集成事件
var factory = new ConnectionFactory();
// 服务器配置,生产环境建议写入配置文件
factory.HostName = "127.0.0.1";
factory.UserName = "guest"; // 默认账号
factory.Password = "guest"; // 默认密码
factory.DispatchConsumersAsync = true;
// 交换机、路由键(与消费者保持一致)
string exchangeName = "OrderExchange";
string eventName = "OrderPaidEvent"; // RoutingKey,对应集成事件名称
// 复用TCP连接,避免频繁创建
using var conn = factory.CreateConnection();
while (true)
{
// 模拟订单支付消息,生产环境用实体序列化(JSON)
string msg = $"订单{Random.Shared.Next(1000, 9999)}已支付,支付时间:{DateTime.Now}";
// 信道用完即释放
using (var channel = conn.CreateModel())
{
// 声明持久化交换机,不存在则创建
channel.ExchangeDeclare(exchange: exchangeName, type: "direct", durable: true, autoDelete: false);
// 消息持久化配置,服务器重启不丢失
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // 2=持久化,1=非持久化
// 消息编码
byte[] body = Encoding.UTF8.GetBytes(msg);
// 发布消息
channel.BasicPublish(exchange: exchangeName, routingKey: eventName, mandatory: true, basicProperties: properties, body: body);
}
Console.WriteLine($"【生产者】发布集成事件:{msg}");
Thread.Sleep(1000);
}
消息消费者(订阅集成事件)
cs
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
// 模拟通知微服务:订阅订单支付集成事件
var factory = new ConnectionFactory();
factory.HostName = "127.0.0.1";
factory.UserName = "guest";
factory.Password = "guest";
factory.DispatchConsumersAsync = true;
string exchangeName = "OrderExchange";
string eventName = "OrderPaidEvent";
string queueName = "OrderPaid_NotificationQueue"; // 消费者专属队列
using var conn = factory.CreateConnection();
using var channel = conn.CreateModel();
// 声明交换机、队列、绑定路由
channel.ExchangeDeclare(exchangeName, "direct", durable: true);
// 声明持久化队列
channel.QueueDeclare(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
// 队列绑定交换机,通过RoutingKey路由
channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: eventName);
// 异步消费者
var consumer = new AsyncEventingBasicConsumer(channel);
// 绑定消息处理事件
consumer.Received += Consumer_Received;
// 启动消费,关闭自动ACK,手动确认避免消息丢失
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
Console.WriteLine("【消费者】等待订单支付事件...");
Console.ReadLine();
// 消息处理逻辑
async Task Consumer_Received(object sender, BasicDeliverEventArgs args)
{
try
{
var bytes = args.Body.ToArray();
string msg = Encoding.UTF8.GetString(bytes);
Console.WriteLine($"【消费者】收到集成事件:{msg}");
// 执行业务:发送短信、推送通知、更新库存
await Task.Delay(800);
// 手动ACK,确认消息处理完成
channel.BasicAck(args.DeliveryTag, multiple: false);
}
catch (Exception ex)
{
// 消费失败:重新放回队列重试,生产环境可加死信队列
channel.BasicReject(args.DeliveryTag, requeue: true);
Console.WriteLine($"【消费者】消息处理失败:{ex.Message},已重新入队");
}
}
注意:原生RabbitMQ代码冗余、切换MQ成本高,推荐使用杨老师的Zack.EventBus开源库,优势在于屏蔽底层MQ差异,切换RabbitMQ/Kafka/Redis无需修改业务代码;自动实现消息序列化、ACK、异常处理、重试机制;贴合DDD集成事件规范,与MediatR领域事件无缝衔接;降低分布式事件开发门槛,减少重复代码。
二、洋葱架构
洋葱架构是DDD落地的核心架构模式,彻底解决传统三层架构的痛点,实现业务核心与技术实现解耦,是复杂业务系统、微服务的首选架构。
1.传统三层架构缺点
- 面向数据库设计,而非业务驱动,违背DDD理念。
- 简单CRUD也需要BLL层转发,代码冗余严重。
- 单向依赖,内层无法调用外层,扩展性差。
- 业务逻辑散落各层,难以维护和测试。
2.洋葱架构原则
- 依赖方向一律向内,内层定义接口,外层实现细节。
- 内层更抽象,表达业务核心(领域、规则),不依赖任何框架、数据库、UI。
- 外层更具体,实现技术细节(EF、MQ、控制器、仓储)。
- 内层通过依赖注入间接调用外层,实现解耦。
- 业务逻辑集中在领域层,保证核心代码纯净、可移植。
3.洋葱架构分层
- 领域核心层:实体、值对象、聚合根、领域事件、领域接口。
- 领域服务层:封装核心业务规则,纯业务逻辑,无技术依赖。
- 应用服务层:协调领域服务、外部系统,处理业务用例,不写核心规则。
- 基础设施层:实现仓储、EF Core、MQ、日志、缓存等技术细节。
- 接口层:Web API、控制器、前端交互,对外提供服务入口。
4.代码示例
领域层:定义仓储接口
领域层只定义接口,不涉及具体实现,完全脱离数据库、EF框架。
cs
// 领域层:通用仓储接口(内层)
public interface IRepository<T> where T : class, IAggregateRoot
{
Task<T?> GetByIdAsync(int id);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
// 领域层:用户专属仓储接口
public interface IUserRepository : IRepository<User>
{
// 自定义业务查询
Task<User?> GetByUserNameAsync(string userName);
}
领域服务层:核心业务逻辑
领域服务只依赖内层仓储接口,不接触任何技术细节,专注业务规则。
cs
// 领域服务:用户领域核心业务(DDD核心)
public class UserDomainService
{
private readonly IUserRepository _userRepository;
// 依赖注入仓储接口,不依赖具体实现
public UserDomainService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
/// <summary>
/// 用户注册业务(包含完整业务规则)
/// </summary>
public async Task RegisterUserAsync(string userName, string password)
{
// 业务规则1:校验用户名是否重复
var existUser = await _userRepository.GetByUserNameAsync(userName);
if (existUser != null)
throw new InvalidOperationException("用户名已存在,请勿重复注册");
// 业务规则2:通过充血模型创建用户,自带参数校验
var user = new User(userName);
user.ChangePassword(password); // 充血模型内置密码长度校验
// 持久化交由仓储实现,领域服务不关心数据库操作
await _userRepository.AddAsync(user);
}
/// <summary>
/// 用户积分增加业务
/// </summary>
public void AddUserCredit(User user, int credit)
{
if (credit <= 0)
throw new InvalidOperationException("积分必须为正数");
// 直接操作充血模型,保证状态合法性
user.AddCredit(credit);
}
}
基础设施层:仓储实现
基础设施层实现领域层定义的仓储接口,依托EF Core完成数据库操作。
cs
// 基础设施层:EF仓储基类
public class EfRepository<T> : IRepository<T> where T : class, IAggregateRoot
{
protected readonly AppDbContext DbContext;
public EfRepository(AppDbContext dbContext)
{
DbContext = dbContext;
}
public async Task<T?> GetByIdAsync(int id)
{
return await DbContext.Set<T>().FindAsync(id);
}
public async Task AddAsync(T entity)
{
await DbContext.Set<T>().AddAsync(entity);
}
public Task UpdateAsync(T entity)
{
DbContext.Set<T>().Update(entity);
return Task.CompletedTask;
}
public Task DeleteAsync(T entity)
{
DbContext.Set<T>().Remove(entity);
return Task.CompletedTask;
}
}
// 基础设施层:用户仓储实现
public class EfUserRepository : EfRepository<User>, IUserRepository
{
public EfUserRepository(AppDbContext dbContext) : base(dbContext)
{
}
public async Task<User?> GetByUserNameAsync(string userName)
{
return await DbContext.Users.FirstOrDefaultAsync(u => u.UserName == userName);
}
}
应用服务层:业务用例协调
cs
// 应用服务:对外提供业务接口,不写核心规则
public class UserAppService
{
private readonly UserDomainService _userDomainService;
private readonly IUnitOfWork _unitOfWork;
public UserAppService(UserDomainService userDomainService, IUnitOfWork unitOfWork)
{
_userDomainService = userDomainService;
_unitOfWork = unitOfWork;
}
/// <summary>
/// 用户注册接口(供控制器调用)
/// </summary>
public async Task RegisterAsync(string userName, string password)
{
// 应用层仅协调,业务逻辑交由领域服务
await _userDomainService.RegisterUserAsync(userName, password);
// 工作单元统一提交事务
await _unitOfWork.SaveChangesAsync();
}
}
三、DDD完整版脚手架
补充洋葱架构必备的工作单元、规约模式、事务管控、全局异常。
1.工作单元模式
统一管理事务、批量提交数据库操作,保证数据一致性,避免多次SaveChanges导致的事务问题。
cs
// 领域层:工作单元接口
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
// 基础设施层:工作单元实现
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _dbContext;
public UnitOfWork(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// 统一提交事务,附带领域事件发布
return await _dbContext.SaveChangesAsync(cancellationToken);
}
public void Dispose()
{
_dbContext.Dispose();
}
}
2.规约模式
封装查询条件,将查询规则从应用层剥离至领域层,保证查询逻辑复用、业务规则统一。
cs
// 领域层:规约基类
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
return ToExpression().Compile()(entity);
}
}
// 领域层:用户活跃规约示例
public class ActiveUserSpecification : Specification<User>
{
public override Expression<Func<User, bool>> ToExpression()
{
return u => u.Credit > 0 && !string.IsNullOrEmpty(u.PasswordHash);
}
}
3.事务管控与分布式事务
- 单体事务:依托工作单元+EF Core事务,保证单服务数据一致性。
- 分布式事务:集成事件+最终一致性,通过RabbitMQ消息确认、重试机制实现。
- 避免本地事务+消息队列混合使用导致的数据不一致。
4.全局异常处理
统一捕获业务异常、框架异常,返回标准化响应,提升接口健壮性。
cs
// 全局异常中间件
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
public GlobalExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (InvalidOperationException ex)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new { Code = 400, Msg = ex.Message });
}
catch (Exception ex)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new { Code = 500, Msg = "服务器异常,请稍后重试" });
}
}
}
5.服务注册
Program.cs
cs
// 注册DDD核心服务
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IUserRepository, EfUserRepository>();
builder.Services.AddScoped<UserDomainService>();
builder.Services.AddScoped<UserAppService>();
// 注册全局异常中间件
app.UseMiddleware<GlobalExceptionMiddleware>();
总结
- 集成事件:RabbitMQ实现跨微服务通信,手动ACK+持久化保证消息可靠,Zack.EventBus简化开发。
- 洋葱架构:依赖向内、业务与技术解耦,领域层纯净无框架依赖,领域服务封装核心规则。
- 脚手架核心:工作单元管控事务、规约模式封装查询、全局异常统一处理,落地DDD实践。