将lambda表达式转换为用于缓存的唯一键

本文关键字:缓存 唯一 一键 用于 lambda 表达式 转换 | 更新日期: 2023-09-27 18:28:01

我看过其他类似的问题,但找不到任何可行的答案。

我一直在使用以下代码生成唯一密钥,用于将linq查询的结果存储到缓存中。

    string key = ((LambdaExpression)expression).Body.ToString();
    foreach (ParameterExpression param in expression.Parameters)
    {
        string name = param.Name;
        string typeName = param.Type.Name;
        key = key.Replace(name + ".", typeName + ".");
    }
    return key;

它似乎适用于包含整数或布尔值的简单查询,但当我的查询包含嵌套常量表达式时,例如

// Get all the crops on a farm where the slug matches the given slug.
(x => x.Crops.Any(y => slug == y.Slug) && x.Deleted == false)

因此,返回的密钥是:

(True AndAlso(Farm.Crops.Any(y=>(value(OzFarmGuide.Controllers.FarmController+<>c_DisplayClassd).slug==y.Slug))以及(Farm.Deleted==False))

正如你所看到的,我传递的任何作物名称都会给出相同的关键结果。有没有一种方法可以提取给定参数的值,以便区分我的查询?

另外,将y转换为正确的类型名称也很好。。。。。

将lambda表达式转换为用于缓存的唯一键

正如Polity和Marc在评论中所说,您需要的是LINQ表达式的部分求值器。您可以在Matt Warren的《LINQ:构建IQueryable Provider-Part III》中阅读如何使用ExpressionVisitor来实现这一点。Pete Montgomery(Polity链接到)的文章《缓存LINQ查询的结果》(Caching The results of LINQ querys)描述了关于这种缓存的更多细节,例如如何在查询中表示集合。

此外,我不确定我是否会像这样依赖ToString()。我认为这主要是为了调试目的,将来可能会发生变化。另一种选择是创建自己的IEqualityComparer<Expression>,它可以为任何表达式创建哈希代码,并可以比较两个表达式是否相等。我可能也会使用ExpressionVisitor来做到这一点,但这样做会非常乏味。

我一直在尝试找出一种方案,在这种方案中,这种方法可能很有用,而不会导致难以维护的缓存膨胀。

我知道这并不能直接回答你的问题,但我想提出一些关于这种方法的问题,一开始可能听起来很诱人:

  • 您计划如何管理参数排序?Ie.(x=>x.blah=="slug"&&!x.Deleted)缓存键应等于(x=>!x.Deleted&&x.blah==="slue")缓存键
  • 您计划如何避免缓存中的重复对象?Ie.根据设计,来自多个查询的同一场将与每个查询单独缓存。比方说,对于农场中出现的每只蛞蝓,我们都有一个农场的单独副本
  • 用更多的参数(如包裹、农民等)扩展以上内容将导致更多的匹配查询,每个查询都缓存了农场的单独副本。这同样适用于您可能查询的每种类型,而且参数的顺序可能不相同
  • 现在,如果更新农场会发生什么?在不知道哪些缓存查询将包含您的场的情况下,您将被迫终止整个缓存。哪一种会对你想要实现的目标产生反作用

我能理解这种方法背后的原因。0维护性能层。然而,如果不考虑以上几点,这种方法将首先扼杀性能,然后导致大量的维护尝试,然后被证明是完全无法维护的。

我一直走在这条路上。最终浪费了很多时间,放弃了。

当结果来自后端时,我发现了一种更好的方法,即分别缓存每个结果实体,并为每个类型分别或通过一个公共接口使用扩展方法。

然后,您可以为lambda表达式构建扩展方法,以便在访问数据库之前先尝试缓存。

var query = (x => x.Crops.Any(y => slug == y.Slug) && x.Deleted == false);
var results = query.FromCache();
if (!results.Any()) {
    results = query.FromDatabase();
    results.ForEach(x = x.ToCache());
}

当然,您仍然需要跟踪哪些查询实际命中了数据库,以避免查询A从数据库返回3个场,满足查询B从缓存中返回一个匹配场的要求,而数据库实际上有20个匹配场可用。因此,每个查询stll至少需要命中DB一次。

并且,您需要跟踪返回0结果的查询,以避免它们因此而毫无意义地访问DB。

但总的来说,你可以用更少的代码,而且作为奖励,当你更新农场时,你可以

var farm = (f => f.farmId == farmId).FromCache().First();
farm.Name = "My Test Farm";
var updatedFarm = farm.ToDatabase();
updatedFarm.ToCache();

这个怎么样?

public class KeyGeneratorVisitor : ExpressionVisitor
{
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return Expression.Parameter(node.Type, node.Type.Name);
    }
    protected override Expression VisitMember(MemberExpression node)
    {
        if (CanBeEvaluated(node))
        {
            return Expression.Constant(Evaluate(node));
        }
        else
        {
            return base.VisitMember(node);
        }
    }
    private static bool CanBeEvaluated(MemberExpression exp)
    {
        while (exp.Expression.NodeType == ExpressionType.MemberAccess)
        {
            exp = (MemberExpression) exp.Expression;
        }
        return (exp.Expression.NodeType == ExpressionType.Constant);
    }
    private static object Evaluate(Expression exp)
    {
        if (exp.NodeType == ExpressionType.Constant)
        {
            return ((ConstantExpression) exp).Value;
        }
        else
        {
            MemberExpression mexp = (MemberExpression) exp;
            object value = Evaluate(mexp.Expression);
            FieldInfo field = mexp.Member as FieldInfo;
            if (field != null)
            {
                return field.GetValue(value);
            }
            else
            {
                PropertyInfo property = (PropertyInfo) mexp.Member;
                return property.GetValue(value, null);
            }
        }
    }
}

这将把复杂常量表达式替换为其原始值,并把参数名称替换为其类型名称。因此,只需创建一个新的KeyGeneratorVisitor实例,并使用表达式调用其VisitVisitAndConvert方法。

请注意,Expression.ToString方法也将在您的复杂类型上调用,因此可以重写它们的ToString方法,也可以在Evaluate方法中为它们编写自定义逻辑。

怎么样:

var call = expression.Body as MethodCallExpression;
if (call != null)
{
    List<object> list = new List<object>();
    foreach (Expression argument in call.Arguments)
    {
        object o = Expression.Lambda(argument, expression.Parameters).Compile().DynamicInvoke();
        list.Add(o);
    }
    StringBuilder keyValue = new StringBuilder();
    keyValue.Append(expression.Body.ToString());
    list.ForEach(e => keyValue.Append(String.Format("_{0}", e.ToString())));
    string key = keyValue.ToString();
}