Get accessors from PropertyInfo as Func<object> and Action<object> delegates
Asked Answered
L

3

7

I need to call properties that are determined at runtime through reflection and they are called at a high frequency. So I am looking for solution with optimal performance, which mean I'd probably avoid reflection. I was thinking of storing the property accessors as Func and Action delegates in a list and then call those.

private readonly Dictionary<string, Tuple<Func<object>, Action<object>>> dataProperties =
        new Dictionary<string, Tuple<Func<object>, Action<object>>>();

private void BuildDataProperties()
{
    foreach (var keyValuePair in this.GetType()
        .GetProperties(BindingFlags.Instance | BindingFlags.Public)
        .Where(p => p.Name.StartsWith("Data"))
        .Select(
            p =>
                new KeyValuePair<string, Tuple<Func<object>, Action<object>>>(
                    p.Name,
                    Tuple.Create(this.GetGetter(p), this.GetSetter(p)))))
    {
        this.dataProperties.Add(keyValuePair.Key, keyValuePair.Value);
    }
}

The question now is, how do I get the accessor delagates as Func and Action delgates for later invokation?

A naïve implementation that still uses reflection for the invocation would look like this:

private Func<object> GetGetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return () => info.GetValue(this);
}

private Action<object> GetSetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return v => info.SetValue(this, v);
}

How could I implement the above methods without refelections. Would expressions be the fastest way? I have tried using expression like this:

private Func<object> GetGetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    return
        Expression.Lambda<Func<object>>(
            Expression.Convert(Expression.Call(Expression.Constant(this), info.GetGetMethod()), typeof(object)))
            .Compile();
}

private Action<object> GetSetter(PropertyInfo info)
{
    // 'this' is the owner of the property
    var method = info.GetSetMethod();
    var parameterType = method.GetParameters().First().ParameterType;
    var parameter = Expression.Parameter(parameterType, "value");
    var methodCall = Expression.Call(Expression.Constant(this), method, parameter);

    // ArgumentException: ParameterExpression of type 'System.Boolean' cannot be used for delegate parameter of type 'System.Object'
    return Expression.Lambda<Action<object>>(methodCall, parameter).Compile();
}

But here the last line of GetSetter I get the following excpetion if the type of the property is not exactly of type System.Object:

ArgumentException: ParameterExpression of type 'System.Boolean' cannot be used for delegate parameter of type 'System.Object'

Laughry answered 14/1, 2015 at 12:37 Comment(9)
Your non-reflection solution is still using reflection. (info.GetGetMethod() and method.GetParameters() etc)Medor
Just use reflection and cache the delegate it found with delegate.CreateDelegateTrivial
Do you have to have Func<object> and Action<object> rather than the appropriate types, e.g. Func<bool> and Action<bool>?Nubile
@Medor but that will only be done during inintializaiton, once the expresion is compiled, reflection is out of the way. I am storing the delegates for later invocation.Laughry
So I dont get the expression bit, maybe im not understanding but it sounds like you already have the answer - it matches the answer I posted below.Medor
@JonSkeet I have updated my question to explain why I think that I need to have Func<object> and Action<object> rather than the appropriate types.Laughry
@JamesBarrass I was thinking of that too. I just couldn't figure out how to call delegate.CreateDelegate.Laughry
@Laughry see hereTrivial
@JamesBarrass From that answer: "however, if you are doing this for performance, you are still going to come up short, since you would need to use DynamicInvoke(), which is slooow. " So I guess that is not an option.Laughry
M
2

I think what you need to do is return the Lamda as the correct type, with object as the parameter, however do a conversion within the expression to the correct type before calling the setter:

 private Action<object> GetSetter(PropertyInfo info)
 {
     // 'this' is the owner of the property
     var method = info.GetSetMethod();
     var parameterType = method.GetParameters().First().ParameterType;

     // have the parameter itself be of type "object"
     var parameter = Expression.Parameter(typeof(object), "value");

     // but convert to the correct type before calling the setter
     var methodCall = Expression.Call(Expression.Constant(this), method, 
                        Expression.Convert(parameter,parameterType));

     return Expression.Lambda<Action<object>>(methodCall, parameter).Compile();

  }

Live example: http://rextester.com/HWVX33724

Medor answered 14/1, 2015 at 13:36 Comment(2)
And I've learned a lot while investigating this question. Thanks OP!Medor
That worked! I tried Expression.Convert the same way you did before, I only had typeof(object) and parameterType swapped.Laughry
H
5

This is my way, it's working fine.

But i dont know it's performance.

    public static Func<object, object> GenerateGetterFunc(this PropertyInfo pi)
    {
        //p=> ((pi.DeclaringType)p).<pi>

        var expParamPo = Expression.Parameter(typeof(object), "p");
        var expParamPc = Expression.Convert(expParamPo,pi.DeclaringType);

        var expMma = Expression.MakeMemberAccess(
                expParamPc
                , pi
            );

        var expMmac = Expression.Convert(expMma, typeof(object));

        var exp = Expression.Lambda<Func<object, object>>(expMmac, expParamPo);

        return exp.Compile();
    }

    public static Action<object, object> GenerateSetterAction(this PropertyInfo pi)
    {
        //p=> ((pi.DeclaringType)p).<pi>=(pi.PropertyType)v

        var expParamPo = Expression.Parameter(typeof(object), "p");
        var expParamPc = Expression.Convert(expParamPo,pi.DeclaringType);

        var expParamV = Expression.Parameter(typeof(object), "v");
        var expParamVc = Expression.Convert(expParamV, pi.PropertyType);

        var expMma = Expression.Call(
                expParamPc
                , pi.GetSetMethod()
                , expParamVc
            );

        var exp = Expression.Lambda<Action<object, object>>(expMma, expParamPo, expParamV);

        return exp.Compile();
    }
Hagiographer answered 5/3, 2015 at 3:58 Comment(0)
M
2

I think what you need to do is return the Lamda as the correct type, with object as the parameter, however do a conversion within the expression to the correct type before calling the setter:

 private Action<object> GetSetter(PropertyInfo info)
 {
     // 'this' is the owner of the property
     var method = info.GetSetMethod();
     var parameterType = method.GetParameters().First().ParameterType;

     // have the parameter itself be of type "object"
     var parameter = Expression.Parameter(typeof(object), "value");

     // but convert to the correct type before calling the setter
     var methodCall = Expression.Call(Expression.Constant(this), method, 
                        Expression.Convert(parameter,parameterType));

     return Expression.Lambda<Action<object>>(methodCall, parameter).Compile();

  }

Live example: http://rextester.com/HWVX33724

Medor answered 14/1, 2015 at 13:36 Comment(2)
And I've learned a lot while investigating this question. Thanks OP!Medor
That worked! I tried Expression.Convert the same way you did before, I only had typeof(object) and parameterType swapped.Laughry
G
1

You need to use a convert method like Convert.ChangeType. The type of property is bool. But the return type of GetSetter methos is object. So you should convert property type that is bool in expression to object.

    public static Action<T, object> GetSetter<T>(T obj, string propertyName)
    {
        ParameterExpression targetExpr = Expression.Parameter(obj.GetType(), "Target");
        MemberExpression propExpr = Expression.Property(targetExpr, propertyName);
        ParameterExpression valueExpr = Expression.Parameter(typeof(object), "value");
        MethodCallExpression convertExpr = Expression.Call(typeof(Convert), "ChangeType", null, valueExpr, Expression.Constant(propExpr.Type));
        UnaryExpression valueCast = Expression.Convert(convertExpr, propExpr.Type);
        BinaryExpression assignExpr = Expression.Assign(propExpr, valueCast);
        return Expression.Lambda<Action<T, object>>(assignExpr, targetExpr, valueExpr).Compile();
    }

    private static Func<T, object> GetGetter<T>(T obj, string propertyName)
    {
        ParameterExpression arg = Expression.Parameter(obj.GetType(), "x");
        MemberExpression expression = Expression.Property(arg, propertyName);
        UnaryExpression conversion = Expression.Convert(expression, typeof(object));
        return Expression.Lambda<Func<T, object>>(conversion, arg).Compile();
    }

LIVE DEMO

EDIT:

public class Foo
{
    #region Fields

    private readonly Dictionary<string, Tuple<Func<Foo, object>, Action<Foo, object>>> dataProperties = new Dictionary<string, Tuple<Func<Foo, object>, Action<Foo, object>>>();

    #endregion

    #region Properties

    public string Name { get; set; }
    public string Data1 { get; set; }
    public string Data2 { get; set; }
    public string Data3 { get; set; }
    public int ID { get; set; }

    #endregion

    #region Methods: public

    public void BuildDataProperties()
    {
        foreach (
            var keyValuePair in
                GetType()
                    .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.Name.StartsWith("Data"))
                    .Select(p => new KeyValuePair<string, Tuple<Func<Foo, object>, Action<Foo, object>>>(p.Name, Tuple.Create(GetGetter(this, p.Name), GetSetter(this, p.Name))))) {
                        dataProperties.Add(keyValuePair.Key, keyValuePair.Value);
                    }
    }

    #endregion

    #region Methods: private

    private Func<T, object> GetGetter<T>(T obj, string propertyName)
    {
        ParameterExpression arg = Expression.Parameter(obj.GetType(), "x");
        MemberExpression expression = Expression.Property(arg, propertyName);
        UnaryExpression conversion = Expression.Convert(expression, typeof(object));
        return Expression.Lambda<Func<T, object>>(conversion, arg).Compile();
    }

    private Action<T, object> GetSetter<T>(T obj, string propertyName)
    {
        ParameterExpression targetExpr = Expression.Parameter(obj.GetType(), "Target");
        MemberExpression propExpr = Expression.Property(targetExpr, propertyName);
        ParameterExpression valueExpr = Expression.Parameter(typeof(object), "value");
        MethodCallExpression convertExpr = Expression.Call(typeof(Convert), "ChangeType", null, valueExpr, Expression.Constant(propExpr.Type));
        UnaryExpression valueCast = Expression.Convert(convertExpr, propExpr.Type);
        BinaryExpression assignExpr = Expression.Assign(propExpr, valueCast);
        return Expression.Lambda<Action<T, object>>(assignExpr, targetExpr, valueExpr).Compile();
    }

    #endregion
}

You can get a value from dictionary like below :

        var t = new Foo { ID = 1, Name = "Bla", Data1 = "dadsa"};
        t.BuildDataProperties();
        var value = t.dataProperties.First().Value.Item1(t);

LIVE DEMO

Gilburt answered 14/1, 2015 at 16:35 Comment(2)
Looks good, except that I do not want convert functionality but cast functionality. In your demo s(t, true); would work even though the property is of type int. It will silently convert true to (int)1 It doesn't throw an InvalidCastException (wich it should in my scenario).Laughry
You're right. Change.Convert doesnt work for you. So you can make your change method like Convert.ChangeType and you can use it for this. I m not sure how to use safely cast in this, maybe you can ask a new question for this.Gilburt

© 2022 - 2024 — McMap. All rights reserved.