Преобразование лямбда-выражения в уникальный ключ для кэширования - PullRequest
10 голосов
/ 17 марта 2012

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

Я использовал следующий код для генерации уникальных ключей для хранения результатов моих запросов 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 в правильное имя типа .....

Ответы [ 4 ]

5 голосов
/ 17 марта 2012

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

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

3 голосов
/ 14 ноября 2012

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

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

  • Как вы планировали управлять упорядочением параметров? То есть. (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());
}

Конечно, вам все равно нужно будет отслеживать, какие запросы действительно попали в базу данных, чтобы избежать запроса A, возвращающего 3 фермы из БД, удовлетворяющих запросу B, с одной совпадающей фермой из кэша, в то время как в базе данных фактически будет доступно 20 совпадающих ферм. Таким образом, каждый запрос stll должен попадать в БД хотя бы один раз.

И вам нужно отслеживать запросы, возвращающие 0 результатов, чтобы избежать их, следовательно, бесполезного попадания в БД.

Но в целом вы получаете намного меньше кода и в качестве бонуса, когда вы обновляете ферму, вы можете

var farm = (f => f.farmId == farmId).FromCache().First();
farm.Name = "My Test Farm";
var updatedFarm = farm.ToDatabase();
updatedFarm.ToCache();
0 голосов
/ 06 августа 2013

Как насчет:

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

}
0 голосов
/ 17 марта 2012

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

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.

...