Rozpoczynając pracę z ASP.NET Core, napotykamy na szereg narzędzi oraz funkcjonalności, które upraszczają tworzenie nowoczesnych aplikacji webowych. Wśród tych nowości, które pojawiły się w wersji ASP.NET Core 8, szczególne miejsce zajmują tzw. minimalne API. Czym są i dlaczego warto je wykorzystać? Minimalne API pozwala na stworzenie bardzo lekkiej, lecz funkcjonalnej aplikacji webowej z użyciem zaledwie kilku linii kodu, bez konieczności implementowania pełnej struktury z kontrolerami. W tej części przyjrzymy się, jak wykorzystać minimalne API oraz czym różni się ono od tradycyjnych podejść.
W przypadku minimalnych API, cała aplikacja opiera się na jednym pliku – Program.cs, który pełni rolę zarówno konfiguratora, jak i miejsca do implementacji logiki API. Ten sposób działania jest wyjątkowy, ponieważ pozwala na pełne ograniczenie rozmiaru aplikacji i ułatwia jej zrozumienie oraz utrzymanie. Konfiguracja aplikacji minimalizuje się do niezbędnego minimum, a dzięki temu, że ASP.NET Core nadal korzysta z pełnego pipeline’u, możemy korzystać ze wszystkich zalet systemu wstrzykiwania zależności czy middleware’ów, które są obecne także w bardziej rozbudowanych projektach.
Pierwszym krokiem do stworzenia minimalnego API jest utworzenie projektu typu „ASP.NET Core Empty”. Po jego stworzeniu, domyślnie dostępny będzie endpoint „Hello World!”, który można rozbudować o dodatkowe funkcje. Kolejnym etapem jest dodanie konfiguracji Swaggera, który automatycznie wygeneruje dokumentację API. Warto dodać, że w wersji 8, C# umożliwia dekorowanie wyrażeń lambda atrybutami, takimi jak [FromRoute], co pozwala na jeszcze łatwiejsze mapowanie parametrów w routingu.
Dzięki minimalnym API możliwe jest szybkie prototypowanie, testowanie oraz rozwój aplikacji. Często, szczególnie w przypadku prostych projektów lub testów, taki sposób tworzenia API jest znacznie bardziej efektywny niż używanie pełnej struktury z kontrolerami i innymi komponentami. Tego typu podejście również sprzyja łatwiejszemu utrzymaniu kodu, ponieważ każdy endpoint jest prosty, zrozumiały i nie wymaga nadmiarowych zasobów.
Warto również wspomnieć o narzędziach, które pomagają w testowaniu i eksplorowaniu API. Oprócz Swaggera, który jest często używany w połączeniu z minimalnymi API, popularnym wyborem wśród programistów jest Postman. To narzędzie pozwala na wygodne generowanie zapytań HTTP, testowanie endpointów oraz konfigurację różnych parametrów zapytań, takich jak nagłówki, metody HTTP czy ciało zapytania. Dzięki niemu możemy łatwo sprawdzić, jak API reaguje na różne dane wejściowe, co pozwala na szybsze wykrywanie błędów i optymalizację aplikacji.
Należy także pamiętać, że chociaż minimalne API jest znakomitym narzędziem do szybkiego tworzenia aplikacji, nie zawsze będzie najlepszym rozwiązaniem w bardziej złożonych projektach. W większych aplikacjach, gdzie wymagane są zaawansowane mechanizmy autentykacji, autoryzacji czy skomplikowane logiki biznesowe, tradycyjny sposób korzystania z kontrolerów i pełnej struktury ASP.NET Core może okazać się bardziej odpowiedni. W takich przypadkach, minimalizm może prowadzić do trudności w utrzymaniu i rozwoju aplikacji.
Aby zrozumieć pełnię możliwości minimalnych API w ASP.NET Core 8, warto zagłębić się w kwestie zależności, które są wstrzykiwane do aplikacji. System wstrzykiwania zależności w minimalnym API jest równie potężny jak w pełnej wersji aplikacji, co umożliwia łatwą konfigurację usług i ich wykorzystanie w całym projekcie. Należy również zwrócić uwagę na możliwość dynamicznego generowania dokumentacji API, co w przypadku bardziej rozbudowanych aplikacji jest nieocenione, szczególnie przy współpracy z zespołami.
Warto również pamiętać, że minimalne API to nie tylko łatwość w implementacji, ale także pewna odpowiedzialność w zakresie zarządzania kodem. Aby utrzymać aplikację w odpowiedniej jakości, należy przestrzegać dobrych praktyk programistycznych, takich jak stosowanie czystego kodu, strukturalne podejście do organizacji kodu, oraz zapewnienie, że aplikacja będzie łatwa w utrzymaniu, nawet jeśli początkowo nie wymaga dużych zasobów.
Na koniec, nie zapominajmy o bezpieczeństwie aplikacji. Choć minimalne API wydaje się być narzędziem, które ułatwia życie programiście, to nie zwalnia nas z odpowiedzialności za ochronę aplikacji przed zagrożeniami, takimi jak ataki typu SQL Injection, XSS czy CSRF. Zastosowanie odpowiednich technik zabezpieczeń oraz przestrzeganie zasad OWASP (Open Worldwide Application Security Project) powinno być podstawą przy tworzeniu każdej aplikacji, niezależnie od jej rozmiaru czy złożoności.
Jak bezpiecznie i efektywnie uzyskiwać dane zdalne przy użyciu Refit i Polly
W kontekście dostępu do danych zdalnych, ważnym aspektem jest nie tylko efektywność, ale również niezawodność operacji. Korzystanie z zewnętrznych API wymaga nie tylko poprawnej obsługi żądań, ale także umiejętności radzenia sobie z sytuacjami, które mogą prowadzić do awarii, np. błędami sieciowymi. W tym celu można wykorzystać bibliotekę Polly, która wspomaga zarządzanie niezawodnością, implementując wzorce Retry (ponowne próby) oraz Circuit-Breaker (wyłącznik obwodu). Dzięki nim, aplikacja może skutecznie obsługiwać sytuacje, w których dostęp do zdalnych zasobów jest niestabilny lub zawodzi.
Refit, będący frameworkiem do łatwego tworzenia klientów API, nie tylko umożliwia deklarowanie kontraktów dla wywołań HTTP, ale także pozwala na zastosowanie wzorców takich jak Retry bez zbędnego komplikowania logiki repozytoriów. Przy użyciu Refit, możemy odseparować logikę dostępu do danych od samego zarządzania niezawodnością, co poprawia strukturę kodu oraz ułatwia jego utrzymanie.
Aby skutecznie zaimplementować Retry i Circuit-Breaker, najpierw musimy dodać odpowiednią konfigurację w naszym projekcie. W pierwszej kolejności, musimy zainstalować pakiet Microsoft.Extensions.Http.Polly, który umożliwi nam zarządzanie politykami retry i circuit-breaker. Następnie należy utworzyć odpowiednią klasę, która będzie zawierała logikę tych wzorców. Warto zauważyć, że obie polityki – retry i circuit-breaker – mogą być połączone, dzięki czemu otrzymujemy bardziej zaawansowaną kontrolę nad procesem obsługi błędów.
W klasie RetryPolicy możemy skonfigurować retry na 3 próby, z opóźnieniem między nimi wynoszącym 3 sekundy. Dodatkowo, po 4 nieudanych próbach, uruchamiamy Circuit-Breaker, który zablokuje dalsze żądania na określony czas – w tym przypadku 15 sekund. Wartością dodaną w tym podejściu jest to, że możemy oddzielić logikę Retry od głównej logiki repozytorium, dzięki czemu nasza aplikacja będzie bardziej modularna i łatwiejsza do rozbudowy.
Przykład implementacji klasy RetryPolicy jest następujący:
Aby zastosować te polityki w naszym kliencie Refit, wystarczy dodać metodę AddFaultHandlingPolicy() do konfiguracji klienta HTTP w pliku Program.cs, jak pokazano w poniższym przykładzie:
Dzięki temu, każde żądanie do zdalnego zasobu będzie podlegało politykom retry i circuit-breaker, co zapewnia lepszą niezawodność aplikacji, nawet w przypadku awarii sieciowych.
Jednak implementacja tych wzorców to tylko część układanki. Ważne jest, aby pamiętać, że wzorce Retry i Circuit-Breaker powinny być stosowane tam, gdzie naprawdę mogą przynieść korzyści. Nie każda aplikacja wymaga takich mechanizmów – ich użycie ma sens w przypadkach, gdy musimy zapewnić niezawodność połączeń z zewnętrznymi API, które mogą doświadczać tymczasowych problemów, takich jak awarie serwerów lub przeciążenia sieciowe.
Ponadto, korzystając z Polly, warto rozważyć dostosowanie polityk retry i circuit-breaker do specyficznych potrzeb aplikacji. Na przykład, w przypadku aplikacji o dużym natężeniu ruchu, może być konieczne dostosowanie liczby prób czy czasu oczekiwania na odpowiedź, aby zminimalizować czas oczekiwania na odpowiedź, zachowując jednocześnie odpowiednią niezawodność.
Kiedy implementujemy takie wzorce, pamiętajmy o tym, aby nie wprowadzać niepotrzebnej złożoności w prostych scenariuszach. Zastosowanie Retry i Circuit-Breaker jest zbędne w przypadku, gdy API z którym się łączymy jest stabilne i nie występują błędy sieciowe. W takich przypadkach dodawanie tych wzorców może jedynie zwiększyć złożoność kodu bez realnych korzyści.
Warto również pamiętać, że implementując Retry i Circuit-Breaker, powinniśmy mieć świadomość, że każda próba ponowienia żądania wiąże się z dodatkowymi opóźnieniami, które w kontekście dużej liczby użytkowników mogą wpływać na ogólną wydajność systemu. Dlatego dobór odpowiednich parametrów retry, takich jak liczba prób czy opóźnienie między próbami, powinien być dostosowany do charakterystyki aplikacji oraz specyficznych wymagań biznesowych.
Jak asynchroniczne programowanie wpływa na optymalizację API?
Asynchroniczne programowanie jest jednym z kluczowych narzędzi w budowaniu wydajnych i skalowalnych aplikacji internetowych, szczególnie w kontekście API, które muszą obsługiwać różnorodne, czasochłonne operacje. Dzięki zastosowaniu słów kluczowych async i await, procesy wykonują się równolegle, bez blokowania głównego wątku, co pozwala na bardziej responsywne aplikacje. Te dwa słowa kluczowe są ze sobą nierozerwalnie związane i nie mogą być używane oddzielnie. Stosowanie async bez await lub odwrotnie skutkuje synchronizacją, a kompilator zazwyczaj zgłosi błąd, informując o tej niezgodności.
Zrozumienie asynchroniczności staje się kluczowe w kontekście dostępu do zewnętrznych źródeł danych, takich jak bazy danych czy zapytania HTTP. Każde takie połączenie, zwłaszcza w środowisku API, gdzie odpowiedzi mogą być opóźnione, powinno być realizowane za pomocą asynchronicznych metod. Przykład zapytania do bazy danych przy użyciu Entity Framework Core, jak w metodzie GetAllAsync, jest świetnym przykładem, jak asynchroniczność pozwala na odciążenie wątku i umożliwia dalsze operacje w aplikacji.
Warto zauważyć, że w przypadku operacji na bazach danych w Entity Framework Core istnieją asynchroniczne wersje wielu metod, takich jak ToListAsync(), ExecuteUpdateAsync(), ExecuteDeleteAsync(), czy SaveChangesAsync(). Synchronous counterparts, takie jak ToList(), mogą być używane, ale prowadzą do blokowania wątku, co znacząco wpływa na wydajność aplikacji. Dlatego zawsze warto wybierać metody asynchroniczne, szczególnie gdy mamy do czynienia z operacjami, których czas wykonania jest trudny do przewidzenia.
Programowanie asynchroniczne ma szczególne zastosowanie w przypadkach, gdy korzystamy z zewnętrznych zasobów, na przykład API czy baz danych. Te operacje mogą być czasochłonne, a stosowanie async i await sprawia, że aplikacja jest bardziej responsywna, a użytkownicy nie doświadczają opóźnień, co jest niezwykle istotne w dzisiejszych czasach, kiedy oczekiwania względem czasu reakcji aplikacji są bardzo wysokie.
Czasami jednak zdarza się, że w aplikacjach korzystamy z starszych bibliotek, które nie wspierają asynchroniczności. W takim przypadku, gdy mamy do czynienia z blokującym kodem, warto rozważyć użycie Task.Run, który pozwala na uruchomienie synchronizowanego kodu w tle w sposób asynchroniczny. Na przykład: var result = await Task.Run(() => DoSomething());. Dzięki temu unikniemy blokowania wątku głównego, a operacja będzie mogła przebiegać w tle.
Z drugiej strony, asynchroniczne operacje wiążą się również z koniecznością zarządzania anulowaniem zadań, co jest szczególnie istotne w aplikacjach, które wykonują długotrwałe zapytania. Użytkownicy mogą przerwać operację, zamykając aplikację lub przerywając zapytanie HTTP w przeglądarce, co może prowadzić do niechcianego kontynuowania procesu w tle, np. w bazie danych. Aby temu zapobiec, należy implementować możliwość anulowania operacji, na przykład przy użyciu CancellationToken. Dzięki temu, jeżeli użytkownik zrezygnuje z działania, zapytanie HTTP lub SQL również zostanie przerwane.
W przykładzie z punktu końcowego, kiedy używamy CancellationToken w metodzie LongRunningQueryAsync, ASP.NET Core automatycznie przekaże ten token do wszystkich warstw aplikacji, w tym do zapytań do bazy danych. Jeśli użytkownik anuluje zapytanie przed zakończeniem operacji, SQL również zgłosi wyjątek, przerywając dalsze przetwarzanie. Takie podejście zapewnia, że aplikacja nie będzie wykonywać niepotrzebnych operacji po zakończeniu interakcji z użytkownikiem, co jest niezbędne dla optymalizacji zasobów.
Anulowanie działa w praktyce na każdej asynchronicznej operacji, niezależnie od tego, czy jest to zapytanie do bazy danych, operacja HTTP, czy operacja wykonywana przez aplikację za pomocą biblioteki takiej jak Refit, która pozwala na łatwą obsługę zapytań HTTP z obsługą CancellationToken. W przykładzie z MediaRepository widać, jak łatwo jest obsługiwać żądania HTTP, a anulowanie takich operacji może być równie proste, co w przypadku zapytań do bazy danych.
Asynchroniczność daje także możliwość wydajnego zarządzania długotrwałymi zadaniami w tle. Dzięki zastosowaniu usług w tle, takich jak BackgroundService w ASP.NET Core, możliwe jest wykonywanie zadań, które nie muszą być realizowane w ramach bieżącego żądania, co pozwala na jeszcze lepszą optymalizację aplikacji. Dzięki takim mechanizmom można oddzielić operacje wymagające dużych zasobów od głównych interakcji z użytkownikiem, co dodatkowo poprawia responsywność systemu.
Asynchroniczność to nie tylko sposób na unikanie blokowania wątków, ale także podejście do zarządzania zasobami w sposób bardziej zrównoważony, z możliwością ich anulowania i kontrolowania. Oczywiście jest to temat znacznie szerszy, który obejmuje wiele zaawansowanych technik, w tym zarządzanie zadaniami w tle czy bardziej skomplikowane mechanizmy synchronizacji, jednak podstawowe zrozumienie zasad działania async i await, a także prawidłowego zarządzania anulowaniem zadań, stanowi fundament dla efektywnego tworzenia nowoczesnych API.
Jak optymalizować zadania w tle w aplikacji webowej?
W aplikacjach webowych oparte na platformie ASP.NET Core, często zachodzi potrzeba wykonywania zadań w tle. Są to procesy, które powinny działać niezależnie od interakcji użytkownika z aplikacją, na przykład pobieranie plików, przetwarzanie danych lub synchronizowanie z innymi systemami. W tym kontekście, z pomocą przychodzi interfejs IHostedService dostępny w przestrzeni nazw Microsoft.Extensions.Hosting, który umożliwia łatwe zarządzanie takimi zadaniami w tle.
Warto zauważyć, że interfejs IHostedService nie jest wywoływany bezpośrednio. Aby go wykorzystać, należy zaimplementować klasę pochodną BackgroundService. Klasa ta oferuje nam trzy metody, które mogą być nadpisane:
-
StartAsync
-
StopAsync
-
ExecuteAsync
Metody StartAsync i StopAsync są wywoływane automatycznie podczas uruchamiania i zatrzymywania aplikacji. Z kolei metoda ExecuteAsync jest kluczowa, ponieważ to w niej wykonujemy zadania w tle. Możemy w niej uruchamiać operacje, które muszą działać w sposób ciągły lub w zadanych odstępach czasu.
Przykład implementacji zadania w tle
Aby zilustrować, jak działa taka implementacja, rozważmy przykład klasy CountryFileIntegrationBackgroundService, której zadaniem będzie zarządzanie procesem pobierania plików z serwera. Przykład ten pokazuje, jak można zaimplementować zadanie w tle, które nie przerywa swojej pracy po zamknięciu przeglądarki, ale zatrzymuje się wraz z zatrzymaniem aplikacji ASP.NET Core.
Powyższy kod jest tylko szkieletem, który nie wykonuje żadnych operacji. Jednakże, jeżeli użytkownik zażąda anulowania zadania (np. podczas zamknięcia aplikacji), metoda ExecuteAsync zostanie przerwana.
Wykorzystanie IServiceProvider w zadaniach w tle
Ponieważ zadanie działa jako singleton w aplikacji, podczas gdy w przypadku żądań HTTP wymagane są nowe instancje dla każdego serwisu, należy pamiętać o konieczności utworzenia specjalnego zakresu usług dla zadań w tle. Aby uzyskać dostęp do instancji serwisów, używamy interfejsu IServiceProvider, który pozwala na dynamiczne tworzenie zakresów i dostęp do usług o długim cyklu życia, takich jak serwisy Scoped.
Warto zauważyć, że wykorzystanie IServiceProvider w zadaniach w tle wymaga stworzenia odpowiedniego zakresu, co jest realizowane za pomocą metody CreateScope().
W tym przypadku, każdorazowo tworzymy nowy zakres (scope), który pozwala na uzyskanie instancji serwisu ICountryService, odpowiedzialnego za przetwarzanie plików.
Komunikacja między zadaniem w tle a główną aplikacją
Zadania w tle działają w tej samej aplikacji i tym samym procesie co API, dlatego możliwa jest komunikacja między nimi. Aby umożliwić wymianę danych, stosujemy mechanizm Channels, który jest częścią przestrzeni nazw System.Threading.Channels. Channels pozwalają na przesyłanie wiadomości pomiędzy różnymi częściami aplikacji w sposób asynchroniczny i bezpieczny.
W naszej aplikacji, możemy stworzyć Channel, który będzie przekazywał dane w postaci strumienia, który następnie zostanie przetworzony w zadaniu w tle. Poniżej znajduje się przykład interfejsu ICountryFileIntegrationChannel, który opisuje dwie główne metody:
-
SubmitAsync — do przesyłania danych do kanału.
-
ReadAllAsync — do asynchronicznego odczytu wiadomości z kanału.
Warto zaznaczyć, że mechanizm Channel jest szczególnie przydatny, gdy chcemy zapewnić komunikację między różnymi komponentami aplikacji, które działają asynchronicznie, np. przesyłanie dużych plików do przetworzenia w tle.
Przetwarzanie wiadomości w tle
Korzystając z mechanizmu Channel, możemy łatwo przetwarzać wiadomości w tle. W przykładzie poniżej, klasa CountryFileIntegrationChannel implementuje interfejs ICountryFileIntegrationChannel, tworząc kanał, który obsługuje nieograniczoną liczbę wiadomości.
Wartością dodaną w tym przykładzie jest możliwość konfigurowania kanału jako Unbounded, co oznacza, że nie ma limitu na liczbę wiadomości w kolejce. Dzięki temu możemy obsługiwać dane w sposób bardziej elastyczny, co jest istotne w przypadku przetwarzania dużych plików lub obsługi długotrwałych zadań.
Zrozumienie całokształtu rozwiązania
Kluczowym aspektem, który należy zrozumieć, jest to, że procesy w tle i aplikacja API są ze sobą ściśle powiązane. Obie działają w tym samym procesie, dzieląc tę samą konfigurację i dane aplikacji. Możliwość komunikacji między nimi za pomocą Channels pozwala na płynne i efektywne zarządzanie zadaniami w tle bez zakłócania działania samego API.

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский