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

C# 将lambda表达式转换为用于缓存的唯一键,c#,linq,caching,linq-expressions,C#,Linq,Caching,Linq Expressions,我看过其他类似的问题,但找不到任何可行的答案 我一直在使用以下代码生成唯一的密钥,用于将linq查询的结果存储到缓存中 string key = ((LambdaExpression)expression).Body.ToString(); foreach (ParameterExpression param in expression.Parameters) { string name = param.Name; string type

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

我一直在使用以下代码生成唯一的密钥,用于将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)
因此,返回的密钥是:

(正确)和(农场、农作物、任何) (值(OzFarmGuide.Controllers.FarmController+c__DisplayClassd)。slug ==y.Slug)和(Farm.Deleted==False)))

正如您所看到的,我通过的任何作物名称都将给出相同的关键点结果。是否有一种方法可以提取给定参数的值,以便区分查询


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

正如Polity和Marc在他们的评论中所说,您需要的是LINQ表达式的部分求值器。您可以在中阅读如何使用。这篇文章(由Polity链接到)描述了有关这种缓存的更多细节,例如如何在查询中表示集合

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

这个呢

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
方法中为它们编写一个自定义逻辑。

我一直在试图找出一种场景,在这种场景中,这种方法可能很有用,而不会导致难以维护的臃肿缓存

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

  • 您计划如何管理参数排序?即,(x=>x.blah==“slug”&&&!x.Deleted)缓存键应等于(x=>!x.Deleted&&x.blah==“slug”)缓存键
  • 您计划如何避免缓存中的重复对象?根据设计,来自多个查询的同一个服务器场将与每个查询一起单独缓存。比如说,对于农场中出现的每一只蛞蝓,我们都有一份农场的单独副本
  • 使用更多参数(如parcel、farmer等)扩展上述内容将导致更多匹配查询,每个查询都有一个单独的农场副本缓存。这同样适用于您可能查询的每个类型,加上参数的顺序可能不同
  • 现在,如果更新农场会发生什么?如果不知道哪些缓存查询将包含您的服务器场,您将被迫杀死整个缓存。哪种方式会对你想要实现的目标产生反作用
我可以理解这种方法背后的原因。0维护性能层。但是,如果不考虑上述几点,该方法将首先破坏性能,然后导致大量的维护尝试,然后被证明是完全无法维护的

我一直沿着那条路走。最终浪费了很多时间,放弃了

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

然后,您可以为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从DB返回3个场,满足查询B的要求,同时从缓存返回一个匹配场,而数据库实际上有20个匹配场可用。因此,每个查询stll至少需要命中DB一次

您需要跟踪返回0个结果的查询,以避免它们无缘无故地命中数据库

但总的来说,你只需要很少的代码,作为奖励,当你更新一个农场时,你可以

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

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();

}
var call=expression.Body作为MethodCallExpression;
如果(调用!=null)
{
列表=新列表();
foreach(call.Arguments中的表达式参数)
{
对象o=Expression.Lambda(参数,表达式.Parameters).Compile().DynamicInvoke();
列表。添加(o);
}
StringBuilder keyValue=新的StringBuilder();
Append(expression.Body.ToString());
ForEach(e=>keyValue.Append(String.Format(“{0}”),e.ToString());
字符串key=keyValue.ToString();
}

使用
GetHashCode()
方法和
HashSet
有什么问题?它不是唯一的,但在大多数情况下,
HashSet
能够在O(1)中接收和添加项。@CommuSoft,这不起作用,因为即使两个看起来完全相同的表达式也不会被认为相等(除非您提供自己的相等比较器)。@CommuSoft-此外,hashcode不一定是唯一的,因此在您的代码中可能存在错误此链接:准确地解释了问题以及如何处理它。我怀疑要解决此问题,您可以使用ExpressionVisitor在ConstantExpression上检测并展平MemberExpression(*n)的任何序列,对此进行评估,并替换为直线恒压;不是小事,但也不是太疯狂。阅读Polity的评论,这正是PartialEval所做的。我真的搞到了一些东西[