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
- Qué es un atributo y cómo se aplica
- Atributos predefinidos ya vistos:
[JsonPropertyName]y[JsonPolymorphic]en detalle - Crear un atributo personalizado heredando de
Attribute - Parámetros en un atributo personalizado:
[RequiereRol("Bibliotecario")] - Restringir dónde se puede aplicar un atributo:
[AttributeUsage] - Leer atributos personalizados por reflexión
[CampoObligatorio]: validación genérica del dominio de BiblioTech
- 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(...)] 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.
- Atributos predefinidos ya vistos:
[JsonPropertyName] y [JsonPolymorphic] en detalle
[JsonPropertyName] y [JsonPolymorphic] en detalleAhora 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 |
- Crear un atributo personalizado heredando de
Attribute
AttributeDefinir 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):
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.
- Parámetros en un atributo personalizado:
[RequiereRol("Bibliotecario")]
[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.
- Restringir dónde se puede aplicar un atributo:
[AttributeUsage]
[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.
- 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 CampoObligatorioGetCustomAttribute<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.
[CampoObligatorio]: validación genérica del dominio de BiblioTech
[CampoObligatorio]: validación genérica del dominio de BiblioTechUniendo 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
nullal leer un atributo conGetCustomAttribute<T>(): si el elemento no lleva ese atributo, el resultado esnull; tratarlo como si siempre estuviera presente lanza unaNullReferenceExceptionen 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
-
Define el atributo
RequiereRolAttributede esta lección, con[AttributeUsage(AttributeTargets.Method)]para restringirlo a métodos. Aplícalo con[RequiereRol("Bibliotecario")]sobre un método ficticioEliminarMaterialdeBiblioteca. -
Escribe una función
string? ObtenerRolRequerido(MethodInfo metodo)que useGetCustomAttribute<RequiereRolAttribute>()sobre elMethodInforecibido y devuelvaatributo.Rolsi el atributo está presente, onullsi no lo está. Pruébala obteniendo elMethodInfodeEliminarMaterialcontypeof(Biblioteca).GetMethod("EliminarMaterial"). -
Define
CampoObligatorioAttributey la funciónValidarCamposObligatorios(object obj)de esta lección. Aplica[CampoObligatorio]sobreTituloyAutordeMaterialBibliotecario, y valida unLibroconAutorvacío, comprobando que la lista de errores devuelta contiene exactamente un mensaje sobreAutor.
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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
