Expression<Func<TModel,string>> to Expression<Action<TModel>> "Getter" to "Setter"
Asked Answered
S

5

9

I'm new to expressions, and i'd like to know how if it's in any way possible to convert my expression

Let's say in this example my TModel is of type Customer, and assigned it somewhere like this:

Expression<Func<TModel, string>> getvalueexpression = customer =>customer.Name

to something like

Expression<Action<TModel,string>> setvalueexpression = [PSEUDOCODE] getvalueexpression = input
Action<TModel,string> Setter  = setvalueexpression.Compile();
Setter(mycustomer,value);

So in short, i want to somehow build and compile an expression that sets the customer name specified by my getter expression, to a specific value.

Stanfield answered 11/10, 2011 at 9:10 Comment(5)
What should happen if there is no getter (i.e. the property only has a setter)?Rosaline
I'm not creating a general solution here. The properties I plan using this on will have acessible getters and setters, i'm just wondering how to build the SetterStanfield
Take a look at first answer form this question. #2823736Youlandayoulton
Maybe you should rather go the other direction - from setter to getter.Looby
CreateDelegate based approach is often faster than compiling expression trees based ones. I also suggest the one mentioned in Marc's answer in #2823736 or mine here https://mcmap.net/q/206497/-reflection-performance-create-delegate-properties-c.Valenza
S
11

Modified version. This class is probably better than many other ones you can find around :-) This is because this version support direct properties (p => p.B) (as everyone else :-) ), nested properties (p => p.B.C.D), fields (both "terminal" and "in the middle", so in p => p.B.C.D both B and D could be fields) and "inner" casting of types (so p => ((BType)p.B).C.D and p => (p.B as BType).C.D). The only thing that isn't supported is casting of the "terminal" element (so no p => (object)p.B).

There are two "codepaths" in the generator: for simple Expressions (p => p.B) and for "nested" expressions. There are code variants for .NET 4.0 (that has the Expression.Assign expression type). From some benchmarks of mine the fastest delegates are: "simple" Delegate.CreateDelegate for properties, Expression.Assign for fields and "simple" FieldSetter for fields (this one is just a little slower than Expression.Assign for fields). So under .NET 4.0 you should take away all the code marked as 3.5.

Part of the code isn't mine. The initial (simple) version was based on the Fluent NHibernate code (but it supported only direct properties), some other parts are based on code from How do I set a field value in an C# Expression tree? and Assignment in .NET 3.5 expression trees.

public static class FluentTools
{
    public static Action<T, TValue> GetterToSetter<T, TValue>(Expression<Func<T, TValue>> getter)
    {
        ParameterExpression parameter;
        Expression instance;
        MemberExpression propertyOrField;

        GetMemberExpression(getter, out parameter, out instance, out propertyOrField);

        // Very simple case: p => p.Property or p => p.Field
        if (parameter == instance)
        {
            if (propertyOrField.Member.MemberType == MemberTypes.Property)
            {
                // This is FASTER than Expression trees! (5x on my benchmarks) but works only on properties
                PropertyInfo property = propertyOrField.Member as PropertyInfo;
                MethodInfo setter = property.GetSetMethod();
                var action = (Action<T, TValue>)Delegate.CreateDelegate(typeof(Action<T, TValue>), setter);
                return action;
            }
            #region .NET 3.5
            else // if (propertyOrField.Member.MemberType == MemberTypes.Field)
            {
                // 1.2x slower than 4.0 method, 5x faster than 3.5 method
                FieldInfo field = propertyOrField.Member as FieldInfo;
                var action = FieldSetter<T, TValue>(field);
                return action;
            }
            #endregion
        }

        ParameterExpression value = Expression.Parameter(typeof(TValue), "val");

        Expression expr = null;

        #region .NET 3.5
        if (propertyOrField.Member.MemberType == MemberTypes.Property)
        {
            PropertyInfo property = propertyOrField.Member as PropertyInfo;
            MethodInfo setter = property.GetSetMethod();
            expr = Expression.Call(instance, setter, value);
        }
        else // if (propertyOrField.Member.MemberType == MemberTypes.Field)
        {
            expr = FieldSetter(propertyOrField, value);
        }
        #endregion

        //#region .NET 4.0
        //// For field access it's 5x faster than the 3.5 method and 1.2x than "simple" method. For property access nearly same speed (1.1x faster).
        //expr = Expression.Assign(propertyOrField, value);
        //#endregion

        return Expression.Lambda<Action<T, TValue>>(expr, parameter, value).Compile();
    }

    private static void GetMemberExpression<T, U>(Expression<Func<T, U>> expression, out ParameterExpression parameter, out Expression instance, out MemberExpression propertyOrField)
    {
        Expression current = expression.Body;

        while (current.NodeType == ExpressionType.Convert || current.NodeType == ExpressionType.TypeAs)
        {
            current = (current as UnaryExpression).Operand;
        }

        if (current.NodeType != ExpressionType.MemberAccess)
        {
            throw new ArgumentException();
        }

        propertyOrField = current as MemberExpression;
        current = propertyOrField.Expression;

        instance = current;

        while (current.NodeType != ExpressionType.Parameter)
        {
            if (current.NodeType == ExpressionType.Convert || current.NodeType == ExpressionType.TypeAs)
            {
                current = (current as UnaryExpression).Operand;
            }
            else if (current.NodeType == ExpressionType.MemberAccess)
            {
                current = (current as MemberExpression).Expression;
            }
            else
            {
                throw new ArgumentException();
            }
        }

        parameter = current as ParameterExpression;
    }

    #region .NET 3.5

    // Based on https://mcmap.net/q/385921/-how-do-i-set-a-field-value-in-an-c-expression-tree/321686#321686
    private static Action<T, TValue> FieldSetter<T, TValue>(FieldInfo field)
    {
        DynamicMethod m = new DynamicMethod("setter", typeof(void), new Type[] { typeof(T), typeof(TValue) }, typeof(FluentTools));
        ILGenerator cg = m.GetILGenerator();

        // arg0.<field> = arg1
        cg.Emit(OpCodes.Ldarg_0);
        cg.Emit(OpCodes.Ldarg_1);
        cg.Emit(OpCodes.Stfld, field);
        cg.Emit(OpCodes.Ret);

        return (Action<T, TValue>)m.CreateDelegate(typeof(Action<T, TValue>));
    }

    // Based on https://mcmap.net/q/110422/-assignment-in-net-3-5-expression-trees/3972359#3972359
    private static Expression FieldSetter(Expression left, Expression right)
    {
        return
            Expression.Call(
                null,
                typeof(FluentTools)
                    .GetMethod("AssignTo", BindingFlags.NonPublic | BindingFlags.Static)
                    .MakeGenericMethod(left.Type),
                left,
                right);
    }

    private static void AssignTo<T>(ref T left, T right)  // note the 'ref', which is
    {                                                     // important when assigning
        left = right;                                     // to value types!
    }

    #endregion
}
Stcyr answered 11/10, 2011 at 9:28 Comment(10)
This looks very promising, i'm getting an argumentexception on the CreateDelegate call tough, "Error binding to target method." the expression in debug mode has a NodeType of Parameter, and the member i'm trying to set is a DateTimeStanfield
@Mvision What are you trying to bind? This will work only on public Properties.Stcyr
I assume all properties i'm binding are public, i handpick them from EF4 entities like this : customer=>customer.DateCreated etcStanfield
@Mvision Here it works correctly. I've just generated an EF model to test it. Try looking at the definition of DateCreated (right click, go to definition)Stcyr
public global::System.DateTime Datecreated, i'm using "object" as U tough, because i'm not sure wich type the value will be, might that be it?Stanfield
@Mvision Try posting the code you use to create the delegate to DateCreated.Stcyr
Aha, actually i oversimplified my previous answer. It's customer.Product.DateCreated there's a foreign key property in between, might that give the "not a member" error?Stanfield
it's done like this: var columninfo = new MyColumnInfo<Customer>(model => model.Product.DateCreated); inside MyColumnInfo's constructor i'm building the setter with your codeStanfield
@Mvision I have to tell the truth, I don't know how to solve the problem. It works only for "direct" properties.Stcyr
let us continue this discussion in chatStanfield
R
4
static Expression<Action<T, TProperty>> MakeSetter<T, TProperty>(Expression<Func<T, TProperty>> getter)
{
    var memberExpr = (MemberExpression)getter.Body;
    var @this = Expression.Parameter(typeof(T), "$this");
    var value = Expression.Parameter(typeof(TProperty), "value");
    return Expression.Lambda<Action<T, TProperty>>(
        Expression.Assign(Expression.MakeMemberAccess(@this, memberExpr.Member), value),
        @this, value);
}
Reptant answered 11/10, 2011 at 10:7 Comment(0)
R
2

I have this helper method which returns the property info for a property:

public static PropertyInfo GetPropertyInfo<T, U>(Expression<Func<T, U>> property) where T : class
{
    var memberExpression = (property.Body as MemberExpression);

    if (memberExpression != null && memberExpression.Member is PropertyInfo)
    {
        return memberExpression.Member as PropertyInfo;
    }

    throw new InvalidOperationException("Invalid usage of GetPropertyInfo");
}

Usage: GetPropertyInfo((MyClass c) => c.PropertyName);

You can then use the PropertyInfo to set the value of the property on a class.

You will need to modify the code to suit your needs but hopefully it will help.

Rosaline answered 11/10, 2011 at 9:22 Comment(0)
H
1

As the correct answer didn't work for me (collections in the expression) but pushed me to the right direction, I needed to investigate this a lot and I think I came up with a method which can generate setter for literally any member expression.

For properties and fields it behaves the same as the marked answer (I believe it is much more transparent though).

It has additional support for Lists and Dictionaries - please see in the comments.

public static Action<TObject, TPropertyOnObject> GetSetter<TObject, TPropertyOnObject>(Expression<Func<TObject, TPropertyOnObject>> getterExpression)
    {
        /*** SIMPLE PROPERTIES AND FIELDS ***/
        // check if the getter expression refers directly to a PROPERTY or FIELD
        var memberAcessExpression = getterExpression.Body as MemberExpression;

        if (memberAcessExpression != null)
        {
            //to here we assign the SetValue method of a property or field
            Action<object, object> propertyOrFieldSetValue = null;

            // property
            var propertyInfo = memberAcessExpression.Member as PropertyInfo;

            if (propertyInfo != null)
            {
                propertyOrFieldSetValue = (declaringObjectInstance, propertyOrFieldValue) => propertyInfo.SetValue(declaringObjectInstance, propertyOrFieldValue);
            };

            // field
            var fieldInfo = memberAcessExpression.Member as FieldInfo;

            if (fieldInfo != null)
            {
                propertyOrFieldSetValue = (declaringObjectInstance, propertyOrFieldValue) => fieldInfo.SetValue(declaringObjectInstance, propertyOrFieldValue);
            }

            // This is the expression to get declaring object instance.
            // Example: for expression "o=>o.Property1.Property2.CollectionProperty[3].TargetProperty" it gives us the "o.Property1.Property2.CollectionProperty[3]" part
            var memberAcessExpressionCompiledLambda = Expression.Lambda(memberAcessExpression.Expression, getterExpression.Parameters.Single()).Compile();
            Action<TObject, TPropertyOnObject> setter = (expressionParameter, value) =>
            {
                // get the object instance on which is the property we want to set
                var declaringObjectInstance = memberAcessExpressionCompiledLambda.DynamicInvoke(expressionParameter);
                Debug.Assert(propertyOrFieldSetValue != null, "propertyOrFieldSetValue != null");
                // set the value of the property
                propertyOrFieldSetValue(declaringObjectInstance, value);
            };

            return setter;
        }

        /*** COLLECTIONS ( IDictionary<,> and IList<,>) ***/
        /*
         * DICTIONARY:
         * Sample expression: 
         *      "myObj => myObj.Property1.ListProperty[5].AdditionalInfo["KEY"]"
         * Setter behaviour:
         *      The same as adding to a dictionary. 
         *      It does Add("KEY", <value to be set>) to the dictionary. It fails if the jey already exists.
         *      
         * 
         * LIST
         * Sample expression: 
         *      "myObj => myObj.Property1.ListProperty[INDEX]"
         * Setter behaviour:
         *      If INDEX >= 0 and the index exists in the collection it behaves the same like inserting to a collection.
         *      IF INDEX <  0 (is negative) it adds the value at the end of the collection.
         */
        var methodCallExpression = getterExpression.Body as MethodCallExpression;
        if (
            methodCallExpression != null && methodCallExpression.Object != null &&
            methodCallExpression.Object.Type.IsGenericType)
        {
            var collectionGetterExpression = methodCallExpression.Object as MemberExpression;
            Debug.Assert(collectionGetterExpression != null, "collectionGetterExpression != null");

            // This gives us the collection instance when it is invoked on the object instance whic the expression is for
            var collectionGetterCompiledLambda =Expression.Lambda(collectionGetterExpression, getterExpression.Parameters.Single()).Compile();

            // this returns the "KEY" which is the key (object) in case of Dictionaries and Index (integer) in case of other collections
            var collectionKey = ((ConstantExpression) methodCallExpression.Arguments[0]).Value;
            var collectionType = collectionGetterExpression.Type;

            // IDICTIONARY
            if (collectionType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                // Create an action which accepts the instance of the object which the "dictionary getter" expression is for and a value
                // to be added to the dictionary.
                Action<TObject, TPropertyOnObject> dictionaryAdder = (expressionParameter, value) =>
                {
                    try
                    {
                        var dictionaryInstance = (IDictionary)collectionGetterCompiledLambda.DynamicInvoke(expressionParameter);
                        dictionaryInstance.Add(collectionKey, value);
                    }
                    catch (Exception exception)
                    {
                        throw new Exception(
                            string.Format(
                                "Addition to dictionary failed [Key='{0}', Value='{1}']. The \"adder\" was generated from getter expression: '{2}'.",
                                collectionKey,
                                value,
                                getterExpression.ToString()), exception);
                    }
                };

                return dictionaryAdder;
            }

            // ILIST
            if (typeof (IList<>).MakeGenericType(typeof (bool)).IsAssignableFrom(collectionType.GetGenericTypeDefinition().MakeGenericType(typeof(bool))))
            {
                // Create an action which accepts the instance of the object which the "collection getter" expression is for and a value
                // to be inserted
                Action<TObject, TPropertyOnObject> collectionInserter = (expressionParameter, value) =>
                {
                    try
                    {
                        var collectionInstance = (IList<TPropertyOnObject>)collectionGetterCompiledLambda.DynamicInvoke(expressionParameter);
                        var collectionIndexFromExpression = int.Parse(collectionKey.ToString());

                        // The semantics of a collection setter is to add value if the index in expression is <0 and set the item at the index
                        // if the index >=0.
                        if (collectionIndexFromExpression < 0)
                        {
                            collectionInstance.Add(value);
                        }
                        else
                        {
                            collectionInstance[collectionIndexFromExpression] = value;
                        }
                    }
                    catch (Exception invocationException)
                    {
                        throw new Exception(
                            string.Format(
                                "Insertion to collection failed [Index='{0}', Value='{1}']. The \"inserter\" was generated from getter expression: '{2}'.",
                                collectionKey,
                                value,
                                getterExpression.ToString()), invocationException);
                    }
                };

                return collectionInserter;
            }

            throw new NotSupportedException(
                string.Format(
                    "Cannot generate setter from the given expression: '{0}'. Collection type: '{1}' not supported.",
                    getterExpression, collectionType));
        }

        throw new NotSupportedException("Cannot generate setter from the given expression: "+getterExpression);
    }
Henning answered 29/9, 2014 at 14:36 Comment(0)
G
0

this is my way

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

        var expParamP = Expression.Parameter(typeof(T), "p");
        var expParamV = Expression.Parameter(typeof(object), "v");

        var expParamVc = Expression.Convert(expParamV, pi.PropertyType);

        var mma = Expression.Call(
                expParamP
                , pi.GetSetMethod()
                , expParamVc
            );

        var exp = Expression.Lambda<Action<T, object>>(mma, expParamP, expParamV);

        return exp.Compile();
    }
Guesthouse answered 10/7, 2014 at 5:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.