将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
转换为正确的类型名称也很好。。。。。
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
实例,并使用表达式调用其Visit
或VisitAndConvert
方法。
请注意,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();
}