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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 13:22:04