12

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

我一直在使用以下代码生成唯一键,用于将 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正确的类型名称会很好......

4

4 回答 4

7

正如 Polity 和 Marc 在他们的评论中所说,您需要的是 LINQ 表达式的部分求值器。ExpressionVisitor您可以在Matt Warren 的LINQ: Building an IQueryable Provider - Part III中阅读如何做到这一点。Pete Montgomery的文章缓存 LINQ 查询的结果(由 Polity 链接)描述了有关这种缓存的更多细节,例如如何在查询中表示集合。

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

于 2012-03-17T11:53:22.217 回答
4

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

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

  • 您打算如何管理参数排序?IE。(x => x.blah == "slug" && !x.Deleted) 缓存键应该等于 (x => !x.Deleted && x.blah == "slug") 缓存键。
  • 你打算如何避免缓存中的重复对象?IE。来自多个查询的同一个场将按设计与每个查询分开缓存。比如说,对于农场中出现的每个蛞蝓,我们都有一个单独的农场副本。
  • 使用更多参数扩展上述内容,例如 parcel、farmer 等,将导致更多匹配查询,每个查询都缓存了一个单独的农场副本。这同样适用于您可能查询的每种类型,而且参数的顺序可能不同
  • 现在,如果你更新农场会发生什么?在不知道哪些缓存查询将包含您的场的情况下,您将被迫杀死整个缓存。哪种对你想要达到的目标适得其反。

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

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

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

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

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

但总而言之,您可以使用更少的代码,并且作为奖励,当您更新农场时,您可以

var farm = (f => f.farmId == farmId).FromCache().First();
farm.Name = "My Test Farm";
var updatedFarm = farm.ToDatabase();
updatedFarm.ToCache();
于 2012-11-13T23:19:53.703 回答
1

那这个呢?

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实例并用你的表达式调用它的Visitor方法。VisitAndConvert

请注意,该Expression.ToString方法也将在您的复杂类型上调用,因此要么覆盖它们的ToString方法,要么在方法中为它们编写自定义逻辑Evaluate

于 2012-03-17T16:13:43.307 回答
0

怎么样:

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

}
于 2013-08-06T08:10:05.597 回答