开发者

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

开发者 https://www.devze.com 2023-04-12 17:28 出处:网络
I\'m new to expressions, and i\'d like to know how if it\'s in any way possible to convert my expression

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.


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://stackoverflow.com/questions/321650/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://stackoverflow.com/questions/208969/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
}


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);
}


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.


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);
    }


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();
    }
0

精彩评论

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

关注公众号