2012-04-11 43 views
11

Trong khi tạo khung thử nghiệm của tôi, tôi đã tìm thấy một vấn đề lạ.So sánh PropertyInfo từ Type.GetProperties() và biểu thức lambda

Tôi muốn tạo một lớp tĩnh cho phép tôi so sánh các đối tượng cùng loại với thuộc tính của chúng, nhưng với khả năng bỏ qua một số trong số chúng.

Tôi muốn có API thông thạo đơn giản cho điều này, do đó, một cuộc gọi như TestEqualityComparer.Equals(first.Ignore(x=>x.Id).Ignore(y=>y.Name), second); sẽ trả về true nếu các đối tượng đã cho bằng nhau trên mọi thuộc tính ngoại trừ IdName (chúng sẽ không được kiểm tra bình đẳng).

Đây là mã của tôi. Tất nhiên đó là một ví dụ tầm thường (với một số rõ ràng quá tải các phương thức bị thiếu), nhưng tôi muốn trích xuất mã đơn giản nhất có thể. Kịch bản trường hợp thực sự phức tạp hơn một chút, vì vậy tôi không thực sự muốn thay đổi cách tiếp cận này.

Phương pháp FindProperty gần như là bản sao dán từ AutoMapper library.

Object wrapper cho API thạo:

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

thứ thạo:

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

Các Comparer thực tế:

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

Đó là về cơ bản nó.

Và đây là hai đoạn thử nghiệm, người đầu tiên làm việc, điều thứ hai thất bại:

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

Vấn đề là với Except phương pháp trong DoMyEquals.

Thuộc tính được trả về bởi FindProperty không bằng với số được trả lại bởi Type.GetProperties. Sự khác biệt tôi phát hiện là ở PropertyInfo.ReflectedType.

  • không phân biệt các loại của các đối tượng của tôi, FindProperty nói với tôi rằng loại phản ánh là Base.

  • thuộc tính được trả lại bởi Type.GetPropertiesReflectedType được đặt thành Base hoặc Derived, tùy thuộc vào loại đối tượng thực tế.

Tôi không biết cách giải quyết. Tôi có thể kiểm tra kiểu tham số trong lambda, nhưng trong bước tiếp theo tôi muốn cho phép các cấu trúc như Ignore(x=>x.Some.Deep.Property), vì vậy có thể nó sẽ không làm.

Bất kỳ đề xuất nào về cách so sánh PropertyInfo hoặc cách truy xuất chúng từ lambdas đúng cách sẽ được đánh giá cao.

+1

Bạn đã thử chơi với giá trị BindingFlags.FlattenHierarchy trong GetProperties chưa? Xem nó có thay đổi gì không? –

+1

Không có may mắn ở đó, nhưng cảm ơn bạn đã gợi ý. Tôi nghĩ ** BindingFlags chỉ có thể thay đổi _which_ thành viên được trả về, nhưng chúng sẽ không ảnh hưởng đến các thuộc tính của chúng. Tôi tin rằng giải pháp sẽ có trong một số trò chơi với FindProperty. –

+1

Có thể thêm một bước thứ hai, hacky vào FindProperty, sau khi bạn đã nhận GetProperty thành viên chạy trên loại (bạn cũng có thể nhận được thông qua biểu thức) với tên thành viên? Đó là một hack, nhưng nó có thể hoạt động. –

Trả lời

5

Lý do FindProperty đang cho bạn biết phản ánh TypeBase là vì đó là lớp lambda sẽ sử dụng cho lời gọi.

Bạn có thể biết điều này :)

Thay vì GetProperties() từ Type, bạn có thể sử dụng này

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

Để giải thích thêm về lý do tại sao lambda là thực sự sử dụng các phương pháp cơ sở trực tiếp, và bạn thấy về cơ bản một PropertyInfo khác nhau, có thể là tốt hơn giải thích nhìn vào IL

xem xét mã này:

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

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

Và đây là IL cho b.Id

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

Và IL cho d.Id

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

Điều này có vẻ tốt, nhưng tôi không thực sự biết, tại sao lambda sẽ sử dụng Base? Không có phép đúc, 'lambdaExpression.Parameters [0] .Type' nói' Derived'. Bạn có thể giải thích tại sao nó xảy ra như thế này? (một liên kết giải thích hoặc một số từ khóa sẽ là quá đủ ;-)) –

+0

Tham số lambda Kiểu là kiểu tham số, như được khai báo, nhưng lời gọi phương thức thực tế là _using_ kiểu cơ sở để thực hiện cuộc gọi (phương thức nằm trong loại cơ sở). – payo

+0

@xavier Xem thông tin bổ sung trong câu trả lời của tôi [cũng vậy, nếu giải pháp này hoạt động và 6 người đã upvoted nó - ngay cả một trong những đóng vai chính nó, tại sao không có tình yêu cho câu trả lời của tôi: (Tôi không hiểu SO đôi khi] – payo

5

Không biết nếu điều này sẽ giúp, nhưng tôi đã nhận thấy rằng tài sản MetaDataToken giá trị của hai thể hiện PropertyInfo là bằng nhau, nếu cả hai cá thể đều tham chiếu đến cùng một thuộc tính lôgíc, bất kể kiểu ReflectedType của một trong hai. Đó là, các tham số Name, PropertyType, DeclaringType và chỉ mục của cả hai thể hiện của PropertyInfo đều bằng nhau.

+3

Điều này rất thú vị! Theo msdn, 'MetadataToken', kết hợp với' Module', xác định duy nhất phần tử. Cảm ơn bạn! –

Các vấn đề liên quan