Kolejną rzeczą, która imponuje podczas całej transformacji z .NET Framework do .NET Core i najnowszych wersji .NET, jest fakt, że nie tylko dodawane są nowe funkcjonalności, ale jednocześnie adresowane są problemy ze starymi rozwiązaniami. Bardzo dobrym przykładem jest klasa HttpClient
. Typowe użycie w przeszłości wyglądało tak: tworzymy nową instancję, opakowujemy ją w using
i ruszamy dalej.
using (var client = new HttpClient())
{
var response = await client.GetAsync("http://myapi.com");
// dalsza obsługa
}
Teoretycznie wszystko zgodnie ze sztuką – zawsze powtarzano, że należy zwalniać zasoby, gdy nie są już potrzebne. Używamy using
i powinno być dobrze. Niestety, w przypadku HttpClient
nie rozwiązuje to wszystkich problemów. Choć zasoby są zwalniane, to otwarte połączenia TCP mogą wciąż pozostawać aktywne. Na szczęście, jak wspomniałem wcześniej, problemy w starych rozwiązaniach zostały zaadresowane!
Rozwiązania problemów z trzymaniem połączeń
Skoro wiemy, jaki jest problem, pora na rozwiązania. Najprostsze z nich to ponowne wykorzystanie tej samej instancji HttpClient
. Skoro przy każdej nowej instancji HttpClient
tworzone jest również połączenie, korzystanie z jednej wspólnej instancji może pomóc ograniczyć liczbę otwartych gniazd. Oczywiście, nie jest to rozwiązanie idealne.
Kolejnym, bardziej zalecanym podejściem jest użycie IHttpClientFactory
. Jest to mechanizm wprowadzony w .NET Core 2.1, który zarządza cyklem życia zarówno HttpClient
, jak i utrzymywanych połączeń. Dzięki IHttpClientFactory
tworzony jest zoptymalizowany, zarządzany pulą połączeń klient HTTP.
public class ApiService
{
private readonly IHttpClientFactory _httpClientFactory;
public ApiService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<int> GetCount()
{
using var client = _httpClientFactory.CreateClient();
await client.GetAsync("http://myapi.com");
// obsługa
}
}
Named Clients
Dodatkowo, jeśli korzystamy z kontenera zależności, możemy wykorzystać wbudowane metody pomocnicze do rejestrowania klientów HTTP. Dzięki temu jesteśmy w stanie wstępnie skonfigurować każdego klienta, co pozwala na bardziej wygodne i elastyczne zarządzanie ich konfiguracją. Na przykład, możemy dla konkretnego klienta ustawić bazowy adres URL. Dzięki temu w dalszym użyciu wystarczy podać jedynie adres endpointu, co znacznie upraszcza kod – moim zdaniem, jest to mega wygodne. Następnie, przy użyciu metody CreateClient
, jako parametr podajemy nazwę klienta, którego chcemy utworzyć.
var services = new ServiceCollection();
services.AddHttpClient("client1", client =>
{
client.BaseAddress = new Uri("http://myapi.com");
});
services.AddHttpClient("client2");
using var client = _httpClientFactory.CreateClient("client1");
Typed Clients
Named Clients wydają się ciekawym rozwiązaniem, ale mają też swoje wady. Trzeba pamiętać, jak nazwaliśmy klienta, a później w naszym kodzie posługiwać się dokładnie tą samą nazwą. Na szczęście można to zrobić jeszcze wygodniej. Możemy skorzystać z innego rozszerzenia, AddHttpClient
, z parametrem generycznym. Dzięki temu możemy zarejestrować naszą klasę, która będzie korzystać z HttpClient
. W praktyce nadal będziemy używać Named Client, ale zamiast nazwy możemy posługiwać się typem, co jest znacznie wygodniejsze w użyciu. Dodatkowo, w tym podejściu nie musimy już ręcznie tworzyć klientów – kontener DI zrobi to za nas, korzystając z IHttpClientFactory
.
// lekko modyfikujemy kod naszego ApiService
public class ApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<int> GetCount()
{
await _httpClient.GetAsync("http://myapi.com");
// obsługa
}
}
// przy generycznej wersji również możemy skonfigurować http client.
services.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("http://myapi.com");
});
Podsumowanie
Gdy już poznamy zmiany, jakie zostały wprowadzone, tworzenie klientów HTTP w nowy sposób szybko wchodzi w nawyk. Dodatkowo metody rejestrujące HttpClient
są znacznie wygodniejsze w użyciu i pozwalają na większą elastyczność oraz efektywne zarządzanie konfiguracją w jednym miejscu. Mając przy tym świadomość, że jest to dobre podejście, które minimalizuje ryzyko niewłaściwego zarządzania zasobami, dla mnie wybór jest oczywisty!