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

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

我一直在使用以下代码生成唯一键,用于将我的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))AndAlso(Farm.Deleted == False)))

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

同样转换y来说出正确的类型名称会很好…..

正如Polity和Marc在他们的评论中所说,你需要的是LINQ表达式的部分评估者。 您可以在Matt Warren的LINQ:构建IQueryable Provider中使用ExpressionVisitor来阅读如何做到这一点– 第三部分 。 由Pete Montgomery (链接到Polity) 缓存LINQ查询结果的文章描述了关于这种缓存的一些更具体的细节,例如如何在查询中表示集合。

另外,我不确定我会像这样依赖ToString() 。 我认为它主要用于调试目的,并且可能在将来发生变化。 另一种方法是创建自己的IEqualityComparer ,它可以为任何表达式创建哈希码,并可以比较两个表达式的相等性。 我也许会使用ExpressionVisitor来做这件事,但这样做会非常繁琐。

我一直试图弄清楚这种方法可能有用的情况,而不会导致难以维护的膨胀缓存。

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

  • 您是如何计划管理参数排序的? IE浏览器。 (x => x.blah ==“slug”&&!x.Deleted)缓存键应该等于(x =>!x.Deleted && x.blah ==“slug”)缓存键。
  • 您是如何计划避免缓存中的重复对象的? IE浏览器。 根据设计,来自多个查询的相同服务器区域将与每个查询单独缓存。 比如说,对于农场中出现的每个slu,,我们都有一个单独的农场副本。
  • 使用更多参数(例如parcel,farmer等)扩展上述内容将导致更多匹配的查询,每个查询都具有缓存的服务器场的单独副本。 这同样适用于您可能查询的每种类型以及参数可能不是相同的顺序
  • 现在,如果您更新服务器场会发生什么? 在不知道哪个缓存查询将包含您的服务器场的情况下,您将被迫终止整个缓存。 哪种方式会对你想要实现的目标起反作用。

我可以看到这种方法背后的原因。 0维护性能层。 但是,如果不考虑上述几点,该方法将首先杀死性能,然后导致大量尝试维护它,然后certificate是完全不可维护的。

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

当结果来自后端,并且每种类型的扩展方法单独或通过公共接口时,我通过单独缓存每个结果实体找到了更好的方法。

然后,您可以为lambda表达式构建扩展方法,以便在命中db之前首先尝试缓存。

 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从数据库返回满足查询B的3个服务器场,其中一个匹配服务器场来自缓存,而数据库实际上有20个匹配服务器场可用。 因此,每个查询stll需要至少命中一次DB。

并且您需要跟踪返回0结果的查询,以避免它们因此无法访问数据库。

但总而言之,你可以减少很多代码,作为奖励,当你更新一个农场时,你可以

 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 list = new List(); 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(); }