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
- Què és un atribut i com s'aplica
- Atributs predefinits ja vistos:
[JsonPropertyName]i[JsonPolymorphic]en detall - Crear un atribut personalitzat heretant d'
Attribute - Paràmetres en un atribut personalitzat:
[RequereixRol("Bibliotecari")] - Restringir on es pot aplicar un atribut:
[AttributeUsage] - Llegir atributs personalitzats per reflexió
[CampObligatori]: validació genèrica del domini de BiblioTech
- 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.
- Atributs predefinits ja vistos:
[JsonPropertyName] i [JsonPolymorphic] en detall
[JsonPropertyName] i [JsonPolymorphic] en detallAra 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 |
- Crear un atribut personalitzat heretant d'
Attribute
AttributeDefinir 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):
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.
- Paràmetres en un atribut personalitzat:
[RequereixRol("Bibliotecari")]
[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.
- Restringir on es pot aplicar un atribut:
[AttributeUsage]
[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.
- 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 CampObligatoriGetCustomAttribute<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.
[CampObligatori]: validació genèrica del domini de BiblioTech
[CampObligatori]: validació genèrica del domini de BiblioTechUnint 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
nullen llegir un atribut ambGetCustomAttribute<T>(): si l'element no porta aquest atribut, el resultat ésnull; tractar-lo com si sempre fos present llança unaNullReferenceExceptiontan 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
-
Defineix l'atribut
RequereixRolAttributed'aquesta lliçó, amb[AttributeUsage(AttributeTargets.Method)]per restringir-lo a mètodes. Aplica'l amb[RequereixRol("Bibliotecari")]sobre un mètode ficticiEliminarMaterialdeBiblioteca. -
Escriu una funció
string? ObtenirRolRequerit(MethodInfo metode)que faci servirGetCustomAttribute<RequereixRolAttribute>()sobre elMethodInforebut i retorniatribut.Rolsi l'atribut és present, onullsi no ho és. Prova-la obtenint elMethodInfod'EliminarMaterialambtypeof(Biblioteca).GetMethod("EliminarMaterial"). -
Defineix
CampObligatoriAttributei la funcióValidarCampsObligatoris(object obj)d'aquesta lliçó. Aplica[CampObligatori]sobreTitoliAutordeMaterialBibliotecari, i valida unLlibreambAutorbuit, comprovant que la llista d'errors retornada conté exactament un missatge sobreAutor.
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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
