2011-10-11 18 views
8

Soy nuevo en las expresiones, y me gustaría saber cómo es posible de alguna manera convertir mi expresiónExpresión <Func <TModel, string >> a Expression <Acción <TModel>> "Getter" a "Setter"

Digamos que en este ejemplo mi TModel es de tipo cliente, y le asignó un sitio como este:

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

a algo así como

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

Así que en resumen, quiero construir de alguna manera y compilar una expresión que establece el nombre del cliente especificado por mi expresión getter, a un valor específico.

+0

¿Qué debería pasar si no hay getter (es decir, la propiedad solo tiene un setter)? – Joey

+0

No estoy creando una solución general aquí. Las propiedades que planeo usar esto en adelante tendrán getters y setters accesibles, me pregunto cómo construir el Setter –

+2

Mire la primera respuesta desde esta pregunta. http://stackoverflow.com/questions/2823236/creating-a-property-setter-delegate – baalazamon

Respuesta

10

Versión modificada. Esta clase es probablemente mejor que muchas otras que puede encontrar :-) Esto se debe a que esta versión admite propiedades directas (p => p.B) (como todos los demás :-)), propiedades anidadas (p => p.B.C.D), campos (tanto "terminal" como " en el medio", por lo que en p => p.B.C.D tanto B y D podría ser campos) y '' casting de tipos (por lo p => ((BType)p.B).C.D y p => (p.B as BType).C.D). la única cosa que no es compatible se fundición de la 'interna elemento terminal' (por lo que no p => (object)p.B) .

Hay dos "codepaths" en el generador:. a expresiones simples (p => p.B) y para las expresiones "anidados" Hay variantes de código para .NET 4.0 (que tiene el tipo Expression.Assign expresión) Desde algunos puntos de referencia de. Los delegados más rápidos son: "simple" Delegate.CreateDelegate para las propiedades, Expression.Assign para los campos y "simple" FieldSetter para los campos (este es solo un poco más lento que Expression.Assign para los campos). Por lo tanto, en .NET 4.0 debe quitar todo el código marcado como 3.5.

Parte del código no es mío. La versión inicial (simple) se basó en el código de Fluidez NHibernate (pero solo admite propiedades directas), algunas otras partes se basan en el código de How do I set a field value in an C# Expression tree? y 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 
} 
+0

Esto se ve muy prometedor, obtengo una opinión argumentada en la llamada de CreateDelegate difícil, "Error al enlazar al método de destino". la expresión en modo de depuración tiene un NodeType of Parameter, y el miembro que estoy tratando de establecer es un DateTime –

+0

@Mvision ¿Qué intentas vincular? Esto funcionará solo en Propiedades públicas. – xanatos

+0

Supongo que todas las propiedades que estoy vinculando son públicas, las selecciono manualmente de entidades EF4 como esta: customer => customer.DateCreated etc –

1

que tienen este método auxiliar que devuelve la información de la propiedad para una propiedad:

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

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

A continuación, puede utilizar el PropertyInfo para establecer el valor de la propiedad en una clase.

Tendrá que modificar el código para adaptarlo a sus necesidades, pero con suerte lo ayudará.

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

Ésta es mi manera

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

Como la respuesta correcta no funcionaba para mí (colecciones en la expresión), pero me empujó a la dirección correcta, que necesitaba para investigar este mucho y creo que se me ocurrió un método que puede generar setter para literalmente cualquier expresión miembro.

Para propiedades y campos se comporta igual que la respuesta marcada (creo que es mucho más transparente).

Tiene soporte adicional para listas y diccionarios - vea en los comentarios.

public static Action<TObject, TPropertyOnObject> GetSetter<TObject, TPropertyOnObject>(Expression<Func<TObject, TPropertyOnObject>> getterExpression) 
    { 
     /*** SIMPLE PROPERTIES AND FIELDS ***/ 
     // check if the getter expression reffers 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 "dictionarry 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); 
    } 
Cuestiones relacionadas