如何获取触发数据操作的MediatR命令名称以扩展实体审计功能
实现思路
- 无侵入实现的核心是用
AsyncLocal<T>做命令上下文传递,这个类型会绑定异步执行流,每个请求/任务的上下文独立,不会出现并发请求下命令名串值的问题,适配性比HttpContext Item、静态变量更强,哪怕是不经过HTTP管道的后台MediatR任务,逻辑也能正常生效。 - 实现MediatR通用管道行为,在所有命令执行前把当前命令的类型名写入AsyncLocal存储,请求执行完成(无论成功还是抛出异常)后清空存储,避免上下文污染。
- 直接在你现有ApplicationDbContext的ChangeTracker审计逻辑里,从AsyncLocal读取当前关联的命令名,写入实体对应的审计字段即可,不需要修改任何现有业务Command和Handler的代码。
- 如果不需要记录查询类请求的名称,可以额外定义一个空的
ICommand标记接口,让所有写操作命令实现该接口,管道行为只拦截实现了ICommand的请求即可。
参考代码示例
1. 定义审计上下文存储
/// <summary> /// 跨异步流传递审计相关上下文信息 /// </summary> public static class AuditContext { private static readonly AsyncLocal<string> _currentCommandName = new(); /// <summary> /// 获取当前执行流关联的MediatR命令名 /// </summary> public static string CurrentCommandName => _currentCommandName.Value ?? "DirectDbOperation"; /// <summary> /// 设置当前执行流关联的命令名 /// </summary> public static void SetCurrentCommand(string commandName) { _currentCommandName.Value = commandName; } /// <summary> /// 清空当前上下文,避免后续操作关联错误命令 /// </summary> public static void Clear() { _currentCommandName.Value = null; } }
这里默认值给DirectDbOperation,用来标识没有走MediatR管道、直接操作DbContext的场景,方便后续排查问题。
2. 实现MediatR审计管道行为
/// <summary> /// MediatR管道,自动记录当前执行的命令名到审计上下文 /// </summary> public class AuditCommandPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { // 只需要记录写命令的话,这里可以加判断,比如if(!typeof(TRequest).IsAssignableTo(typeof(ICommand))) 直接return await next(); var commandName = typeof(TRequest).Name; AuditContext.SetCurrentCommand(commandName); try { return await next(); } finally { // 务必放在finally块,哪怕请求抛异常也要清空上下文 AuditContext.Clear(); } } }
注册管道到DI容器,在Program.cs里添加如下代码:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(AuditCommandPipelineBehavior<,>));
3. 扩展现有DbContext审计逻辑
在你原来遍历ChangeTracker填充审计字段的逻辑里,补充命令名的赋值即可:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { var operationTime = DateTime.UtcNow; var operatorId = GetCurrentAuthenticatedUserId(); // 你原有获取当前操作人ID的逻辑 var sourceCommand = AuditContext.CurrentCommandName; foreach (var entry in ChangeTracker.Entries<IAuditableEntity>()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedTime = operationTime; entry.Entity.CreatorId = operatorId; entry.Entity.CreateCommand = sourceCommand; // 新增审计字段,记录创建操作来源命令 break; case EntityState.Modified: entry.Entity.LastModifiedTime = operationTime; entry.Entity.LastModifierId = operatorId; entry.Entity.LastModifyCommand = sourceCommand; // 新增审计字段,记录修改操作来源命令 break; case EntityState.Deleted: // 如果你用了软删除审计,逻辑和上面一致 if (entry.Entity is ISoftDelete softDeleteEntity) { softDeleteEntity.IsDeleted = true; softDeleteEntity.DeleteTime = operationTime; softDeleteEntity.DeleterId = operatorId; softDeleteEntity.DeleteCommand = sourceCommand; entry.State = EntityState.Modified; } break; } } return await base.SaveChangesAsync(cancellationToken); }
补充说明:如果单个命令内存在多次SaveChangesAsync调用,这个逻辑也能正常关联到同一个命令名;如果需要区分同个命令内的多次保存,可以在AuditContext里扩展额外的标识字段按需记录即可。
内容的提问来源于stack exchange,提问作者Yan
相关产品推荐
相关产品推荐

