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
. 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)}");
}
That works. Now, any idea how to set the value?
In the loop, you can check if it is the last iteration and set the value of the Property / Field.
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.
Instead of
outExpr.Compile().Invoke(item)
, you can useoutExpr.Compile()(item)
. That is because iff
is aFunc<,>
, you can invoke it simply asf(x)
.