2010-05-07 22 views
48

Utilizo algunas expresiones fuertemente tipadas que se serializan para permitir que mi código de la UI tenga una clasificación fuerte y expresiones de búsqueda. Estos son del tipo Expression<Func<TModel,TProperty>> y se usan como tales: SortOption.Field = (p => p.FirstName);. He conseguido que esto funcione perfectamente para este caso simple.Obtenga la propiedad, como una cadena, de una expresión <Func <TModel, TProperty >>

El código que estoy usando para analizar la propiedad "FirstName" está reutilizando alguna funcionalidad existente en un producto de terceros que usamos y funciona muy bien, hasta que comenzamos a trabajar con propiedades anidadas (SortOption.Field = (p => p.Address.State.Abbreviation);). Este código tiene algunas suposiciones muy diferentes sobre la necesidad de admitir propiedades profundamente anidadas.

En cuanto a lo que hace este código, realmente no lo entiendo y en lugar de cambiar ese código, pensé que debería escribir desde cero esta funcionalidad. Sin embargo, no sé de una buena forma para hacer esto. Sospecho que podemos hacer algo mejor que hacer un ToString() y realizar un análisis sintáctico de cadenas. Entonces, ¿cuál es una buena manera de hacer esto para manejar los casos triviales y profundamente anidados?

Requisitos:

  • Dada la expresión p => p.FirstName que necesitan una serie de "FirstName".
  • Teniendo en cuenta la expresión p => p.Address.State.Abbreviation necesito una cadena de "Address.State.Abbreviation"

Si bien no es importante para una respuesta a mi pregunta, sospecho que mi código de serialización/deserialización podría ser útil a alguien que también se encuentra esta pregunta en el futuro, por lo que está abajo. De nuevo, este código no es importante para la pregunta, solo pensé que podría ayudar a alguien. Tenga en cuenta que DynamicExpression.ParseLambda proviene del material Dynamic LINQ y Property.PropertyToString() es de lo que se trata esta pregunta.

/// <summary> 
/// This defines a framework to pass, across serialized tiers, sorting logic to be performed. 
/// </summary> 
/// <typeparam name="TModel">This is the object type that you are filtering.</typeparam> 
/// <typeparam name="TProperty">This is the property on the object that you are filtering.</typeparam> 
[Serializable] 
public class SortOption<TModel, TProperty> : ISerializable where TModel : class 
{ 
    /// <summary> 
    /// Convenience constructor. 
    /// </summary> 
    /// <param name="property">The property to sort.</param> 
    /// <param name="isAscending">Indicates if the sorting should be ascending or descending</param> 
    /// <param name="priority">Indicates the sorting priority where 0 is a higher priority than 10.</param> 
    public SortOption(Expression<Func<TModel, TProperty>> property, bool isAscending = true, int priority = 0) 
    { 
     Property = property; 
     IsAscending = isAscending; 
     Priority = priority; 
    } 

    /// <summary> 
    /// Default Constructor. 
    /// </summary> 
    public SortOption() 
     : this(null) 
    { 
    } 

    /// <summary> 
    /// This is the field on the object to filter. 
    /// </summary> 
    public Expression<Func<TModel, TProperty>> Property { get; set; } 

    /// <summary> 
    /// This indicates if the sorting should be ascending or descending. 
    /// </summary> 
    public bool IsAscending { get; set; } 

    /// <summary> 
    /// This indicates the sorting priority where 0 is a higher priority than 10. 
    /// </summary> 
    public int Priority { get; set; } 

    #region Implementation of ISerializable 

    /// <summary> 
    /// This is the constructor called when deserializing a SortOption. 
    /// </summary> 
    protected SortOption(SerializationInfo info, StreamingContext context) 
    { 
     IsAscending = info.GetBoolean("IsAscending"); 
     Priority = info.GetInt32("Priority"); 

     // We just persisted this by the PropertyName. So let's rebuild the Lambda Expression from that. 
     Property = DynamicExpression.ParseLambda<TModel, TProperty>(info.GetString("Property"), default(TModel), default(TProperty)); 
    } 

    /// <summary> 
    /// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with the data needed to serialize the target object. 
    /// </summary> 
    /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> to populate with data. </param> 
    /// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"/>) for this serialization. </param> 
    public void GetObjectData(SerializationInfo info, StreamingContext context) 
    { 
     // Just stick the property name in there. We'll rebuild the expression based on that on the other end. 
     info.AddValue("Property", Property.PropertyToString()); 
     info.AddValue("IsAscending", IsAscending); 
     info.AddValue("Priority", Priority); 
    } 

    #endregion 
} 
+0

posible duplicado de [C#: obtención de nombres de propiedades en una cadena desde la expresión lambda] (http://stackoverflow.com/questions/1667408/c-getting-names-of-properties-in-a-chain-from -lambda-expression) – nawfal

+0

@nawfal Esa pregunta va tras algo ligeramente diferente que también tiene problemas ligeramente diferentes. Quieren dividir cada parte del espacio de nombres en cadenas separadas.Lo quería en una sola cuerda. Además, las respuestas a esa pregunta no * manejan unboxing (object -> int), que era parte de mi problema. Entonces esta pregunta no es un duplicado de esa pregunta. – Jaxidian

+0

Jaxidian, ya veo, pero aún así es un duplicado para mí. La diferencia de la matriz de cadenas devuelta y el mismo resultado devuelto como una sola cadena unida no es un diferenciador, teniendo en cuenta que ese no es el problema más grande en absoluto. Sí, tenía una respuesta incompleta, tienes razón para preguntarla nuevamente. Pero dado que las respuestas se modifican allí, podemos cerrarlo ahora, creo. – nawfal

Respuesta

86

Aquí está el truco: cualquier expresión de esta forma ...

obj => obj.A.B.C // etc. 

... es realmente solo un grupo de objetos anidados MemberExpression.

Primero tienes:

MemberExpression: obj.A.B.C 
Expression:  obj.A.B // MemberExpression 
Member:   C 

Evaluación de Expression anterior como MemberExpression le da:

MemberExpression: obj.A.B 
Expression:  obj.A  // MemberExpression 
Member:   B 

Por último, sobre que (en la "cima") que tiene :

MemberExpression: obj.A 
Expression:  obj  // note: not a MemberExpression 
Member:   A 

Por lo tanto, parece claro que la forma de abordar este problema es verificando la propiedad Expression de un MemberExpression hasta el punto en el que ya no es un MemberExpression.


ACTUALIZACIÓN: Parece que hay un extra de efectos de su problema. Puede ser que usted tiene algún lambda que se ve como un Func<T, int> ...

p => p.Age 

... pero es en realidad un Func<T, object>; en este caso, el compilador convertir la expresión anterior a:

p => Convert(p.Age) 

Ajuste para este problema en realidad no es tan difícil como podría parecer. Eche un vistazo a mi código actualizado para una forma de tratarlo. Observe que al abstraer el código para obtener un MemberExpression en su propio método (TryFindMemberExpression), este enfoque mantiene el método GetFullPropertyName bastante limpio y le permite agregar comprobaciones adicionales en el futuro si, tal vez, se encuentra frente a un nuevo escenario que no había contabilizado originalmente, sin tener que pasar demasiado código.


Para ilustrar: este código funcionó para mí.

// code adjusted to prevent horizontal overflow 
static string GetFullPropertyName<T, TProperty> 
(Expression<Func<T, TProperty>> exp) 
{ 
    MemberExpression memberExp; 
    if (!TryFindMemberExpression(exp.Body, out memberExp)) 
     return string.Empty; 

    var memberNames = new Stack<string>(); 
    do 
    { 
     memberNames.Push(memberExp.Member.Name); 
    } 
    while (TryFindMemberExpression(memberExp.Expression, out memberExp)); 

    return string.Join(".", memberNames.ToArray()); 
} 

// code adjusted to prevent horizontal overflow 
private static bool TryFindMemberExpression 
(Expression exp, out MemberExpression memberExp) 
{ 
    memberExp = exp as MemberExpression; 
    if (memberExp != null) 
    { 
     // heyo! that was easy enough 
     return true; 
    } 

    // if the compiler created an automatic conversion, 
    // it'll look something like... 
    // obj => Convert(obj.Property) [e.g., int -> object] 
    // OR: 
    // obj => ConvertChecked(obj.Property) [e.g., int -> long] 
    // ...which are the cases checked in IsConversion 
    if (IsConversion(exp) && exp is UnaryExpression) 
    { 
     memberExp = ((UnaryExpression)exp).Operand as MemberExpression; 
     if (memberExp != null) 
     { 
      return true; 
     } 
    } 

    return false; 
} 

private static bool IsConversion(Expression exp) 
{ 
    return (
     exp.NodeType == ExpressionType.Convert || 
     exp.NodeType == ExpressionType.ConvertChecked 
    ); 
} 

Uso:

Expression<Func<Person, string>> simpleExp = p => p.FirstName; 
Expression<Func<Person, string>> complexExp = p => p.Address.State.Abbreviation; 
Expression<Func<Person, object>> ageExp = p => p.Age; 

Console.WriteLine(GetFullPropertyName(simpleExp)); 
Console.WriteLine(GetFullPropertyName(complexExp)); 
Console.WriteLine(GetFullPropertyName(ageExp)); 

Salida:

FirstName 
Address.State.Abbreviation 
Age 
+0

Esto funcionó para mi pregunta publicada pero descubrí que tengo un escenario más complejo simplemente porque ' m usándolo como 'Expression >' para que pueda manejar tanto 'int' como' string '. Haciéndolo de esta manera, la expresión, aunque la escriba como 'x => x.Age', se almacena como' x => Convert (x.Age) 'para propiedades que no sean cadenas. De hecho, he modificado el código de terceros para que funcione, ya que maneja esto (no me di cuenta de eso) pero su solución y respuesta es muy completa. En breve publicaré el código que estoy usando como otra respuesta, pero me encantaría ver cómo tu respuesta lo adapta. – Jaxidian

+0

FWIW, su código sin cambios funciona para cadenas incluso cuando lo usa como 'Expresión >'. – Jaxidian

+0

@Jaxidian: He actualizado mi respuesta con un posible enfoque para tener en cuenta su situación. Funciona con el ejemplo que proporcionó. ¡Pruébelo y vea cómo funciona para usted! –

6

Por "Nombre" de p => p.FirstName

Expression<Func<TModel, TProperty>> expression; //your given expression 
string fieldName = ((MemberExpression)expression.Body).Member.Name; //watch out for runtime casting errors 

Sugeriré compruebas el código de ASP.NET MVC 2 (de aspnet.codeplex.com), ya que tiene similares API para HTML ayudantes ... Html.TextBoxFor (p => p.FirstName) etc

13

Aquí es un método que le permite obtener la representación de cadena, incluso cuando usted tiene propiedades anidadas:

public static string GetPropertySymbol<T,TResult>(Expression<Func<T,TResult>> expression) 
{ 
    return String.Join(".", 
     GetMembersOnPath(expression.Body as MemberExpression) 
      .Select(m => m.Member.Name) 
      .Reverse()); 
} 

private static IEnumerable<MemberExpression> GetMembersOnPath(MemberExpression expression) 
{ 
    while(expression != null) 
    { 
     yield return expression; 
     expression = expression.Expression as MemberExpression; 
    } 
} 

Si todavía está en .NET 3.5, necesita st ick un ToArray() después de la llamada a Reverse(), debido a la sobrecarga de String.Join que toma un IEnumerable se añadió por primera vez en .NET 4.

+1

+1 para LINQ, ¡Agradable y simple! –

4

escribí un poco de código para esto, y parece que ha funcionado.

Teniendo en cuenta los siguientes tres definiciones de clases:

class Person { 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public Address Address { get; set; } 
} 

class State { 
    public string Abbreviation { get; set; } 
} 

class Address { 
    public string City { get; set; } 
    public State State { get; set; } 
} 

El siguiente método le dará el camino plena propiedad

static string GetFullSortName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression) { 
    var memberNames = new List<string>(); 

    var memberExpression = expression.Body as MemberExpression; 
    while (null != memberExpression) { 
     memberNames.Add(memberExpression.Member.Name); 
     memberExpression = memberExpression.Expression as MemberExpression; 
    } 

    memberNames.Reverse(); 
    string fullName = string.Join(".", memberNames.ToArray()); 
    return fullName; 
} 

Para las dos llamadas:

fullName = GetFullSortName<Person, string>(p => p.FirstName); 
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation); 
1

El código que He trabajado al 100% ahora es lo siguiente, pero realmente no entiendo lo que está haciendo (a pesar del hecho que lo modifiqué para hacerlo manejar estos escenarios profundamente anidados gracias al depurador).

internal static string MemberWithoutInstance(this LambdaExpression expression) 
    { 
     var memberExpression = expression.ToMemberExpression(); 

     if (memberExpression == null) 
     { 
      return null; 
     } 

     if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess) 
     { 
      var innerMemberExpression = (MemberExpression) memberExpression.Expression; 

      while (innerMemberExpression.Expression.NodeType == ExpressionType.MemberAccess) 
      { 
       innerMemberExpression = (MemberExpression) innerMemberExpression.Expression; 
      } 

      var parameterExpression = (ParameterExpression) innerMemberExpression.Expression; 

      // +1 accounts for the ".". 
      return memberExpression.ToString().Substring(parameterExpression.ToString().Length + 1); 
     } 

     return memberExpression.Member.Name; 
    } 

    internal static MemberExpression ToMemberExpression(this LambdaExpression expression) 
    { 
     var memberExpression = expression.Body as MemberExpression; 

     if (memberExpression == null) 
     { 
      var unaryExpression = expression.Body as UnaryExpression; 

      if (unaryExpression != null) 
      { 
       memberExpression = unaryExpression.Operand as MemberExpression; 
      } 
     } 

     return memberExpression; 
    } 

    public static string PropertyToString<TModel, TProperty>(this Expression<Func<TModel, TProperty>> source) 
    { 
     return source.MemberWithoutInstance(); 
    } 

Esta solución se encarga de que cuando mi expresión es de tipo Expression<Func<TModel,object>> y paso todo tipo de tipos de objetos en mis parámetros. Cuando hago esto, mi expresión x => x.Age se convierte en x => Convert(x.Age) y eso rompe las otras soluciones aquí. Sin embargo, no entiendo qué maneja la parte Convert. : -/

2

Otro enfoque simple es usar el método System.Web.Mvc.ExpressionHelper.GetExpressionText. En mi próximo golpe escribiré más en detalle. Eche un vistazo al http://carrarini.blogspot.com/.

+0

Pero esto requiere que agregue dependencias a MVC y cosas de la web. No quiero hacer esto en aplicaciones winforms, aplicaciones WCF Service o DALs. No es de ninguna manera, ¡de ninguna manera! Sin embargo, si esto solo es necesario en una aplicación MVC, entonces quizás esta sea una opción. – Jaxidian

+1

luego simplemente tome el código fuente - es un código abierto https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/ExpressionHelper.cs –

2

Basado en esto y varios relacionados preguntas/respuestas aquí, aquí está el método simple que estoy usando:

protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop) 
{ 
    // http://stackoverflow.com/questions/2789504/get-the-property-as-a-string-from-an-expressionfunctmodel-tproperty 
    // http://stackoverflow.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct 
    // http://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct 
    MemberExpression expr; 

    if (prop.Body is MemberExpression) 
     // .Net interpreted this code trivially like t => t.Id 
     expr = (MemberExpression)prop.Body; 
    else 
     // .Net wrapped this code in Convert to reduce errors, meaning it's t => Convert(t.Id) - get at the 
     // t.Id inside 
     expr = (MemberExpression)((UnaryExpression)prop.Body).Operand; 

    string name = expr.Member.Name; 

    return name; 
} 

se puede utilizar simplemente les gusta:

string name = propertyNameFromExpression(t => t.Id); // returns "Id" 

Este método, sin embargo no menos de comprobación de errores que otros publicada aquí - básicamente se da por sentado que es c correctamente, lo que puede no ser una suposición segura en su aplicación.

0

la publicación cruzada de Retrieving Property name from lambda expression

Como la cuestión aludida, la respuesta astuto es que si usted llama expression.ToString(), que le dará algo como:

"o => o.ParentProperty.ChildProperty" 

que luego se puede simplemente subcadena de el primer periodo

Basado en algunos LinqPad tests, el rendimiento fue comparable.

Cuestiones relacionadas