How to get Property Value from MemberExpression wi

2019-03-27 12:18发布

问题:

I'm having issues trying to get the value of an object out of the Expression Tree without using .Compile()

The object is quite simple.

var userModel = new UserModel { Email = "John@Doe.com"};

The method giving me issues looks like this.

private void VisitMemberAccess(MemberExpression expression, MemberExpression left)
{
    var key = left != null ? left.Member.Name : expression.Member.Name;
    if (expression.Expression.NodeType.ToString() == "Parameter")
    {
        // add the string key
        _strings.Add(string.Format("[{0}]", key));
    }
    else
    {
        // add the string parameter
        _strings.Add(string.Format("@{0}", key));

        // Potential NullReferenceException
        var val = (expression.Member as FieldInfo).GetValue((expression.Expression as ConstantExpression).Value);

        // add parameter value
        Parameters.Add("@" + key, val);
    }
}

The tests I'm running are quite simple

[Test]  // PASS
public void ShouldVisitExpressionByGuidObject ()
{
    // Setup
    var id = new Guid( "CCAF57D9-88A4-4DCD-87C7-DB875E0D4E66" );
    const string expectedString = "[Id] = @Id";
    var expectedParameters = new Dictionary<string, object> { { "@Id", id } };

    // Execute
    var actualExpression = TestExpression<UserModel>( u => u.Id == id );
    var actualParameters = actualExpression.Parameters;
    var actualString = actualExpression.WhereExpression;

    // Test
    Assert.AreEqual( expectedString, actualString );
    CollectionAssert.AreEquivalent( expectedParameters, actualParameters );
}
[Test]  // FAIL [System.NullReferenceException : Object reference not set to an instance of an object.]
public void ShouldVisitExpressionByStringObject ()
{
    // Setup
    var expectedUser = new UserModel {Email = "john@doe.com"};

    const string expectedString = "[Email] = @Email";
    var expectedParameters = new Dictionary<string, object> { { "@Email", expectedUser.Email } };

    // Execute
    var actualExpression = TestExpression<UserModel>( u => u.Email == expectedUser.Email );
    var actualParameters = actualExpression.Parameters;
    var actualString = actualExpression.WhereExpression;

    // Assert
    Assert.AreEqual( expectedString, actualString );
    CollectionAssert.AreEquivalent( expectedParameters, actualParameters );
}

I should note that changing

var val = (expression.Member as FieldInfo).GetValue((expression.Expression as ConstantExpression).Value);

to

var val = Expression.Lambda( expression ).Compile().DynamicInvoke().ToString();

will allow the test to pass, however this code needs to run on iOS, and therefore can't use .Compile()

回答1:

TLDR;
Reflection is ok to use as long as you're not using Emit or Compile. In the question, the value is being extracted for FieldInfo, but it is not being extracted for PropertyInfo. Make sure you can get BOTH.

if ((expression.Member as PropertyInfo) != null)
{
    // get the value from the PROPERTY

}
else if ((expression.Member as FieldInfo) != null)
{
    // get the value from the FIELD
}
else
{
    throw new InvalidMemberException();
}

Long-winded version

So the comments pointed me in the right direction. I struggled slightly with getting the PropertyInfo, but in the end, here's what I came up with.

private void VisitMemberAccess(MemberExpression expression, MemberExpression left)
{
    // To preserve Case between key/value pairs, we always want to use the LEFT side of the expression.
    // therefore, if left is null, then expression is actually left. 
    // Doing this ensures that our `key` matches between parameter names and database fields
    var key = left != null ? left.Member.Name : expression.Member.Name;

    // If the NodeType is a `Parameter`, we want to add the key as a DB Field name to our string collection
    // Otherwise, we want to add the key as a DB Parameter to our string collection
    if (expression.Expression.NodeType.ToString() == "Parameter")
    {
        _strings.Add(string.Format("[{0}]", key));
    }
    else
    {
        _strings.Add(string.Format("@{0}", key));

        // If the key is being added as a DB Parameter, then we have to also add the Parameter key/value pair to the collection
        // Because we're working off of Model Objects that should only contain Properties or Fields,
        // there should only be two options. PropertyInfo or FieldInfo... let's extract the VALUE accordingly
        var value = new object();
        if ((expression.Member as PropertyInfo) != null)
        {
            var exp = (MemberExpression) expression.Expression;
            var constant = (ConstantExpression) exp.Expression;
            var fieldInfoValue = ((FieldInfo) exp.Member).GetValue(constant.Value);
            value = ((PropertyInfo) expression.Member).GetValue(fieldInfoValue, null);

        }
        else if ((expression.Member as FieldInfo) != null)
        {
            var fieldInfo = expression.Member as FieldInfo;
            var constantExpression = expression.Expression as ConstantExpression;
            if (fieldInfo != null & constantExpression != null)
            {
                value = fieldInfo.GetValue(constantExpression.Value);
            }
        }
        else
        {
            throw new InvalidMemberException();
        }

        // Add the Parameter Key/Value pair.
        Parameters.Add("@" + key, value);
    }
}

Essentially, if the Member.NodeType is a Parameter, then I'm going to use it as a SQL Field. [FieldName]

Otherwise, I'm using it as a SQL Parameter @FieldName ... backwards I know.

If the Member.NodeType is NOT a Parameter, then I check to see if it's either a Model Fieldor a Model Property. From there, I get the appropriate value, and add the Key/Value pair to a Dictionary to be used as SQL Parameters.

The end result is that I build a string that looks something like

SELECT * FROM TableName WHERE
[FieldName] = @FieldName

Then the parameters are passed

var parameters = new Dictionary<string, object> Parameters;
parameters.Add("@FieldName", "The value of the field");