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

  1. Repàs i aprofundiment: System.Text.Json en escenaris més complexos
  2. Col·leccions imbricades i opcions de nomenclatura amb JsonNamingPolicy
  3. HttpClient: la porta d'entrada als serveis HTTP
  4. Consumir un GET i deserialitzar la resposta
  5. Enviar dades amb POST
  6. Bones pràctiques amb HttpClient: reutilització i IHttpClientFactory
  7. Tractament d'errors de xarxa i HTTP
  8. BiblioTech consulta metadades externes d'un llibre per ISBN

  1. Repàs i aprofundiment: System.Text.Json en escenaris més complexos

La 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.

  1. Col·leccions imbricades i opcions de nomenclatura amb JsonNamingPolicy

Considera 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.

  1. HttpClient: la porta d'entrada als serveis HTTP

HttpClient (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.

  1. Consumir un GET i deserialitzar la resposta

using 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).

  1. Enviar dades amb POST

Per 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.

  1. Bones pràctiques amb HttpClient: reutilització i IHttpClientFactory

A 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).

  1. 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.

  1. 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 HttpClient nou per cada petició amb using: sota càrrega, esgota els sòcols disponibles del sistema; reutilitza una única instància (o fes servir IHttpClientFactory en aplicacions ASP.NET Core).
  • No distingir una fallada de xarxa d'una resposta HTTP d'error: GetAsync no llança cap excepció per si sola davant un 404 o un 500 —cal cridar EnsureSuccessStatusCode() (o comprovar IsSuccessStatusCode) explícitament per tractar-los com a error.
  • Assumir que el JSON extern segueix el PascalCase: la majoria d'APIs reals fan servir camelCase o snake_case; configura PropertyNamingPolicy (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 HttpClient en una aplicació real necessita el seu try/catch corresponent.
  • 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 JsonDocument o fins i tot string amb WriteIndented) i mirar-ne l'estructura real, en lloc d'endevinar les propietats a cegues.

Exercicis

  1. Defineix les classes Editorial i MetadadesLlibreExtern tal com s'han presentat en aquesta lliçó. Deserialitza el JSON d'exemple de l'apartat 2 fent servir JsonNamingPolicy.CamelCase, i mostra per consola Editorial.Pais i el primer element de Generes.

  2. Escriu un mètode async Task<bool> ExisteixLlibreAlServeiExternAsync(HttpClient client, string isbn) que faci un GetAsync($"llibres/{isbn}") i retorni true si resposta.IsSuccessStatusCode és cert, false en cas contrari, sense llançar cap excepció per un 404.

  3. Afegeix a Biblioteca el mètode ObtenirMetadadesPerIsbnAsync d'aquesta lliçó. Crida'l des d'un Main así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 retorni null.

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#

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