La lección de Serialización (Módulo 5) ya usó [JsonPropertyName("nombre_completo")] y [JsonPolymorphic]/[JsonDerivedType] para personalizar cómo System.Text.Json convierte objetos a JSON y viceversa, sin explicar entonces qué eran exactamente esos corchetes. Esta lección lo hace: un atributo es una anotación de metadatos que se adjunta a una clase, una propiedad, un método..., y que algún código —normalmente un framework, no el propio programa en tiempo de ejecución normal— puede leer más tarde por reflexión (la lección anterior) para decidir cómo comportarse. Tras ver atributos predefinidos en detalle, esta lección enseña a crear un atributo propio para BiblioTech y a leerlo por reflexión para validar datos.

Contenido

  1. Qué es un atributo y cómo se aplica
  2. Atributos predefinidos ya vistos: [JsonPropertyName] y [JsonPolymorphic] en detalle
  3. Crear un atributo personalizado heredando de Attribute
  4. Parámetros en un atributo personalizado: [RequiereRol("Bibliotecario")]
  5. Restringir dónde se puede aplicar un atributo: [AttributeUsage]
  6. Leer atributos personalizados por reflexión
  7. [CampoObligatorio]: validación genérica del dominio de BiblioTech

  1. Qué es un atributo y cómo se aplica

Un atributo es una clase especial (derivada, en última instancia, de System.Attribute) cuyas instancias no se crean con new como el resto de objetos del programa, sino que se adjuntan a un elemento del código —una clase, un método, una propiedad, un parámetro— escribiéndolo entre corchetes justo encima:

[Obsolete("Usa PrestarLibroAsync en su lugar")]
void PrestarLibro(Libro libro)
{
    // ...
}

[Obsolete(...)] es un atributo predefinido de .NET: marca un miembro como "no usar más", y el propio compilador lo lee para mostrar una advertencia allá donde se llame a PrestarLibro. Este ejemplo ya revela la idea central de los atributos: no cambian el comportamiento del método por sí solos en tiempo de ejecución normal —PrestarLibro sigue haciendo exactamente lo mismo si se ejecuta—; es el compilador, o algún otro código que decida leerlo (por reflexión, como se verá en el apartado 6), quien le da significado.

  1. Atributos predefinidos ya vistos: [JsonPropertyName] y [JsonPolymorphic] en detalle

Ahora que se sabe qué es un atributo, vale la pena volver sobre los que ya se usaron sin detenerse, en la lección de Serialización:

class SocioJson
{
    [JsonPropertyName("nombre_completo")]
    public string Nombre { get; set; } = string.Empty;
}

[JsonPropertyName("nombre_completo")] es un atributo que JsonSerializer lee por reflexión sobre la propiedad Nombre antes de serializar o deserializar: al encontrarlo, usa la cadena indicada ("nombre_completo") como clave JSON en lugar del nombre real de la propiedad en C#. Sin ese atributo, JsonSerializer también usa reflexión —para descubrir que Nombre existe y es una propiedad pública—, solo que sin ninguna instrucción adicional sobre cómo llamarla en el JSON resultante.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "tipo")]
[JsonDerivedType(typeof(Libro), "libro")]
[JsonDerivedType(typeof(Revista), "revista")]
abstract class MaterialBibliotecario : IPrestable, IBuscable
{
    // ...
}

[JsonPolymorphic] y [JsonDerivedType] (varios atributos pueden apilarse sobre el mismo elemento, cada uno en su propia línea) le indican a JsonSerializer, también mediante lectura por reflexión al arrancar, cómo distinguir entre Libro y Revista dentro de una lista mixta de MaterialBibliotecario: exactamente el mismo mecanismo que un atributo personalizado, solo que [JsonPolymorphic] ya viene definido de fábrica en System.Text.Json.Serialization.

Atributo Dónde se aplica Quién lo lee y cuándo
[JsonPropertyName("clave")] Propiedad JsonSerializer, al serializar/deserializar
[JsonPolymorphic] / [JsonDerivedType] Clase JsonSerializer, al serializar/deserializar tipos con herencia
[Obsolete("mensaje")] Cualquier miembro El compilador, al compilar código que lo usa
[CampoObligatorio] (apartado 7, personalizado) Propiedad Una función de validación propia, por reflexión, cuando se invoque

  1. Crear un atributo personalizado heredando de Attribute

Definir un atributo propio consiste en crear una clase que herede de System.Attribute, por convención con el sufijo Attribute en su nombre (aunque al usarlo entre corchetes ese sufijo se omite):

using System;

class AuditableAttribute : Attribute
{
}
[Auditable]
class Prestamo
{
    // ...
}

class AuditableAttribute : Attribute define el atributo; [Auditable] es cómo se aplica sobre Prestamo —el compilador reconoce automáticamente que Auditable se refiere a AuditableAttribute, buscando primero el nombre exacto y, si no lo encuentra, el mismo nombre con el sufijo Attribute añadido. Este atributo, tal cual está, no hace nada por sí solo: como Prestamo en BiblioTech no lleva un sistema de auditoría real, queda como ejemplo mínimo del mecanismo; el resto de la lección construye uno con más utilidad práctica.

  1. Parámetros en un atributo personalizado: [RequiereRol("Bibliotecario")]

Un atributo puede recibir parámetros igual que cualquier otra clase, a través de su constructor, guardándolos en propiedades para que quien lo lea después (por reflexión) pueda consultarlos:

using System;

class RequiereRolAttribute : Attribute
{
    public string Rol { get; }

    public RequiereRolAttribute(string rol)
    {
        Rol = rol;
    }
}
class Biblioteca
{
    [RequiereRol("Bibliotecario")]
    public void EliminarMaterial(MaterialBibliotecario material)
    {
        // ... logica de eliminacion ...
    }
}

[RequiereRol("Bibliotecario")] documenta, de forma legible tanto para una persona como para código que lo lea por reflexión, que EliminarMaterial exige un rol concreto para ejecutarse. Igual que con [Auditable], el atributo por sí solo no impide que cualquiera llame a EliminarMaterial: hace falta código explícito que lo lea y actúe en consecuencia —el mismo patrón que sigue, por ejemplo, un framework de autorización en ASP.NET Core (Módulo 7), que sí implementa esa comprobación automáticamente sobre atributos similares a este.

  1. Restringir dónde se puede aplicar un atributo: [AttributeUsage]

Por defecto, un atributo personalizado puede aplicarse sobre casi cualquier elemento del código (clases, métodos, propiedades...). [AttributeUsage] —un atributo que se aplica sobre la propia definición del atributo— restringe dónde tiene sentido usarlo, y el compilador hace cumplir esa restricción:

using System;

[AttributeUsage(AttributeTargets.Property)]
class CampoObligatorioAttribute : Attribute
{
}
class MaterialBibliotecario
{
    [CampoObligatorio]
    public string Titulo { get; set; }

    // [CampoObligatorio]
    // public void Prestar() { }   // Error de compilacion: CampoObligatorio solo es valido sobre propiedades
}

AttributeTargets.Property indica que [CampoObligatorio] solo tiene sentido semántico sobre una propiedad; intentar aplicarlo sobre un método (Prestar()) sería un error de compilación, no un error silencioso descubierto más tarde en tiempo de ejecución. AttributeTargets es una enumeración con indicadores combinables (Class, Method, Property, Field...) mediante el operador |, para permitir varios lugares de uso a la vez si el atributo lo necesita.

  1. Leer atributos personalizados por reflexión

Un atributo aplicado sobre un elemento de código queda disponible, en tiempo de ejecución, a través de la reflexión vista en la lección anterior: GetCustomAttribute<T>() (o GetCustomAttributes(), en plural, si puede haber varios) sobre el PropertyInfo, MethodInfo o Type correspondiente:

using System.Reflection;

PropertyInfo? propiedadTitulo = typeof(Libro).GetProperty("Titulo");

CampoObligatorioAttribute? atributo =
    propiedadTitulo?.GetCustomAttribute<CampoObligatorioAttribute>();

Console.WriteLine(atributo is not null); // True: Titulo lleva el atributo CampoObligatorio

GetCustomAttribute<T>() devuelve la instancia del atributo si está presente sobre ese PropertyInfo, o null si no lo está —el mismo patrón de "buscar por reflexión y comprobar null" ya visto con GetProperty. Con parámetros (como RequiereRolAttribute.Rol), el objeto devuelto expone esas propiedades normalmente: atributo.Rol sería accesible tras comprobar que no es null.

  1. [CampoObligatorio]: validación genérica del dominio de BiblioTech

Uniendo todo lo anterior, se puede escribir una función de validación genérica: recorre las propiedades de cualquier objeto por reflexión (como MostrarPropiedades de la lección anterior) y, para cada una marcada con [CampoObligatorio], comprueba que no esté vacía.

using System;
using System.Reflection;

[AttributeUsage(AttributeTargets.Property)]
class CampoObligatorioAttribute : Attribute
{
}
abstract class MaterialBibliotecario : IPrestable, IBuscable
{
    [CampoObligatorio]
    public string Titulo { get; set; }

    [CampoObligatorio]
    public string Autor { get; set; }

    public bool Disponible { get; private set; } = true;

    // ... constructor, Prestar(), Devolver(), MostrarFicha(), Describir(), Coincide() sin cambios ...
}
static List<string> ValidarCamposObligatorios(object obj)
{
    List<string> errores = new List<string>();
    Type tipo = obj.GetType();

    foreach (PropertyInfo propiedad in tipo.GetProperties())
    {
        bool esObligatorio = propiedad.GetCustomAttribute<CampoObligatorioAttribute>() is not null;
        if (!esObligatorio)
        {
            continue;
        }

        object? valor = propiedad.GetValue(obj);
        if (valor is null || (valor is string texto && string.IsNullOrWhiteSpace(texto)))
        {
            errores.Add($"El campo '{propiedad.Name}' es obligatorio y esta vacio.");
        }
    }

    return errores;
}
Libro libroIncompleto = new Libro("", "Julio Cortazar", "978-84-376-0495-4");

List<string> errores = ValidarCamposObligatorios(libroIncompleto);
foreach (string error in errores)
{
    Console.WriteLine(error);
}
// El campo 'Titulo' es obligatorio y esta vacio.

ValidarCamposObligatorios no conoce Libro, Revista ni Socio de antemano: funciona con cualquier clase que use [CampoObligatorio] sobre alguna de sus propiedades, porque descubre en tiempo de ejecución, por reflexión, tanto la lista de propiedades como cuáles llevan el atributo. Esta combinación —reflexión más atributos— es exactamente el patrón que usan frameworks de validación reales (más completos que este ejemplo, con atributos como [Required], [Range], etc., habituales en ASP.NET Core, Módulo 7): las reglas se declaran de forma declarativa junto al dato, y una única función genérica las aplica sin repetir lógica de validación por cada clase.

flowchart TD
    A["ValidarCamposObligatorios(obj)"] --> B["obj.GetType().GetProperties()"]
    B --> C{"Tiene CampoObligatorioAttribute?"}
    C -->|No| B
    C -->|Si| D["GetValue(obj)"]
    D --> E{"Vacio o null?"}
    E -->|Si| F["Anadir error"]
    E -->|No| B

Errores Comunes y Consejos

  • Esperar que un atributo cambie el comportamiento por sí solo: [CampoObligatorio] o [RequiereRol], aplicados sobre una propiedad o un método, no hacen absolutamente nada hasta que algún código explícito los lee por reflexión y actúa; son metadatos declarativos, no lógica ejecutable.
  • Olvidar [AttributeUsage] en un atributo pensado para un único contexto: sin esa restricción, nada impide aplicar por error [CampoObligatorio] sobre un método o una clase entera, un uso que no tendría sentido y que la función de validación ignoraría en silencio.
  • No comprobar null al leer un atributo con GetCustomAttribute<T>(): si el elemento no lleva ese atributo, el resultado es null; tratarlo como si siempre estuviera presente lanza una NullReferenceException en cuanto se accede a alguna de sus propiedades.
  • Abusar de atributos personalizados para lógica que encajaría mejor como código normal: los atributos brillan para metadatos declarativos leídos por infraestructura genérica (validación, serialización, autorización); la lógica de negocio específica de BiblioTech (como Prestamo.RegistrarDevolucion()) debe seguir viviendo como código normal, no como atributos.
  • Consejo: al diseñar un atributo personalizado, piensa primero en quién lo va a leer y cómo (qué método de reflexión, sobre qué tipo de miembro); un atributo sin ningún lector asociado es, en la práctica, un simple comentario con sintaxis más rígida.

Ejercicios

  1. Define el atributo RequiereRolAttribute de esta lección, con [AttributeUsage(AttributeTargets.Method)] para restringirlo a métodos. Aplícalo con [RequiereRol("Bibliotecario")] sobre un método ficticio EliminarMaterial de Biblioteca.

  2. Escribe una función string? ObtenerRolRequerido(MethodInfo metodo) que use GetCustomAttribute<RequiereRolAttribute>() sobre el MethodInfo recibido y devuelva atributo.Rol si el atributo está presente, o null si no lo está. Pruébala obteniendo el MethodInfo de EliminarMaterial con typeof(Biblioteca).GetMethod("EliminarMaterial").

  3. Define CampoObligatorioAttribute y la función ValidarCamposObligatorios(object obj) de esta lección. Aplica [CampoObligatorio] sobre Titulo y Autor de MaterialBibliotecario, y valida un Libro con Autor vacío, comprobando que la lista de errores devuelta contiene exactamente un mensaje sobre Autor.

Soluciones

[AttributeUsage(AttributeTargets.Method)]
class RequiereRolAttribute : Attribute
{
    public string Rol { get; }

    public RequiereRolAttribute(string rol)
    {
        Rol = rol;
    }
}

class Biblioteca
{
    [RequiereRol("Bibliotecario")]
    public void EliminarMaterial(MaterialBibliotecario material)
    {
        // ...
    }
}
static string? ObtenerRolRequerido(MethodInfo metodo)
{
    RequiereRolAttribute? atributo = metodo.GetCustomAttribute<RequiereRolAttribute>();
    return atributo?.Rol;
}

MethodInfo? metodoEliminar = typeof(Biblioteca).GetMethod("EliminarMaterial");
if (metodoEliminar is not null)
{
    Console.WriteLine(ObtenerRolRequerido(metodoEliminar)); // "Bibliotecario"
}
Libro libroSinAutor = new Libro("Rayuela", "", "978-84-376-0495-4");

List<string> errores = ValidarCamposObligatorios(libroSinAutor);

Console.WriteLine(errores.Count); // 1
Console.WriteLine(errores[0]);    // El campo 'Autor' es obligatorio y esta vacio.

Conclusión

En esta lección has aprendido qué es un atributo y cómo funcionan de verdad [JsonPropertyName] y [JsonPolymorphic], ya usados en el Módulo 5: metadatos declarativos que un framework lee por reflexión. También has creado atributos personalizados propios ([Auditable], [RequiereRol], [CampoObligatorio]), restringido dónde pueden aplicarse con [AttributeUsage], y construido una función de validación genérica que lee [CampoObligatorio] por reflexión sobre cualquier clase del dominio de BiblioTech, sin acoplarse a Libro, Revista ni Socio en particular.

La siguiente lección, Programación Dinámica, cambia de tema dentro del mismo Módulo 6: en vez de inspeccionar tipos conocidos en tiempo de ejecución (reflexión) o anotarlos con metadatos (atributos), presenta el tipo dynamic, que renuncia por completo a la comprobación de tipos en tiempo de compilación. Verás por qué esa renuncia rara vez conviene en un dominio tipado como BiblioTech, y en qué escenarios concretos sí tiene sentido.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados