Доступ к значению выражения члена

Если у меня есть продукт.

var p = new Product { Price = 30 };

и у меня есть следующий запрос linq.

var q = repo.Products().Where(x=>x.Price == p.Price).ToList()

В провайдере IQueryable я получаю выражение MemberExpression для p.Price, которое содержит выражение константы, однако я не могу получить от него значение "30".

Обновление Я пробовал это, но это не похоже на работу.

var memberExpression = (MemberExpression)GetRootConstantExpression(m);
var fi = (PropertyInfo)memberExpression.Member;
var val = fi.GetValue(((ConstantExpression)memberExpression.Expression).Value, null);

Приветствия.

8 ответов

Решение

Вы можете скомпилировать и вызвать лямбда-выражение, тело которого является доступом к члену:

private object GetValue(MemberExpression member)
{
    var objectMember = Expression.Convert(member, typeof(object));

    var getterLambda = Expression.Lambda<Func<object>>(objectMember);

    var getter = getterLambda.Compile();

    return getter();
}

Локальная оценка является распространенной техникой при разборе деревьев выражений. LINQ to SQL делает это точно во многих местах.

 MemberExpression right = (MemberExpression)((BinaryExpression)p.Body).Right;
 Expression.Lambda(right).Compile().DynamicInvoke();

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

var p = new Product { Price = 30 };
Expression<Func<Product, bool>> predicate = x => x.Price == p.Price;
BinaryExpression eq = (BinaryExpression)predicate.Body;
MemberExpression productToPrice = (MemberExpression)eq.Right;
MemberExpression captureToProduct = (MemberExpression)productToPrice.Expression;
ConstantExpression captureConst = (ConstantExpression)captureToProduct.Expression;
object product = ((FieldInfo)captureToProduct.Member).GetValue(captureConst.Value);
object price = ((PropertyInfo)productToPrice.Member).GetValue(product, null);

price сейчас 30, Обратите внимание, что я предполагаю, что Price это свойство, но на самом деле вы бы написали GetValue метод, который обрабатывает свойство / поле.

По состоянию на 2020 год

Этот вспомогательный метод аккуратно извлечет любое значение выражения без "взлома компиляции":

public static object GetMemberExpressionValue (MemberExpression expression)
{
    // Dependency chain of a MemberExpression is of the form:
    // MemberExpression expression
    //    MemberExpression expression.Expression
    //        ... MemberExpression expression.[...].Expression
    //            ConstantExpression expression.[...].Expression.Expression <- base object
    var dependencyChain = new List<MemberExpression>();
    var pointingExpression = expression;
    while (pointingExpression != null)
    {
        dependencyChain.Add(pointingExpression);
        pointingExpression = pointingExpression.Expression as MemberExpression;
    }

    if (!(dependencyChain.Last().Expression is ConstantExpression baseExpression))
    {
        throw new Exception(
            $"Last expression {dependencyChain.Last().Expression} of dependency chain of {expression} is not a constant." +
            "Thus the expression value cannot be found.");
    }

    var resolvedValue = baseExpression.Value;

    for (var i = dependencyChain.Count; i > 0; i--)
    {
        var expr = dependencyChain[i - 1];
        resolvedValue = new PropOrField(expr.Member).GetValue(resolvedValue);
    }

    return resolvedValue;
}

PropOrField класс:

public class PropOrField
{
    public readonly MemberInfo MemberInfo;

    public PropOrField (MemberInfo memberInfo)
    {
        if (!(memberInfo is PropertyInfo) && !(memberInfo is FieldInfo))
        {
            throw new Exception(
                $"{nameof(memberInfo)} must either be {nameof(PropertyInfo)} or {nameof(FieldInfo)}");
        }

        MemberInfo = memberInfo;
    }

    public object GetValue (object source)
    {
        if (MemberInfo is PropertyInfo propertyInfo) return propertyInfo.GetValue(source);
        if (MemberInfo is FieldInfo fieldInfo) return fieldInfo.GetValue(source);

        return null;
    }

    public void SetValue (object target, object source)
    {
        if (MemberInfo is PropertyInfo propertyInfo) propertyInfo.SetValue(target, source);
        if (MemberInfo is FieldInfo fieldInfo) fieldInfo.SetValue(target, source);
    }

    public Type GetMemberType ()
    {
        if (MemberInfo is PropertyInfo propertyInfo) return propertyInfo.PropertyType;
        if (MemberInfo is FieldInfo fieldInfo) return fieldInfo.FieldType;

        return null;
    }
}

Если у вас был класс:

public class Item
{
    public int Id { get; set; }
}

и экземпляр объекта:

var myItem = new Item { Id = 7 };

Вы можете получить значение Id, используя выражение, используя следующий код:

Expression<Func<Item, int>> exp = x => x.Id;
var me = exp.Body as MemberExpression;
var propInfo = me.Member as PropertyInfo;
var value = propInfo.GetValue(myItem, null);

значение будет содержать "7"

С помощью Expression.Lambda(myParameterlessExpression).Compile().Invoke() имеет несколько недостатков:

  • .Compile() медленно Это может занять несколько миллисекунд даже для небольших фрагментов выражения. Invoke-call после этого очень быстрый, занимает всего несколько наносекунд для простых арифметических выражений или доступа к элементам.
  • .Compile() будет генерировать (излучать) код MSIL. Это может звучать идеально (и объясняет отличную скорость выполнения), но проблема в том, что этот код занимает память, которую нельзя освободить до завершения приложения, даже когда GC собрал ссылку на делегат!

Можно либо избежать Compile() в целом, чтобы избежать этих проблем или кэшировать скомпилированные делегаты для их повторного использования. Эта моя маленькая библиотека предлагает обе интерпретации Expressions а также кэшированная компиляция, где все константы и замыкания выражения автоматически заменяются дополнительными параметрами, которые затем повторно вставляются в замыкание, которое возвращается пользователю. Оба процесса хорошо проверены, используются в производстве, оба имеют свои плюсы и минусы друг против друга, но намного более чем в 100 раз быстрее, чем Compile() - и избежать утечки памяти!

q имеет тип List<Product>, В списке нет свойства Price - только отдельные товары.

Первый или последний продукт будет иметь цену.

q.First().Price
q.Last().Price

Если вы знаете, что в коллекции есть только один экземпляр, вы можете выровнять его, используя

q.Single().Price

Можете ли вы использовать следующее:

var price = p.Price;
var q = repo.Products().Where(x=>x.Price == price).ToList()

И что именно вы пытаетесь достичь?

Потому что для доступа к значению PriceВы должны сделать что-то вроде:

var valueOfPrice = q[0].Price;
Другие вопросы по тегам