EF Core中BeforeSaveChanges结合缓存做实体校验的实现方案咨询
实现方案
基于你当前的MediatR+EF Core+CQRS技术栈,最合理的实现方式是用请求作用域级别的状态容器做跨层数据传递,完全不破坏原有架构分层,也不会出现跨请求的数据污染问题,具体实现步骤如下:
1. 定义作用域状态容器
先定义一个Scoped生命周期的存储容器,单次请求内全局共享,用来存UI层传入的需要额外校验的实体名称列表,避免UI层和DbContext直接耦合。
/// <summary> /// 单次请求内共享的待校验实体存储 /// </summary> public interface IValidatedEntityListStore { HashSet<string> EntityNamesToValidate { get; } } public class ValidatedEntityListStore : IValidatedEntityListStore { // 忽略大小写对比实体名,避免大小写不一致导致匹配失效 public HashSet<string> EntityNamesToValidate { get; } = new(StringComparer.OrdinalIgnoreCase); }
在Program.cs中注册服务:
builder.Services.AddScoped<IValidatedEntityListStore, ValidatedEntityListStore>();
2. 用MediatR管道统一填充待校验列表
不用在每个接口处理逻辑里手动传值,利用MediatR的管道行为,在请求进入Handler之前统一把UI传入的待校验实体列表写入状态容器,减少重复代码。
public class PopulateValidatedEntityListBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IValidatedEntityListStore _store; private readonly IHttpContextAccessor _httpContextAccessor; public PopulateValidatedEntityListBehavior( IValidatedEntityListStore store, IHttpContextAccessor httpContextAccessor) { _store = store; _httpContextAccessor = httpContextAccessor; } public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { // 示例:从请求头读取UI传入的待校验实体列表,逗号分隔,你可以根据自己的传参位置调整 if (_httpContextAccessor.HttpContext?.Request.Headers .TryGetValue("x-validate-entities", out var entityNames) == true) { var names = entityNames.ToString() .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var name in names) { _store.EntityNamesToValidate.Add(name); } } // 如果待校验列表是放在请求DTO中传递,可以让对应请求实现IHasEntityValidationList接口,直接取值即可,性能更好 // if (request is IHasEntityValidationList validationRequest) // { // foreach (var name in validationRequest.ValidatedEntityNames) // { // _store.EntityNamesToValidate.Add(name); // } // } return await next(); } }
在Program.cs中注册管道行为:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PopulateValidatedEntityListBehavior<,>));
3. 改造DbContext的BeforeSaveChanges逻辑
在DbContext中注入状态容器,保存前过滤出待校验列表内的实体,执行额外校验逻辑。为了解耦校验规则,先定义实体校验接口,让需要校验的实体自己实现校验逻辑,避免校验规则散落在DbContext中。
/// <summary> /// 需要触发保存前校验的实体实现该接口 /// </summary> public interface IBeforeSaveValidate { /// <summary> /// 校验不通过直接抛业务异常即可,由全局异常处理中间件统一捕获返回UI /// </summary> void ValidateOnSave(); }
重写DbContext的保存方法,加入BeforeSaveChanges逻辑:
public class ApplicationDbContext : DbContext { private readonly IValidatedEntityListStore? _validatedStore; // 构造函数注入,给存储参数加默认null,兼容后台任务等无HttpContext的场景,避免报错 public ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, IValidatedEntityListStore? validatedStore = null) : base(options) { _validatedStore = validatedStore; } public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { RunBeforeSaveValidation(); return await base.SaveChangesAsync(cancellationToken); } public override int SaveChanges() { RunBeforeSaveValidation(); return base.SaveChanges(); } private void RunBeforeSaveValidation() { // 没有待校验实体直接跳过,不影响原有逻辑 if (_validatedStore?.EntityNamesToValidate.Any() != true) return; // 取所有被追踪的、状态为新增/修改/删除的实体 var changedEntries = ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .ToList(); foreach (var entry in changedEntries) { var entityName = entry.Entity.GetType().Name; // 只校验UI传入列表内的实体 if (!_validatedStore.EntityNamesToValidate.Contains(entityName)) continue; // 实体实现了校验接口就执行校验 if (entry.Entity is IBeforeSaveValidate validateEntity) { validateEntity.ValidateOnSave(); } } } }
4. 实体侧实现校验规则
需要做额外校验的实体直接实现IBeforeSaveValidate接口,把校验规则内聚在实体内部:
public class Product : IBeforeSaveValidate { public int Id { get; set; } public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public void ValidateOnSave() { if (string.IsNullOrWhiteSpace(Name)) throw new BusinessException("产品名称不能为空"); if (Price <= 0) throw new BusinessException("产品价格必须大于0"); } }
方案优势
- 完全符合CQRS分层规范,各层职责清晰,没有硬依赖
- Scoped生命周期的容器保证请求间数据隔离,不会出现串数据的问题
- 管道统一处理传参逻辑,不用每个Handler重复写传值代码
- 校验逻辑内聚在实体中,符合领域驱动设计的思路,维护成本低
- 兼容后台任务、数据迁移等非UI场景,不会因为缺少依赖报错
内容的提问来源于stack exchange,提问作者Yan
相关产品推荐
相关产品推荐

