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

EF Core中如何实现基于查询参数的全局自动缓存(仿OutputCache)

基于Repository模式的LINQ自动缓存方案

核心思路

模仿ASP.NET Core OutputCache的声明式配置逻辑,通过自定义特性+代理拦截器,在Repository层自动拦截带缓存标记的LINQ查询方法,结合方法签名、参数及查询表达式生成唯一缓存键,自动缓存查询结果。同时支持全局默认配置与局部方法手动覆盖,彻底替代逐个手动编写缓存的重复工作。

实现步骤

1. 定义缓存配置特性

创建自定义特性,用于标记需要缓存的Repository方法,支持配置过期时间、缓存键前缀、是否忽略参数等:

[AttributeUsage(AttributeTargets.Method)]
public class QueryCacheAttribute : Attribute
{
    // 默认缓存5分钟
    public int ExpirationSeconds { get; set; } = 300;
    public string? CacheKeyPrefix { get; set; }
    // 是否忽略参数生成缓存键(适用于无参数的固定查询)
    public bool IgnoreParameters { get; set; } = false;
}

2. 实现缓存代理拦截器

使用.NET原生DispatchProxy创建Repository代理类,拦截方法调用处理缓存逻辑:

public class RepositoryCacheProxy<T> : DispatchProxy
{
    private T? _targetRepo;
    private ICacheService _cacheService;
    private ILogger<RepositoryCacheProxy<T>> _logger;

    public static T Create(T targetRepo, ICacheService cacheService, ILogger<RepositoryCacheProxy<T>> logger)
    {
        var proxy = Create<T, RepositoryCacheProxy<T>>();
        ((RepositoryCacheProxy<T>)proxy).SetDependencies(targetRepo, cacheService, logger);
        return (T)proxy;
    }

    private void SetDependencies(T targetRepo, ICacheService cacheService, ILogger<RepositoryCacheProxy<T>> logger)
    {
        _targetRepo = targetRepo ?? throw new ArgumentNullException(nameof(targetRepo));
        _cacheService = cacheService;
        _logger = logger;
    }

    protected override object? Invoke(MethodInfo targetMethod, object?[]? args)
    {
        var cacheAttr = targetMethod.GetCustomAttribute<QueryCacheAttribute>();
        // 无缓存标记,直接执行原方法
        if (cacheAttr == null) return targetMethod.Invoke(_targetRepo, args);

        try
        {
            var cacheKey = GenerateCacheKey(targetMethod, args, cacheAttr);
            // 尝试从缓存获取结果
            var cachedResult = _cacheService.Get(cacheKey);
            if (cachedResult != null) return cachedResult;

            // 执行数据库查询
            var dbResult = targetMethod.Invoke(_targetRepo, args);
            if (dbResult != null)
            {
                // 将结果存入缓存
                _cacheService.Set(cacheKey, dbResult, TimeSpan.FromSeconds(cacheAttr.ExpirationSeconds));
            }
            return dbResult;
        }
        catch (Exception ex)
        {
            // 缓存出错时降级执行原查询,不阻断业务
            _logger.LogError(ex, "Cache operation failed, falling back to direct DB query");
            return targetMethod.Invoke(_targetRepo, args);
        }
    }

    private string GenerateCacheKey(MethodInfo method, object?[]? args, QueryCacheAttribute cacheAttr)
    {
        var keyParts = new List<string>();
        // 添加自定义前缀(可选)
        if (!string.IsNullOrEmpty(cacheAttr.CacheKeyPrefix))
            keyParts.Add(cacheAttr.CacheKeyPrefix);
        
        // 添加方法唯一标识
        keyParts.Add($"{method.DeclaringType?.FullName}.{method.Name}");

        // 若不忽略参数,添加参数哈希值
        if (!cacheAttr.IgnoreParameters && args != null && args.Length > 0)
        {
            foreach (var arg in args)
                keyParts.Add(arg?.GetHashCode().ToString() ?? "null");
        }

        // 若返回IQueryable,添加查询表达式哈希值
        if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>))
        {
            var queryArg = args?.FirstOrDefault(a => a is IQueryable);
            if (queryArg != null)
                keyParts.Add(((IQueryable)queryArg).Expression.GetHashCode().ToString());
        }

        return string.Join(":", keyParts);
    }
}

3. 实现统一缓存服务接口

封装缓存操作,支持MemoryCache(开发环境)和Redis(生产环境)切换:

public interface ICacheService
{
    object? Get(string key);
    void Set(string key, object value, TimeSpan expiration);
    void Remove(string key);
}

// MemoryCache实现
public class MemoryCacheService : ICacheService
{
    private readonly IMemoryCache _cache;

    public MemoryCacheService(IMemoryCache cache)
    {
        _cache = cache;
    }

    public object? Get(string key) => _cache.TryGetValue(key, out var value) ? value : null;

    public void Set(string key, object value, TimeSpan expiration) => _cache.Set(key, value, expiration);

    public void Remove(string key) => _cache.Remove(key);
}

4. 全局注册与配置

在Program.cs中注册缓存服务及代理包装后的Repository:

builder.Services.AddMemoryCache();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

// 注册Repository时用缓存代理包装
builder.Services.AddScoped<ICommandQueryRepository>(sp =>
{
    var repo = new CommandQueryRepository(sp.GetRequiredService<AppDbContext>());
    var cacheService = sp.GetRequiredService<ICacheService>();
    var logger = sp.GetRequiredService<ILogger<RepositoryCacheProxy<ICommandQueryRepository>>>();
    return RepositoryCacheProxy<ICommandQueryRepository>.Create(repo, cacheService, logger);
});

5. 改造业务代码

将原查询逻辑迁移到Repository并标记缓存特性:

public interface ICommandQueryRepository
{
    // 标记为缓存10分钟
    [QueryCache(ExpirationSeconds = 600)]
    CommandQuery? GetCommandQuery(string commandFullName, string? responseFullName);
}

public class CommandQueryRepository : ICommandQueryRepository
{
    private readonly AppDbContext _dbContext;

    public CommandQueryRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public CommandQuery? GetCommandQuery(string commandFullName, string? responseFullName)
    {
        return _dbContext.CommandQueries
            .Include(x => x.CommandApprovalFlowConfiguration)
            .ThenInclude(x => x.ApprovalFlowConfiguration)
            .ThenInclude(x => x.ApprovalFlowConfigurationDetails)
            .Where(x =>
                x.FullName == commandFullName
                && x.ResponseFullName == responseFullName
            )
            .FirstOrDefault();
    }
}

原业务方法简化为:

public bool IsApprovalFlowRequired(Type commandType, Type? responseType)
{
    try
    {
        var command = _commandQueryRepository.GetCommandQuery(
            commandType.FullName!, 
            responseType?.FullName
        );

        return command?.CommandApprovalFlowConfiguration?.ApprovalFlowConfiguration?.ApprovalFlowConfigurationDetails?.Count > 0;
    }
    catch (Exception ex)
    {
        return false;
    }
}

扩展优化点

  • 缓存失效:新增InvalidateCache特性,在数据更新方法上标记,自动清除关联缓存键。
  • 全局配置:通过appsettings.json设置默认过期时间、缓存提供器等全局参数。
  • 表达式优化:对复杂LINQ表达式的哈希生成逻辑做优化,避免因表达式树的细微差异导致缓存键不一致。

内容的提问来源于stack exchange,提问作者Abir Mahmud

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 08:35:59