2012-04-11 8 views
11

Al crear mi marco de prueba, he encontrado un problema extraño.Comparar PropertyInfo de Type.GetProperties() y expresiones lambda

Quiero crear una clase estática que me permita comparar objetos del mismo tipo por sus propiedades, pero con la posibilidad de ignorar algunos de ellos.

Quiero tener un simple API fluida para esto, así como una llamada TestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second); devolverá verdadero si los objetos dados son iguales en todos los bienes, excepto Id y Name (que no serán comprobados por la igualdad).

Aquí va mi código. Por supuesto, es un ejemplo trivial (con algunas obvias sobrecargas de métodos faltantes), pero quería extraer el código más simple posible. El escenario real es un poco más complejo, así que realmente no quiero cambiar el enfoque.

El método FindProperty es casi un copiar y pegar de AutoMapper library.

envoltorio objeto de API fluida:

public class TestEqualityHelper<T> 
{ 
    public List<PropertyInfo> IgnoredProps = new List<PropertyInfo>(); 
    public T Value; 
} 

cosas Fluido:

public static class FluentExtension 
{ 
    //Extension method to speak fluently. It finds the property mentioned 
    // in 'ignore' parameter and adds it to the list. 
    public static TestEqualityHelper<T> Ignore<T>(this T value, 
     Expression<Func<T, object>> ignore) 
    { 
     var eh = new TestEqualityHelper<T> { Value = value }; 

     //Mind the magic here! 
     var member = FindProperty(ignore); 
     eh.IgnoredProps.Add((PropertyInfo)member); 
     return eh; 
    } 

    //Extract the MemberInfo from the given lambda 
    private static MemberInfo FindProperty(LambdaExpression lambdaExpression) 
    { 
     Expression expressionToCheck = lambdaExpression; 

     var done = false; 

     while (!done) 
     { 
      switch (expressionToCheck.NodeType) 
      { 
       case ExpressionType.Convert: 
        expressionToCheck 
         = ((UnaryExpression)expressionToCheck).Operand; 
        break; 
       case ExpressionType.Lambda: 
        expressionToCheck 
         = ((LambdaExpression)expressionToCheck).Body; 
        break; 
       case ExpressionType.MemberAccess: 
        var memberExpression 
         = (MemberExpression)expressionToCheck; 

        if (memberExpression.Expression.NodeType 
          != ExpressionType.Parameter && 
         memberExpression.Expression.NodeType 
          != ExpressionType.Convert) 
        { 
         throw new Exception("Something went wrong"); 
        } 

        return memberExpression.Member; 
       default: 
        done = true; 
        break; 
      } 
     } 

     throw new Exception("Something went wrong"); 
    } 
} 

El comparador real:

public static class TestEqualityComparer 
{ 
    public static bool MyEquals<T>(TestEqualityHelper<T> a, T b) 
    { 
     return DoMyEquals(a.Value, b, a.IgnoredProps); 
    } 

    private static bool DoMyEquals<T>(T a, T b, 
     IEnumerable<PropertyInfo> ignoredProperties) 
    { 
     var t = typeof(T); 
     IEnumerable<PropertyInfo> props; 

     if (ignoredProperties != null && ignoredProperties.Any()) 
     { 
      //THE PROBLEM IS HERE! 
      props = 
       t.GetProperties(BindingFlags.Instance | BindingFlags.Public) 
        .Except(ignoredProperties); 
     } 
     else 
     { 
      props = 
       t.GetProperties(BindingFlags.Instance | BindingFlags.Public); 
     } 
     return props.All(f => f.GetValue(a, null).Equals(f.GetValue(b, null))); 
    } 
} 

Eso es básicamente la misma.

Y aquí hay dos fragmentos de prueba, la primera de ellas funciona, la segunda falla:

//These are the simple objects we'll compare 
public class Base 
{ 
    public decimal Id { get; set; } 
    public string Name { get; set; } 
} 
public class Derived : Base 
{ } 

[TestMethod] 
public void ListUsers() 
{ 
    //TRUE 
    var f = new Base { Id = 5, Name = "asdas" }; 
    var s = new Base { Id = 6, Name = "asdas" }; 
    Assert.IsTrue(TestEqualityComparer.MyEquals(f.Ignore(x => x.Id), s)); 

    //FALSE 
    var f2 = new Derived { Id = 5, Name = "asdas" }; 
    var s2 = new Derived { Id = 6, Name = "asdas" }; 
    Assert.IsTrue(TestEqualityComparer.MyEquals(f2.Ignore(x => x.Id), s2)); 
} 

El problema es con el método Except en DoMyEquals.

Las propiedades devueltas por FindProperty no son iguales a las devueltas por Type.GetProperties. La diferencia que veo es en PropertyInfo.ReflectedType.

  • sin tener en cuenta el tipo de mis objetos, FindProperty me dice que el tipo reflejada es Base.

  • propiedades devueltos por Type.GetProperties tienen su conjunto ReflectedType-Base o Derived, en función del tipo de objetos reales.

no sé cómo resolverlo. Pude comprobar el tipo de parámetro en lambda, pero en el siguiente paso quiero permitir construcciones como Ignore(x=>x.Some.Deep.Property), por lo que probablemente no funcionará.

Cualquier sugerencia sobre cómo comparar PropertyInfo o cómo recuperarlos de lambdas correctamente sería apreciada.

+1

¿Ha intentado jugar con el valor BindingFlags.FlattenHierarchy en GetProperties? ¿Ves si cambia algo? –

+1

No hubo suerte, pero gracias por una sugerencia. Creo ** que BindingFlags solo puede cambiar _que _se devuelven miembros, pero no afectarán sus propias propiedades. Creo que la solución estaría en algún juego con FindProperty. –

+1

Quizás agregando un segundo paso hacky a FindProperty, después de obtener el miembro, ejecute GetProperty en el tipo (que también puede obtener a través de la expresión) con el nombre del miembro. Es un truco, pero podría funcionar. –

Respuesta

5

La razón FindProperty que está diciendo la Type reflejada es Base es porque esa es la clase de la lambda usaría para la invocación.

Probablemente lo saben :)

En lugar de GetProperties() de Tipo, se puede utilizar este

static IEnumerable<PropertyInfo> GetMappedProperties(Type type) 
{ 
    return type 
    .GetProperties() 
    .Select(p => GetMappedProperty(type, p.Name)) 
    .Where(p => p != null); 
} 

static PropertyInfo GetMappedProperty(Type type, string name) 
{ 
    if (type == null) 
    return null; 

    var prop = type.GetProperty(name); 

    if (prop.DeclaringType == type) 
    return prop; 
    else 
    return GetMappedProperty(type.BaseType, name); 
} 

Para explicar más acerca de por qué el lambda es la utilización de dicho método Base directamente y ver esencialmente una diferente PropertyInfo, podría ser mejor explicado mirando el IL

Considere este código:

static void Foo() 
{ 
    var b = new Base { Id = 4 }; 
    var d = new Derived { Id = 5 }; 

    decimal dm = b.Id; 
    dm = d.Id; 
} 

Y aquí es la IL para b.Id

IL_002f: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id() 

Y la IL para d.Id

IL_0036: callvirt instance valuetype [mscorlib]System.Decimal ConsoleApplication1.Base::get_Id() 
+0

Esto se ve bien, pero realmente no sé, ¿por qué usaría lambda Base? No hay conversión, 'lambdaExpression.Parameters [0] .Type' dice' Derived'. ¿Podría explicar por qué sucede así? (un enlace explicativo o algunas palabras clave serían más que suficientes ;-)) –

+0

El parámetro lambda Type es el tipo del parámetro, como declarado, pero la invocación del método real es _using_ el tipo base para hacer la llamada (el método está en el tipo de base). – payo

+0

@xavier Vea la información adicional en mi respuesta [también, si esta solución funciona y 6 personas la han votado, incluso una la ha marcado, ¿por qué no hay amor por mi respuesta ?(NO lo entiendo, a veces] – payo

5

No sabe si esto ayuda, pero me he dado cuenta que la propiedad MetaDataToken el valor de dos instancias de PropertyInfo es igual, si ambas instancias hacen referencia a la misma propiedad lógica, independientemente del ReflectedType de cualquiera. Es decir, Name, PropertyType, DeclaringType y los parámetros de índice de ambas instancias de PropertyInfo son todos iguales.

+3

¡Esto es muy interesante! De acuerdo con msdn, 'MetadataToken', en combinación con' Module', identifica de manera única el elemento. ¡Gracias! –