Create an Action to “set” a property, when I am

2019-02-01 23:11发布

问题:

I'd like to be able to generate a compiled expression to set a property, given the lambda expression that provides the "get" method for a property.

Here's what I'm looking for:

public Action<int> CreateSetter<T>(Expression<Func<T, int>> getter)
{
    // returns a compiled action using the details of the getter expression tree, or null
    // if the write property is not defined.
}

I'm still trying to understand the various types of Expression classes, so if you can point me in the right direction that would be great.

回答1:

Using @Ani's answer as a starting point, you can use the following to generate a compiled expression.

[TestMethod]
public void CreateSetterFromGetter()
{
    Action<Person, int> ageSetter = InitializeSet((Person p) => p.Age);
    Action<Person, string> nameSetter = InitializeSet((Person p) => p.Name);

    Person p1 = new Person();
    ageSetter(p1, 29);
    nameSetter(p1, "John");

    Assert.IsTrue(p1.Name == "John");
    Assert.IsTrue(p1.Age == 29);
}

public class Person { public int Age { get; set; } public string Name { get; set; } }

public static Action<TContainer, TProperty> InitializeSet<TContainer, TProperty>(Expression<Func<TContainer, TProperty>> getter)
{
    PropertyInfo propertyInfo = (getter.Body as MemberExpression).Member as PropertyInfo;

    ParameterExpression instance = Expression.Parameter(typeof(TContainer), "instance");
    ParameterExpression parameter = Expression.Parameter(typeof(TProperty), "param");

    return Expression.Lambda<Action<TContainer, TProperty>>(
        Expression.Call(instance, propertyInfo.GetSetMethod(), parameter),
        new ParameterExpression[] { instance, parameter }).Compile();
}

You should cache the compiled expression to keep it handy for multiple uses.



回答2:

You could of course walk the expression-tree and then use Delegate.CreateDelegate to create the appropriate Action<,>. It's quite simple, except for all of the validation-checks (I'm unsure if I've covered everything):

I'm no expression-tree expert, but I don't think building an expression-tree and then calling Compile is possible here since expression-trees can't contain assignment statements, as far as I know. (EDIT: Apparently, these have been added in .NET 4. It's a hard-to-find feature since the C# compiler doesn't seem to be able to build them from lambdas).

public static Action<TContaining, TProperty>
    CreateSetter<TContaining, TProperty>
    (Expression<Func<TContaining, TProperty>> getter)
{
    if (getter == null)
        throw new ArgumentNullException("getter");

    var memberEx = getter.Body as MemberExpression;

    if (memberEx == null)
        throw new ArgumentException("Body is not a member-expression.");

    var property = memberEx.Member as PropertyInfo;

    if (property == null)
        throw new ArgumentException("Member is not a property.");

    if(!property.CanWrite)
        throw new ArgumentException("Property is not writable.");

    return (Action<TContaining, TProperty>)
           Delegate.CreateDelegate(typeof(Action<TContaining, TProperty>),
                                   property.GetSetMethod());
}

Usage:

public class Person { public int Age { get; set; } }

...

static void Main(string[] args)
{
    var setter = CreateSetter((Person p) => p.Age);
    var person = new Person();
    setter(person, 25);

    Console.WriteLine(person.Age); // 25     
}

Do note that this creates an open instance delegate, meaning that it's not bound to any particular instance of TContaining. It's simple to modify it to be bound to a specific instance; you'll have to pass a TContaining as well to the method and then use a different overload of Delegate.CreateDelegate. The signature of the method would then look something like:

public static Action<TProperty> CreateSetter<TContaining, TProperty>
        (Expression<Func<TContaining, TProperty>> getter, TContaining obj)


回答3:

Pointers only I'm afraid (I'm not at a pc) - but;

  • the lambda's .Body will most likely be MemberExpression
  • do a safe cast (as etc) and access the .Member
  • since you belive this a property, this should be a PropertyInfo, so test/cast etc
  • from a PropertyInfo, call GetSetMethod() to get the corresponding MethodInfo
  • use Delegate.CreateDelegate to get that as a delegate (passing the action type)
  • finally, cast the Delegate returned onto the expected delegate type