ASP.NET Core中如何为Ardalis Specification查询生成唯一缓存键
为Ardalis Specification生成唯一缓存键的解决方案
要解决不同Specification实例的缓存键唯一性问题,核心是把Specification中所有决定查询结果的关键信息(查询条件、关联包含、排序、分页等)都纳入缓存键的生成逻辑。以下是具体实现方案:
1. 核心思路
Ardalis Specification的Criteria、Includes、OrderExpressions、Skip/Take等属性完整描述了查询的行为,我们可以将这些属性转化为稳定的字符串,再通过哈希生成唯一标识,最终组合成缓存键。
2. 实现缓存键生成器
创建一个静态工具类,负责提取Specification的关键信息并生成唯一哈希值:
using Ardalis.Specification; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Linq.Expressions; public static class SpecificationCacheKeyGenerator { public static string Generate<T>(ISpecification<T> specification) where T : class { var keyParts = new List<string> { // 加入实体类型全限定名,区分不同实体的查询 typeof(T).FullName }; // 加入查询条件表达式 if (specification.Criteria != null) { keyParts.Add(GetStableExpressionString(specification.Criteria)); } // 加入Include路径(排序后拼接,避免顺序不同导致键不一致) var includePaths = specification.Includes .Select(GetFullIncludePath) .OrderBy(path => path); keyParts.AddRange(includePaths); // 加入排序规则(包含字段和排序方向) var orderRules = specification.OrderExpressions .Select(o => $"{GetStableExpressionString(o.Expression)}_{o.OrderType}") .OrderBy(rule => rule); keyParts.AddRange(orderRules); // 加入分页参数 if (specification.Skip.HasValue) { keyParts.Add($"Skip:{specification.Skip.Value}"); } if (specification.Take.HasValue) { keyParts.Add($"Take:{specification.Take.Value}"); } // 加入额外的Where条件(如果使用了AddWhere扩展) var whereExpressions = specification.WhereExpressions .Select(w => GetStableExpressionString(w.Expression)) .OrderBy(expr => expr); keyParts.AddRange(whereExpressions); // 拼接所有部分并生成SHA256哈希(避免过长字符串) var combinedString = string.Join("|", keyParts); using var sha256 = SHA256.Create(); var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combinedString)); // 转换为URL安全的Base64字符串 return Convert.ToBase64String(hashBytes).Replace('/', '_').Replace('+', '-'); } // 获取Include的完整路径(包含ThenInclude) private static string GetFullIncludePath<T>(IncludeExpressionInfo<T> include) { var path = include.PropertyPath; if (include.ThenIncludeExpressions.Any()) { path += "." + string.Join(".", include.ThenIncludeExpressions.Select(t => t.PropertyPath)); } return path; } // 将表达式转换为稳定的字符串(可根据需求替换为更精准的实现) private static string GetStableExpressionString(Expression expression) { // 基础实现:使用Expression.ToString(),如果需要更稳定的结果,可以: // 1. 使用Ardalis.Specification中的ExpressionPrinter // 2. 自定义表达式树遍历,标准化参数名称等 return expression.ToString(); } }
3. 在缓存代理类中使用生成器
修改CachedReadonlyRepositoryProxy<T>,在查询方法中调用生成器创建缓存键:
using Ardalis.Specification; using Microsoft.Extensions.Caching.Memory; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; public class CachedReadonlyRepositoryProxy<T> : IQueryRepository<T> where T : class { private readonly IQueryRepository<T> _innerRepo; private readonly IMemoryCache _cache; private readonly MemoryCacheEntryOptions _defaultCacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }; public CachedReadonlyRepositoryProxy(IQueryRepository<T> innerRepo, IMemoryCache cache) { _innerRepo = innerRepo; _cache = cache; } public async Task<T> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken cancellationToken = default) { var cacheKey = $"CachedRepo:FirstOrDefault:{SpecificationCacheKeyGenerator.Generate(spec)}"; return await _cache.GetOrCreateAsync(cacheKey, async entry => { entry.SetOptions(_defaultCacheOptions); return await _innerRepo.FirstOrDefaultAsync(spec, cancellationToken); }); } public async Task<List<T>> ListAsync(ISpecification<T> spec, CancellationToken cancellationToken = default) { var cacheKey = $"CachedRepo:List:{SpecificationCacheKeyGenerator.Generate(spec)}"; return await _cache.GetOrCreateAsync(cacheKey, async entry => { entry.SetOptions(_defaultCacheOptions); return await _innerRepo.ListAsync(spec, cancellationToken); }); } // 实现IQueryRepository<T>的其他方法,同理处理缓存键 }
4. 关键注意事项
- 表达式字符串稳定性:默认的
Expression.ToString()可能会因为变量名、表达式写法不同生成不同字符串(比如x => x.Id == id和x => x.Id == userId逻辑相同但字符串不同)。如果需要严格按查询逻辑生成键,可以自定义表达式树遍历,标准化参数名称,或者使用System.Linq.Dynamic.Core库将表达式转换为动态查询字符串。 - 动态参数处理:如果Specification通过构造函数传入动态参数(如
public UserByIdSpec(int id) => Criteria = x => x.Id == id;),Criteria.ToString()不会包含参数值,这时候需要手动将参数值加入缓存键的parts列表。比如在生成器中添加反射逻辑,提取Specification的自定义属性值并加入keyParts。 - 缓存策略:根据业务场景调整过期时间,可使用滑动过期(
SlidingExpiration)替代绝对过期,或者结合缓存刷新机制。
内容的提问来源于stack exchange,提问作者voryba
相关产品推荐
相关产品推荐

