开发者

Create lambda action from function expression

开发者 https://www.devze.com 2022-12-31 10:17 出处:网络
It is relatively easy to create a lambda function that will return the value of a property from an object, even including deep properties...

It is relatively easy to create a lambda function that will return the value of a property from an object, even including deep properties...

Func<Category, string> getCategoryName = new Func<Category, string>(c => c.Name);

and this can be called as follows...

string categoryName = getCategoryName(this.category);

But, given only the resulting function above (or the expression originally used to create the function), can anybody provide an easy way to create the opposing action...

Action<Category, string> setCategoryName = new Action<Category, string>((c, s) => c.Name = s);

...that will enable the same property value to be set as follows?

setCategoryName(this.category, "");

Note that I am looking for a way to create the action programatically from the function or expression - I hope that I have shown that I already know how to create it manually.

I am open to answers that work in both .net 3.5 and 4.0.

Thanks.

UPDATE:

Perhaps I am not being clear in my question, so let me try and demonstrate more clearly what I am trying to do.

I have the following method (that I have created for the purposes of this question)...

void DoLambdaStuff<TObject, TValue>(TObject obj, Expression<Func<TObject, TValue>> expression) {

    Func<TObject, TValue> getValue = expression.Compile();
    TValue stuff = getValue(obj);

    Expression<Action<TObject, TValue>> assignmentExpression = (o, v) => Expression<TObject>.Assign(expression, Expression.Constant(v, typeof(TValue)));
    Action<TObject, TValue> setValue = assignmentExpression.Compile();

    setValue(obj, stuff);

}

What I am looking for is how do I create the "assignmentExpression" within the code so that I can compile it into setValue? I figure it is related to Expression.Assign, but I simply cannot work out the correct combination of parameters to complete the code.

The eventual result is to be able to call

Category category = *<get object from somewhere>*;
this.DoLambdaStuff(category, 开发者_StackOverflow中文版c => c.Name);

and this in turn will create a getter and a setter for the "Name" property of the Category object.

The version above compiles, but when I call setValue() it results in an ArgumentException with "Expression must be writeable".

Thanks again.


void DoLambdaStuff<TObject, TValue>(TObject obj, Expression<Func<TObject, TValue>> expression) {

    Func<TObject, TValue> getValue = expression.Compile();
    TValue stuff = getValue(obj);

    var p = Expression.Parameter(typeof(TValue), "v");
    Expression<Action<TObject, TValue>> assignmentExpression = 
        Expression.Lambda<Action<TObject, TValue>>(Expression.Assign(expression.Body, p), expression.Parameters[0], p);

    Action<TObject, TValue> setValue = assignmentExpression.Compile();

    setValue(obj, stuff);
}


Ok, the code I am looking for goes something like this...

ParameterExpression objectParameterExpression = Expression.Parameter(
  typeof(TObject)),
  valueParameterExpression = Expression.Parameter(typeof(TValue)
);
Expression<Action<TObject, TValue>> setValueExpression = Expression.Lambda<Action<TObject, TValue>>(
  Expression.Block(
    Expression.Assign(
      Expression.Property(
        objectParameterExpression,
        ((MemberExpression) expression.Body).Member.Name
      ),
      valueParameterExpression
    )
  ),
  objectParameterExpression,
  valueParameterExpression
);
Action<TObject, TValue> setValue = setValueExpression.Compile();

This code works, but only for shallow properties (that is, properties of the immediate object) but does not work for deep properties - although the getter function can work for deep properties. It would be interesting to know if anybody can help me modify this to work with deep properties but I will raise this as a seperate question.


As Martin noted above, "deep" properties break his solution. The reason why it does not work is this expression:

Expression.Property(
    objectParameterExpression
,  ((MemberExpression)expression.Body).Member.Name
)

The reason for it is not immediately obvious: the derived class declares its own property getter, which redirects to the base class, but does not define a corresponding setter.

In order to work around this issue, you need to locate the property through reflection, look for its declaring type, and then get the corresponding property from the declarer. Unlike the property you get on the derived class, the property from the declarer has a getter and a setter, and is therefore assignable. (This assumes that a setter of the property is declared at all: if the declarer does not provide a setter, then nobody else could possibly provide it.)

Here is a skeletal implementation that addresses this problem. Replace the above call to Expression.Property with the call of GetPropertyOrField below, and Martin's solution is going to work.

private static MemberExpression GetPropertyOrField(Expression baseExpr, string name) {
    if (baseExpr == null) {
        throw new ArgumentNullException("baseExpr");
    }
    if (string.IsNullOrWhiteSpace(name)) {
        throw new ArgumentException("name");
    }
    var type = baseExpr.Type;
    var properties = type
        .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
        .Where(p => p.Name.Equals(name))
        .ToArray();
    if (properties.Length == 1) {
        var res = properties[0];
        if (res.DeclaringType != type) {
            // Here is the core of the fix:
            var tmp = res
                .DeclaringType
                .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                .Where(p => p.Name.Equals(name))
                .ToArray();
            if (tmp.Length == 1) {
                return Expression.Property(baseExpr, tmp[0]);
            }
        }
        return Expression.Property(baseExpr, res);
    }
    if (properties.Length != 0) {
        throw new NotSupportedException(name);
    }
    var fields = type
        .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
        .Where(p => p.Name.Equals(name))
        .ToArray();
    if (fields.Length == 1) {
        return Expression.Field(baseExpr, fields[0]);
    }
    if (fields.Length != 0) {
        throw new NotSupportedException(name);
    }
    throw new ArgumentException(
        string.Format(
            "Type [{0}] does not define property/field called [{1}]"
        ,   type
        ,   name
        )
    );
}


This should be possible using expression trees, which can be created from lambda expressions, modified, and then compiled into a delegate.

0

精彩评论

暂无评论...
验证码 换一张
取 消