Dirk Richter
Software-Entwicklung und Architektur

Nutzung des HttpClient mit Factory


Inhaltsverzeichnis

Der HttpClient

Der HttpClient verwendet den HttpMessageHandler, um Request über den SocketsHttpHandler über das Netzwerk zu verschicken. D.h. der HttpClient hat Abhängigkeiten zum SocketsHttpHandler, die in der Folge hier analysiert werden.

Disposable HttpClient

Die Klasse HttpClient implementiert IDisposable. Daher liegt es nahe, das Objekt über using zu verwenden.

 1using (var client = new HttpClient())
 2{
 3    var response = await client.GetAsync(https://example.com);
 4    
 5    if (response.IsSuccessStatusCode)
 6    {
 7        var content = await response.Content.ReadAsStringAsync();
 8        // process the content
 9    }
10}

Das Problem hierbei ist jedoch, dass der HttpClient intern SocketsHttpHandler verwendet, der die Connections verwaltet. Wenn der HttpClient disposed wird, wird auch SocketsHttpHandler disposed, was zu unerwünschten Nebeneffekten führen kann, wenn die Verbindung plötzlich geschlossen wird. Bei jeder Erzeugung eine HttpClients muss auch der SocketsHttpHandler neu erzeugt werden und die Verbindung neu erstellt werden.

Die geöffneten Sockets kann man über den Befehl

1netstat -abn
2a: all connections
3b: processes
4n: addresses and port numbers

anzeigen lassen.

HttpClient als Singleton

Um eine Socket Exhaustion zu vermeiden, liegt es nahe, eine langlebige Instanz des HttpClients zu erzeugen. In der Folge wird der SocketsHttpHandler nicht disposed und der Socket wird wieder verwendet. Es gibt jedoch Szenarien, in denen dies zu unerwünschten Nebeneffekten führen kann. Bei vielen Diensten, die in der Cloud laufen, kann sich der DNS dynamisch ändern. In diesem Fall gäbe es jedoch keine dynamische Anpassung, so dass Anfragen ins Leere laufen könnten.

Verwendung der HttpClientFactory

Hintergrund

Mit der HttpClientFactory kann ein HttpClient erzeugt werden. Der HttpClient erzeugt jedoch keinen neuen HttpMessageHandler und keinen neuen SocketsHttpHandler, sondern greift auf einen Pool von Handlern zu und umgeht hierdurch das Problem mit nicht verfügbaren Sockets/Verbindungen.

Die Handler werden dabei by default alle 240 Sekunden neu erzeugt, um das oben beschriebene DNS Problem zu vermeiden.

Es folgen Beispiele, wie die HttpClientFactory

Direkte Verwendung von HttpClientFactory

Registrierung:

1public void ConfigureServices(IServiceCollection services)
2{
3    services.AddHttpClient();
4    // other services...
5}

Durch AddHttpClient steht das IHttpClientFactory Interface zur Verfügung

 1public class MyService
 2{
 3    private readonly HttpClient _client;
 4
 5    public MyService(IHttpClientFactory clientFactory)
 6    {
 7        _client = clientFactory.CreateClient();
 8    }
 9
10    public async Task<string> GetPageAsync(string url)
11    {
12        var response = await _client.GetAsync(url);
13        if (response.IsSuccessStatusCode)
14        {
15            return await response.Content.ReadAsStringAsync();
16        }
17        else
18        {
19            return "";
20        }
21    }
22}

‘Named’ HttpClient

Um einen HttpClient einen String als Namen zuzuweisen und zu verwenden, kann man folgendermaßen vorgehen. Hierdurch ist es einfacher den Client zu konfigurieren.

 1public void ConfigureServices(IServiceCollection services)
 2{
 3    services.AddHttpClient("FooClient", c =>
 4    {
 5        c.BaseAddress = new Uri(https://api.fooservice.com/);
 6        c.DefaultRequestHeaders.Add("Accept", "application/vnd.foo.v3+json");
 7        c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactoryFooExample");
 8    });
 9
10    // other services...
11}
12 
13public class MyService
14{
15    private readonly HttpClient _client;
16
17    public MyService(IHttpClientFactory clientFactory)
18    {
19        _client = clientFactory.CreateClient("FooClient");
20    }
21
22    public async Task<string> GetFooUserAsync(string userName)
23    {
24        var response = await _client.GetAsync($"/users/{userName}");
25        if (response.IsSuccessStatusCode)
26        {
27            return await response.Content.ReadAsStringAsync();
28        }
29        else
30        {
31            return "";
32        }
33    }
34}

Typsierter HttpClient

Registrierung:

1public void ConfigureServices(IServiceCollection services)
2{
3    services.AddHttpClient<FooService>();
4
5    // other services...
6}

Implementierung:

 1public void ConfigureServices(IServiceCollection services)
 2{
 3public class FooService
 4{
 5    public HttpClient Client { get; }
 6
 7    public FooService(HttpClient client)
 8    {
 9        client.BaseAddress = new Uri(https://api.fooservice.com/);
10        client.DefaultRequestHeaders.Add("Accept", "application/vnd.foo.v3+json");
11        client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactoryFooExample");
12
13        Client = client;
14    }
15
16    public async Task<string> GetGithubUserAsync(string userName)
17    {
18        var response = await Client.GetAsync($"/users/{userName}");
19        if (response.IsSuccessStatusCode)
20        {
21            return await response.Content.ReadAsStringAsync();
22        }
23        else
24        {
25            return "";
26        }
27    }
28}
29}

#Codereview #Technicaldebt #Httpclient #Development