1

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.

  • 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` – AndrewToasterr Nov 29 '20 at 11:30
  • Why do you want to do this? Why not simply `public static void OutputField(Person p, Func outExpr) => Console.WriteLine($"value={outExpr(p)}");`? – Klaus Gütter Nov 29 '20 at 11:38
  • @Klaus This is just the test function. Eventually, I'll want to be both getting and setting the property identified in the expression. – John Bruestle Nov 29 '20 at 11:58
  • Does this answer your question? [Combine lambda expressions to retrieve nested values](https://stackoverflow.com/questions/22378852/combine-lambda-expressions-to-retrieve-nested-values) – CSharpie Nov 29 '20 at 11:59
  • @Andrew I want to be able to specify my expressions relative to the top object. Klaus's code lets me do this, but I need something that going to allow me to both get and set values. I use this property of a property notation all the time when I call helper functions in my ASP.NET MVC views, e.g. TextBoxFor(m=>m.Name.First), which use Expressions, so I know it's possible. – John Bruestle Nov 29 '20 at 12:04

2 Answers2

1

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)}");
}
AndrewToasterr
  • 459
  • 1
  • 5
  • 16
  • That works. Now, any idea how to set the value? – John Bruestle Nov 29 '20 at 12:12
  • In the loop, you can check if it is the last iteration and set the value of the Property / Field. – AndrewToasterr Nov 29 '20 at 12:17
  • Thanks @Andrew, I like the simplicity of the Compile/Invoke. I marking this as the accepted answer. I did figure out how to do a set and have shown it in the next answer. I'm looking for a way to simplify the set. – John Bruestle Nov 30 '20 at 01:12
  • Instead of `outExpr.Compile().Invoke(item)`, you can use `outExpr.Compile()(item)`. That is because if `f` is a `Func<,>`, you can invoke it simply as `f(x)`. – Jeppe Stig Nielsen Nov 30 '20 at 01:35
0

Here's what I cam up with based on @Andrew's answer for the get and some other code I found for the get. I'm not sure if there is an equivalent simpler way to do the set.

public static void OutputField<T>(T p, Expression<Func<T, string>> get)
{
    // get the value
    string v = get.Compile().Invoke(p).ToString();
    Console.WriteLine($"retrieved value={v}");

    // set a value
    var expr = (MemberExpression)get.Body;
    var param = Expression.Parameter(typeof(string), "value");
    var set = Expression.Lambda<Action<T, string>>(Expression.Assign(expr, param), get.Parameters[0], param);
    var action = set.Compile();
    action(p, "a new value");
}