简体   繁体   English

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

[英]Converting a lambda expression into a unique key for caching

I've had a look at other questions similar to this one but I couldn't find any workable answers. 我看过其他与此问题类似的问题,但找不到任何可行的答案。

I've been using the following code to generate unique keys for storing the results of my linq queries to the cache. 我一直在使用以下代码来生成唯一键,以将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;

It seems to work fine for simple queries containing integers or booleans but when my query contains nested constant expressions eg 对于包含整数或布尔值的简单查询,它似乎工作正常,但是当我的查询包含嵌套常量表达式时,例如

// 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)

The key returned is thus: 因此,返回的密钥为:

(True AndAlso (Farm.Crops.Any(y => (value(OzFarmGuide.Controllers.FarmController+<>c__DisplayClassd).slug == y.Slug)) AndAlso (Farm.Deleted == False))) (真实AndAlso(Farm.Crops.Any(y =>(value(OzFarmGuide.Controllers.FarmController + <> c__DisplayClassd).slug == y.Slug))AndAlso(Farm.Deleted == False))

As you can see any crop name I pass will give the same key result. 如您所见,我通过的任何农作物名称都会给出相同的关键结果。 Is there a way I can extract the value of the given parameter so that I can differentiate between my queries? 有没有一种方法可以提取给定参数的值,以便区分查询?

Also converting the y to say the correct type name would be nice..... 也可以将y转换为正确的类型名称,.....

As Polity and Marc said in their comments, what you need is a partial evaluator of the LINQ expression. 正如Polity和Marc在评论中所说,您需要的是LINQ表达式的部分评估器。 You can read how to do that using ExpressionVisitor in Matt Warren's LINQ: Building an IQueryable Provider - Part III . 您可以在Matt Warren的LINQ:构建IQueryable提供程序-第III部分中阅读如何使用ExpressionVisitor进行操作。 The article Caching the results of LINQ queries by Pete Montgomery (linked to by Polity) describes some more specifics regarding this kind of caching, eg how to represent collections in the query. Pete Montgomery (链接到Polity) 对LINQ查询的结果进行缓存的文章描述了有关这种缓存的更多详细信息,例如,如何在查询中表示集合。

Also, I'm not sure I would rely on ToString() like this. 另外,我不确定是否会像这样依赖ToString() I think it's meant mostly for debugging purposes and it might change in the future. 我认为这主要是出于调试目的,并且将来可能会改变。 The alternative would be creating your own IEqualityComparer<Expression> that can create a hash code for any expression and can compare two expressions for equality. 另一种方法是创建您自己的IEqualityComparer<Expression> ,它可以为任何表达式创建哈希码,并且可以比较两个表达式是否相等。 I would probably do that using ExpressionVisitor too, but doing so would be quite tedious. 我可能也可以使用ExpressionVisitor进行此操作,但是这样做会很繁琐。

I've been trying to figure out a scenario where this kind of approach could be useful without leading to bloated cache that is insanely hard to maintain. 我一直在尝试找出一种方案,在这种情况下,这种方法可能有用而又不会导致难以维护的in肿的缓存。

I know this isn't directly answering your question, but I want to raise a few questions about this approach that, at first, may sound tempting: 我知道这并不能直接回答您的问题,但是我想提出一些有关此方法的问题,这些问题一开始可能很诱人:

  • How did you plan to manage parameter ordering? 您打算如何管理参数排序? Ie. 就是 (x => x.blah == "slug" && !x.Deleted) cache key should equal (x => !x.Deleted && x.blah == "slug") cache key. (x => x.blah ==“ slug” &&!x.Deleted)缓存键应等于(x =>!x.Deleted && x.blah ==“ slug”)缓存键。
  • How did you plan to avoid duplicate objects in cache? 您如何计划避免在缓存中重复对象? Ie. 就是 Same farm from multiple queries would by design be cached separately with each query. 根据设计,来自多个查询的同一服务器场将与每个查询分别缓存。 Say, for each slug that appears in the farm, we have a separate copy of the farm. 说,对于场中出现的每个子弹,我们都有场的单独副本。
  • Extending the above with more parameters, such as parcel, farmer etc. would lead to more matching queries with each having a separate copy of the farm cached. 用更多参数(例如包裹,农夫等)扩展上述内容将导致更多匹配查询,每个查询都具有缓存的场的单独副本。 The same applies to each type you might query plus the parameters might not be in the same order 相同的适用于您可能查询的每种类型,并且参数的顺序可能不同
  • Now, what happens if you update the farm? 现在,如果更新服务器场会怎样? Without knowing which cached queries would contain your farm, you'd be forced to kill your whole cache. 不知道哪些缓存查询将包含您的服务器场,您将被迫杀死整个缓存。 Which kind of is counterproductive to what you're trying to achieve. 哪种方法适得其反。

I can see the reasoning behind this approach. 我可以看到这种方法背后的原因。 A 0-maintenance performance layer. 零维护性能层。 However, if the above points are not taken into consideration, the approach will first kill the performance, then lead to a lot of attempts to maintain it, then prove to be completely unmaintainable. 但是,如果不考虑上述几点,则该方法将首先破坏性能,然后导致进行很多维护尝试,然后证明是完全无法维护的。

I've been down that road. 我一直走那条路。 Eventually wasted a lot of time and gave up. 最终浪费了很多时间,放弃了。

I found a much better approach by caching each resulting entity separately when the results come from the backend with an extension method for each type separately or through a common interface. 我发现了一种更好的方法,即当结果来自后端时分别缓存每个结果实体,并分别为每种类型或通过公共接口使用扩展方法。

Then you can build extension method for your lambda expressions to first try the cache before hitting the db. 然后,您可以为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());
}

Of course, you will still need to track which queries have actually hit the database to avoid query A returning 3 farms from DB satisfying query B with one matching farm from cache while the database would actually have 20 matching farms available. 当然,您仍然需要跟踪哪些查询实际命中了数据库,以避免查询A从数据库返回满足查询B的3个服务器场,并从缓存中返回一个匹配场,而数据库实际上将有20个匹配场可用。 So, each query stll need to hit DB at least once. 因此,每个查询至少需要访问数据库一次。

And you need to track queries returning 0 results to avoid them consequently hitting the DB for nothing. 而且,您需要跟踪返回0结果的查询,以避免它们因此无济于事。

But all in all, you get away with a lot less code and as a bonus, when you update a farm, you can 但总而言之,您可以省去更少的代码,并且,作为奖励,当您更新服务器场时,您可以

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

How about: 怎么样:

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

}

What about this? 那这个呢?

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

This will replace the complex constant expressions to their original values as well as the parameter names to their type names. 这会将复杂的常量表达式替换为其原始值,并将参数名称替换为其类型名称。 So just have to create a new KeyGeneratorVisitor instance and call its Visit or VisitAndConvert method with your expression. 因此,只需创建一个新的KeyGeneratorVisitor实例,然后使用您的表达式调用其VisitVisitAndConvert方法即可。

Please note that the Expression.ToString method will be also invoked on your complex types, so either override their ToString methods or write a custom logic for them in the Evaluate method. 请注意,还将对您的复杂类型调用Expression.ToString方法,因此可以覆盖其ToString方法或在Evaluate方法中为其编写自定义逻辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM