Obtener propiedades en orden de declaración usando reflexión

81

Necesito obtener todas las propiedades usando la reflexión en el orden en que se declaran en la clase. Según MSDN, no se puede garantizar el pedido al usarGetProperties()

El método GetProperties no devuelve propiedades en un orden particular, como orden alfabético o de declaración.

Pero he leído que hay una solución al ordenar las propiedades por MetadataToken. Entonces mi pregunta es, ¿es seguro? Parece que no puedo encontrar ninguna información en MSDN al respecto. ¿O hay alguna otra forma de solucionar este problema?

Mi implementación actual tiene el siguiente aspecto:

var props = typeof(T)
   .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
   .OrderBy(x => x.MetadataToken);
Magnus
fuente
12
De todos modos, es mala idea. Cree su propio atributo con el valor del pedido o cualquier otro metadato y marque los campos de la clase con ese atributo.
Kirill Polishchuk
1
Quizás podría agregar un nuevo atributo que contenga un int del orden. Luego obtenga las propiedades, obtenga el DisplayOrderAttribute de cada propiedad y ordene por eso.
BlueChippy
1
Por curiosidad, ¿por qué estás haciendo esto? ¿Qué estás tratando de lograr?
Sam Greenhalgh
6
@Magnus Sin embargo, la pregunta sigue siendo interesante porque algunas partes del marco en sí dependen en gran medida de esto. Por ejemplo, la serialización con el atributo Serializable almacena miembros en el orden en que fueron definidos. Al menos Wagner afirma esto en su libro "Efectivo C #"
Pavel Voronin

Respuestas:

143

En .net 4.5 (e incluso .net 4.0 en vs2012) puede hacerlo mucho mejor con la reflexión utilizando un truco inteligente con [CallerLineNumber]atributo, permitiendo que el compilador inserte el orden en sus propiedades por usted:

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

Y luego usa la reflexión:

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

Si tiene que lidiar con clases parciales, también puede ordenar las propiedades usando [CallerFilePath].

ghord
fuente
2
¡Esto es realmente muy inteligente! Me pregunto si tiene argumentos en contra en su contra. En realidad, me parece bastante elegante. Estoy usando Linq2CSV y creo que heredaré y usaré CsvColumnAttributeesto como FieldIndexvalor predeterminado
julealgon
2
@julealgon Supongo que el argumento en contra es que hay una API posicional indocumentada que alguien podría romper al refactorizar si no entendiera que el atributo se estaba usando de esta manera. Sigo pensando que es bastante elegante, solo digo en caso de que alguien copie / pegue esto y esté buscando motivación para dejar algunos comentarios para el próximo chico.
Joel B
2
Esto funciona excepto en el caso en el que 1 clase hereda otra clase y necesito el orden de todas las propiedades. ¿Hay algún truco para eso también?
Paul Baxter
Creo que esto se romperá si la clase se declara de forma parcial. A diferencia de la herencia, la API no puede resolverlo.
Wu Zhenwei
1
Enfoque muy inteligente, pero arrojará una excepción de referencia nula si algunos olvidan establecer el atributo [Orden]. Es mejor comprobar nulo antes de llamar. Ej: var properties = from property in typeof (Test) .GetProperties () let orderAttribute = (property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault () == null? New OrderAttribute (0): property.GetCustomAttributes (typeof (OrderAttribute), falso) .SingleOrDefault ()) como OrderAttribute orderby orderAttribute.Order seleccionar propiedad;
Sopan Maiti
15

Si va por la ruta de los atributos, aquí hay un método que he usado en el pasado;

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

Entonces utilícelo así;

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

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

Dónde;

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

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

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

El método fallará si lo ejecuta en un tipo sin atributos comparables en todas sus propiedades, obviamente, así que tenga cuidado con cómo se usa y debería ser suficiente para los requisitos.

He omitido la definición de Orden: atributo, ya que hay una buena muestra en el enlace de Yahia a la publicación de Marc Gravell.

Christopher McAtackney
fuente
2
Si este es un requisito de visualización, sería apropiado utilizar DataAnnotations.DisplayAttribute , que tiene un campo Order.
Jerph
12

Según MSDN, MetadataToken es único dentro de un módulo: no hay nada que diga que garantice ningún pedido.

INCLUSO si se comportara de la forma deseada, sería específico de la implementación y podría cambiar en cualquier momento sin previo aviso.

Vea esta antigua entrada de blog de MSDN .

Recomendaría encarecidamente que se mantenga alejado de cualquier dependencia de dichos detalles de implementación; consulte esta respuesta de Marc Gravell .

SI necesita algo en tiempo de compilación, puede echar un vistazo a Roslyn (aunque está en una etapa muy temprana).

Yahia
fuente
5

Lo que he probado en la clasificación por MetadataToken funciona.

Algunos de los usuarios aquí afirman que este de alguna manera no es un buen enfoque / no es confiable, pero aún no he visto ninguna evidencia de eso, ¿tal vez pueda publicar algún fragmento de código aquí cuando el enfoque dado no funcione?

Acerca de la compatibilidad con versiones anteriores, mientras que ahora está trabajando en su .net 4 / .net 4.5, Microsoft está haciendo .net 5 o superior, por lo que puede asumir que este método de clasificación no se romperá en el futuro.

Por supuesto, tal vez en 2017, cuando se actualice a .net9, se produzca una ruptura de compatibilidad, pero para ese momento, los chicos de Microsoft probablemente descubrirán el "mecanismo de clasificación oficial". No tiene sentido retroceder o romper cosas.

Jugar con atributos adicionales para la ordenación de propiedades también requiere tiempo e implementación: ¿por qué molestarse si la ordenación de MetadataToken funciona?

TarmoPikaro
fuente
1
Es 2019 y ni siquiera se ha lanzado .net-4.9 aún :-p.
binki
Cada vez que .net lanza una versión principal es una gran oportunidad para que realicen cambios en cosas como MetadataToken o en el pedido de la devolución GetProperties(). Su argumento de por qué puede confiar en el orden es exactamente el mismo que el argumento de por qué no puede confiar en que las versiones futuras de .net no cambien este comportamiento: cada nueva versión es libre de cambiar los detalles de implementación. Ahora, .net en realidad sigue la filosofía de que “los errores son características”, por lo que en realidad probablemente nunca cambiarían el orden de GetProperties(). Es solo que la API dice que pueden hacerlo.
binki
1

Puede utilizar DisplayAttribute en System.Component.DataAnnotations, en lugar de un atributo personalizado. De todos modos, su requisito tiene que ver con la pantalla.

Panos Roditakis
fuente
0

Si está satisfecho con la dependencia adicional, Protobuf-Net de Marc Gravell se puede usar para hacer esto sin tener que preocuparse por la mejor manera de implementar la reflexión y el almacenamiento en caché, etc. Simplemente decore sus campos usando [ProtoMember]y luego acceda a los campos en orden numérico usando:

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

metaData.GetFields();
Tom Makin
fuente
0

Lo hice de esta manera:

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




    }

con la propiedad declarada de la siguiente manera:

  [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_; } }

}
Centinela
fuente
¿Cómo es esto diferente o mejor que la respuesta aceptada?
Ian Kemp
0

Sobre la base de la solución aceptada anteriormente, para obtener el índice exacto, podría usar algo como esto

Dado

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

Extensiones

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

Uso

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

Tenga en cuenta que no hay verificación de errores ni tolerancia a fallas, puede agregar pimienta y sal al gusto

El general
fuente
0

Otra posibilidad es utilizar la System.ComponentModel.DataAnnotations.DisplayAttribute Orderpropiedad. Dado que está integrado, no es necesario crear un nuevo atributo específico.

Luego seleccione propiedades ordenadas como esta

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

Y la clase se puede presentar así

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

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