Search code examples
c#.netexpressionfastmember

Can I set the property of a struct using Expressions?


I have the following method which sets the value for the given PropertyInfo on the given TInstance. This is to avoid the inefficiency of reflection.

public static Action<TInstance, object> CreateSetter<TInstance>(PropertyInfo propertyInfo, bool includeNonPublic = false)
{
    var setMethod = propertyInfo.GetSetMethod(includeNonPublic);

    var instance = Expression.Parameter(typeof(TInstance), "instance");
    var value = Expression.Parameter(typeof(object), "value");
    var valueCast = !propertyInfo.PropertyType.IsValueType
        ? Expression.TypeAs(value, propertyInfo.PropertyType)
        : Expression.Convert(value, propertyInfo.PropertyType);

    return Expression.Lambda<Action<TInstance, object>>(
        Expression.Call(instance, setMethod, valueCast), instance, value).Compile();
}

So given the following model:

public sealed class PersonClass
{
    public string Name {get; set;}    
}

I can set the Name using:

var person = new PersonClass(); 
var nameProp = person.GetType().GetProperties().Where(p => p.Name == "Name").First();
var nameSetter = CreateSetter<PersonClass>(nameProp);
nameSetter(person, "Foo");

This is all good however if I try the method with a struct e.g.:

public struct PersonStruct
{
    public string Name {get; set;}    
}

The name is always null. I suspect boxing/unboxing is biting me somehow.

In fact if I use FastMember the same behavior exhibits when using:

PersonStruct person = new PersonStruct();   
var accessor = TypeAccessor.Create(person.GetType());       
accessor[person, "Name"] = "Foo";

However when I box the person as object then FastMember is able to set the value correctly:

object person = new PersonStruct(); 
var accessor = TypeAccessor.Create(person.GetType());       
accessor[person, "Name"] = "Foo";

Any ideas how I can handle this boxing inside the CreateSetter for when TInstance is a value type?


Solution

  • As noted in the comments, you really shouldn't create mutable structs. However, to answer the question, structs are value types and therefore a copy of your struct is passed to the Action and therefore the original person value is not changed.

    You need a way to pass the struct by reference. However, expressions do not support creating "methods" that take parameters by reference.

    What you can do is use the DynamicMethod class to do something like this:

    public delegate void StructSetter<TInstance>(ref TInstance instance, object value) where TInstance : struct;
    
    public StructSetter<TInstance> CreateSetter<TInstance>(
        PropertyInfo propertyInfo,
        bool includeNonPublic = false) where TInstance : struct
    {
        DynamicMethod method =
            new DynamicMethod(
                "Set",
                typeof(void),
                new [] { typeof(TInstance).MakeByRefType(), typeof(object )},
                this.GetType());
    
        var generator = method.GetILGenerator();
    
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldarg_1);
        generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod(includeNonPublic));
        generator.Emit(OpCodes.Ret);
    
        return (StructSetter<TInstance>)method.CreateDelegate(typeof (StructSetter<TInstance> ));
    }
    

    We had to create a StructSetter delegate because the standard Action delegates do not support passing by reference.

    Don't forget to cache the delegate or otherwise the cost of compiling is going to slow down your application.