HttpClient e il consumo di API REST in C#
Nella terza serie abbiamo costruito una Web API con ASP.NET Core; ora ci mettiamo dall'altra parte della comunicazione, imparando a consumare servizi HTTP. Lo strumento centrale in .NET è HttpClient, una classe di alto livello che gestisce le richieste HTTP nascondendo la complessità dei socket e del protocollo. In questo articolo vediamo come effettuare richieste, gestire le risposte, lavorare con JSON e, soprattutto, come usare HttpClient correttamente tramite IHttpClientFactory, evitando un errore molto comune.
Una prima richiesta GET
Il caso più semplice è recuperare del contenuto da un URL. Il metodo GetStringAsync effettua una richiesta GET e restituisce il corpo della risposta come stringa. Come da principio della sezione precedente, l'operazione è asincrona.
using System.Net.Http;
using var client = new HttpClient();
// Richiesta GET che restituisce il corpo come stringa
string content = await client.GetStringAsync("https://api.example.com/status");
Console.WriteLine(content);
Gestire la risposta completa
Spesso non basta il corpo: occorre esaminare il codice di stato, gli header e altre informazioni. Il metodo GetAsync restituisce un oggetto HttpResponseMessage che rappresenta l'intera risposta. Il metodo EnsureSuccessStatusCode solleva un'eccezione se lo stato indica un errore, integrandosi con la gestione delle eccezioni vista in precedenza.
HttpResponseMessage response = await client.GetAsync("https://api.example.com/products");
if (response.IsSuccessStatusCode)
{
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
else
{
Console.WriteLine($"Errore: {(int)response.StatusCode} {response.ReasonPhrase}");
}
Lavorare con JSON
Le API REST scambiano quasi sempre dati in formato JSON. Combinando HttpClient con la serializzazione studiata nella quarta serie, si possono convertire direttamente le risposte in oggetti C#. I metodi di estensione del namespace System.Net.Http.Json rendono l'operazione immediata, integrando System.Text.Json.
using System.Net.Http.Json;
class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}
// Deserializza direttamente la risposta JSON in un oggetto
Product? product = await client.GetFromJsonAsync<Product>(
"https://api.example.com/products/1");
// Oppure in una collezione
List<Product>? products = await client.GetFromJsonAsync<List<Product>>(
"https://api.example.com/products");
Inviare dati con POST
Per creare risorse si usa il verbo POST, inviando un oggetto nel corpo della richiesta. Il metodo PostAsJsonAsync serializza automaticamente l'oggetto in JSON e imposta gli header appropriati, riducendo il codice a una sola riga.
var newProduct = new Product { Name = "Tastiera", Price = 59.90m };
HttpResponseMessage response = await client.PostAsJsonAsync(
"https://api.example.com/products", newProduct);
response.EnsureSuccessStatusCode();
// Legge la risorsa creata restituita dal server
Product? created = await response.Content.ReadFromJsonAsync<Product>();
Personalizzare le richieste
Molte API richiedono header specifici, ad esempio per l'autenticazione o per indicare il formato accettato. Per un controllo completo si costruisce manualmente un HttpRequestMessage, specificando verbo, indirizzo, header e corpo, e lo si invia con SendAsync.
using System.Net.Http.Headers;
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data");
// Header di autorizzazione con token bearer
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "il-token");
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await client.SendAsync(request);
Timeout e annullamento
Una richiesta di rete può bloccarsi indefinitamente se il server non risponde. È buona pratica imporre un timeout e supportare l'annullamento tramite un CancellationToken, il meccanismo introdotto nell'articolo sull'asincronia. In questo modo l'applicazione resta reattiva anche di fronte a un servizio lento o irraggiungibile.
// Annullamento automatico dopo cinque secondi
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
string result = await client.GetStringAsync("https://api.example.com/slow", cts.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("La richiesta e scaduta");
}
L'errore da evitare: creare troppi HttpClient
Un errore diffuso è creare una nuova istanza di HttpClient per ogni richiesta, tipicamente all'interno di un blocco using. Sebbene la classe implementi l'interfaccia di rilascio, ogni istanza mantiene connessioni di rete che non vengono liberate immediatamente alla chiusura. Sotto carico, questo esaurisce le porte disponibili sul sistema, causando errori difficili da diagnosticare. La classe è inoltre progettata per essere riutilizzata da più richieste in sicurezza.
La soluzione: IHttpClientFactory
Nelle applicazioni moderne, la soluzione raccomandata è IHttpClientFactory, che gestisce il ciclo di vita delle connessioni sottostanti e fornisce istanze di HttpClient in modo sicuro ed efficiente. Si registra nel contenitore di dependency injection studiato nella terza serie e si riceve tramite iniezione.
// Registrazione nel punto di avvio
builder.Services.AddHttpClient();
// Utilizzo tramite dependency injection
class WeatherService
{
private readonly IHttpClientFactory _factory;
public WeatherService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<string> GetForecastAsync()
{
// La factory fornisce un client gestito correttamente
HttpClient client = _factory.CreateClient();
return await client.GetStringAsync("https://api.example.com/weather");
}
}
I client tipizzati
Un'evoluzione ancora più pulita sono i client tipizzati: si definisce una classe di servizio che riceve un HttpClient preconfigurato nel costruttore, con l'indirizzo base e gli header di default già impostati al momento della registrazione. Il codice che usa il servizio si concentra così sulla logica, non sulla configurazione HTTP.
class ApiClient
{
private readonly HttpClient _client;
public ApiClient(HttpClient client)
{
_client = client;
}
public Task<List<Product>?> GetProductsAsync()
=> _client.GetFromJsonAsync<List<Product>>("products");
}
// Registrazione con configurazione centralizzata del client
builder.Services.AddHttpClient<ApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});
Conclusione
Abbiamo imparato a consumare servizi HTTP con HttpClient: le richieste GET e POST, la gestione delle risposte e dei codici di stato, l'integrazione con JSON tramite GetFromJsonAsync e PostAsJsonAsync, la personalizzazione degli header e la gestione di timeout e annullamento. Soprattutto, abbiamo compreso perché non creare istanze usa e getta e come IHttpClientFactory e i client tipizzati risolvano il problema. Nel prossimo articolo scenderemo di livello lavorando direttamente con i socket TCP.