Преобразование лямбда-выражения в уникальный ключ для кеширования

Я посмотрел на другие вопросы, подобные этому, но я не смог найти никаких реальных ответов.

Я использовал следующий код для создания уникальных ключей для хранения результатов моих запросов 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 = > (Значение (OzFarmGuide.Controllers.FarmController + < > c__DisplayClassd).slug == y.Slug)) AndAlso (Farm.Deleted == False)))

Как вы видите, любое имя урожая, которое я передаю, даст тот же самый результат. Есть ли способ, который я могу извлечь значение данного параметра, чтобы я мог различать мои запросы?

Также преобразование y, чтобы сказать правильное имя типа было бы хорошо.....

Ответ 1

Как заявили Polity и Marc в своих комментариях, вам нужен частичный оценщик выражения LINQ. Вы можете прочитать, как это сделать, используя ExpressionVisitor в Matt Warren LINQ: Создание поставщика IQueryable - часть III. Статья Кэширование результатов запросов LINQ Питом Монтгомери (связанное с Polity) описывает некоторые дополнительные особенности в отношении такого типа кеширования, например. как представлять коллекции в запросе.

Кроме того, я не уверен, что буду полагаться на ToString() следующим образом. Я думаю, что это в основном предназначалось для целей отладки, и это может измениться в будущем. Альтернативой будет создание собственного IEqualityComparer<Expression>, который может создать хэш-код для любого выражения и может сравнить два выражения для равенства. Я, вероятно, сделаю это, используя ExpressionVisitor тоже, но делать это было бы довольно утомительно.

Ответ 2

Я пытался определить сценарий, в котором такой подход может быть полезен, не приводя к раздутому кешу, который безумно трудно поддерживать.

Я знаю, что это прямо не отвечает на ваш вопрос, но я хочу поднять несколько вопросов об этом подходе, который сначала может казаться соблазнительным:

  • Как вы планировали управлять настройкой параметров? То есть. (x = > x.blah == "slug" &! x.Deleted) кэш-ключ должен равняться (x = > ! x.Deleted & x.blah == "slug" ) кэш-ключ.
  • Как вы планировали избегать дублирования объектов в кеше? То есть. Одной фермой из нескольких запросов по дизайну будет кэшироваться отдельно с каждым запросом. Скажем, для каждого пула, который появляется в ферме, у нас есть отдельная копия фермы.
  • Расширение выше с помощью большего количества параметров, таких как парцелла, фермер и т.д., приведет к большему количеству совпадающих запросов, причем каждая из них будет иметь отдельную копию кэширования фермы. То же самое относится к каждому типу, который вы можете запросить, плюс параметры могут быть не в том же порядке.
  • Теперь, что произойдет, если вы обновите ферму? Не зная, какие кэшированные запросы будут содержать вашу ферму, вы должны будете убить весь свой кеш. Какой тип контрпродуктивен для того, чего вы пытаетесь достичь.

Я вижу причины такого подхода. Уровень обслуживания 0-обслуживания. Однако, если вышеуказанные моменты не принимаются во внимание, этот подход вначале убьет производительность, а затем приведет к большим попыткам его поддерживать, а затем окажется совершенно недостижимым.

Я был на той дороге. В конце концов потратил много времени и сдался.

Я нашел гораздо лучший подход, кэшируя каждый результирующий объект отдельно, когда результаты поступают из бэкэнд с помощью метода расширения для каждого типа отдельно или через общий интерфейс.

Затем вы можете создать метод расширения для ваших лямбда-выражений, чтобы сначала попробовать кеш, прежде чем нанести удар по 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());
}

Конечно, вам все равно нужно будет отслеживать, какие запросы на самом деле попали в базу данных, чтобы избежать запроса. Возвращение 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();

Ответ 3

Как насчет этого?

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.

Ответ 4

Как насчет:

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

}