3

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?

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
MaYaN
  • 6,683
  • 12
  • 57
  • 109
  • Compiling an expression like this is rather likely to be *more* expensive than reflection, in most contexts. – Servy Jan 10 '17 at 16:54
  • Side note: just some reading - http://stackoverflow.com/questions/441309/why-are-mutable-structs-evil – Alexei Levenkov Jan 10 '17 at 16:54
  • 1
    structs are passed by value. So when you pass the `sturct` to the action, a copy of that struct is passed. I guess you need a way to pass by reference. – Yacoub Massad Jan 10 '17 at 16:55
  • @Servy, the expression can be created once and cached for reuse, we have this working and benchmarking also beats any other method we have tried including `FastReflection`, `IL Weaving` and `FastMember` – MaYaN Jan 10 '17 at 16:56
  • @YacoubMassad, Every time I think I know all about value types I am proven otherwise! Cannot believe how I overlooked such an obvious fact! :-) – MaYaN Jan 10 '17 at 16:59
  • Expression trees were specifically designed to NOT support mutating operations because they were built for LINQ, which is about querying, not updating. Fetching the set method does a nice end-run around this, but any time you need to do some sort of end-run around a designed-in restriction is maybe a time to ask yourself whether you're using the right tool for the job. – Eric Lippert Jan 10 '17 at 17:30
  • @EricLippert Thank you for the comment, I specifically wrote the method in question to support reference types only; In fact I excluded the generic constraint of `where TInstance : class` from the post I was just trying to figure out why I was unable to set the props of a `struct` otherwise I do agree with the consensus here that value types should not be mutated in such a way the answer to this question is trying to do. – MaYaN Jan 10 '17 at 17:49
  • @EricLippert true, but a lot of the methods added to expressions with 4.0 are designed specifically to mutate and `DynamicObject` among other things uses them for that. I'm not sure this is the best way to approach the querent's problem, but I don't think that expression trees can be described as designed to not mutate any more, either. – Jon Hanna Jan 10 '17 at 17:51

2 Answers2

2

You need an expression that creates a delegate that takes a by-ref argument, so that it will affect the struct passed, rather than a copy. E.g.:

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

delegate void FirstByRefAction<T1, T2>(ref T1 arg1, T2 arg2);

void Main()
{
    ParameterExpression par1 = Expression.Parameter(typeof(PersonStruct).MakeByRefType());
    ParameterExpression par2 = Expression.Parameter(typeof(string));
    FirstByRefAction<PersonStruct, string> setter = Expression.Lambda<FirstByRefAction<PersonStruct, string>>(
        Expression.Assign(Expression.Property(par1, "Name"), par2),
        par1, par2
        ).Compile();
    PersonStruct testStruct = new PersonStruct();
    setter(ref testStruct, "Test Name");
    Console.Write(testStruct.Name); // outputs "Test Name"
}

This is to avoid the inefficiency of reflection.

Note that the method- and property-calling expression types largely use reflection internally. In the case of interpreted expressions, they do so with each call, in the case of IL-compiled expressions reflection is still used in the compilation step.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
1

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.

Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
  • "However, expressions do not support creating "methods" that take parameters by reference.". Yes they do. – Jon Hanna Jan 10 '17 at 17:30
  • 1
    I didn't know that. Thanks. – Yacoub Massad Jan 10 '17 at 17:34
  • The end result is pretty close to what your code does when the expression is IL-compiled. When interpreted it does something close to the code in the question and then writes back to the parameter, which is less efficient but can be used when IL generation isn't available. – Jon Hanna Jan 10 '17 at 17:36