I’m trying to create expressions to access either fields or properties in a nested structure.
I managed to create both getters and setters (as lambda expressions) for fields and properties on a flat object. It works like so:
Delegate getter = getGetterExpression(objectType,"PropertyOrFieldName").Compile();
Delegate setter = getSetterExpression(objectType,"PropertyorFieldName").Compile();
I found this post (Marc Gravells answer) using a custom expression visitor to "chain" those lambda expressions to access nested objects. Is this the right way to do this (by chaining lambda expressions), if you have some deep (dynamic) nesting like the following example code? Or is there a more efficient way to achieve this?
// 'regular' C# Code
obj.PropA.FieldB.FieldC.PropD = "Hello World";
// targeted 'expression approach'
Delegate setter = GetPathSetterLambda(obj.GetType(), "PropA.FieldB.FieldC.PropD").Compile();
setter.DynamicInvoke(obj, "Hello World!");
The getters and setters are created like this:
private static LambdaExpression getSetterExpression(Type objectType, string fieldOrPropertyName)
{
ParameterExpression parameterExpression = Expression.Parameter(objectType);
MemberExpression memberExpression = Expression.PropertyOrField(parameterExpression, fieldOrPropertyName);
ParameterExpression valueParameterExpression = Expression.Parameter(memberExpression.Type);
BinaryExpression assignExpression = Expression.Assign(memberExpression, valueParameterExpression);
Type setterType = typeof(Action<,>).MakeGenericType(objectType, memberExpression.Type);
return Expression.Lambda(setterType, assignExpression, parameterExpression, valueParameterExpression);
}
private static LambdaExpression getGetterExpression(Type objectType, string fieldOrPropertyName)
{
ParameterExpression parameterExpression = Expression.Parameter(objectType);
MemberExpression memberExpression = Expression.PropertyOrField(parameterExpression, fieldOrPropertyName);
Type getterType = typeof(Func<,>).MakeGenericType(objectType, memberExpression.Type);
return Expression.Lambda(getterType, memberExpression, parameterExpression);
}
I’m trying to do this mostly to improve the performance compared to using reflection.
Compiling dynamic lambda expressions, so you get direct accessor and mutators for the object might really be more efficient than repeating reflection to access properties. While expressions are really not meant to be used like that, my implementation below just uses them internally to create direct delegates to read or set the values.
This is what I came up with. I made sure to not expose anything of the expressions to the outside, but just return simple delegates. Because of how lambda expressions work, they require the base type which I provide via the generic parameter. If you only have the type at run time in a Type object, you can easily change that, but you will also have to change the delegate types. I avoid having to specify the actual property type. In the accessor, I just return an object, and for the mutator, I make a type cast within the expression.
public static Func<T, object> GetAccessor<T>(string path)
{
ParameterExpression paramObj = Expression.Parameter(typeof(T), "obj");
Expression body = paramObj;
foreach (string property in path.Split('.'))
{
body = Expression.PropertyOrField(body, property);
}
return Expression.Lambda<Func<T, object>>(body, new ParameterExpression[] { paramObj }).Compile();
}
public static Action<T, object> GetMutator<T>(string path)
{
ParameterExpression paramObj = Expression.Parameter(typeof(T), "obj");
ParameterExpression paramValue = Expression.Parameter(typeof(object), "value");
Expression body = paramObj;
foreach(string property in path.Split('.'))
{
body = Expression.PropertyOrField(body, property);
}
body = Expression.Assign(body, Expression.TypeAs(paramValue, body.Type));
return Expression.Lambda<Action<T, object>>(body, new ParameterExpression[] { paramObj, paramValue }).Compile();
}
The implementation can be used like this:
obj.SomeB.SomeC.Foo = "bar";
var getter = GetAccessor<A>("SomeB.SomeC.Foo");
var setter = GetMutator<A>("SomeB.SomeC.Foo");
Console.WriteLine(getter(obj)); // "bar"
setter(obj, "baz");
Console.WriteLine(getter(obj)); // "baz"
As for errors, both methods will raise an ArgumentException for invalid paths when the delegates are created. Other than that, I don’t think there is anything that can go wrong. The mutator will silently set the value to null for invalid types.
The solution currently does not work for value types; you might have to add some additional type casting to make it work—or if that’s acceptable to you, add another type parameter for the property type.
Using expressions does not really make sense here. The whole point of expressions is to have actual C# syntax, so you have IntelliSense, syntax validation, and type safety. By accepting strings of expressions, you do the exact reverse of that, and building expressions from that and compiling them just so you get access to getters and setters makes really no sense.
If you have strings that express the path, then you should just use standard reflection.
An example implementation (sans error detection for invalid property names or NULL values—you should add that!):
class PropertyAccessor
{
private string[] properties;
public PropertyAccessor (string path)
{
properties = path.Split('.');
}
private object GetValue (object obj, string property)
{
return obj.GetType().GetProperty(property).GetValue(obj);
}
private void SetValue (object obj, string property, object value)
{
return obj.GetType().GetProperty(property).SetValue(obj, value);
}
public object Get (object obj)
{
object o = obj;
for (int i = 0; i < properties.Length; i++)
{
o = GetValue(o, properties[i]);
}
return o;
}
public void Set (object obj, object value)
{
object o = obj;
for (int i = 0; i < properties.Length - 1; i++)
{
o = GetValue(o, properties[i]);
}
SetValue(o, properties[properties.Length - 1], value);
}
}
Used like this:
obj.SomeB.SomeC.Foo = "bar";
var pa = new PropertyAccessor("SomeB.SomeC.Foo");
Console.WriteLine(pa.Get(obj)); // "bar"
pa.Set(obj, "baz");
Console.WriteLine(pa.Get(obj)); // "baz"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With