[C#] ¿Cómo funcionan los atributos?

Siguiendo en la línea de mis últimos artículos en los que desgrano un poco el framework de .NET para ver qué hace por dentro, ahora le toca el turno a los atributos.

¿Qué es un atributo?

Un atributo no es más que una clase que sirve para decorar un tipo. Esta decoración puede ser de tipo informativa o funcional. Por ejemplo, podemos decorar una propiedad con el atributo Description para indicar más información a nivel de metadatos y los que trabajamos en ASP.MVC tenemos el archiconocido atributo Authorize que indica si el usuario está autenticado o no para permitir entrar en la acción seleccionada o redirigirlo a una página de error.

¿Cómo crear un atributo?

Muy sencillo, heredamos de Attribute (o si partimos de otro atributo ya creado) y sobreescribimos los métodos que queramos:

   1: public class MyAttribute : Attribute

   2:         {

   3:             public override bool IsDefaultAttribute()

   4:             {

   5:                 return base.IsDefaultAttribute();

   6:             }

   7:  

   8:             public override bool Match(object obj)

   9:             {

  10:                 return base.Match(obj);

  11:             }

  12:         }

Como lo que nos interesa es reusar uno que ya existe, vamos a heredar de Description para añadirle un campo más:

   1: [AttributeUsage(System.AttributeTargets.Property,

   2:                    AllowMultiple = false,

   3:                    Inherited = false)]

   4:         public class CustomDescriptionAttribute : DescriptionAttribute

   5:         {

   6:             private readonly string shortDescription;

   7:  

   8:             public CustomDescriptionAttribute(string shortDescription, string longDescription)

   9:                 : base(longDescription)

  10:             {

  11:                 this.shortDescription = shortDescription;

  12:             }

  13:         }

Con AttributeUsage podemos definir cómo queremos que se comporte ese atributo y para qué queremos que sea: para cualquier tipo de objeto, sólo para clases, métodos, propiedades, etc.:

  • AttributeTarget: Indica cuál es el destino del atributo.
  • AllowMultiple: Indica si para un mismo tipo, podemos colocar el atributo más de una vez.
  • Inherited: Indica si este atributo se heredará en las clases que hereden del atributo actual.

Una vez lo tenemos creado, lo asociamos a lo que queramos o hayamos definido previamente. Sé que el ejemplo no es el más adecuado, pero me gustaría usarlo sólo para ver cómo funciona:

   1: public class Person

   2:        {

   3:            [CustomDescription("First name", 

   4:                "A given name or the name that occurs first in a given name.")]

   5:            public string Name { get; set; }

   6:        }

Con este atributo podemos decorar una propiedad para indicarle una descripción corta y una larga. Esto podría ser útil si queremos exportar esta entidad a un fichero o un documento donde muestre una información más detallada del campo que se está mostrando.

¿Y qué hace por dentro?

Bien, vamos por partes. Evidentemente aquí no hay magia, si ponemos un breakpoint o una salida por consola vemos que el atributo no hace nada. Pero antes de eso vamos a ver que el código que ha generado en CIL:

   1: .class auto ansi nested public beforefieldinit CustomDescriptionAttribute

   2:         extends [System]System.ComponentModel.DescriptionAttribute

Primero vemos que muestra una herencia de una clase normal y que no hay nada del otro mundo. Un objeto cualquiera, vamos. Sigamos:

   1: .custom instance void [mscorlib]System.AttributeUsageAttribute::.ctor(valuetype [mscorlib]System.AttributeTargets) = ( 01 00 80 00 00 00 02 00 54 02 0D 41 6C 6C 6F 77   // ........T..Allow

   2:                                                                                                                            4D 75 6C 74 69 70 6C 65 00 54 02 09 49 6E 68 65   // Multiple.T..Inhe

   3:                                                                                                                            72 69 74 65 64 00 )                               // rited.

Aquí tenemos la línea que indica el AttributeUsage mencionado anteriormente. Quedemos con esto porque es bastante importante… Pero mientras, sigamos:

   1: .field private initonly string shortDescription

   2:     .method public hidebysig specialname rtspecialname 

   3:             instance void  .ctor(string shortDescription,

   4:                                  string longDescription) cil managed

   5:     {

   6:       // Code size       18 (0x12)

   7:       .maxstack  8

   8:       IL_0000:  ldarg.0

   9:       IL_0001:  ldarg.2

  10:       IL_0002:  call       instance void [System]System.ComponentModel.DescriptionAttribute::.ctor(string)

  11:       IL_0007:  nop

  12:       IL_0008:  nop

  13:       IL_0009:  ldarg.0

  14:       IL_000a:  ldarg.1

  15:       IL_000b:  stfld      string Blog.AttributeToCil.Program/CustomDescriptionAttribute::shortDescription

  16:       IL_0010:  nop

  17:       IL_0011:  ret

  18:     } // end of method CustomDescriptionAttribute::.ctor

Aquí tenemos el resto de la clase. No hay diferencia alguna con cualquier otro tipo de clase en MSIL. Una vez tenemos esto, vamos a ver cómo queda la propiedad Name de la clase Person a la que le añadimos el atributo:

   1: .property instance string Name()

   2:    {

   3:      .custom instance void Blog.AttributeToCil.Program/CustomDescriptionAttribute::.ctor(string,

   4:                                                                                          string) = ( 01 00 0A 46 69 72 73 74 20 6E 61 6D 65 3B 41 20   // ...First name;A 

   5:                                                                                                      67 69 76 65 6E 20 6E 61 6D 65 20 6F 72 20 74 68   // given name or th

   6:                                                                                                      65 20 6E 61 6D 65 20 74 68 61 74 20 6F 63 63 75   // e name that occu

   7:                                                                                                      72 73 20 66 69 72 73 74 20 69 6E 20 61 20 67 69   // rs first in a gi

   8:                                                                                                      76 65 6E 20 6E 61 6D 65 2E 00 00 )                // ven name...

   9:      .get instance string Blog.AttributeToCil.Program/Person::get_Name()

  10:      .set instance void Blog.AttributeToCil.Program/Person::set_Name(string)

  11:    } // end of property Person::Name

Vemos el get/set y además, una referencia a una instancia del atributo que hemos creado. El mismo tipo de referencia que había en el caso anterior con el AttributeUsage. En CIL, .custom se usa para indicar CustomAttributes que es todo aquello que hereda de System.Attribute. Y CIL obtiene la información del atributo y la coloca además en el propio código generado, en el metadata asociado. La forma de instanciar un objeto de este tipo es por su nombre y un array de bytes en el que previamente se indican el tipo de objeto que irá en el constructor:

   1: .ctor(string,

   2:   string) = ( 01 00 0A 46 69 72 73 74 20 6E 61 6D 65 3B 41 20   // ...First name;A 

   3:               67 69 76 65 6E 20 6E 61 6D 65 20 6F 72 20 74 68   // given name or th

   4:               65 20 6E 61 6D 65 20 74 68 61 74 20 6F 63 63 75   // e name that occu

   5:               72 73 20 66 69 72 73 74 20 69 6E 20 61 20 67 69   // rs first in a gi

   6:               76 65 6E 20 6E 61 6D 65 2E 00 00 )                // ven name...

Con esto vemos que por un lado cada tipo mantiene una referencia a una instancia del tipo del atributo. Pero no la instancia en sí misma. La instancia se crea en cuanto se consulta la información del atributo. Si no hay nadie que la consulte, no se crea.

¿Cómo obtengo la información del atributo?

Por Reflection. Tenemos dos vías (en función del atributo que busquemos):

  • Obtener directamente el atributo empleando Reflection y navegando en los campos, métodos, etc para buscarlo.
  • Usar Attribute.GetCustomAttributes que nos permitirá dado un assembly, obtener los tipos de atributos que hemos creado para ello.

Afortunadamente hace un par de días publicaron una actualización que permite navegar por el código fuente de .NET de una forma más amena. Podemos encontrar como la clase Attribute y ver cómo obtiene los atributos por reflexión.

Por ejemplo, con el siguiente código podemos obtener el valor de CustomDescription que hemos creado anteriormente. Básicamente consiste en obtener la primera propiedad de la clase Person y preguntarle por sus CustomAttributes:

   1: var customDescription = person.GetType().GetProperties().First().GetCustomAttributes(true);

   2:  

* Si quieres saber más sobre atributos, puedes consultar la definición en el ECMA (sección II.22; página 205 y siguientes)