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
相关产品推荐
相关产品推荐

