Holen Sie sich Eigenschaften in der Reihenfolge der Deklaration mithilfe von Reflection

81

Ich muss alle Eigenschaften mithilfe von Reflection in der Reihenfolge abrufen, in der sie in der Klasse deklariert sind. Laut MSDN kann die Bestellung bei Verwendung nicht garantiert werdenGetProperties()

Die GetProperties-Methode gibt keine Eigenschaften in einer bestimmten Reihenfolge zurück, z. B. in alphabetischer Reihenfolge oder in Deklarationsreihenfolge.

Aber ich habe gelesen, dass es eine Problemumgehung gibt, indem die Eigenschaften nach dem sortiert werden MetadataToken. Meine Frage ist also, ist das sicher? Ich kann anscheinend keine Informationen auf MSDN darüber finden. Oder gibt es eine andere Möglichkeit, dieses Problem zu lösen?

Meine aktuelle Implementierung sieht wie folgt aus:

var props = typeof(T)
   .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
   .OrderBy(x => x.MetadataToken);
Magnus
quelle
12
Auf jeden Fall ist es eine schlechte Idee. Erstellen Sie Ihr eigenes Attribut mit dem Bestellwert oder anderen Metadaten und markieren Sie Klassenfelder mit diesem Attribut.
Kirill Polishchuk
1
Sie könnten vielleicht ein neues Attribut hinzufügen, das ein int der Reihenfolge enthält. Dann die Eigenschaften abrufen, das DisplayOrderAttribute jeder Eigenschaft abrufen und danach sortieren?
BlueChippy
1
Warum machst du das aus Neugier, was versuchst du zu erreichen?
Sam Greenhalgh
6
@Magnus Die Frage ist jedoch immer noch interessant, da einige Teile des Frameworks selbst stark davon abhängen. Bei der Serialisierung mit dem Attribut Serializable werden die Mitglieder beispielsweise in der Reihenfolge gespeichert, in der sie definiert wurden. Zumindest sagt Wagner dies in seinem Buch "Effective C #"
Pavel Voronin

Antworten:

141

Auf .net 4.5 (und sogar .net 4.0 in vs2012) können Sie mit Reflexion viel besser arbeiten, indem Sie einen cleveren Trick mit [CallerLineNumber]Attribut verwenden, bei dem der Compiler die Reihenfolge für Sie in Ihre Eigenschaften einfügt :

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

Und dann Reflexion verwenden:

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

Wenn Sie sich mit Teilklassen befassen müssen, können Sie die Eigenschaften zusätzlich mit sortieren [CallerFilePath].

Ghord
quelle
2
Das ist in der Tat sehr klug! Ich frage mich, ob es Gegenargumente dagegen gibt. Scheint mir eigentlich ziemlich elegant zu sein. Ich benutze Linq2CSV und ich denke, ich werde von erben CsvColumnAttributeund dies als Standardwert FieldIndexverwenden
Julealgon
2
@julealgon Ich nehme an, das Argument dagegen ist, dass es eine undokumentierte Positions- API gibt, die jemand beim Refactoring beschädigen könnte, wenn er nicht versteht, dass das Attribut auf diese Weise verwendet wird. Ich finde es immer noch ziemlich elegant, nur zu sagen, falls jemand dies kopiert / einfügt und nach Motivation sucht, einige Kommentare für den nächsten Mann zu hinterlassen.
Joel B
2
Dies funktioniert außer in dem Fall, in dem 1 Klasse eine andere Klasse erbt und ich die Reihenfolge aller Eigenschaften benötige. Gibt es auch dafür einen Trick?
Paul Baxter
Ich denke, das wird kaputt gehen, wenn die Klasse teilweise deklariert wird. Im Gegensatz zur Vererbung kann die API dies nicht herausfinden.
Wu Zhenwei
1
Sehr cleverer Ansatz, aber es wird eine Nullreferenzausnahme ausgelöst, wenn einige vergessen, das Attribut [Order] zu setzen. Überprüfen Sie besser null, bevor Sie anrufen. Beispiel: var properties = from property in typeof (Test) .GetProperties () let orderAttribute = (property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault () == null? New OrderAttribute (0): property.GetCustomAttributes (typeof (OrderAttribute), false) .SingleOrDefault ()) als OrderAttribute orderby orderAttribute.Order select property;
Sopan Maiti
15

Wenn Sie die Attributroute wählen, ist hier eine Methode, die ich in der Vergangenheit verwendet habe.

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

Dann benutze es so;

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

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

Wo;

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

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

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

Die Methode wird gesperrt, wenn Sie sie auf einem Typ ohne vergleichbare Attribute für alle Ihre Eigenschaften ausführen. Seien Sie also vorsichtig, wie sie verwendet wird, und sie sollte für die Anforderungen ausreichen.

Ich habe die Definition von Ordnung: Attribut weggelassen, da Yahias Link zu Marc Gravells Beitrag ein gutes Beispiel enthält.

Christopher McAtackney
quelle
2
Wenn dies eine Anzeigeanforderung ist, ist es angebracht, DataAnnotations.DisplayAttribute zu verwenden , das ein Bestellfeld enthält .
Jerph
12

Laut MSDN MetadataToken ist innerhalb eines Moduls einzigartig - es gibt nichts zu sagen, dass es überhaupt eine Bestellung garantiert.

AUCH wenn es sich so verhält, wie Sie es möchten, wäre dies implementierungsspezifisch und könnte sich jederzeit ohne vorherige Ankündigung ändern.

Siehe diesen alten MSDN-Blogeintrag .

Ich würde dringend empfehlen, sich von jeglichen Abhängigkeiten von solchen Implementierungsdetails fernzuhalten - siehe diese Antwort von Marc Gravell .

Wenn Sie zur Kompilierungszeit etwas benötigen, können Sie sich Roslyn ansehen (obwohl es sich in einem sehr frühen Stadium befindet).

Yahia
quelle
5

Was ich beim Sortieren nach MetadataToken getestet habe, funktioniert.

Einige Benutzer hier behaupten, dies sei irgendwie kein guter Ansatz / nicht zuverlässig, aber ich habe noch keine Beweise dafür gesehen - vielleicht können Sie hier ein Code-Snipet posten, wenn der angegebene Ansatz nicht funktioniert?

Informationen zur Abwärtskompatibilität - während Sie jetzt an Ihrem .net 4 / .net 4.5 arbeiten - Microsoft stellt .net 5 oder höher her, sodass Sie davon ausgehen können, dass diese Sortiermethode in Zukunft nicht mehr funktioniert.

Natürlich werden Sie vielleicht bis 2017, wenn Sie auf .net9 upgraden, eine Kompatibilitätsunterbrechung erleben, aber bis dahin werden Microsoft-Leute wahrscheinlich den "offiziellen Sortiermechanismus" herausfinden. Es macht keinen Sinn, zurück zu gehen oder Dinge zu zerbrechen.

Das Spielen mit zusätzlichen Attributen für die Eigenschaftsreihenfolge erfordert auch Zeit und Implementierung - warum sollte man sich die Mühe machen, wenn die MetadataToken-Sortierung funktioniert?

TarmoPikaro
quelle
1
Es ist 2019 und noch nicht einmal .net-4.9 wurde veröffentlicht :-p.
Binki
Jedes Mal, wenn .net eine Hauptversion veröffentlicht, ist dies eine großartige Gelegenheit für sie, Änderungen an Dingen wie MetadataToken oder der Reihenfolge der Rückgabe von vorzunehmen GetProperties(). Ihr Argument, warum Sie sich auf die Reihenfolge verlassen können, ist genau das gleiche wie das Argument, warum Sie sich nicht auf zukünftige Versionen von .net verlassen können, die dieses Verhalten nicht ändern: Jede neue Version kann die Implementierungsdetails ändern. Nun folgt .net tatsächlich der Philosophie, dass „Bugs Features sind“, sodass sie in Wirklichkeit wahrscheinlich nie die Reihenfolge von ändern würden GetProperties(). Es ist nur so, dass die API sagt, dass sie das dürfen.
Binki
1

Sie können DisplayAttribute in System.Component.DataAnnotations anstelle eines benutzerdefinierten Attributs verwenden. Ihre Anforderung hat sowieso etwas mit Anzeige zu tun.

Panos Roditakis
quelle
0

Wenn Sie mit der zusätzlichen Abhängigkeit zufrieden sind, können Sie dies mit Protobuf-Net von Marc Gravell tun, ohne sich Gedanken darüber machen zu müssen, wie Reflektion und Caching am besten implementiert werden können. Dekorieren Sie Ihre Felder einfach mit [ProtoMember]und greifen Sie dann in numerischer Reihenfolge auf die Felder zu:

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

metaData.GetFields();
Tom Makin
quelle
0

Ich habe es so gemacht:

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




    }

mit der Eigenschaft wie folgt deklariert:

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

}
Wächter
quelle
Wie ist das anders oder besser als die akzeptierte Antwort?
Ian Kemp
0

Aufbauend auf der oben akzeptierten Lösung können Sie so etwas verwenden, um den genauen Index zu erhalten

Gegeben

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

Erweiterungen

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

Verwendung

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

Beachten Sie , dass es keine Fehlerprüfung oder Fehlertoleranz gibt. Sie können nach Belieben Pfeffer und Salz hinzufügen

Die allgemeine
quelle
0

Eine andere Möglichkeit ist die Nutzung der System.ComponentModel.DataAnnotations.DisplayAttribute OrderEigenschaft. Da es eingebaut ist, muss kein neues spezifisches Attribut erstellt werden.

Wählen Sie dann solche geordneten Eigenschaften aus

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

Und Klasse kann so präsentiert werden

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

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