Cześć, z tej strony Jarek Armatys. W dziewiątej lekcji kursu ASP.NET Core zajmujemy się czymś, co w codziennej pracy .NET developera jest chlebem powszednim — konsumowaniem zewnętrznych API. W odcinku używałem OpenWeather i WebClient z System.Net, ale dzisiaj chcę pokazać ci to wszystko w nowocześniejszym wydaniu, bo przez kilka lat w .NET sporo się w tym obszarze zmieniło.
Jeżeli oglądałeś materiał, to wiesz, że pobieraliśmy prognozę pogody dla konkretnego miasta, a dane wyświetlaliśmy tylko dla zalogowanych użytkowników — żeby jakiś anonimowy gość nie zrobił ci "milionów gwiazd" na liczniku requestów i nie wygenerował niepotrzebnych kosztów. To bardzo praktyczne podejście, zwłaszcza jeśli API ma limity albo płacisz za pojedyncze wywołania.
HttpClient w nowoczesnym .NET
Na filmie korzystałem z WebClient, bo było szybko, prosto i wystarczało do pokazania idei. Ale jeśli dzisiaj piszesz kod produkcyjny w .NET 6/7/8, to WebClient jest przestarzały i w dokumentacji Microsoftu masz wręcz napisane "don't use it in new development". Standardem jest HttpClient.
HttpClient z pozoru wygląda tak samo — tworzysz obiekt, wywołujesz metodę, dostajesz odpowiedź. Diabeł jak zwykle tkwi w szczegółach. Jeżeli napiszesz w kontrolerze coś takiego:
using var client = new HttpClient();
var json = await client.GetStringAsync("https://api.openweathermap.org/data/2.5/weather?q=London");
to lokalnie zadziała bez zarzutu, ale w produkcji trafisz na dwa klasyczne problemy:
- Socket exhaustion —
HttpClientpod spodem trzyma pulę socketów TCP. Jeżeli tworzysz nowe instancje pod każdy request, sockety nie zwalniają się natychmiast (przechodzą w stanTIME_WAIT), a po chwili brakuje ci portów i aplikacja zaczyna rzucać wyjątkami. - Stale DNS — długożyjący singleton
HttpClientcache'uje rozwiązania DNS, więc jak twoje API zmieni adres IP (np. przy wdrożeniu w chmurze), klient tego nie zauważy i zacznie lecieć na stary adres.
Dlatego zamiast new HttpClient() używamy IHttpClientFactory.
IHttpClientFactory — czemu nie new HttpClient()
IHttpClientFactory pojawił się w .NET Core 2.1 i to jest dziś domyślny sposób pracy z HTTP w ASP.NET Core. Fabryka zarządza pulą HttpMessageHandlerów pod spodem, rotuje je co jakiś czas (domyślnie co 2 minuty) i dzięki temu masz odporność na oba problemy opisane wyżej.
Najbardziej elegancki sposób rejestracji to typed client — czyli tworzysz swój serwis, a fabryka podstawia mu HttpClient przez konstruktor. W Program.cs (albo Startup.cs, zależnie od szablonu):
services.AddHttpClient<IWeatherService, WeatherService>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
});
Dzięki temu w serwisie nie musisz znać adresu API — dostajesz gotowy klient z ustawionym BaseAddress, ewentualnymi nagłówkami, timeoutem i tym, co sobie skonfigurujesz. Idealnie pasuje to do tego, co pokazywałem w odcinku: interfejs IApiService, klasa ApiService, wstrzyknięcie do kontrolera, wywołanie jednej metody.
Typed client to moim zdaniem najczystsze API do pracy z zewnętrznymi serwisami w .NET. Masz jeden interfejs, jedną implementację i wszystkie szczegóły HTTP zamknięte w jednym miejscu.
Deserializacja JSON
Na nagraniu używałem JsonConvert.DeserializeObject<WeatherResponse>(json) z biblioteki Newtonsoft.Json. Newtonsoft przez lata był de facto standardem w .NET i do dzisiaj działa świetnie — jeśli masz projekt, w którym jest już wpięty, nie ma sensu go wyrzucać.
Natomiast od .NET Core 3.0 Microsoft wprowadził wbudowany System.Text.Json, który jest szybszy, alokuje mniej pamięci i nie wymaga żadnej dodatkowej paczki NuGet. W ASP.NET Core jest dostępny bez niczego.
W połączeniu z HttpClient całą robotę z deserializacją możesz załatwić jedną linijką — extension method GetFromJsonAsync:
public class WeatherService : IWeatherService
{
private readonly HttpClient _http;
public WeatherService(HttpClient http) => _http = http;
public async Task<Forecast> GetForecastAsync(string city)
{
var response = await _http.GetFromJsonAsync<Forecast>($"forecast?q={city}");
return response ?? throw new InvalidOperationException("No data");
}
}
Zwróć uwagę na kilka rzeczy:
GetFromJsonAsyncsam pobiera treść, sam deserializuje, sam rzuci wyjątkiem jeśli status nie jest 2xx.- Nie piszę ręcznie
JsonConvert.DeserializeObjectaniresponse.Content.ReadAsStringAsync()— kod jest krótszy i czytelniejszy. - Metoda jest
asynci zwracaTask<T>. Po stronie kontrolera wystarczyawait _apiService.GetForecastAsync("London").
Co do modelu — na filmie korzystałem z narzędzia json2csharp.com i szczerze ci to polecam. Wrzucasz odpowiedź JSON, dostajesz gotowe klasy C#, zmieniasz nazwy tam gdzie chcesz i masz temat załatwiony w 30 sekund. W nowszych wersjach JetBrains Ridera i Visual Studio masz nawet wbudowane "Paste Special → Paste JSON as Classes".
Jedną rzecz warto pamiętać: System.Text.Json domyślnie jest case-sensitive, a JSON z API zazwyczaj ma camelCase. Jeżeli twoje właściwości C# są w PascalCase (a tak powinny być), dodaj opcje:
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
Albo — lepiej — skorzystaj z [JsonPropertyName("main")] na konkretnych właściwościach, jeśli chcesz pełną kontrolę nad mapowaniem.
async/await, czyli dlaczego blokowanie wątku to zły pomysł
W odcinku metoda serwisu była async Task<WeatherResponse>, a w kontrolerze robiłem na niej await. To jest absolutna podstawa pracy z I/O w ASP.NET Core i warto przypomnieć dlaczego.
Każde zapytanie HTTP do twojej aplikacji obsługuje wątek z puli. Wątków nie masz w nieskończoność — typowa pula to kilkaset. Jeżeli jeden request idzie do zewnętrznego API i trwa 500 ms, to synchronicznie ten wątek stoi i nic nie robi. Z async/await wątek wraca do puli, obsługuje inne requesty, a po powrocie odpowiedzi z API aplikacja kontynuuje kod.
Synchroniczne wywołania I/O w ASP.NET Core to prosta droga do thread pool starvation i 502 Bad Gateway pod obciążeniem.
Dlatego:
- Metody serwisowe zawsze
async Tasklubasync Task<T>. - W kontrolerach:
public async Task<IActionResult> Index(). - Nigdy
.Resultani.Wait()naTask-u — to blokuje wątek i w najgorszym wypadku kończy się deadlockiem.
Obsługa błędów i timeoutów
Na filmie trzymaliśmy się happy-path, bo 10 minut to niewiele, ale w realnym kodzie zawsze pytaj siebie "co zrobię jak API padnie?".
Minimum, które warto dopisać do naszego WeatherService:
public async Task<Forecast?> GetForecastAsync(string city)
{
try
{
return await _http.GetFromJsonAsync<Forecast>($"forecast?q={city}");
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch forecast for {City}", city);
return null;
}
catch (TaskCanceledException)
{
_logger.LogWarning("Forecast fetch timed out for {City}", city);
return null;
}
}
HttpRequestException— leci przy błędach sieci i statusach innych niż 2xx.TaskCanceledException— leci przy przekroczeniu timeoutu (domyślnie 100 sekund, ale możesz to zmienić wAddHttpClient).
Jak chcesz iść dalej, rzuć okiem na bibliotekę Polly i integrację Microsoft.Extensions.Http.Polly. Dostajesz retry, circuit breaker, timeout i bulkhead w paru linijkach konfiguracji — wystarczy dopiąć do rejestracji typed clienta:
services.AddHttpClient<IWeatherService, WeatherService>(client =>
{
client.BaseAddress = new Uri("https://api.openweathermap.org/data/2.5/");
client.Timeout = TimeSpan.FromSeconds(10);
})
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(500)));
Trzy automatyczne retry co 500 ms na wszystkie błędy tranzytowe — i już twoja aplikacja jest dużo odporniejsza na chwilowe czkawki cudzego API.
Co dalej?
Podsumowując to, co pokazałem w lekcji — przez pryzmat tego, jak byś to napisał dzisiaj:
- Rejestrujesz typed clienta przez
AddHttpClient<IWeatherService, WeatherService>. - W serwisie wstrzykujesz
HttpClientprzez konstruktor. - Używasz
GetFromJsonAsync<T>zSystem.Text.Json, zamiast ręcznie deserializowaćWebClient.DownloadString. - Wszystko jest
async, logujesz błędy, rozważasz Polly do retry.
Kod z odcinka w oryginalnej wersji znajdziesz na GitHubie: github.com/jarmatys/asp-net-core-podstawy/tree/Lekcja-9. Potraktuj go jako punkt wyjścia — jak będziesz chciał potrenować, spróbuj sam przerobić WebClient na IHttpClientFactory i zobacz, jak bardzo kod robi się czytelniejszy.
W kolejnej lekcji pokażę ci, jak naszą aplikację wrzucić na hosting — omówimy opcje i zrobimy deployment na jednej z nich. Dzięki za uwagę i do zobaczenia w następnym odcinku.



