Ich arbeite ja sehr viel mit Reflection und nehme dafür auch desöfteren Attribute her, um Klassen näher spezifizieren zu können bzw. um per Reflection auf gewisse Eigenheiten einer Klasse reagieren zu können.
Aber es gibt dabei einen Punkt, der mir bisher immer noch nicht ganz klar war. Bisher hat zwar immer alles so funktioniert, wie ich es wollte, aber jetzt will ich mir die Methode GetCustomAttributes doch mal ein bisschen genauer anschauen. Um genau zu sein geht es mir um den Boolean-Parameter, den man bei dieser Methode mitgeben kann.

Die Methode des Klasse Type ist folgendermaßen definiert:


public abstract object[] GetCustomAttributes(bool inherit);
public abstract object[] GetCustomAttributes(Type attributeType, bool inherit);

Mir geht es um den Parameter inherit.
MSDN sagt dazu folgendes:

 true, um die Vererbungskette dieses Members nach den Attributen zu durchsuchen; andernfalls false. Dieser Parameter wird für Eigenschaften und Ereignisse ignoriert. Siehe „Hinweise“.
 Hinweise: Diese Methode ignoriert die inherit -Parameter für Eigenschaften und Ereignisse. Um die Vererbungskette für Attribute, Eigenschaften und Ereignisse zu suchen, verwenden Sie die entsprechenden Überladungen der Attribute.GetCustomAttributes Methode.

Um ehrlich zu sein, weiß ich nach dieser Beschreibung genausoviel wie zuvor. Es hat auf jeden Fall irgendwas mit Vererbung zu tun. Also werde ich mir entsprechende Testfälle aufbauen, um raus zu finden, wie sich der Parameter bei unterschiedlichen Konstellationen auswirkt.

Mein Attribute, das ich zum Testen verwende, sieht folgendermaßen aus:


public class MyTestAttribute:System.Attribute
{
    public string Instance { get; private set;}

    public MyTestAttribute(string instance)
    {
        Instance = instance;
    }
}

Über die Property Instance kann ich dann immer rausfinden, auf welche Klasse das Attribute verwendet wurde und kann den Inhalt entsprechend ausgeben.

Die Methode, mit der ich das Attribute ausgeben lasse, sieht so aus:

(
private static void Output(Type type)
{
    foreach (MyTestAttribute attribute in type.GetCustomAttributes(true))
        Console.WriteLine($"inherit = true  -> {attribute.Instance}");
    foreach (MyTestAttribute attribute in type.GetCustomAttributes(false))
        Console.WriteLine($"inherit = false -> {attribute.Instance}");
}

Ich rufe also jeweils zweimal GetCustomAttributes auf. Einmal mit inherit = true und einmal mit inherit = false, damit ich die Auswirkung dieses Parameters gut erkennen kann.

Fall 1: Einfache Klasse ohne Vererbung


[MyTest("TestClass")]
public class TestClass {}

public static void Main()
{
    Output(typeof(TestClass));
}

inherit = true  -> TestClass
inherit = false -> TestClass

Dieser Fall ist klar und da muss ich auch nichts dazu schreiben. Es ist hier egal, ob man inherit auf true oder auf false setzt. Man kommt immer auf das gleiche Ergebnis, weil es ja auch keine Ableitung gibt.

Fall 2: Einfache Vererbung; Attribut auf Basisklasse; Abruf auf Basisklasse


[MyTest("BaseClass")]
public class BaseClass {}

public class ChildClass : BaseClass {}

public static void Main()
{
    Output(typeof(BaseClass));
}

inherit = true  -> BaseClass
inherit = false -> BaseClass

Auch hier ist noch alles klar. Die Childklasse wird ja in keinster Weise angerührt, also kann sie auch nicht ins Spiel kommen.

Fall 3: Einfache Vererbung; Attribut auf Basisklasse; Abruf auf Childklasse


[MyTest("BaseClass")]
public class BaseClass {}

public class ChildClass : BaseClass {}

public static void Main()
{
    Output(typeof(ChildClass));
}

inherit = true  -> BaseClass

Das ist jetzt der erste interessante Fall. Wenn man also mit Inherit = false die Attribute abrufen will, findet man keine Attribute. Grund ist, dass das Attribute nur für die BaseClass angegeben wurde. Wenn man nicht die Vererbungskette zurückgeht, wird das Attribute also auch nicht gefunden.

Fall 4: Einfache Vererbung; Attribut auf beiden Klassen; Abruf auf Basisklasse


[MyTest("BaseClass")]
public class BaseClass {}

[MyTest("ChildClass")]
public class ChildClass : BaseClass {}

public static void Main()
{
    Output(typeof(BaseClass));
}

inherit = true  -> BaseClass
inherit = false -> BaseClass

Hier gilt das Gleich wie bei Fall 2. Die ChildClass wird in keinster Weise angefasst und damit spielt sie hier auch keine Rolle. Das Attribut ist sowohl mit inherit true als auch mit false auffindbar.

Fall 5: Einfache Vererbung; Attribut auf beiden Klassen; Abruf auf Childklasse


[MyTest("BaseClass")]
public class BaseClass {}

[MyTest("ChildClass")]
public class ChildClass : BaseClass {}

public static void Main()
{
    Output(typeof(ChildClass));
}

inherit = true  -> ChildClass
inherit = false -> ChildClass

Das ist in meinen Augen der interessanteste Fall. Eigentlich wäre ich jetzt davon ausgegangen, dass bei inherit = true zwei Attribute ausgegeben werden. Stattdessen wird aber nur das letzte Attribute, als das auf der Child-Klasse gefunden.
Das heißt, dass das letzte Glied der Vererbungskette den Ton angibt.

Damit ist die Funktionsweise an sich ziemlich geklärt. Es wird aber eine weitere Frage aufgeworfen:
Warum bekomme ich beim letzten Fall nur ein Attribut als Ergebnis, obwohl durch die Ableitung eigentlich auch das Attribut der Basisklasse mit reinspielen sollte? Aber darauf werde ich erst später eingehen.

Zuvor will ich aber noch ein paar andere Fälle ausprobieren. Ich bin gespannt, ob sich die Hierarchie mit Interfaces genauso verhält wie mit der bisherigen Basisklasse.
Ich ersetze also BaseClass durch ein Interface und lass nochmal ein paar Fälle durchlaufen

Fall 6: Interface; Attribut auf Interface; Abruf auf Interface


[MyTest("IBaseInterface")]
public interface IBaseInterface {}

public class ChildClass : IBaseInterface {}

public static void Main()
{
    Output(typeof(IBaseInterface));
}

inherit = true  -> IBaseInterface
inherit = false -> IBaseInterface

Was zu erwarten war. Auch hier gilt wieder, dass die Klasse selbst ja nirgendwo erwähnt wird. Dadurch wird nur das Interface herangezogen unabhängig von irgendwelchen Vererbungen.

Fall 7: Interface; Attribute auf Interface; Abruf auf Klasse


[MyTest("IBaseInterface")]
public interface IBaseInterface {}

public class ChildClass : IBaseInterface {}

public static void Main()
{
    Output(typeof(ChildClass));
}

Holla… Was ist das? Es gibt keine Ausgabe in diesem Fall. Eigentlich wäre ich jetzt davon ausgegangen, dass bei inherit = true das Attribut auf dem Interface gefunden wird. Aber aus irgendwelchen Gründen wird es nicht gefunden.

Fall 8: Interface; Attribut auf Interface und Klasse, Abruf auf Klasse


[MyTest("IBaseInterface")]
public interface IBaseInterface {}
[MyTest("IBaseInterface")]
public class ChildClass : IBaseInterface {}

public static void Main()
{
    Output(typeof(ChildClass));
}

inherit = true  -> ChildClass
inherit = false -> ChildClass

Dieses Ergebnis war jetzt auch wieder zu erwarten. Im Grunde ist es genau das Gleiche wie bei Fall 5. Da haben wir ja auch schon gesehen, dass das Attribut der Basis-Klasse ignoriert wird, wenn die Child-Klasse mit dem gleichen Attribut nochmal gekennzeichnet wurde. Aber wie wir später sehen werden, ist der Grund dafür ein ganz andere. Dazu komme ich aber erst später.

Zusammenfassung

Zusammengefasst kann man also sagen, dass der Parameter inherit angibt, ob in der Vererbungshierarchie nach oben gegangen werden soll, um ein entsprechendes Attribut zu finden oder nicht. Wenn in der Vererbungshierarchie nach oben gegangen wird, wird das erste gefunde Attribut - als das, dass in der Hierarchie am weitesten unten steht - gefunden. Weitere Attribute werden ignoriert. Wenn der Parameter auf false gesetzt wird, wird die Vererbung komplett ignoriert.
Desweiteren konnte man feststellen, dass Attribute auf Schnittstellen nicht abgerufen werden, wenn man das über die Klasse versucht.

Man muss also folgende zwei Punkte beachten, da diese auch gerne Fehlerquellen darstellen:

  • Wenn sowohl Basisklasse als auch Childklasse das gleiche Attribut verwenden, wird nur das Attribut der Childklasse berücksichtigt.
  • Wenn ein Interface mit einem Attribut ausgezeichnet wurde, kann dieses nicht über die Klasse abgerufen werden.

AttributeUsage

Was macht man aber, wenn man beide Attribute haben will? Also das Attribut der Basisklasse und das der Childklasse? Natürlich ist auch das möglich. Aber dazu muss man das Attribut etwas abändern. Genauergesagt muss man das Verhalten des Attributes mit einem weiteren Attribute genauer spezifizieren. Dazu verwendet man das Attribut AttributeUsage mit dem Parameter AllowMultible=true.

Um den obigen Fall 5 also dahingehend abzuändern, damit sowohl das Attribut der Basisklasse als auch das Attribut der Childklasse gefunden werden, verändere ich mein Attribut folgendermaßen:


[AttributeUsage(AttributeTargets=All, AllowMultible=true)]
public class MyTestAttribute:System.Attribute
{
    public string Instance { get; private set;}

    public MyTestAttribute(string instance)
    {
        Instance = instance;
    }
}

Das Ergebnis sieht dann so aus:

inherit = true  -> ChildClass
inherit = true  -> BaseClass
inherit = false -> ChildClass

Jetzt hat endlich der Parameter inherit von *GetCustomAttributes eine Auswirkung, die man nachvollziehen kann. Wenn man diesen Parameter auf true setzt, werden die Attribute der gesamten Vererbungskette gefunden.

Es gibt noch einen weiteren Parameter bei AttributeUsage, der bei diesem Thema berücksichtigt werden muss: inherited
Dieser Parameter steht standardmäßig auf true und das bedeutet, dass grundsätzlich Vererbung erlaubt wird. Wenn man ihn auf false setzt, wird das Attribut in der Vererbungskette nicht weitergereicht.
Im letzten Beispiel bedeutet das, dass man als Ergebnis wieder nur die Attribute auf der Childklasse finden würde.


[AttributeUsage(AttributeTargets=All, AllowMultible=true, Inherited=false)]
public class MyTestAttribute:System.Attribute
{
    public string Instance { get; private set;}

    public MyTestAttribute(string instance)
    {
        Instance = instance;
    }
}

Das Ergebnis sieht dann so aus:

inherit = true  -> ChildClass
inherit = false -> ChildClass

Man beachte, dass in diesem Fall dann der Parameter inherit der Methode GetCustomAttributes quasi deaktiviert wird. Das kann auch Sinn machen, wenn man gezielt die Vererbung eines Attributes verhindern will.

Um das Thema zu vervollständigen, erwähne ich auch noch den ersten Parameter des AttributeUsage-Attributes, auch wenn er nicht direkt was mit diesem Thema zu tun hat. Ausgeschrieben heißt der Parameter validOn und man kann damit angeben, auf welche Elemente das Attribute angewendet werden kann. Also soll das Attribut nur für Klassen verwendet werden können, schreibt man


[AttributeUsage(AttributeTargets.Class)]
public class MyTestAttribute:System.Attribute

Attribute der Schnittstellen

Schauen wir uns jetzt auch noch den Fall mit dem Interface an, bei dem gar nichts gefunden wurde (Fall 7). Dieses Problem ist nicht so einfach zu lösen. Dazu muss man sich vor Augen halten, was Interfaces eigentlich sind. Am einfachsten ist es, wenn man sich dazu den normalen Sprachgebrauch zu Nutzen machen:
Basisklasse: Eine Klasse leitet von einer Basisklasse ab
Schnittstelle: Eine Klasse implementiert eine Schnittstelle
Man sieht also hier schon, dass es einen Unterschied gibt. inherit heißt übersetzt ableiten und hat damit nichts mit der Schnittstelle zu tun, von der ja nicht abgeleitet wird, sondern die implementiert wird.

Dennoch kann es manchmal ganz nützlich sein, wenn man Attribute auf den Schnittstellen definiert und diese dann auch über die Klasse auslesen könnte. Mit Hilfe von Reflection ist das auch relativ leicht möglich.
Wie muss man dabei vorgehen? Mit der Methode GetInterfaces kann man sich die implementierten Interfaces eines Typs holen. Dann ruft man für jedes Interface einfach GetCustomAttributes auf und schon hat man ein Ergebnis.

Ich hab mir gedacht, dass ich dazu am besten eine generische Extension bereitstelle, die genau das erledigt:


public static IEnumerable<T> GetAttributes<T>(this Type type) where T: System.Attribute
{
    List<T> result = new List<T>();
    foreach (var iface in type.GetInterfaces())
    {
        result.AddRange(iface.GetCustomAttributes(true).Where(n => n.GetType() == typeof(T))
                        .Cast<T>());
    }
    return result;
}

Verwenden tut man das Ganze dann so:


foreach (MyTestAttribute attribute in type.ReadAttributes<MyTestAttribute>())
	Console.WriteLine($"extension       -> {attribute.Instance}");