How to get the value of a ConstantExpression which

2019-02-02 15:04发布

问题:

I created an ExpressionVisitor implementation that overrides VisitConstant. However, when I create an expression that utilizes a local variable I can't seem to get the actual value of the variable.

public class Person
{
  public string FirstName { get; set; }
}

string name = "Michael";

Expression<Func<Person, object>> exp = p => p.FirstName == name;

How in the world do I get the value of the variable "name" from the ConstantExpression? The only thing that I can think of is this:

string fieldValue = value.GetType().GetFields().First().GetValue(value).ToString();

Obviously this doesn't lend itself to being very flexible though....

A slightly more complicated example would be the following:

Person localPerson = new Person { FirstName = "Michael" };
Expression<Func<Person, object>> exp = p => p.FirstName == localPerson.FirstName;

回答1:

Here's how I solved it for both cases you listed.

Basically assuming that the right hand side of your '==' can be treated like a function that takes no arguments and returns a value, it can be compiled to a C# delegate and invoked to retrieve this value without worrying about exactly what the code on the right hand side does.

So the basic example code is below

class Visitor : ExpressionVisitor {

  protected override Expression VisitBinary( BinaryExpression node ) {

    var memberLeft = node.Left as MemberExpression;
    if ( memberLeft != null && memberLeft.Expression is ParameterExpression ) {

      var f = Expression.Lambda( node.Right ).Compile();
      var value = f.DynamicInvoke();
      }

    return base.VisitBinary( node );
    }
  }

It looks for a binary op looking for "arg.member == something" then just compiles/evaluates the right hand side, and for both examples your provide the result is a string "Michael".

Note, this fails if your right hand side involved using the lamda argument like

p.FirstName == CallSomeFunc( p.FirstName )



回答2:

EDIT: Okay, it's clearer what you mean now, thanks to AHM's comment.

Basically the code is compiled to capture name in a separate class - and then apply a field access to get at its value from the constant expression which refers to an instance of it. (It has to do this as you may change the value of name after creating the expression - but the expression captures the variable, not the value.)

So you don't actually want to do anything on the ConstantExpression in VisitConstant - you want to work on the field access in VisitMember. You'll need to get the value from the ConstantExpression child, then give that to the FieldInfo to get the value:

using System;
using System.Linq.Expressions;
using System.Reflection;

public class Person
{
    public string FirstName { get; set; }
}

static class Program
{
    static void Main(string[] args)
    {
        string name = "Michael";

        Expression<Func<Person, object>> exp = p => p.FirstName == name;

        new Visitor().Visit(exp);
    }
}

class Visitor : ExpressionVisitor    
{
    protected override Expression VisitMember
        (MemberExpression member)
    {
        if (member.Expression is ConstantExpression &&
            member.Member is FieldInfo)
        {
            object container = 
                ((ConstantExpression)member.Expression).Value;
            object value = ((FieldInfo)member.Member).GetValue(container);
            Console.WriteLine("Got value: {0}", value);
        }
        return base.VisitMember(member);
    }
}

EDIT: Okay, slightly more involved version of the visitor class:

class Visitor : ExpressionVisitor    
{
    protected override Expression VisitMember
        (MemberExpression memberExpression)
    {
        // Recurse down to see if we can simplify...
        var expression = Visit(memberExpression.Expression);

        // If we've ended up with a constant, and it's a property or a field,
        // we can simplify ourselves to a constant
        if (expression is ConstantExpression)
        {
            object container = ((ConstantExpression) expression).Value;
            var member = memberExpression.Member;
            if (member is FieldInfo)
            {
                object value = ((FieldInfo)member).GetValue(container);
                return Expression.Constant(value);
            }
            if (member is PropertyInfo)
            {
                object value = ((PropertyInfo)member).GetValue(container, null);
                return Expression.Constant(value);
            }
        }
        return base.VisitMember(memberExpression);
    }
}

Now running that with:

var localPerson = new Person { FirstName = "Jon" };

Expression<Func<Person, object>> exp = p => p.FirstName == localPerson.FirstName;

Console.WriteLine("Before: {0}", exp);
Console.WriteLine("After: {0}", new Visitor().Visit(exp));

Gives the result:

Before: p => Convert((p.FirstName == 
           value(Program+<>c__DisplayClass1).localPerson.FirstName))
After: p => Convert((p.FirstName == "Jon"))


回答3:

In general you need to implement own ExpressionVisitor with overriden VisitConstant and VisitMember, also we need a stack for MemberAccess nodes.

  • in the VisitMember put the node on the stack
  • in the VisitConstant make a 'while loop' to analyze if previous node is MemberExpression:
    • get the Member property of previous node
    • detect if it is FieldInfo or PropertyInfo
    • call GetValue of Field/Property Info - it will be the value of constant you need or value of intermediate member that can be used for obtain next value in complex cases(see bellow)
    • remove MemberExpression from the stack
    • close loop

Loop is needed for cases such this

var a = new { new b { c = true; }  }
var expression = () => a.b.c;

Here is part of visit constant method

    protected override Expression VisitConstant(ConstantExpression node)
    {
                    MemberExpression prevNode;
                    var val = node.Value;
                    while ((prevNode = PreviousNode as MemberExpression) != null)
                    {
                        var fieldInfo = prevNode.Member as FieldInfo;
                        var propertyInfo = prevNode.Member as PropertyInfo;

                        if (fieldInfo != null)
                            val = fieldInfo.GetValue(val);
                        if (propertyInfo != null)
                            val = propertyInfo.GetValue(val);
                        Nodes.Pop();
                    }
                    // we got the value
                    // now val = constant we was looking for

        return node;
    }

PreviousNode is property that do a Stack.Peek



回答4:

The issue with ConstantExpression is that compiler put's uses object of private anonymous class to store values lambda was closed over, so the value of the constant is the value of the object of this private class. To access the "actual" constant you'll have to analyze expressions occuring before the ConstantExpression. An overly simplified solution might look like this:


public sealed class ConstantValueExtractor : ExpressionVisitor
{
    public static object ExtractFirstConstant(Expression expression)
    {
        var visitor = new ConstantValueExtractor();

        visitor.Visit(expression);

        return visitor.ConstantValue;
    }

    private ConstantValueExtractor()
    {

    }

    private object ConstantValue
    {
        get;
        set;
    }

    #region ExpressionVisitor Members
    public override Expression Visit(Expression node)
    {
        this.pathToValue.Push(node);

        var result = base.Visit(node);

        this.pathToValue.Pop();

        return result;
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        // The first expression in the path is a ConstantExpression node itself, so just skip it.
        var parentExpression = this.pathToValue.FirstOrDefault(
            expression => expression.NodeType == ExpressionType.MemberAccess);
        if (parentExpression != null)
        {
            // You might get notable performance overhead here, so consider caching
            // compiled lambda or use other to extract the value.
            var valueProviderExpression = Expression.Lambda>(
                Expression.Convert(parentExpression, typeof(object)));
            var valueProvider = valueProviderExpression.Compile();
            this.ConstantValue = valueProvider();
        }

        return base.VisitConstant(node);
    }
    #endregion

    #region private fields
    private Stack pathToValue = new Stack();
    #endregion
}

class Test
{
    static void Main()
    {
        string name = "Michael";

        Expression> exp = p => p.FirstName == name;

        var value = ConstantValueExtractor.ExtractFirstConstant(exp);
        Console.WriteLine(value);
    }
}

I doubt it will work for complicated enough expressions, but you should get the idea of how it could be done.



回答5:

Okay, this seems very interesting. Apparently what is happening is that C# passes the local stackframe as a constant object, as a parameter to your expression. If you add another expression above the one you got, like fx.:

var count = 18;
Expression<Func<Person, object>> expr2 = p => p.FirstName == name && count > 10;

Then your method will stop working - the "name" field will no longer be the first field in the strange "local-variables" object.

I did not know that expressions behaved this way, but it seems that you have to look for at MemberExpression with a constantexpression as it's inner expression. You can then get the value by evaluating that expression:

protected override Expression VisitMember(MemberExpression node) {
    if (node.Expression.NodeType == ExpressionType.Constant) {
        var inner = (ConstantExpression)node.Expression;
        var value = (node.Member as FieldInfo).GetValue(inner.Value);
    }
    return base.VisitMember(node);
}

I don't know how reliable this is, you may need to inspect the memberexpression in more depth, but in the simplyfied example you have shown here, the above will work.