sobota, 10 maja 2008

Powierzchowna refleksja na atrybuty, czyli CustomAttributeData w akcji

Fakty o klasie CustomAttributeData

Pełne informacje: oczywiście MSDN

A tak konkretnie: CustomAttributeData to klasa przechowująca informacje o atrybucie opisującym typ, członka, parametr,... W odróżnieniu od metody GetCustomAttributes (z Type, MemberInfo, ParameterInfo, itd.) nie tworzy ona instancji atrybutów, ale daje jedynie informacje o tym:

  • jakie parametry zostały przekazane do jego konstruktora,
  • jakie nazwane parametry (w postaci Nazwa=Wartość) zostały zadeklarowane,
  • który konstruktor został wywołany.

Innymi słowy - CustomAttributeData zawiera przepis, jak utworzyć instancję danego atrybutu, a np. Type.GetCustomAttributes() zwraca już utworzone atrybuty.

Zgodnie z zasadą, że jeden przykład zastępuje tysiąc słów - spróbujmy dowiedzieć się jakie atrybuty ma typ System.Web.UI.Page:

[Designer("Microsoft.VisualStudio.Web....", typeof(IRootDesigner))]
[DefaultEvent("Load")]
[DesignerSerializer("Microsoft.VisualStudio...", "System.ComponentModel...")]
[DesignerCategory("ASPXCodeBehind")]
[ToolboxItem(false)]
public class Page //...



 


W tym celu wywołujemy statyczną metodę GetCustomAttributes klasy CustomAttributeData:




   1: IList<CustomAttributeData> data = 


   2:     CustomAttributeData.GetCustomAttributes(


   3:     typeof (System.Web.UI.Page));




Następnie iterujemy przez wszystkie dane atrybutów i wyświetlany uzyskane informacje:




   1: foreach (var attributeData in data)


   2: {


   3:     Console.WriteLine("Typ atrybutu: {0}",attributeData.Constructor.DeclaringType.FullName);


   4:     Console.WriteLine(" Argumenty konstruktora:");


   5:     for (int i = 0; i < attributeData.ConstructorArguments.Count; i++)


   6:     {


   7:         ParameterInfo param = attributeData.Constructor.GetParameters()[i];


   8:         object value = attributeData.ConstructorArguments[i].Value;


   9:         Console.WriteLine("  {0} = {1}", param.Name, value);


  10:     }


  11:     Console.WriteLine(" Nazwane argumenty:");


  12:     foreach (var arg in attributeData.NamedArguments)


  13:     {


  14:         Console.WriteLine("  {0} = {1}", 


  15:             arg.MemberInfo.Name, 


  16:             arg.TypedValue.Value);


  17:     }


  18: }




Oto wynik działania kodu:


custattrdata


Czyli dokładnie te informacje, których się spodziewaliśmy.




Wydajność



Pomimo, że CustomAttributeData nie tworzy instancji obiektów, to koszt (czas) jego uzyskania jest znacznie większy niż w przypadku np. Type.GetCustomAttributes. Justin Rogers na swoim blogu prezentuje własne badanie, które wykazało różnicę około czterokrotną. Tak znaczącą koszt może wynikać chociażby z faktu, iż CustomAttributeData musi pobrać bardzo wiele metadanych (ConstructorInfo oraz PropertyInfo dla każdego nazwanego argumentu). Po dokładniejsze informacje odsyłam do wyżej wspomnianego posta.



Kiedy może się taki sposób refleksji przydać?



1. Tryb ReflectionOnly



Assembly, które definiuje typ, mamy załadowane w trybie ReflectionOnly. Przykładowo:





   1: Assembly.ReflectionOnlyLoad("mscorlib");


   2: Type reflectionOnlyString = 


   3:     Type.ReflectionOnlyGetType(typeof(string).AssemblyQualifiedName, 


   4:     false, false);




W takiej sytuacji użycie CustomAttributeData jest jedyną możliwością uzyskania informacji o atrybutach, gdyż w trybie ReflectionOnly żaden kod zdefiniowany w assembly nie może zostać uruchomiony. Metoda Type.GetCutomAttibutes() nie może zostać uruchomiona, gdyż tworzy ona instancje wszystkich atrybutów.





2. Dostęp do wszystkich informacji podanych do kontruktora atrybutu



Zdarzają się takie sytuacje, że potrzebujemy uzyskać dostęp bezpośrednio do danych przekazanych w konstruktorze atrybutu. Niestety, nie zawsze twórcy udostępniają je jako publiczne pola/właściwości lub też są dostępne w sposób pośredni. Prosty przykład - rozważmy przykład atrybutu, którego zadaniem jest walidacja czy właściwość, którą opisuje, nie jest nullem lub pustym stringiem (taka mikro wersja Validation Application Block):





   1: public class NotNullOrEmptyAttribute: Attribute


   2: {


   3:     public string ErrorMessage { get; private set; }


   4:     private bool _negated;


   5:  


   6:     public NotNullOrEmptyAttribute(string fieldName, bool negated)


   7:     {


   8:         ErrorMessage = String.Format(


   9:             "Value for field {0} cannot be a null value or empty string.", 


  10:             fieldName);


  11:         _negated = negated;


  12:     }


  13:  


  14:     public bool IsValid(string value)


  15:     {


  16:         return String.IsNullOrEmpty(value) == _negated;


  17:     }


  18: }



Zastosowanie takiego atrybutu mogłoby wyglądać następująco:



   1: public class User


   2: {


   3:     [NotNullOrEmpty("user name", false)]


   4:     public string UserName { get; set; }


   5:  


   6:     //...


   7: }




Oczywiście w takiej sytuacji dokonanie walidacji, czy dana wartość jest poprawna dla danej właściwości, będzie dziecinnie proste. Gorzej jednak, gdy chcemy wykorzytać ten atrybut do innych celów niż przewidział twórca. Na przykład, potrzebujemy utworzyć stronę ASP.NET, która edytuje dane użytkownika i która automatycznie na podstawie atrybutu wstawia walidator RequiredFieldValidator. Wówczas potrzebujemy wartości podanych do kontruktora atrybutu, czyli fieldName oraz negated. Dostęp do tych danych możemy uzyskać:




  • nieelegancko i w sposób wrażliwy na przyszłe zmiany - "wydłubując" z ErrorMessage wartość fieldName


  • modląc się, że docelowo nasz kod będzie miał odpowiednio wysokie uprawnienia - za pomocą refleksji odczytać prywatną wartość pola _negated (jak? polecam wpis na blogu Maćka Aniserowicza)



Możemy jednak elegancko zastosować właśnie CustomAttributeData:





   1: IList<CustomAttributeData> data = 


   2:     CustomAttributeData.GetCustomAttributes(


   3:     typeof (User).GetProperty("UserName"));


   4:  


   5: CustomAttributeData nullOrEmptyData =


   6:     data.First(d => d.Constructor.DeclaringType == typeof (NotNullOrEmptyAttribute));


   7:  


   8: string fieldName = (string)nullOrEmptyData.ConstructorArguments[0].Value;


   9: bool isNegated = (bool) nullOrEmptyData.ConstructorArguments[1].Value;




Mała uwaga - jeżeli dodatkowo klasę User mamy załadowaną w trybie ReflectionOnly to porównanie




d.Constructor.DeclaringType == typeof (NotNullOrEmptyAttribute)



nigdy nie zwróci true (bo w trybie ReflectionOnly typ NullOrEmptyAttribute to inny typ niż NullOrEmptyAttribute w "normalnym" trybie). Wówczas wyjściem jest sprawdzenie nazwy typu (najbezpieczniej razem z kwalifikowaną nazwą assembly):




d.Constructor.DeclaringType.AssemblyQualifiedName == 
"CustomAttributeDataTest.NotNullOrEmptyAttribute, CustomAttributeDataTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

Brak komentarzy: