La lliçó de Serialització (Mòdul 5) ja va fer servir [JsonPropertyName("nom_complet")] i [JsonPolymorphic]/[JsonDerivedType] per personalitzar com System.Text.Json converteix objectes a JSON i a l'inrevés, sense explicar llavors què eren exactament aquells claudàtors. Aquesta lliçó ho fa: un atribut és una anotació de metadades que s'adjunta a una classe, una propietat, un mètode..., i que algun codi —normalment un framework, no el propi programa en temps d'execució normal— pot llegir més tard per reflexió (la lliçó anterior) per decidir com comportar-se. Després de veure atributs predefinits en detall, aquesta lliçó ensenya a crear un atribut propi per a BiblioTech i a llegir-lo per reflexió per validar dades.

Contingut

  1. Què és un atribut i com s'aplica
  2. Atributs predefinits ja vistos: [JsonPropertyName] i [JsonPolymorphic] en detall
  3. Crear un atribut personalitzat heretant d'Attribute
  4. Paràmetres en un atribut personalitzat: [RequereixRol("Bibliotecari")]
  5. Restringir on es pot aplicar un atribut: [AttributeUsage]
  6. Llegir atributs personalitzats per reflexió
  7. [CampObligatori]: validació genèrica del domini de BiblioTech

  1. Què és un atribut i com s'aplica

Un atribut és una classe especial (derivada, en última instància, de System.Attribute) les instàncies de la qual no es creen amb new com la resta d'objectes del programa, sinó que s'adjunten a un element del codi —una classe, un mètode, una propietat, un paràmetre— escrivint-lo entre claudàtors just a sobre:

[Obsolete("Fes servir PrestarLlibreAsync en el seu lloc")]
void PrestarLlibre(Llibre llibre)
{
    // ...
}

[Obsolete(...)] és un atribut predefinit de .NET: marca un membre com "no fer servir més", i el propi compilador el llegeix per mostrar un avís allà on es cridi PrestarLlibre. Aquest exemple ja revela la idea central dels atributs: no canvien el comportament del mètode per si sols en temps d'execució normal —PrestarLlibre continua fent exactament el mateix si s'executa—; és el compilador, o algun altre codi que decideixi llegir-lo (per reflexió, com es veurà a l'apartat 6), qui li dona significat.

  1. Atributs predefinits ja vistos: [JsonPropertyName] i [JsonPolymorphic] en detall

Ara que se sap què és un atribut, val la pena tornar sobre els que ja es van fer servir sense aturar-s'hi, a la lliçó de Serialització:

class SociJson
{
    [JsonPropertyName("nom_complet")]
    public string Nom { get; set; } = string.Empty;
}

[JsonPropertyName("nom_complet")] és un atribut que JsonSerializer llegeix per reflexió sobre la propietat Nom abans de serialitzar o deserialitzar: en trobar-lo, fa servir la cadena indicada ("nom_complet") com a clau JSON en lloc del nom real de la propietat en C#. Sense aquest atribut, JsonSerializer també fa servir reflexió —per descobrir que Nom existeix i és una propietat pública—, només que sense cap instrucció addicional sobre com anomenar-la al JSON resultant.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "tipus")]
[JsonDerivedType(typeof(Llibre), "llibre")]
[JsonDerivedType(typeof(Revista), "revista")]
abstract class MaterialBibliotecari : IPrestable, ICercable
{
    // ...
}

[JsonPolymorphic] i [JsonDerivedType] (diversos atributs es poden apilar sobre el mateix element, cadascun a la seva pròpia línia) li indiquen a JsonSerializer, també mitjançant lectura per reflexió en arrencar, com distingir entre Llibre i Revista dins d'una llista mixta de MaterialBibliotecari: exactament el mateix mecanisme que un atribut personalitzat, només que [JsonPolymorphic] ja ve definit de fàbrica a System.Text.Json.Serialization.

Atribut On s'aplica Qui el llegeix i quan
[JsonPropertyName("clau")] Propietat JsonSerializer, en serialitzar/deserialitzar
[JsonPolymorphic] / [JsonDerivedType] Classe JsonSerializer, en serialitzar/deserialitzar tipus amb herència
[Obsolete("missatge")] Qualsevol membre El compilador, en compilar codi que el fa servir
[CampObligatori] (apartat 7, personalitzat) Propietat Una funció de validació pròpia, per reflexió, quan s'invoqui

  1. Crear un atribut personalitzat heretant d'Attribute

Definir un atribut propi consisteix a crear una classe que hereti de System.Attribute, per convenció amb el sufix Attribute al seu nom (encara que en fer-lo servir entre claudàtors aquest sufix s'ometi):

using System;

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

class AuditableAttribute : Attribute defineix l'atribut; [Auditable] és com s'aplica sobre Prestec —el compilador reconeix automàticament que Auditable es refereix a AuditableAttribute, cercant primer el nom exacte i, si no el troba, el mateix nom amb el sufix Attribute afegit. Aquest atribut, tal com està, no fa res per si sol: com que Prestec a BiblioTech no porta un sistema d'auditoria real, queda com a exemple mínim del mecanisme; la resta de la lliçó en construeix un amb més utilitat pràctica.

  1. Paràmetres en un atribut personalitzat: [RequereixRol("Bibliotecari")]

Un atribut pot rebre paràmetres igual que qualsevol altra classe, a través del seu constructor, guardant-los en propietats perquè qui el llegeixi després (per reflexió) els pugui consultar:

using System;

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

    public RequereixRolAttribute(string rol)
    {
        Rol = rol;
    }
}
class Biblioteca
{
    [RequereixRol("Bibliotecari")]
    public void EliminarMaterial(MaterialBibliotecari material)
    {
        // ... logica d'eliminacio ...
    }
}

[RequereixRol("Bibliotecari")] documenta, de forma llegible tant per a una persona com per a codi que el llegeixi per reflexió, que EliminarMaterial exigeix un rol concret per executar-se. Igual que amb [Auditable], l'atribut per si sol no impedeix que qualsevol cridi EliminarMaterial: cal codi explícit que el llegeixi i actuï en conseqüència —el mateix patró que segueix, per exemple, un framework d'autorització a ASP.NET Core (Mòdul 7), que sí que implementa aquesta comprovació automàticament sobre atributs semblants a aquest.

  1. Restringir on es pot aplicar un atribut: [AttributeUsage]

Per defecte, un atribut personalitzat es pot aplicar sobre gairebé qualsevol element del codi (classes, mètodes, propietats...). [AttributeUsage] —un atribut que s'aplica sobre la pròpia definició de l'atribut— restringeix on té sentit fer-lo servir, i el compilador fa complir aquesta restricció:

using System;

[AttributeUsage(AttributeTargets.Property)]
class CampObligatoriAttribute : Attribute
{
}
class MaterialBibliotecari
{
    [CampObligatori]
    public string Titol { get; set; }

    // [CampObligatori]
    // public void Prestar() { }   // Error de compilacio: CampObligatori nomes es valid sobre propietats
}

AttributeTargets.Property indica que [CampObligatori] només té sentit semàntic sobre una propietat; intentar aplicar-lo sobre un mètode (Prestar()) seria un error de compilació, no un error silenciós descobert més tard en temps d'execució. AttributeTargets és una enumeració amb indicadors combinables (Class, Method, Property, Field...) mitjançant l'operador |, per permetre diversos llocs d'ús alhora si l'atribut ho necessita.

  1. Llegir atributs personalitzats per reflexió

Un atribut aplicat sobre un element de codi queda disponible, en temps d'execució, a través de la reflexió vista a la lliçó anterior: GetCustomAttribute<T>() (o GetCustomAttributes(), en plural, si n'hi pot haver diversos) sobre el PropertyInfo, MethodInfo o Type corresponent:

using System.Reflection;

PropertyInfo? propietatTitol = typeof(Llibre).GetProperty("Titol");

CampObligatoriAttribute? atribut =
    propietatTitol?.GetCustomAttribute<CampObligatoriAttribute>();

Console.WriteLine(atribut is not null); // True: Titol porta l'atribut CampObligatori

GetCustomAttribute<T>() retorna la instància de l'atribut si és present sobre aquest PropertyInfo, o null si no ho és —el mateix patró de "cercar per reflexió i comprovar null" ja vist amb GetProperty. Amb paràmetres (com RequereixRolAttribute.Rol), l'objecte retornat exposa aquestes propietats amb normalitat: atribut.Rol seria accessible després de comprovar que no és null.

  1. [CampObligatori]: validació genèrica del domini de BiblioTech

Unint tot l'anterior, es pot escriure una funció de validació genèrica: recorre les propietats de qualsevol objecte per reflexió (com MostrarPropietats de la lliçó anterior) i, per cada una marcada amb [CampObligatori], comprova que no estigui buida.

using System;
using System.Reflection;

[AttributeUsage(AttributeTargets.Property)]
class CampObligatoriAttribute : Attribute
{
}
abstract class MaterialBibliotecari : IPrestable, ICercable
{
    [CampObligatori]
    public string Titol { get; set; }

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

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

    // ... constructor, Prestar(), Retornar(), MostrarFitxa(), Descriure(), Coincideix() sense canvis ...
}
static List<string> ValidarCampsObligatoris(object obj)
{
    List<string> errors = new List<string>();
    Type tipus = obj.GetType();

    foreach (PropertyInfo propietat in tipus.GetProperties())
    {
        bool esObligatori = propietat.GetCustomAttribute<CampObligatoriAttribute>() is not null;
        if (!esObligatori)
        {
            continue;
        }

        object? valor = propietat.GetValue(obj);
        if (valor is null || (valor is string text && string.IsNullOrWhiteSpace(text)))
        {
            errors.Add($"El camp '{propietat.Name}' es obligatori i esta buit.");
        }
    }

    return errors;
}
Llibre llibreIncomplet = new Llibre("", "Julio Cortazar", "978-84-376-0495-4");

List<string> errors = ValidarCampsObligatoris(llibreIncomplet);
foreach (string error in errors)
{
    Console.WriteLine(error);
}
// El camp 'Titol' es obligatori i esta buit.

ValidarCampsObligatoris no coneix Llibre, Revista ni Soci d'antuvi: funciona amb qualsevol classe que faci servir [CampObligatori] sobre alguna de les seves propietats, perquè descobreix en temps d'execució, per reflexió, tant la llista de propietats com quines porten l'atribut. Aquesta combinació —reflexió més atributs— és exactament el patró que fan servir frameworks de validació reals (més complets que aquest exemple, amb atributs com [Required], [Range], etc., habituals a ASP.NET Core, Mòdul 7): les regles es declaren de forma declarativa junt amb la dada, i una única funció genèrica les aplica sense repetir lògica de validació per cada classe.

flowchart TD
    A["ValidarCampsObligatoris(obj)"] --> B["obj.GetType().GetProperties()"]
    B --> C{"Te CampObligatoriAttribute?"}
    C -->|No| B
    C -->|Si| D["GetValue(obj)"]
    D --> E{"Buit o null?"}
    E -->|Si| F["Afegir error"]
    E -->|No| B

Errors Comuns i Consells

  • Esperar que un atribut canviï el comportament per si sol: [CampObligatori] o [RequereixRol], aplicats sobre una propietat o un mètode, no fan absolutament res fins que algun codi explícit els llegeix per reflexió i actua; són metadades declaratives, no lògica executable.
  • Oblidar [AttributeUsage] en un atribut pensat per a un únic context: sense aquesta restricció, res impedeix aplicar per error [CampObligatori] sobre un mètode o una classe sencera, un ús que no tindria sentit i que la funció de validació ignoraria en silenci.
  • No comprovar null en llegir un atribut amb GetCustomAttribute<T>(): si l'element no porta aquest atribut, el resultat és null; tractar-lo com si sempre fos present llança una NullReferenceException tan bon punt s'accedeix a alguna de les seves propietats.
  • Abusar d'atributs personalitzats per a lògica que encaixaria millor com a codi normal: els atributs brillen per a metadades declaratives llegides per infraestructura genèrica (validació, serialització, autorització); la lògica de negoci específica de BiblioTech (com Prestec.RegistrarDevolucio()) ha de continuar vivint com a codi normal, no com a atributs.
  • Consell: en dissenyar un atribut personalitzat, pensa primer en qui el llegirà i com (quin mètode de reflexió, sobre quin tipus de membre); un atribut sense cap lector associat és, a la pràctica, un simple comentari amb sintaxi més rígida.

Exercicis

  1. Defineix l'atribut RequereixRolAttribute d'aquesta lliçó, amb [AttributeUsage(AttributeTargets.Method)] per restringir-lo a mètodes. Aplica'l amb [RequereixRol("Bibliotecari")] sobre un mètode fictici EliminarMaterial de Biblioteca.

  2. Escriu una funció string? ObtenirRolRequerit(MethodInfo metode) que faci servir GetCustomAttribute<RequereixRolAttribute>() sobre el MethodInfo rebut i retorni atribut.Rol si l'atribut és present, o null si no ho és. Prova-la obtenint el MethodInfo d'EliminarMaterial amb typeof(Biblioteca).GetMethod("EliminarMaterial").

  3. Defineix CampObligatoriAttribute i la funció ValidarCampsObligatoris(object obj) d'aquesta lliçó. Aplica [CampObligatori] sobre Titol i Autor de MaterialBibliotecari, i valida un Llibre amb Autor buit, comprovant que la llista d'errors retornada conté exactament un missatge sobre Autor.

Solucions

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

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

class Biblioteca
{
    [RequereixRol("Bibliotecari")]
    public void EliminarMaterial(MaterialBibliotecari material)
    {
        // ...
    }
}
static string? ObtenirRolRequerit(MethodInfo metode)
{
    RequereixRolAttribute? atribut = metode.GetCustomAttribute<RequereixRolAttribute>();
    return atribut?.Rol;
}

MethodInfo? metodeEliminar = typeof(Biblioteca).GetMethod("EliminarMaterial");
if (metodeEliminar is not null)
{
    Console.WriteLine(ObtenirRolRequerit(metodeEliminar)); // "Bibliotecari"
}
Llibre llibreSenseAutor = new Llibre("Rayuela", "", "978-84-376-0495-4");

List<string> errors = ValidarCampsObligatoris(llibreSenseAutor);

Console.WriteLine(errors.Count); // 1
Console.WriteLine(errors[0]);    // El camp 'Autor' es obligatori i esta buit.

Conclusió

En aquesta lliçó has après què és un atribut i com funcionen de veritat [JsonPropertyName] i [JsonPolymorphic], ja fets servir al Mòdul 5: metadades declaratives que un framework llegeix per reflexió. També has creat atributs personalitzats propis ([Auditable], [RequereixRol], [CampObligatori]), restringit on es poden aplicar amb [AttributeUsage], i construït una funció de validació genèrica que llegeix [CampObligatori] per reflexió sobre qualsevol classe del domini de BiblioTech, sense acoblar-se a Llibre, Revista ni Soci en particular.

La lliçó següent, Programació Dinàmica, canvia de tema dins del mateix Mòdul 6: en comptes d'inspeccionar tipus coneguts en temps d'execució (reflexió) o anotar-los amb metadades (atributs), presenta el tipus dynamic, que renuncia completament a la comprovació de tipus en temps de compilació. Veuràs per què aquesta renúncia rarament convé en un domini tipat com BiblioTech, i en quins escenaris concrets sí que té sentit.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats