You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 13:45:34