按照使用反射声明的顺序获取属性

我需要使用反射按照它们在类中声明的顺序获取所有属性。根据 MSDN 的规定,在使用 GetProperties()时无法保证订单

GetProperties 方法不返回特定 顺序,如字母顺序或声明顺序。

但是我读到有一个变通方法,通过 MetadataToken对属性进行排序。所以我的问题是,这样安全吗?我似乎找不到任何关于 MSDN 的信息。还有别的办法解决这个问题吗?

我目前的执行情况如下:

var props = typeof(T)
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.OrderBy(x => x.MetadataToken);
48198 次浏览

According to MSDN MetadataToken is unique inside one Module - there is nothing saying that it guarantees any order at all.

EVEN if it did behave the way you want it to that would be implementation-specific and could change anytime without notice.

See this old MSDN blog entry.

I would strongly recommend to stay away from any dependency on such implementation details - see this answer from Marc Gravell.

IF you need something at compile time you could take a look at Roslyn (although it is in a very early stage).

If you're going the attribute route, here's a method I've used in the past;

public static IOrderedEnumerable<PropertyInfo> GetSortedProperties<T>()
{
return typeof(T)
.GetProperties()
.OrderBy(p => ((Order)p.GetCustomAttributes(typeof(Order), false)[0]).Order);
}

Then use it like this;

var test = new TestRecord { A = 1, B = 2, C = 3 };


foreach (var prop in GetSortedProperties<TestRecord>())
{
Console.WriteLine(prop.GetValue(test, null));
}

Where;

class TestRecord
{
[Order(1)]
public int A { get; set; }


[Order(2)]
public int B { get; set; }


[Order(3)]
public int C { get; set; }
}

The method will barf if you run it on a type without comparable attributes on all of your properties obviously, so be careful how it's used and it should be sufficient for requirement.

I've left out the definition of Order : Attribute as there's a good sample in Yahia's link to Marc Gravell's post.

If you are happy with the extra dependency, Marc Gravell's Protobuf-Net can be used to do this without having to worry about the best way to implement reflection and caching etc. Just decorate your fields using [ProtoMember] and then access the fields in numerical order using:

MetaType metaData = ProtoBuf.Meta.RuntimeTypeModel.Default[typeof(YourTypeName)];


metaData.GetFields();

On .net 4.5 (and even .net 4.0 in vs2012) you can do much better with reflection using clever trick with [CallerLineNumber] attribute, letting compiler insert order into your properties for you:

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class OrderAttribute : Attribute
{
private readonly int order_;
public OrderAttribute([CallerLineNumber]int order = 0)
{
order_ = order;
}


public int Order { get { return order_; } }
}




public class Test
{
//This sets order_ field to current line number
[Order]
public int Property2 { get; set; }


//This sets order_ field to current line number
[Order]
public int Property1 { get; set; }
}

And then use reflection:

var properties = from property in typeof(Test).GetProperties()
where Attribute.IsDefined(property, typeof(OrderAttribute))
orderby ((OrderAttribute)property
.GetCustomAttributes(typeof(OrderAttribute), false)
.Single()).Order
select property;


foreach (var property in properties)
{
//
}

If you have to deal with partial classes, you can additionaly sort the properties using [CallerFilePath].

What I have tested sorting by MetadataToken works.

Some of users here claims this is somehow not good approach / not reliable, but I haven't yet seen any evidence of that one - perhaps you can post some code snipet here when given approach does not work ?

About backwards compatibility - while you're now working on your .net 4 / .net 4.5 - Microsoft is making .net 5 or higher, so you pretty much can assume that this sorting method won't be broken in future.

Of course maybe by 2017 when you will be upgrading to .net9 you will hit compatibility break, but by that time Microsoft guys will probably figure out the "official sort mechanism". It does not makes sense to go back or break things.

Playing with extra attributes for property ordering also takes time and implementation - why to bother if MetadataToken sorting works ?

You may use DisplayAttribute in System.Component.DataAnnotations, instead of custom attribute. Your requirement has to do something with display anyway.

I did it this way:

 internal static IEnumerable<Tuple<int,Type>> TypeHierarchy(this Type type)
{
var ct = type;
var cl = 0;
while (ct != null)
{
yield return new Tuple<int, Type>(cl,ct);
ct = ct.BaseType;
cl++;
}
}


internal class PropertyInfoComparer : EqualityComparer<PropertyInfo>
{
public override bool Equals(PropertyInfo x, PropertyInfo y)
{
var equals= x.Name.Equals(y.Name);
return equals;
}


public override int GetHashCode(PropertyInfo obj)
{
return obj.Name.GetHashCode();
}
}


internal static IEnumerable<PropertyInfo> GetRLPMembers(this Type type)
{


return type
.TypeHierarchy()
.SelectMany(t =>
t.Item2
.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(prop => Attribute.IsDefined(prop, typeof(RLPAttribute)))
.Select(
pi=>new Tuple<int,PropertyInfo>(t.Item1,pi)
)
)
.OrderByDescending(t => t.Item1)
.ThenBy(t => t.Item2.GetCustomAttribute<RLPAttribute>().Order)
.Select(p=>p.Item2)
.Distinct(new PropertyInfoComparer());








}

with the property declared as follows:

  [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RLPAttribute : Attribute
{
private readonly int order_;
public RLPAttribute([CallerLineNumber]int order = 0)
{
order_ = order;
}


public int Order { get { return order_; } }


}

Building on the above accepted solution, to get the exact Index you could use something like this

Given

public class MyClass
{
[Order] public string String1 { get; set; }
[Order] public string String2 { get; set; }
[Order] public string String3 { get; set; }
[Order] public string String4 { get; set; }
}

Extensions

public static class Extensions
{


public static int GetOrder<T,TProp>(this T Class, Expression<Func<T,TProp>> propertySelector)
{
var body = (MemberExpression)propertySelector.Body;
var propertyInfo = (PropertyInfo)body.Member;
return propertyInfo.Order<T>();
}


public static int Order<T>(this PropertyInfo propertyInfo)
{
return typeof(T).GetProperties()
.Where(property => Attribute.IsDefined(property, typeof(OrderAttribute)))
.OrderBy(property => property.GetCustomAttributes<OrderAttribute>().Single().Order)
.ToList()
.IndexOf(propertyInfo);
}
}

Usage

var myClass = new MyClass();
var index = myClass.GetOrder(c => c.String2);

Note, there is no error checking or fault tolerance, you can add pepper and salt to taste

Another possibility is to use the System.ComponentModel.DataAnnotations.DisplayAttribute Order property. Since it is builtin, there is no need to create a new specific attribute.

Then select ordered properties like this

const int defaultOrder = 10000;
var properties = type.GetProperties().OrderBy(p => p.FirstAttribute<DisplayAttribute>()?.GetOrder() ?? defaultOrder).ToArray();

And class can be presented like this

public class Toto {
[Display(Name = "Identifier", Order = 2)
public int Id { get; set; }


[Display(Name = "Description", Order = 1)
public string Label {get; set; }
}

If you can enforce your type has a known memory layout, you can rely on StructLayout(LayoutKind.Sequential) then sort by the field offsets in memory.

This way you don't need any attribute on each field in the type.

Some serious drawbacks though:

  • All field types must have a memory representation (practically no other reference types other than fixed-length arrays or strings). This includes parent types, even if you just want to sort the child type's fields.
  • You can use this for classes including inheritance, but all parent classes need to also have sequential layout set.
  • Obviously, this doesn't sort properties but fields might be fine for POCOs.
[StructLayout(LayoutKind.Sequential)]
struct TestStruct
{
public int x;
public decimal y;
}


[StructLayout(LayoutKind.Sequential)]
class TestParent
{
public int Base;
public TestStruct TestStruct;
}


[StructLayout(LayoutKind.Sequential)]
class TestRecord : TestParent
{
public bool A;
public string B;
public DateTime C;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 42)] // size doesn't matter
public byte[] D;
}


class Program
{
static void Main(string[] args)
{
var fields = typeof(TestRecord).GetFields()
.OrderBy(field => Marshal.OffsetOf(field.DeclaringType, field.Name));
foreach (var field in fields) {
Console.WriteLine($"{field.Name}: {field.FieldType}");
}
}
}

Outputs:

Base: System.Int32
TestStruct: TestStruct
A: System.Boolean
B: System.String
C: System.DateTime
D: System.Byte[]

If you try to add any forbidden field types, you'll get System.ArgumentException: Type 'TestRecord' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.

Even it's a very old thread, here is my working solution based on @Chris McAtackney

        var props = rootType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(p =>
(
p.GetCustomAttributes(typeof(AttrOrder), false).Length != 0 ? // if we do have this attribute
((p.GetCustomAttributes(typeof(AttrOrder), false)[0]) as AttrOrder).Order
: int.MaxValue // or just a big value
)
);

And the Attribute is like this

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AttrOrder : Attribute
{
public int Order { get; }


public AttrOrder(int order)
{
Order = order;
}
}

Use like this

[AttrOrder(1)]
public string Name { get; set; }