Fins ara, BiblioTech ha desat i recuperat el seu propi estat: en text pla, en JSON, i en una
base de dades relacional. Però cap aplicació real viu aïllada: gairebé sempre necessita
comunicar-se amb altres sistemes a través de la xarxa, típicament mitjançant una API
REST —un servei que exposa dades i operacions sobre HTTP, amb JSON com a format habitual
d'intercanvi. Aquesta lliçó aprofundeix en System.Text.Json per a escenaris més complexos
que els vistos fins ara, i presenta HttpClient, la classe de .NET per fer peticions HTTP,
consumint un servei extern (simulat) que retorna metadades addicionals d'un llibre a partir
del seu ISBN. Tanca així el Mòdul 5 (Treballant amb Dades), l'última peça abans del Mòdul 6,
dedicat a temes més avançats del llenguatge i la plataforma.
Contingut
- Repàs i aprofundiment:
System.Text.Jsonen escenaris més complexos - Col·leccions imbricades i opcions de nomenclatura amb
JsonNamingPolicy HttpClient: la porta d'entrada als serveis HTTP- Consumir un
GETi deserialitzar la resposta - Enviar dades amb
POST - Bones pràctiques amb
HttpClient: reutilització iIHttpClientFactory - Tractament d'errors de xarxa i HTTP
- BiblioTech consulta metadades externes d'un llibre per ISBN
- Repàs i aprofundiment:
System.Text.Json en escenaris més complexos
System.Text.Json en escenaris més complexosLa lliçó de Serialització va presentar JsonSerializer.Serialize/Deserialize sobre objectes
senzills i col·leccions planes. El JSON que retorna una API externa real, però, sol tenir una
estructura més rica: objectes imbricats dins d'altres objectes, llistes dins d'un objecte, i
convencions de noms que no coincideixen amb el PascalCase. Aquesta lliçó retoma exactament
aquests casos.
- Col·leccions imbricades i opcions de nomenclatura amb
JsonNamingPolicy
JsonNamingPolicyConsidera una resposta típica d'una API externa de metadades de llibres, amb una llista imbricada de gèneres i un objecte imbricat amb dades de l'editorial:
{
"isbn": "978-84-376-0495-4",
"editorial": { "nom": "Sudamericana", "pais": "Argentina" },
"generes": ["Novel·la", "Literatura llatinoamericana"],
"puntuacioMitjana": 4.6
}Per deserialitzar aquesta estructura, es modelen les classes imbricades tal com reflecteixen el JSON:
class Editorial
{
public string Nom { get; set; } = string.Empty;
public string Pais { get; set; } = string.Empty;
}
class MetadadesLlibreExtern
{
public string Isbn { get; set; } = string.Empty;
public Editorial Editorial { get; set; } = new Editorial();
public List<string> Generes { get; set; } = new List<string>();
public double PuntuacioMitjana { get; set; }
}JsonSerializer.Deserialize<MetadadesLlibreExtern>(json) reconstrueix automàticament tant
l'objecte imbricat (Editorial) com la llista (Generes), sense cap codi addicional: EF
Core, a la lliçó anterior, i System.Text.Json, aquí, comparteixen la mateixa filosofia de
mapejar estructures completes per convenció.
Moltes APIs reals fan servir camelCase (puntuacioMitjana, no PuntuacioMitjana) a les
seves claus JSON, en lloc del PascalCase habitual de les propietats de C#. En lloc d'anotar
cada propietat amb [JsonPropertyName] una a una (vist a la lliçó de Serialització),
JsonSerializerOptions.PropertyNamingPolicy aplica la conversió a totes les propietats
de cop:
var opcions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
MetadadesLlibreExtern? metadades = JsonSerializer.Deserialize<MetadadesLlibreExtern>(json, opcions);
Console.WriteLine(metadades?.Editorial.Nom); // "Sudamericana"
string jsonGenerat = JsonSerializer.Serialize(metadades, opcions);
// les claus es generen en camelCase: "isbn", "editorial", "generes", "puntuacioMitjana"PropertyNamingPolicy funciona en totes dues direccions (serialitzar i deserialitzar), i és
l'opció recomanada davant [JsonPropertyName] quan tota una classe segueix la mateixa
convenció de noms; reserva [JsonPropertyName] per a les excepcions puntuals dins d'una
classe que, per la resta, segueix la convenció per defecte.
HttpClient: la porta d'entrada als serveis HTTP
HttpClient: la porta d'entrada als serveis HTTPHttpClient (de l'espai de noms System.Net.Http) és la classe de .NET per fer peticions
HTTP: GET per obtenir dades, POST per enviar-ne, i la resta de verbs HTTP habituals
(PUT, DELETE...). El seu ús bàsic consisteix a crear una instància, indicar (opcionalment)
una BaseAddress, i fer peticions contra rutes relatives a ella:
using System.Net.Http;
HttpClient client = new HttpClient
{
BaseAddress = new Uri("https://api.bibliotech-extern.example/")
};El paquet System.Net.Http.Json (inclòs de sèrie a .NET modern) afegeix mètodes d'extensió
que combinen la petició HTTP amb la deserialització JSON en una sola crida:
GetFromJsonAsync<T>, PostAsJsonAsync<T>, evitant el pas intermedi de llegir el cos de la
resposta com a text i deserialitzar-lo a part.
- Consumir un
GET i deserialitzar la resposta
GET i deserialitzar la respostausing System.Net.Http.Json;
MetadadesLlibreExtern? metadades =
await client.GetFromJsonAsync<MetadadesLlibreExtern>("llibres/978-84-376-0495-4");
if (metadades is not null)
{
Console.WriteLine($"Editorial: {metadades.Editorial.Nom} ({metadades.Editorial.Pais})");
Console.WriteLine($"Puntuacio mitjana: {metadades.PuntuacioMitjana}");
}GetFromJsonAsync<T> fa la petició GET, comprova que la resposta va ser satisfactòria, i
deserialitza el cos JSON directament al tipus T indicat —els tres passos que, amb
JsonSerializer a soles i un HttpClient sense l'extensió .Json, caldria escriure per
separat (GetAsync, llegir el cos amb ReadAsStringAsync, i
JsonSerializer.Deserialize).
- Enviar dades amb
POST
POSTPer enviar dades (per exemple, registrar en un servei extern que BiblioTech ha afegit un
llibre nou al catàleg), PostAsJsonAsync<T> serialitza l'objecte indicat a JSON i l'envia com
a cos de la petició:
class NouLlibreExtern
{
public string Isbn { get; set; } = string.Empty;
public string Titol { get; set; } = string.Empty;
}
NouLlibreExtern nouLlibre = new NouLlibreExtern
{
Isbn = "978-84-376-0497-8",
Titol = "El Aleph"
};
HttpResponseMessage resposta = await client.PostAsJsonAsync("llibres", nouLlibre);
if (resposta.IsSuccessStatusCode)
{
Console.WriteLine("Llibre registrat al servei extern.");
}resposta.IsSuccessStatusCode és true per a qualsevol codi d'estat HTTP 2xx (200, 201,
204...); és la manera habitual de comprovar, sense necessitat de llegir el codi numèric exacte,
si una petició ha tingut èxit.
- Bones pràctiques amb
HttpClient: reutilització i IHttpClientFactory
HttpClient: reutilització i IHttpClientFactoryA diferència de SqliteConnection o StreamReader, HttpClient no s'ha de crear amb
using a cada petició i llençar immediatament després. Encara que HttpClient també
implementa IDisposable, crear una instància nova per cada petició pot esgotar els sòcols
disponibles del sistema operatiu sota càrrega (cada HttpClient llençat deixa la seva connexió
de xarxa en un estat de tancament que triga una estona a alliberar-se del tot):
| Patró | Correcte per a... |
|---|---|
Una única instància d'HttpClient, reutilitzada durant tota la vida de l'aplicació (o d'un component de llarga durada) |
Aplicacions de consola i scripts senzills, com els d'aquest curs |
IHttpClientFactory (injectat mitjançant injecció de dependències) |
Aplicacions ASP.NET Core i altres escenaris amb moltes peticions concurrents (Mòdul 7) |
new HttpClient() dins d'un using, a cada petició |
Evitar-ho: pot esgotar els sòcols disponibles sota càrrega sostinguda |
Per a l'abast d'aquest curs —una aplicació de consola com BiblioTech— n'hi ha prou amb crear
una única instància d'HttpClient (per exemple, com a camp static readonly de la
classe que la fa servir) i reutilitzar-la a totes les crides; IHttpClientFactory resol el
mateix problema de forma més sofisticada en aplicacions ASP.NET Core, on el cicle de vida dels
components és diferent, un tema que es retoma al Mòdul 7 (Construcció d'Aplicacions).
- Tractament d'errors de xarxa i HTTP
Una petició HTTP pot fallar de dues maneres molt diferents, i convé distingir-les:
| Tipus de fallada | Excepció / símptoma | Exemple |
|---|---|---|
| Fallada de xarxa | HttpRequestException (o una altra excepció de xarxa) |
El servidor no respon, no hi ha connexió a Internet |
| Resposta HTTP d'error | La petició es completa, però amb un codi 4xx/5xx | El recurs no existeix (404), error del servidor (500) |
try
{
HttpResponseMessage resposta = await client.GetAsync("llibres/isbn-inexistent");
resposta.EnsureSuccessStatusCode(); // llanca HttpRequestException si el codi no es 2xx
MetadadesLlibreExtern? metadades =
await resposta.Content.ReadFromJsonAsync<MetadadesLlibreExtern>();
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Error en consultar el servei extern: {ex.Message}");
}
catch (TaskCanceledException)
{
Console.WriteLine("La peticio ha superat el temps d'espera.");
}EnsureSuccessStatusCode() converteix un codi d'error HTTP en una excepció
HttpRequestException, per poder-lo tractar amb el mateix try/catch que qualsevol altre
error (recordant la Gestió d'Excepcions del Mòdul 2), en lloc de comprovar manualment el codi
numèric a cada crida. TaskCanceledException es pot produir si la petició supera el temps
d'espera configurat a HttpClient.Timeout, un escenari habitual quan el servei extern no
respon a temps.
- BiblioTech consulta metadades externes d'un llibre per ISBN
Unint tot l'anterior, Biblioteca guanya un mètode que consulta un servei extern (simulat, com
el Task.Delay de la lliçó de Programació Asíncrona ho era per a una verificació lenta) per
enriquir la informació d'un Llibre amb dades que BiblioTech no desa per si mateixa:
class Biblioteca
{
// ... Cataleg, Socis, Prestecs, metodes anteriors del curs sense canvis ...
private static readonly HttpClient ClientHttp = new HttpClient
{
BaseAddress = new Uri("https://api.bibliotech-extern.example/")
};
private static readonly JsonSerializerOptions OpcionsJsonExtern = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public async Task<MetadadesLlibreExtern?> ObtenirMetadadesPerIsbnAsync(string isbn)
{
try
{
HttpResponseMessage resposta = await ClientHttp.GetAsync($"llibres/{isbn}");
resposta.EnsureSuccessStatusCode();
return await resposta.Content.ReadFromJsonAsync<MetadadesLlibreExtern>(OpcionsJsonExtern);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"No s'han pogut obtenir metadades externes per a '{isbn}': {ex.Message}");
return null;
}
}
}Llibre rayuela = new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
biblioteca.AfegirMaterial(rayuela);
MetadadesLlibreExtern? metadades = await biblioteca.ObtenirMetadadesPerIsbnAsync(rayuela.Isbn);
if (metadades is not null)
{
Console.WriteLine($"'{rayuela.Titol}' - Editorial: {metadades.Editorial.Nom}, puntuacio: {metadades.PuntuacioMitjana}");
}
else
{
Console.WriteLine($"No hi ha metadades externes disponibles per a '{rayuela.Titol}'.");
}ObtenirMetadadesPerIsbnAsync segueix el mateix patró que PrestarLlibreAsync de la lliçó de
Programació Asíncrona: un mètode async Task<T>, amb el seu Async de rigor al nom, que
encapsula una operació d'E/S (abans, un Task.Delay simulat; ara, una petició HTTP real) i
tracta els seus propis errors retornant null quan la consulta falla, en lloc de propagar
l'excepció cap a qui el crida.
sequenceDiagram
participant Main as Codi client
participant Bib as Biblioteca
participant Http as HttpClient
participant Api as API externa
Main->>Bib: await ObtenirMetadadesPerIsbnAsync(isbn)
Bib->>Http: GetAsync("llibres/{isbn}")
Http->>Api: GET /llibres/{isbn}
Api-->>Http: 200 OK + JSON
Http-->>Bib: HttpResponseMessage
Bib->>Bib: ReadFromJsonAsync<MetadadesLlibreExtern>
Bib-->>Main: MetadadesLlibreExtern
Errors Comuns i Consells
- Crear un
HttpClientnou per cada petició ambusing: sota càrrega, esgota els sòcols disponibles del sistema; reutilitza una única instància (o fes servirIHttpClientFactoryen aplicacions ASP.NET Core). - No distingir una fallada de xarxa d'una resposta HTTP d'error:
GetAsyncno llança cap excepció per si sola davant un404o un500—cal cridarEnsureSuccessStatusCode()(o comprovarIsSuccessStatusCode) explícitament per tractar-los com a error. - Assumir que el JSON extern segueix el PascalCase: la majoria d'APIs reals fan servir
camelCaseosnake_case; configuraPropertyNamingPolicy(o[JsonPropertyName]per a casos puntuals) en lloc d'assumir que coincidirà amb els noms de les propietats de C#. - Oblidar el tractament d'errors en una crida de xarxa: a diferència de llegir un fitxer
local, una petició HTTP depèn d'un sistema extern que pot no respondre, trigar massa, o
retornar un error; tot codi que faci servir
HttpClienten una aplicació real necessita el seutry/catchcorresponent. - Consell: per depurar el JSON exacte que retorna una API externa abans d'escriure les
classes de destinació, és útil deserialitzar primer a un tipus genèric d'inspecció (com
JsonDocumento fins i totstringambWriteIndented) i mirar-ne l'estructura real, en lloc d'endevinar les propietats a cegues.
Exercicis
-
Defineix les classes
EditorialiMetadadesLlibreExterntal com s'han presentat en aquesta lliçó. Deserialitza el JSON d'exemple de l'apartat 2 fent servirJsonNamingPolicy.CamelCase, i mostra per consolaEditorial.Paisi el primer element deGeneres. -
Escriu un mètode
async Task<bool> ExisteixLlibreAlServeiExternAsync(HttpClient client, string isbn)que faci unGetAsync($"llibres/{isbn}")i retornitruesiresposta.IsSuccessStatusCodeés cert,falseen cas contrari, sense llançar cap excepció per un404. -
Afegeix a
Bibliotecael mètodeObtenirMetadadesPerIsbnAsyncd'aquesta lliçó. Crida'l des d'unMainasíncron per a un llibre del catàleg, i tracta tant el cas d'èxit (mostrant l'editorial) com el cas en què el mètode retorninull.
Solucions
string json =
"""
{
"isbn": "978-84-376-0495-4",
"editorial": { "nom": "Sudamericana", "pais": "Argentina" },
"generes": ["Novel·la", "Literatura llatinoamericana"],
"puntuacioMitjana": 4.6
}
""";
var opcions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
MetadadesLlibreExtern? metadades = JsonSerializer.Deserialize<MetadadesLlibreExtern>(json, opcions);
Console.WriteLine(metadades?.Editorial.Pais); // "Argentina"
Console.WriteLine(metadades?.Generes[0]); // "Novel·la"
async Task<bool> ExisteixLlibreAlServeiExternAsync(HttpClient client, string isbn)
{
try
{
HttpResponseMessage resposta = await client.GetAsync($"llibres/{isbn}");
return resposta.IsSuccessStatusCode;
}
catch (HttpRequestException)
{
return false;
}
}
Llibre llibre = biblioteca.Cataleg.OfType<Llibre>().First();
MetadadesLlibreExtern? metadades = await biblioteca.ObtenirMetadadesPerIsbnAsync(llibre.Isbn);
if (metadades is not null)
{
Console.WriteLine($"Editorial de '{llibre.Titol}': {metadades.Editorial.Nom}");
}
else
{
Console.WriteLine($"No hi ha metadades externes disponibles per a '{llibre.Titol}'.");
}
OfType<Llibre>() és un operador LINQ (de la lliçó de LINQ, Mòdul 4) que filtra una
col·lecció quedant-se només amb els elements d'un tipus concret —aquí, només els Llibre
del Cataleg mixt, descartant les Revista.
Conclusió
En aquesta lliçó has aprofundit en System.Text.Json per a estructures imbricades i
convencions de noms reals, i has après a fer servir HttpClient per consumir una API REST
externa: peticions GET i POST, bones pràctiques de reutilització, i tractament diferenciat
d'errors de xarxa davant errors HTTP. Biblioteca ja pot enriquir el seu catàleg amb
informació d'un servei extern, tancant així el Mòdul 5 (Treballant amb Dades): des del fitxer
de text pla de la primera lliçó fins a una API REST externa, passant per JSON, ADO.NET i
Entity Framework, BiblioTech ha deixat de ser una aplicació que només viu en memòria.
El Mòdul 6 (Temes Avançats) retoma, amb més profunditat, diverses eines que aquest mòdul ja
ha fet servir de passada: la reflexió, que és literalment el mecanisme que permet a
JsonSerializer inspeccionar les propietats d'una classe sense codi manual de mapeig; els
atributs, com [JsonPropertyName] o [JsonDerivedType] vistos a la lliçó de
Serialització; la gestió de memòria i el recol·lector d'escombraries (Garbage
Collector), que allibera automàticament els objectes que aquest mòdul ha anat creant; i el
multifil real amb Thread i Parallel, que completa el que async/await va deixar
apuntat al Mòdul 4 sobre concurrència.
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
