Warm tip: This article is reproduced from serverfault.com, please click

Linq Reflection Expression throws exception on property of proprety

发布于 2020-11-29 11:09:42

I'm trying to make functions with Linq Expressions arguments and am stuck on a problem.

Here's my test function:

public static void OutputField(Person p, Expression<Func<Person, string>> outExpr)
{
    var expr = (MemberExpression)outExpr.Body;
    var prop = (PropertyInfo)expr.Member;
    string v = prop.GetValue(p, null).ToString();
    Console.WriteLine($"value={v}");
}

If I define a Person object as

public class Person
{
    public PersonName Name { get; set; }
    public string City { get; set; }
}

public class PersonName
{
    public string First { get; set; }
    public string Last { get; set; }
}

and try to use it this way

Person person = new Person
{
    Name = new PersonName { First = "John", Last = "Doe" },
    City = "New York City"
};

OutputField(person, m => m.City);
OutputField(person, m => m.Name.First);

The output of the property City works, but the output of the property First of the property Name throws a System.Reflection.TargetException: 'Object does not match target type.' error. How do I make my test function work?

"Combine lambda expressions to retrieve nested values" pointed to by CSharpie seems to be addressing a similar question, but it's not clear how I would work the answer into my function.

Questioner
John Bruestle
Viewed
0
AndrewToasterr 2020-11-29 19:53:28

The error is that you are passing Person into prop.GetValue when accessing PersonName.First. The method tries to get the property First from Person but can't any. You need to pass the correct object into prop.GetValue. I made this generic method which achives what you want. One limitation is that it only checks fields and properties, i will try to add methods in a bit

public static void OutputField < T > (T item, Expression < Func < T, string >> outExpr) 
{
  // Get the expression as string
  var str = outExpr.ToString();

  // Get the variable of the expresion (m, m => ...)
  string pObj = str.Substring(0, str.IndexOf(' '));

  // Get all the members in the experesion (Name, First | m.Name.First);
  string[] members = new string(str.Skip(pObj.Length * 2 + 5).ToArray()).Split('.');

  // Last object in the tree
  object lastMember = item;
  // The type of lastMember
  Type lastType = typeof(T);

  // Loop thru each member in members
  for (int i = 0; i < members.Length; i++) 
  {
    // Get the property value
    var prop = lastType.GetProperty(members[i]);
    // Get the field value
    var field = lastType.GetField(members[i]);

    // Get the correct one and set it as last member
    if (prop is null) 
    {
      lastMember = field.GetValue(lastMember);
    }
    else 
    {
      lastMember = prop.GetValue(lastMember, null);
    }

    // Set the type of the last member
    lastType = lastMember.GetType();
  }

  // Print the value
  Console.WriteLine($ "value={lastMember}");
}

EDIT: Turns out you can compile the expression and invoke it

public static void OutputField<T>(T item, Expression<Func<T, string>> outExpr)
{
  Console.WriteLine($"value={outExpr.Compile().Invoke(item)}");
}