Udostępnij za pośrednictwem


Tworzenie aplikacji mvC platformy ASP.NET Core

Wskazówka

Ta treść jest fragmentem eBooka, Architektura nowoczesnych aplikacji internetowych z ASP.NET Core i Azure, dostępny na .NET Docs lub jako bezpłatny plik PDF do pobrania, który można czytać w trybie offline.

Miniatura okładki eBooka

Nie jest ważne, aby zrobić to dobrze za pierwszym razem. Niezwykle ważne jest, aby zrobić to dobrze za ostatnim razem. - Andrew Hunt i David Thomas

ASP.NET Core to międzyplatformowa platforma typu open source do tworzenia nowoczesnych aplikacji internetowych zoptymalizowanych pod kątem chmury. aplikacje ASP.NET Core są lekkie i modułowe, z wbudowaną obsługą wstrzykiwania zależności, co zapewnia większą możliwość testowania i konserwację. W połączeniu z MVC, który obsługuje tworzenie nowoczesnych webowych API oprócz aplikacji opartych na widokach, ASP.NET Core to potężny framework do budowy aplikacji internetowych dla przedsiębiorstw.

MVC i Razor Pages

ASP.NET Core MVC oferuje wiele funkcji, które są przydatne do tworzenia internetowych interfejsów API i aplikacji. Termin MVC oznacza "Model-View-Controller", wzorzec interfejsu użytkownika, który dzieli obowiązki reagowania na żądania użytkowników w kilku częściach. Oprócz tego wzorca można również zaimplementować funkcje w aplikacjach ASP.NET Core jako strony typu Razor.

Strony Razor są wbudowane w ASP.NET Core MVC i używają tych samych funkcji do routingu, powiązania modelu, filtrów, autoryzacji itp. Jednak zamiast oddzielnych folderów i plików dla kontrolerów, modeli, widoków itp. oraz routingu opartego na atrybutach, strony Razor są umieszczane w jednym folderze ("/Pages"), są trasowane na podstawie ich względnej lokalizacji w tym folderze i obsługują żądania za pomocą procedur obsługi, zamiast akcji kontrolera. W związku z tym podczas pracy ze stronami Razor wszystkie potrzebne pliki i klasy zwykle znajdują się w jednym miejscu, a nie są rozłożone w całym projekcie internetowym.

Dowiedz się więcej na temat sposobu stosowania wzorca MVC, Razor Pages i powiązanych wzorców w przykładowej aplikacji eShopOnWeb.

Podczas tworzenia nowej aplikacji ASP.NET Core należy mieć na uwadze plan dotyczący rodzaju aplikacji, którą chcesz skompilować. Podczas tworzenia nowego projektu, czy to w środowisku IDE, czy przy użyciu polecenia dotnet new CLI, będziesz wybierać spośród kilku szablonów. Najbardziej typowe szablony projektów to Empty, Web API, Web App i Web App (Model-View-Controller). Chociaż można podjąć tę decyzję tylko podczas pierwszego tworzenia projektu, nie jest to nieodwołalna decyzja. Projekt Web API używa standardowych kontrolerów modeluView-Controller — domyślnie po prostu nie posiada widoków. Podobnie domyślny szablon aplikacji internetowej używa stron Razor, a więc nie ma również folderu Views. Możesz dodać folder Widoki do tych projektów później, aby obsługiwać zachowanie oparte na widoku. Interfejs API i projekty modeluView-Controller nie zawierają domyślnie folderu "Pages", ale można go dodać później, aby wspierać funkcje oparte na stronach Razor. Te trzy szablony można traktować jako obsługę trzech różnych rodzajów domyślnej interakcji użytkownika: danych (internetowego interfejsu API), opartych na stronach i opartych na widoku. Można jednak mieszać i dopasowywać dowolne lub wszystkie te szablony w jednym projekcie, jeśli chcesz.

Dlaczego Razor Pages?

Platforma Razor Pages to domyślne podejście do nowych aplikacji internetowych w programie Visual Studio. Platforma Razor Pages oferuje prostszy sposób tworzenia funkcji aplikacji opartych na stronach, takich jak formularze inne niż SPA. Używając kontrolerów i widoków, często zdarzało się, że aplikacje miały bardzo duże kontrolery, które współpracowały z wieloma różnymi zależnościami i modelami widoków oraz zwracały wiele różnych widoków. Spowodowało to większą złożoność i często powodowało, że kontrolery nie przestrzegały zasady o pojedynczej odpowiedzialności ani zasad otwartych/zamkniętych. Platforma Razor Pages rozwiązuje ten problem, hermetyzując logikę po stronie serwera dla danej logicznej "strony" w aplikacji internetowej ze znacznikiem Razor. Strona Razor, która nie ma logiki po stronie serwera, może składać się tylko z pliku Razor (na przykład "Index.cshtml"). Jednak większość bardziej złożonych stron Razor będzie miała związaną klasę modelu strony, która zgodnie z konwencją nosi taką samą nazwę jak plik Razor z rozszerzeniem ".cs" (na przykład "Index.cshtml.cs").

Model strony Razor łączy funkcje kontrolera MVC i modelu widoku. Zamiast obsługiwać żądania za pomocą metod akcji kontrolera, wykonuje się procedury obsługi modelu strony, takie jak "OnGet()", które domyślnie renderują skojarzoną stronę. Platforma Razor Pages upraszcza proces tworzenia poszczególnych stron w aplikacji platformy ASP.NET Core, a jednocześnie zapewnia wszystkie funkcje architektury ASP.NET Core MVC. Są one dobrym wyborem domyślnym dla nowych funkcji opartych na stronach.

Kiedy należy używać wzorca MVC

Jeśli tworzysz webowe interfejsy API, wzorzec MVC ma większy sens niż próba użycia stron Razor. Jeśli projekt będzie uwidaczniać tylko punkty końcowe internetowego interfejsu API, najlepiej zacząć od szablonu projektu internetowego interfejsu API. W przeciwnym razie łatwo jest dodać kontrolery i skojarzone punkty końcowe interfejsu API do dowolnej aplikacji ASP.NET Core. Użyj podejścia MVC opartego na widoku, jeśli migrujesz istniejącą aplikację z ASP.NET MVC 5 lub starszej do ASP.NET Core MVC i chcesz to zrobić z najmniejszą ilością wysiłku. Po zakończeniu migracji początkowej możesz rozważyć wdrożenie stron Razor dla nowych funkcji, a nawet jako kompletną migrację. Aby uzyskać więcej informacji na temat przenoszenia aplikacji .NET 4.x do platformy .NET 8, zobacz Przenoszenie istniejących aplikacji ASP.NET do ASP.NET Core eBook.

Niezależnie od tego, czy chcesz utworzyć aplikację internetową przy użyciu widoków Razor Pages czy MVC, aplikacja będzie miała podobną wydajność i będzie obsługiwać wstrzykiwanie zależności, filtry, powiązanie modelu, walidację itd.

Przypisywanie żądań do odpowiedzi

Zasadniczo aplikacje ASP.NET Core mapują przychodzące żądania na odpowiedzi wychodzące. Na niskim poziomie to mapowanie odbywa się za pomocą oprogramowania pośredniczącego, a proste aplikacje i mikrousługi podstawowe ASP.NET mogą składać się wyłącznie z niestandardowego oprogramowania pośredniczącego. W przypadku korzystania z ASP.NET Core MVC można pracować na nieco wyższym poziomie, myśląc pod względem tras, kontrolerów i akcji. Każde żądanie przychodzące jest porównywane z tabelą routingu aplikacji, a jeśli zostanie znaleziona zgodna trasa, skojarzona metoda akcji (należąca do kontrolera) jest wywoływana w celu obsługi żądania. Jeśli nie znaleziono pasującej trasy, wywoływana jest procedura obsługi błędów (w tym przypadku zwracanie wyniku NotFound).

ASP.NET aplikacje CORE MVC mogą używać konwencjonalnych tras, tras atrybutów lub obu tych metod. Konwencjonalne trasy są definiowane w kodzie, określając konwencje routingu przy użyciu składni, takiej jak w poniższym przykładzie:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

W tym przykładzie do tabeli routingu została dodana trasa o nazwie "default". Definiuje szablon trasy z symbolami zastępczymi dla controller, actioni id. controller i action mają ustawione domyślne wartości (Home i Index, odpowiednio), a id jest opcjonalny (ze względu na "?" zastosowany do niego). Konwencja zdefiniowana tutaj określa, że pierwsza część żądania powinna odpowiadać nazwie kontrolera, drugiej części akcji, a następnie w razie potrzeby trzecia część będzie reprezentować parametr ID. Konwencjonalne trasy są zazwyczaj definiowane w jednym miejscu dla aplikacji, na przykład w Program.cs, gdzie konfiguruje się potok przetwarzania pośredniego żądań.

Trasy przypisane atrybutami są stosowane bezpośrednio do kontrolerów i akcji, zamiast być określane globalnie. Takie podejście ma zaletę, dzięki czemu można je znacznie bardziej odnajdywać podczas przeglądania konkretnej metody, ale oznacza to, że informacje o routingu nie są przechowywane w jednym miejscu w aplikacji. Za pomocą tras atrybutów można łatwo określić wiele tras dla danej akcji, a także połączyć trasy między kontrolerami i akcjami. Przykład:

[Route("Home")]
public class HomeController : Controller
{
    [Route("")] // Combines to define the route template "Home"
    [Route("Index")] // Combines to define route template "Home/Index"
    [Route("/")] // Does not combine, defines the route template ""
    public IActionResult Index() {}
}

Trasy można określić na [HttpGet] i podobnych atrybutach, unikając konieczności dodawania oddzielnych atrybutów [Route]. Trasy atrybutów mogą również używać tokenów, aby zmniejszyć potrzebę powtarzania nazw kontrolerów lub akcji, jak pokazano poniżej:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")] // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index() {}
}

Strony Razor nie używają routingu opartego na atrybutach. Można określić dodatkowe informacje dotyczące szablonu trasy dla strony Razor jako część jej dyrektywy @page.

@page "{id:int}"

W poprzednim przykładzie strona, o którą mowa, pasuje do trasy z parametrem integer id . Na przykład strona Products.cshtml znajdująca się w katalogu głównym obiektu odpowiada na żądania podobne do następującego /Pages :

/Products/123

Po dopasowaniu danego żądania do trasy, ale przed wywołaniem metody akcji ASP.NET Core MVC wykona powiązanie modelu i walidację modelu na żądanie. Powiązanie modelu jest odpowiedzialne za konwertowanie przychodzących danych HTTP na typy platformy .NET określone jako parametry metody akcji do wywołania. Jeśli na przykład metoda akcji oczekuje parametru int id , powiązanie modelu spróbuje podać ten parametr z wartości podanej w ramach żądania. W tym celu powiązanie modelu wyszukuje wartości w formularzu opublikowanym, wartości w samej trasie i wartości ciągu zapytania. Zakładając, że zostanie znaleziona id wartość, zostanie ona przekonwertowana na liczbę całkowitą przed przekazaniem jej do metody akcji.

Po powiązaniu modelu, ale przed wywołaniem metody akcji następuje walidacja modelu. Walidacja modelu używa opcjonalnych atrybutów w typie modelu i może pomóc w zapewnieniu, że podany obiekt modelu jest zgodny z pewnymi wymaganiami dotyczącymi danych. Niektóre wartości mogą być określone zgodnie z wymaganiami lub ograniczone do określonej długości lub zakresu liczbowego itp. Jeśli określono atrybuty weryfikacji, ale model nie jest zgodny z ich wymaganiami, właściwość ModelState.IsValid będzie fałszem, a zestaw reguł sprawdzania poprawności zakończonych niepowodzeniem będzie dostępny do wysłania do klienta wysyłającego żądanie.

Jeśli używasz weryfikacji modelu, przed wykonaniem jakichkolwiek poleceń zmiany stanu należy zawsze sprawdzić, czy model jest prawidłowy, aby upewnić się, że aplikacja nie jest uszkodzona przez nieprawidłowe dane. Możesz użyć filtru , aby uniknąć konieczności dodawania kodu do tej walidacji w każdej akcji. Filtry ASP.NET Core MVC oferują sposób przechwytywania grup żądań, dzięki czemu można zastosować standardowe zasady i sprawy o szerszym zakresie. Filtry można stosować do poszczególnych akcji, całych kontrolerów lub globalnie dla aplikacji.

W przypadku internetowych interfejsów API ASP.NET Core MVC obsługuje negocjacje zawartości, umożliwiając żądaniom określenie sposobu formatowania odpowiedzi. Na podstawie nagłówków podanych w żądaniu akcje zwracające dane sformatują odpowiedź w formacie XML, JSON lub innym obsługiwanym formacie. Ta funkcja umożliwia korzystanie z tego samego interfejsu API przez wielu klientów z różnymi wymaganiami dotyczącymi formatu danych.

Projekty internetowego interfejsu API powinny rozważyć użycie atrybutu [ApiController] , który można zastosować do poszczególnych kontrolerów, do klasy kontrolera podstawowego lub do całego zestawu. Ten atrybut dodaje automatyczne sprawdzanie poprawności modelu, a każda akcja z nieprawidłowym modelem zwróci element BadRequest ze szczegółami błędów walidacji. Atrybut wymaga również, aby wszystkie akcje miały trasę atrybutu, zamiast używać konwencjonalnej trasy, i zwraca bardziej szczegółowe informacje ProblemDetails w odpowiedzi na błędy.

Utrzymywanie kontroli kontrolerów

W przypadku aplikacji opartych na stronach Razor Pages świetnie pomagają w zapobieganiu nadmiernemu rozrostowi kontrolerów. Każda strona ma własne pliki i klasy przeznaczone wyłącznie dla jej obsługujących elementów. Przed wprowadzeniem stron Razor wiele aplikacji zorientowanych na widok miało duże klasy kontrolerów odpowiedzialne za wiele różnych akcji i widoków. Z czasem klasy te stają się posiadające wiele obowiązków i zależności, co utrudnia ich utrzymanie. Jeśli okaże się, że kontrolery oparte na widoku są zbyt duże, rozważ refaktoryzowanie ich do korzystania ze stron Razor lub wprowadzenie wzorca takiego jak mediator.

Wzorzec projektowy mediatora służy do zmniejszenia sprzężenia między klasami przy jednoczesnym umożliwieniu komunikacji między nimi. W aplikacjach ASP.NET Core MVC ten wzorzec jest często używany do dzielenia kontrolerów na mniejsze elementy przy użyciu obsługiwaczy do realizacji funkcji metod akcji. Popularny pakiet NuGet MediatR jest często używany do tego celu. Zazwyczaj kontrolery obejmują wiele różnych metod akcji, z których każda może wymagać pewnych zależności. Zestaw wszystkich zależności wymaganych przez dowolną akcję musi zostać przekazany do konstruktora kontrolera. W przypadku korzystania z MediatR jedyną zależnością, z której zwykle korzysta kontroler, jest instancja mediatora. Następnie każde działanie używa wystąpienia mediatora do wysłania komunikatu, który jest przetwarzany przez program obsługi. Procedura obsługi jest specyficzna dla pojedynczej akcji i dlatego wymaga tylko zależności wymaganych przez daną akcję. Przykład kontrolera używającego usługi MediatR jest pokazany tutaj:

public class OrderController : Controller
{
    private readonly IMediator _mediator;

    public OrderController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<IActionResult> MyOrders()
    {
        var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
        return View(viewModel);
    }
    // other actions implemented similarly
}

W akcji MyOrders wywołanie komunikatu Send jest obsługiwane przez tę klasę GetMyOrders.

public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
    private readonly IOrderRepository _orderRepository;
    public GetMyOrdersHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

  public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
    {
        var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
        var orders = await _orderRepository.ListAsync(specification);
        return orders.Select(o => new OrderViewModel
            {
                OrderDate = o.OrderDate,
                OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
                  {
                    PictureUrl = oi.ItemOrdered.PictureUri,
                    ProductId = oi.ItemOrdered.CatalogItemId,
                    ProductName = oi.ItemOrdered.ProductName,
                    UnitPrice = oi.UnitPrice,
                    Units = oi.Units
                  }).ToList(),
                OrderNumber = o.Id,
                ShippingAddress = o.ShipToAddress,
                Total = o.Total()
        });
    }
}

Końcowym wynikiem tego podejścia jest to, że kontrolery stają się znacznie mniejsze i koncentrują się głównie na trasowaniu i wiązaniu modelu, podczas gdy poszczególne procedury obsługi są odpowiedzialne za określone zadania wymagane przez dany punkt końcowy. Takie podejście można również osiągnąć bez MediatR, używając pakietu NuGet ApiEndpoints, który próbuje zapewnić kontrolerom API te same korzyści, jakie Razor Pages oferuje kontrolerom opartym na widokach.

Referencje — mapowanie żądań do odpowiedzi

Praca z zależnościami

ASP.NET Core zapewnia wbudowaną obsługę techniki zwanej wstrzykiwaniem zależności, której używa także wewnętrznie. Wstrzykiwanie zależności to technika, która umożliwia luźne sprzężenie między różnymi częściami aplikacji. Luźniejsze sprzężenie jest pożądane, ponieważ ułatwia izolowanie części aplikacji, co pozwala na testowanie lub zastępowanie. Zmniejsza to również prawdopodobieństwo, że zmiana w jednej części aplikacji będzie miała nieoczekiwany wpływ w innym miejscu w aplikacji. Wstrzykiwanie zależności opiera się na zasadzie inwersji zależności i jest często kluczem do osiągnięcia zasady otwarte/zamknięte. Podczas oceniania sposobu działania aplikacji z jej zależnościami należy uważać na statyczny zapach kodu przylegającego i pamiętać aforyzm "nowy jest klejem".

Statyczne problemy występują, gdy klasy wywołują metody statyczne lub uzyskują dostęp do właściwości statycznych, które powodują skutki uboczne lub mają zależności wpływające na infrastrukturę. Jeśli na przykład masz metodę, która wywołuje metodę statyczną, która z kolei zapisuje w bazie danych, metoda jest ściśle połączona z bazą danych. Wszystkie elementy, które przerywają wywołanie bazy danych, spowoduje przerwanie metody. Testowanie tych metod jest powszechnie uznawane za trudne, ponieważ takie testy wymagają komercyjnych bibliotek do mockowania wywołań statycznych lub mogą być testowane tylko przy użyciu testowej bazy danych. Wywołania statyczne, które nie mają żadnej zależności od infrastruktury, zwłaszcza te, które są całkowicie bezstanowe, można bez problemu wywoływać i nie mają wpływu na sprzęganie ani możliwość testowania (poza związaniem kodu z samym wywołaniem statycznym).

Wielu deweloperów rozumie ryzyko wynikające ze statycznego powiązania i stanu globalnego, ale nadal ściśle wiąże swój kod z konkretnymi implementacjami poprzez bezpośrednią instancję. "Fraza 'Nowe to spoiwo' ma przypominać o tym sprzężeniu, a nie być ogólnym potępieniem użycia słowa kluczowego new." Podobnie jak w przypadku wywołań metod statycznych, nowe wystąpienia typów, które nie mają zależności zewnętrznych, zwykle nie są ściśle powiązane ze szczegółami implementacji ani utrudniają testowania. Jednak za każdym razem, gdy tworzy się instancję klasy, poświęć chwilę, aby zastanowić się, czy ma to sens, aby zaprogramować to konkretne wystąpienie w tym konkretnym miejscu, czy też lepszym rozwiązaniem byłoby zażądanie tego wystąpienia jako zależności.

Deklarowanie zależności

ASP.NET Core opiera się na tym, że metody i klasy deklarują swoje zależności oraz żądają ich jako argumentów. ASP.NET aplikacje są zwykle konfigurowane w Program.cs lub w Startup klasie.

Uwaga / Notatka

Całkowite konfigurowanie aplikacji w Program.cs jest domyślnym podejściem dla aplikacji .NET 6 (i nowszych) i Visual Studio 2022. Szablony projektów zostały zaktualizowane, aby ułatwić rozpoczęcie pracy z tym nowym podejściem. ASP.NET Core projekty nadal mogą używać Startup klasy, jeśli jest to konieczne.

Konfigurowanie usług w usłudze Program.cs

W przypadku bardzo prostych aplikacji można połączyć zależności bezpośrednio w pliku Program.cs przy użyciu elementu WebApplicationBuilder. Po dodaniu wszystkich potrzebnych usług konstruktor jest używany do tworzenia aplikacji.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

Konfigurowanie usług w Startup.cs

Sam Startup.cs jest skonfigurowany do obsługi wstrzykiwania zależności w kilku punktach. Jeśli używasz Startup klasy, możesz nadać mu konstruktor i może żądać zależności za jego pomocą, w następujący sposób:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
    }
}

Klasa jest interesująca Startup , ponieważ nie ma dla niej jawnych wymagań dotyczących typów. Nie dziedziczy ze specjalnej Startup klasy bazowej ani nie implementuje żadnego określonego interfejsu. Można nadać mu konstruktora, a nie, i można określić dowolną liczbę parametrów w konstruktorze. Po uruchomieniu hosta internetowego skonfigurowanego dla aplikacji wywoła klasę Startup (jeśli powiedziano jej, że będzie używana) i użyje iniekcji zależności, aby wypełnić wszelkie zależności wymagane przez Startup klasę. Oczywiście jeśli zażądasz parametrów, które nie są skonfigurowane w kontenerze usług używanym przez platformę ASP.NET Core, otrzymasz wyjątek, ale o ile będziesz trzymać się zależności, o których kontener wie, możesz zażądać dowolnych elementów.

Wstrzykiwanie zależności jest wbudowane w aplikacje ASP.NET Core od samego początku, w momencie tworzenia wystąpienia Startup. Nie zatrzymuje się tam dla klasy Startup. Zależności można również zażądać w metodzie Configure :

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{

}

Metoda ConfigureServices jest wyjątkiem od tego zachowania; musi przyjmować tylko jeden parametr typu IServiceCollection. To nie musi naprawdę obsługiwać wstrzykiwania zależności, ponieważ z jednej strony pełni rolę odpowiedzialnego za dodawanie obiektów do kontenera usług, a z drugiej ma dostęp do wszystkich aktualnie skonfigurowanych usług za pośrednictwem parametru IServiceCollection. Dlatego można pracować z zależnościami zdefiniowanymi w kolekcji usług ASP.NET Core w każdej części klasy Startup, żądając wymaganej usługi jako parametru lub pracując z IServiceCollection w ConfigureServices.

Uwaga / Notatka

Jeśli musisz upewnić się, że niektóre usługi są dostępne dla swojej klasy Startup, możesz je skonfigurować, używając IWebHostBuilder oraz jego metody ConfigureServices wewnątrz wywołania CreateDefaultBuilder.

Klasa Startup jest modelem strukturyzowania innych części aplikacji ASP.NET Core, takich jak kontrolery, middleware, filtry czy własne usługi. W każdym przypadku należy postępować zgodnie z zasadą jawnych zależności, żądając zależności, a nie bezpośrednio tworząc je, i wykorzystując iniekcję zależności w całej aplikacji. Należy uważać, gdzie i jak bezpośrednio instancjujesz implementacje, zwłaszcza usług i obiektów, które pracują z infrastrukturą lub mają skutki uboczne. Preferuj pracę z abstrakcjami zdefiniowanymi w rdzeniu aplikacji i przekazywanymi jako argumenty, zamiast bezpośrednich odniesień do konkretnych typów implementacji.

Tworzenie struktury aplikacji

Aplikacje monolityczne zwykle mają pojedynczy punkt wejścia. W przypadku aplikacji internetowej ASP.NET Core punkt wejścia będzie projektem internetowym ASP.NET Core. Nie oznacza to jednak, że rozwiązanie powinno składać się tylko z jednego projektu. Warto podzielić aplikację na różne warstwy, aby postępować zgodnie z separacją problemów. Po podzieleniu na warstwy warto przejść poza foldery do oddzielnych projektów, co może pomóc osiągnąć lepszą hermetyzację. Najlepszym podejściem do osiągnięcia tych celów za pomocą aplikacji ASP.NET Core jest odmiana czystej architektury omówionej w rozdziale 5. Zgodnie z tym podejściem rozwiązanie aplikacji będzie składać się z oddzielnych bibliotek interfejsu użytkownika, infrastruktury i aplikacjiCore.

Oprócz tych projektów uwzględniane są również oddzielne projekty testowe (testowanie zostało omówione w rozdziale 9).

Model obiektów i interfejsy aplikacji powinny zostać umieszczone w projekcie ApplicationCore. Ten projekt będzie miał jak najmniej zależności (i nie będzie dotyczyć określonych kwestii dotyczących infrastruktury), a inne projekty w rozwiązaniu będą się do niego odwoływać. Jednostki biznesowe, które muszą być utrwalone, są definiowane w projekcie ApplicationCore, podobnie jak usługi, które nie zależą bezpośrednio od infrastruktury.

Szczegóły implementacji, takie jak trwałość lub sposób wysyłania powiadomień do użytkownika, są przechowywane w projekcie Infrastruktura. Ten projekt będzie odwoływać się do pakietów specyficznych dla implementacji, takich jak Entity Framework Core, ale nie powinien ujawniać szczegółów dotyczących tych implementacji poza projektem. Usługi infrastruktury i repozytoria powinny implementować interfejsy zdefiniowane w projekcie ApplicationCore, a implementacje jego trwałości są odpowiedzialne za pobieranie i przechowywanie jednostek zdefiniowanych w projekcie ApplicationCore.

Projekt ASP.NET Core UI jest odpowiedzialny za wszelkie problemy dotyczące poziomu interfejsu użytkownika, ale nie powinien zawierać szczegółów logiki biznesowej ani infrastruktury. W rzeczywistości, najlepiej, aby nawet nie mieć zależności od projektu Infrastruktura, co pomoże zapewnić, że żadna zależność między dwoma projektami nie zostanie wprowadzona przypadkowo. Można to osiągnąć przy użyciu kontenera di innej firmy, takiego jak Autofac, który umożliwia definiowanie reguł di w klasach modułów w każdym projekcie.

Innym podejściem do oddzielenia aplikacji od szczegółów implementacji jest posiadanie mikrousług wywoływania aplikacji, być może wdrożonych w poszczególnych kontenerach platformy Docker. Zapewnia to jeszcze większe rozdzielenie problemów i oddzielenie niż przy użyciu DI między dwoma projektami, ale wiąże się z dodatkową złożonością.

Organizacja funkcjonalności

Domyślnie aplikacje ASP.NET Core organizują strukturę folderów w celu uwzględnienia kontrolerów i widoków oraz często modelu ViewModels. Kod po stronie klienta do obsługi tych struktur po stronie serwera jest zwykle przechowywany oddzielnie w folderze wwwroot. Jednak duże aplikacje mogą napotkać problemy z tą organizacją, ponieważ praca nad daną funkcją często wymaga skoku między tymi folderami. Staje się to coraz trudniejsze w miarę wzrostu liczby plików i podfolderów w każdym folderze, co znacznie utrudnia przewijanie Eksploratora rozwiązań. Jednym z rozwiązań tego problemu jest organizowanie kodu aplikacji według funkcji zamiast według typu pliku. Ten styl organizacyjny jest zwykle określany jako foldery funkcji lub wycinki funkcji (zobacz również: Wycinek pionowy).

ASP.NET Core MVC obsługuje obszary w tym celu. Za pomocą obszarów można tworzyć oddzielne zestawy folderów 'Kontrolery' i 'Widoki' (a także wszystkie skojarzone modele) w każdym folderze obszaru. Rysunek 7–1 przedstawia przykładową strukturę folderów przy użyciu obszaru.

Przykładowa organizacja obszaru

Rysunek 7–1. Przykładowa organizacja obszaru

W przypadku korzystania z obszarów należy użyć atrybutów, aby ozdobić kontrolery nazwą obszaru, do którego należą:

[Area("Catalog")]
public class HomeController
{}

Musisz również dodać obsługę obszarów do swoich tras.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Oprócz wbudowanej obsługi obszarów można również użyć własnej struktury folderów i konwencji zamiast atrybutów i tras niestandardowych. Dzięki temu można mieć foldery funkcji, które nie zawierały oddzielnych folderów dla widoków, kontrolerów itp., zachowując pochlebność hierarchii i ułatwiając wyświetlanie wszystkich powiązanych plików w jednym miejscu dla każdej funkcji. W przypadku interfejsów API foldery mogą służyć do zastępowania kontrolerów, a każdy folder może zawierać wszystkie punkty końcowe interfejsu API i skojarzone z nimi obiekty DTO.

ASP.NET Core używa wbudowanych typów konwencji do kontrolowania jego zachowania. Te konwencje można modyfikować lub zastępować. Można na przykład utworzyć konwencję, która automatycznie pobierze nazwę funkcji dla danego kontrolera na podstawie jego przestrzeni nazw (która zazwyczaj jest skorelowana z folderem, w którym znajduje się kontroler):

public class FeatureConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        controller.Properties.Add("feature",
        GetFeatureName(controller.ControllerType));
    }

    private string GetFeatureName(TypeInfo controllerType)
    {
        string[] tokens = controllerType.FullName.Split('.');
        if (!tokens.Any(t => t == "Features")) return "";
        string featureName = tokens
            .SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
            .Skip(1)
            .Take(1)
            .FirstOrDefault();
        return featureName;
    }
}

Następnie należy określić tę konwencję jako opcję podczas dodawania obsługi MVC do aplikacji w ConfigureServices programie (lub w Program.cs):

// ConfigureServices
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

ASP.NET Core MVC używa również konwencji do lokalizowania widoków. Można ją zastąpić konwencją niestandardową, aby widoki były umieszczone w folderach funkcji, korzystając z nazwy funkcji podanej przez FeatureConvention powyżej. Więcej informacji na temat tego podejścia i pobranie przykładu roboczego można znaleźć w artykule MSDN Magazine , Feature Slices for ASP.NET Core MVC (Fragmentacje funkcji dla ASP.NET Core MVC).

API i Blazor aplikacje

Jeśli Twoja aplikacja zawiera zestaw internetowych interfejsów API, które muszą być zabezpieczone, te interfejsy API powinny być najlepiej skonfigurowane jako oddzielny projekt, oddzielnie od aplikacji View lub Razor Pages. Oddzielenie interfejsów API, zwłaszcza publicznych interfejsów API, od aplikacji internetowej po stronie serwera ma wiele korzyści. Te aplikacje często mają unikatowe właściwości wdrożenia i obciążenia. Jest bardzo prawdopodobne, że zastosują różne mechanizmy bezpieczeństwa, przy czym standardowe aplikacje oparte na formularzach będą korzystać z uwierzytelniania na podstawie plików cookie, a interfejsy API najprawdopodobniej użyją uwierzytelniania na podstawie tokenów.

Ponadto aplikacje, niezależnie od tego, Blazor czy korzystają z serwera Blazor , czy BlazorWebAssembly, powinny być tworzone jako oddzielne projekty. Aplikacje mają różne cechy środowiska uruchomieniowego, a także modele zabezpieczeń. Mogą one współdzielić wspólne typy z aplikacją internetową po stronie serwera (lub projektem interfejsu API), a te typy powinny być zdefiniowane w wspólnym projekcie udostępnionym.

Dodanie interfejsu administracyjnego BlazorWebAssembly do aplikacji eShopOnWeb wymaga dodania kilku nowych projektów. Sam w sobie projekt, BlazorWebAssemblyBlazorAdmin. Nowy zestaw publicznych punktów końcowych interfejsu API używany przez BlazorAdmin i skonfigurowany do używania uwierzytelniania opartego na tokenach jest zdefiniowany w projekcie PublicApi . Niektóre wspólne typy, używane przez oba te projekty, są przechowywane w nowym projekcie BlazorShared.

Można zapytać, dlaczego dodać oddzielny BlazorShared projekt, gdy istnieje już wspólny ApplicationCore projekt, który może służyć do udostępniania jakiegokolwiek typu wymaganego zarówno przez PublicApi i BlazorAdmin. Odpowiedź jest taka, że ten projekt obejmuje całą logikę biznesową aplikacji, przez co jest znacznie większy niż to konieczne i dlatego trzeba go trzymać w bezpieczny sposób na serwerze. Pamiętaj, że każda biblioteka, do której BlazorAdmin się odwołuje, zostanie pobrana do przeglądarek użytkowników podczas ładowania Blazor aplikacji.

W zależności od tego, czy używasz wzorca Backends-For-Frontends (BFF), interfejsy API używane przez BlazorWebAssembly aplikację mogą nie udostępniać ich typów 100% z Blazor. W szczególności publiczny interfejs API, który ma być używany przez wielu różnych klientów, może definiować własne żądania i typy wyników, zamiast udostępniać je w projekcie udostępnionym specyficznym dla klienta. W przykładzie eShopOnWeb przyjmuje się założenie, że PublicApi projekt jest w rzeczywistości hostem publicznego interfejsu API, więc nie wszystkie jego typy żądań i odpowiedzi pochodzą z BlazorShared projektu.

Zagadnienia przekrojowe

W miarę rozwoju aplikacji coraz ważniejsze staje się uwzględnianie przekrojowych zagadnień, aby wyeliminować duplikacje i zachować spójność. Niektóre przykłady zagadnień krzyżowych w aplikacjach ASP.NET Core to uwierzytelnianie, reguły weryfikacji modelu, buforowanie danych wyjściowych i obsługa błędów, choć istnieje wiele innych. ASP.NET Core filtry MVC umożliwiają uruchamianie kodu przed lub po określonych krokach w potoku obsługi żądań. Na przykład filtr może działać przed i po powiązaniu modelu, przed i po akcji lub przed i po wyniku akcji. Możesz również użyć filtru autoryzacji, aby kontrolować dostęp do reszty potoku. Na rysunku 7–2 pokazano, jak wykonywanie żądań przepływa przez filtry, jeśli zostało skonfigurowane.

Żądanie jest przetwarzane za pomocą filtrów autoryzacji, filtrów zasobów, powiązania modelu, filtrów akcji, wykonania akcji i wyników konwersji, filtrów wyjątków, filtrów wyników i wykonywania wyników. W fazie końcowej żądanie jest przetwarzane tylko przez filtry wyników i filtry zasobów przed przekształceniem w odpowiedź wysłaną do klienta.

Rysunek 7–2 Wykonywanie żądań za pośrednictwem filtrów i potoku żądania.

Filtry są zwykle implementowane jako atrybuty, więc można je zastosować do kontrolerów lub akcji (a nawet globalnie). Po dodaniu w ten sposób filtry określone na poziomie akcji zastępują lub opierają się na filtrach określonych na poziomie kontrolera, które same zastępują filtry globalne. Na przykład [Route] atrybut może służyć do tworzenia tras między kontrolerami i akcjami. Podobnie autoryzację można skonfigurować na poziomie kontrolera, a następnie zastąpić poszczególnymi akcjami, jak pokazano w poniższym przykładzie:

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous] // overrides the Authorize attribute
    public async Task<IActionResult> Login() {}
    public async Task<IActionResult> ForgotPassword() {}
}

Pierwsza metoda Login używa filtru [AllowAnonymous] (atrybutu), aby zastąpić filtr Autoryzuj ustawiony na poziomie kontrolera. Akcja (i każda ForgotPassword inna akcja w klasie, która nie ma atrybutu AllowAnonymous), będzie wymagać uwierzytelnionego żądania.

Filtry mogą służyć do eliminowania duplikacji w postaci typowych zasad obsługi błędów dla interfejsów API. Na przykład typowa zasada interfejsu API polega na zwróceniu odpowiedzi NotFound na żądania odwołujące się do kluczy, które nie istnieją, oraz na zwróceniu odpowiedzi BadRequest jeśli weryfikacja modelu zakończy się niepowodzeniem. W poniższym przykładzie przedstawiono te dwie zasady w działaniu:

[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
        return NotFound(id);
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    author.Id = id;
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Unikaj sytuacji, w której Twoje metody akcji stają się zaśmiecone kodem warunkowym. Zamiast tego należy przenieść zasady do filtrów, które można zastosować w razie potrzeby. W tym przykładzie sprawdzanie poprawności modelu, które powinno nastąpić w dowolnym momencie wysłania polecenia do interfejsu API, można zastąpić następującym atrybutem:

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Możesz dodać element ValidateModelAttribute do projektu jako zależność NuGet, dołączając pakiet Ardalis.ValidateModel . W przypadku interfejsów API można użyć atrybutu ApiController , aby wymusić to zachowanie bez konieczności używania oddzielnego ValidateModel filtru.

Podobnie można użyć filtru, aby sprawdzić, czy istnieje rekord i zwrócić kod statusu 404 przed wykonaniem akcji, eliminując konieczność wykonania tych sprawdzeń podczas wykonywania akcji. Po wycofaniu typowych konwencji i zorganizowaniu rozwiązania w celu oddzielenia kodu infrastruktury i logiki biznesowej od interfejsu użytkownika metody akcji MVC powinny być bardzo cienkie:

[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Więcej informacji na temat implementowania filtrów i pobierania przykładu roboczego można uzyskać z artykułu MSDN Magazine Real-World ASP.NET Core MVC Filters (Podstawowe filtry MVC).

Jeśli okaże się, że masz wiele typowych odpowiedzi z interfejsów API opartych na typowych scenariuszach, takich jak błędy walidacji (nieprawidłowe żądanie), nie znaleziono zasobów i błędy serwera, możesz rozważyć użycie abstrakcji wyników . Abstrakcja wyników zostanie zwrócona przez usługi używane przez punkty końcowe interfejsu API, a akcja kontrolera lub punkt końcowy użyje filtru, aby przetłumaczyć je na IActionResults.

Referencje — strukturyzacja aplikacji

Bezpieczeństwo

Zabezpieczanie aplikacji internetowych to duży temat, z wieloma zagadnieniami. Na najbardziej podstawowym poziomie, zabezpieczenia obejmują upewnianie się, od kogo pochodzi dane żądanie, a następnie zapewnianie, że żądanie ma dostęp tylko do zasobów, do których powinno mieć dostęp. Uwierzytelnianie to proces porównywania poświadczeń dostarczonych z żądaniem do tych w zaufanym magazynie danych, aby sprawdzić, czy żądanie powinno być traktowane jako pochodzące ze znanej jednostki. Autoryzacja to proces ograniczania dostępu do niektórych zasobów na podstawie tożsamości użytkownika. Trzeci problem dotyczący zabezpieczeń polega na ochronie żądań przed podsłuchiwaniem przez osoby trzecie, dla których należy przynajmniej upewnić się, że aplikacja korzysta z protokołu SSL.

Tożsamość

ASP.NET Core Identity to system członkostwa, którego można użyć do obsługi funkcji logowania dla aplikacji. Obsługuje konta użytkowników lokalnych oraz logowanie za pomocą zewnętrznych dostawców, takich jak Microsoft Account, Twitter, Facebook, Google i inne. Oprócz ASP.NET Core Identity aplikacja może używać uwierzytelniania systemu Windows lub innego dostawcy tożsamości, takiego jak Identity Server.

ASP.NET Identity Core jest uwzględniane w nowych szablonach projektów, jeśli wybrano opcję Konta indywidualnych użytkowników. Ten szablon obejmuje obsługę rejestracji, logowania, logowania zewnętrznego, zapomnianych haseł i dodatkowych funkcji.

Wybierz pojedyncze konta użytkowników, aby mieć wstępnie skonfigurowaną tożsamość

Rysunek 7–3. Wybierz indywidualne konta użytkowników, aby tożsamość była wstępnie skonfigurowana.

Obsługa tożsamości jest skonfigurowana w Program.cs lub Startup, i obejmuje konfigurowanie usług, a także oprogramowanie pośredniczące.

Konfigurowanie tożsamości w usłudze Program.cs

W Program.cs najpierw konfigurujesz usługi zaczynając od wystąpienia WebHostBuilder, a następnie po utworzeniu aplikacji konfigurujesz jej oprogramowanie pośredniczące. Najważniejsze kwestie, na które należy zwrócić uwagę, to wywołanie do AddDefaultIdentity w celu wymaganionych usług oraz wywołania UseAuthentication i UseAuthorization, które dodają wymagane middleware.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
  app.UseExceptionHandler("/Error");
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

Konfigurowanie tożsamości podczas uruchamiania aplikacji

// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
builder.Services.AddMvc();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

Ważne jest, aby UseAuthentication i UseAuthorization pojawiły się przed MapRazorPages. Podczas konfigurowania usług identity zauważysz wywołanie metody AddDefaultTokenProviders. Nie ma to nic wspólnego z tokenami, które mogą być używane do zabezpieczania komunikacji internetowej, ale dotyczy dostawców, którzy tworzą powiadomienia w celu potwierdzenia tożsamości, wysyłane do użytkowników za pośrednictwem wiadomości SMS lub poczty e-mail.

Więcej informacji na temat konfigurowania uwierzytelniania dwuskładnikowego i włączania zewnętrznych dostawców logowania można uzyskać z oficjalnej dokumentacji ASP.NET Core.

Uwierzytelnianie

Uwierzytelnianie to proces określania, kto uzyskuje dostęp do systemu. Jeśli używasz ASP.NET Core Identity i metod konfiguracji przedstawionych w poprzedniej sekcji, spowoduje to automatyczne skonfigurowanie niektórych ustawień domyślnych uwierzytelniania w aplikacji. Można jednak skonfigurować te wartości domyślne ręcznie lub nadpisać te ustawienia, które zostały określone przez AddIdentity. Jeśli używasz Identity, jest ono skonfigurowane tak, aby domyślnie korzystać z uwierzytelniania opartego na plikach cookie jako schematu.

W przypadku uwierzytelniania internetowego w trakcie uwierzytelniania klienta systemu zazwyczaj można wykonać maksymalnie pięć akcji. Są to:

  • Uwierzytelniania. Użyj informacji dostarczonych przez klienta, aby utworzyć tożsamość do użycia w aplikacji.
  • Wyzwanie. Ta akcja jest używana, aby wymagać od klienta identyfikacji siebie.
  • Zakazywać. Poinformuj klienta, że jest im zabronione wykonywanie czynności.
  • Logowanie. Zachować istniejącego klienta w pewien sposób.
  • Wyloguj się. Usuń klienta z pamięci trwałej.

Istnieje wiele typowych technik przeprowadzania uwierzytelniania w aplikacjach internetowych. Są one określane jako schematy. Dany schemat zdefiniuje akcje dla niektórych lub wszystkich powyższych opcji. Niektóre schematy obsługują tylko podzestaw akcji i mogą wymagać oddzielnego schematu do wykonania tych, których nie obsługuje. Na przykład schemat OpenId-Connect (OIDC) nie obsługuje logowania ani wylogowania, ale jest często skonfigurowany do używania uwierzytelniania plików cookie na potrzeby tej trwałości.

W aplikacji ASP.NET Core można skonfigurować DefaultAuthenticateScheme, a także opcjonalne, specyficzne schematy dla każdej z opisanych powyżej akcji. Przykład: DefaultChallengeScheme i DefaultForbidScheme. Wywoływanie AddIdentity konfiguruje wiele aspektów aplikacji i dodaje wiele wymaganych usług. Obejmuje to również wywołanie konfigurowania schematu uwierzytelniania:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});

Schematy te domyślnie używają plików cookie do utrwalania i przekierowywania do stron logowania na potrzeby uwierzytelniania. Te schematy są odpowiednie dla aplikacji internetowych, które wchodzą w interakcje z użytkownikami za pośrednictwem przeglądarek internetowych, ale nie są zalecane w przypadku interfejsów API. Zamiast tego interfejsy API zwykle korzystają z innej formy uwierzytelniania, takiej jak tokeny JWT typu bearer.

Internetowe interfejsy API są używane przez kod, taki jak HttpClient w aplikacjach platformy .NET i równoważne typy w innych frameworkach. Ci klienci oczekują użytecznej odpowiedzi z wywołania interfejsu API lub kodu stanu wskazującego, jaki problem, jeśli w ogóle, wystąpił. Ci klienci nie wchodzą w interakcje za pośrednictwem przeglądarki i nie renderują ani nie wchodzą w interakcje z kodem HTML, który może zwrócić interfejs API. W związku z tym punkty końcowe interfejsu API nie są odpowiednie do przekierowywania klientów do stron logowania, jeśli nie są uwierzytelnione. Inny schemat jest bardziej odpowiedni.

Aby skonfigurować uwierzytelnianie dla interfejsów API, możesz skonfigurować uwierzytelnianie, takie jak następujące, używane przez PublicApi projekt w aplikacji referencyjnej eShopOnWeb:

builder.Services
    .AddAuthentication(config =>
    {
      config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(config =>
    {
        config.RequireHttpsMetadata = false;
        config.SaveToken = true;
        config.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

Chociaż istnieje możliwość skonfigurowania wielu różnych schematów uwierzytelniania w ramach jednego projektu, znacznie prostsze jest skonfigurowanie pojedynczego schematu domyślnego. Z tego powodu między innymi aplikacja referencyjna eShopOnWeb oddziela swoje interfejsy API do własnego projektu PublicApi, niezależnie od głównego projektu Web, który obejmuje widoki aplikacji i strony Razor.

Uwierzytelnianie w Blazor aplikacjach

Blazor Aplikacje serwera mogą korzystać z tych samych funkcji uwierzytelniania, co każda inna aplikacja ASP.NET Core. Blazor WebAssembly aplikacje nie mogą jednak używać wbudowanych dostawców tożsamości i uwierzytelniania, ponieważ działają w przeglądarce. Blazor WebAssembly aplikacje mogą przechowywać stan uwierzytelniania użytkownika lokalnie i mogą uzyskiwać dostęp do oświadczeń, aby określić, jakie akcje użytkownicy powinni wykonywać. Jednak wszystkie kontrole uwierzytelniania i autoryzacji powinny być wykonywane na serwerze niezależnie od jakiejkolwiek logiki zaimplementowanej w BlazorWebAssembly aplikacji, ponieważ użytkownicy mogą łatwo pominąć aplikację i bezpośrednio korzystać z interfejsów API.

Referencje – Uwierzytelnianie

Autoryzacja

Najprostsza forma autoryzacji polega na ograniczeniu dostępu do użytkowników anonimowych. Tę funkcję można osiągnąć, stosując [Authorize] atrybut do niektórych kontrolerów lub akcji. Jeśli są używane role, atrybut można dodatkowo rozszerzyć, aby ograniczyć dostęp do użytkowników należących do określonych ról, jak pokazano poniżej:

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{

}

W takim przypadku użytkownicy należący do ról HRManager lub Finance (lub obu) będą mieli dostęp do SalaryController. Aby wymagać, aby użytkownik należał do wielu ról (nie tylko jednej z kilku), można wielokrotnie stosować atrybut, określając wymaganą rolę za każdym razem.

Określanie niektórych zestawów ról w postaci ciągów znaków w wielu różnych kontrolerach i akcjach może prowadzić do niepożądanego powtórzenia. Zdefiniuj co najmniej stałe dla tych literałów ciągów i użyj stałych w dowolnym miejscu, w którym trzeba określić ciąg. Można również skonfigurować zasady autoryzacji, które hermetyzują reguły autoryzacji, a następnie określić zasady zamiast poszczególnych ról podczas stosowania atrybutu [Authorize] :

[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
    return View();
}

Korzystając z zasad w ten sposób, można oddzielić rodzaje akcji, które są ograniczone od określonych ról lub reguł, które mają do nich zastosowanie. Później, jeśli utworzysz nową rolę, która musi mieć dostęp do niektórych zasobów, wystarczy zaktualizować zasady, zamiast aktualizować każdą listę ról w każdym [Authorize] atrybucie.

Roszczenia

Oświadczenia to pary wartości nazw, które reprezentują właściwości uwierzytelnionego użytkownika. Możesz na przykład przechowywać numer pracownika użytkowników jako oświadczenie. Oświadczenia mogą być następnie używane w ramach zasad autoryzacji. Można utworzyć zasady o nazwie "EmployeeOnly", które wymagają istnienia oświadczenia o nazwie "EmployeeNumber", jak pokazano w tym przykładzie:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    });
}

Te zasady mogą być następnie używane z atrybutem [Authorize] w celu ochrony dowolnego kontrolera i/lub akcji, zgodnie z powyższym opisem.

Zabezpieczanie internetowych interfejsów API

Większość internetowych interfejsów API powinna implementować system uwierzytelniania opartego na tokenach. Uwierzytelnianie tokenu jest bezstanowe i zaprojektowane tak, aby było skalowalne. W systemie uwierzytelniania opartego na tokenach klient musi najpierw uwierzytelnić się u dostawcy uwierzytelniania. W przypadku powodzenia klient otrzymuje token, który jest po prostu kryptograficznie zrozumiałym ciągiem znaków. Najczęstszym formatem tokenów jest token internetowy JSON lub JWT (często wymawiany jako "jot"). Gdy klient musi wysłać żądanie do interfejsu API, dodaje ten token jako nagłówek żądania. Następnie serwer weryfikuje token znaleziony w nagłówku żądania przed ukończeniem żądania. Rysunek 7–4 przedstawia ten proces.

TokenAuth

Rysunek 7–4. Uwierzytelnianie oparte na tokenach dla internetowych interfejsów API.

Możesz utworzyć własną usługę uwierzytelniania, zintegrować z usługą Azure AD i OAuth lub zaimplementować usługę przy użyciu narzędzia open source, takiego jak IdentityServer.

Tokeny JWT mogą osadzać oświadczenia dotyczące użytkownika, które można odczytać na kliencie lub serwerze. Możesz użyć narzędzia, takiego jak jwt.io , aby wyświetlić zawartość tokenu JWT. Nie przechowuj poufnych danych, takich jak hasła lub klucze w tokenach JTW, ponieważ ich zawartość jest łatwo odczytywana.

W przypadku korzystania z tokenów JWT ze SPA lub BlazorWebAssembly aplikacjami należy przechowywać token gdzieś na kliencie, a następnie dodać go do każdego wywołania interfejsu API. To działanie jest zwykle wykonywane jako nagłówek, jak pokazano w poniższym kodzie:

// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
      var token = await GetToken();
      _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}

Po wywołaniu powyższej metody żądania z użyciem _httpClient będą miały token osadzony w nagłówkach żądania, dzięki czemu serwerowy API może uwierzytelniać i autoryzować żądanie.

Zabezpieczenia niestandardowe

Ostrzeżenie

Ogólnie rzecz biorąc, unikaj implementowania własnych niestandardowych implementacji zabezpieczeń.

Należy zachować szczególną ostrożność podczas wdrażania własnej implementacji kryptografii, członkostwa użytkowników lub systemu generowania tokenów. Dostępnych jest wiele alternatywnych rozwiązań komercyjnych i open source, które prawie na pewno będą miały lepsze zabezpieczenia niż implementacja niestandardowa.

Odniesienia – Bezpieczeństwo

Komunikacja klienta

Oprócz obsługi stron i odpowiadania na żądania dotyczące danych za pośrednictwem internetowych interfejsów API aplikacje ASP.NET Core mogą komunikować się bezpośrednio z połączonymi klientami. Ta komunikacja wychodząca może korzystać z różnych technologii transportu, najczęściej jest to webSocket. ASP.NET Core SignalR to biblioteka, która ułatwia dodawanie funkcji komunikacji między serwerami w czasie rzeczywistym do aplikacji. Usługa SignalR obsługuje różne technologie transportu, w tym protokoły WebSocket, i oddziela wiele szczegółów implementacji od dewelopera.

Komunikacja klienta w czasie rzeczywistym, niezależnie od tego, czy jest realizowana bezpośrednio za pomocą WebSockets czy innych technik, jest przydatna w różnych scenariuszach zastosowań. Oto kilka przykładów:

  • Aplikacje pokoju rozmów na żywo

  • Monitorowanie aplikacji

  • Aktualizacje postępu zadania

  • Powiadomienia

  • Aplikacje formularzy interaktywnych

Podczas kompilowania komunikacji klienta z aplikacjami zazwyczaj istnieją dwa składniki:

  • Menedżer połączeń po stronie serwera (SignalR Hub, WebSocketManager WebSocketHandler)

  • Biblioteka po stronie klienta

Klienci nie są ograniczeni do przeglądarek — aplikacje mobilne, aplikacje konsolowe i inne aplikacje natywne mogą również komunikować się przy użyciu protokołu SignalR/WebSocket. Poniższy prosty program odzwierciedla całą zawartość wysłaną do aplikacji czatu do konsoli w ramach przykładowej aplikacji WebSocketManager:

public class Program
{
    private static Connection _connection;
    public static void Main(string[] args)
    {
        StartConnectionAsync();
        _connection.On("receiveMessage", (arguments) =>
        {
            Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
        });
        Console.ReadLine();
        StopConnectionAsync();
    }

    public static async Task StartConnectionAsync()
    {
        _connection = new Connection();
        await _connection.StartConnectionAsync("ws://localhost:65110/chat");
    }

    public static async Task StopConnectionAsync()
    {
        await _connection.StopConnectionAsync();
    }
}

Rozważ sposoby, w których aplikacje komunikują się bezpośrednio z aplikacjami klienckimi, i zastanów się, czy komunikacja w czasie rzeczywistym poprawi środowisko użytkownika aplikacji.

Odwołania — komunikacja z klientem

Projekt oparty na domenie — czy należy go zastosować?

Domain-Driven Design (DDD) to elastyczne podejście do tworzenia oprogramowania, które podkreśla skupienie się na domenie biznesowej. Kładzie to duży nacisk na komunikację i interakcję z ekspertami z dziedziny biznesowej, którzy mogą przedstawić deweloperom, jak rzeczywisty system funkcjonuje. Jeśli na przykład tworzysz system obsługujący transakcje giełdowe, ekspert domeny może być doświadczonym brokerem giełdowym. DDD jest przeznaczony do rozwiązywania dużych, złożonych problemów biznesowych i często nie jest odpowiedni dla mniejszych, prostszych aplikacji, ponieważ inwestycja w zrozumienie i modelowanie domeny nie jest warta.

Podczas tworzenia oprogramowania zgodnie z podejściem DDD twój zespół (w tym nietechnicznych interesariuszy i współpracowników) powinien opracować wszechobecny język dla obszaru problemu. Oznacza to, że ta sama terminologia powinna być używana do modelowania koncepcji w świecie rzeczywistym, odpowiednika oprogramowania i wszelkich struktur, które mogą istnieć w celu utrwalania koncepcji (na przykład tabel baz danych). W związku z tym koncepcje opisane w wszechobecnym języku powinny stanowić podstawę modelu domeny.

Model domeny składa się z obiektów, które współdziałają ze sobą w celu reprezentowania zachowania systemu. Te obiekty mogą należeć do następujących kategorii:

  • Jednostki reprezentujące obiekty z wątkiem tożsamości. Jednostki są zwykle przechowywane w trwałości przy użyciu klucza, za pomocą którego można je później pobrać.

  • Agregacje reprezentujące grupy obiektów, które powinny być utrwalane jako jednostka.

  • Obiekty wartości reprezentujące koncepcje, które można porównać na podstawie sumy ich wartości właściwości. Na przykład DateRange składająca się z daty rozpoczęcia i zakończenia.

  • Zdarzenia domeny, które reprezentują rzeczy wykonywane w systemie, które są interesujące dla innych części systemu.

Model domeny DDD powinien zakapsułować złożone zachowanie. Byty, w szczególności, nie powinny być jedynie zbiorami właściwości. Gdy model domeny nie ma zachowania i jedynie reprezentuje stan systemu, mówi się, że jest to model anemiczny, który jest niepożądany w DDD.

Oprócz tych typów modeli DDD zwykle stosuje różne wzorce:

  • Repozytorium w celu abstrakcji szczegółów trwałości.

  • Fabryka, aby obudować proces tworzenia złożonych obiektów.

  • Usługi do hermetyzacji złożonych zachowań i/lub szczegółów implementacji infrastruktury.

  • Polecenie, w celu oddzielenia wydawania poleceń od wykonywania samego polecenia.

  • Specyfikacja do zamknięcia szczegółów zapytania.

DDD zaleca również użycie wcześniej omówionej czystej architektury, co pozwala na luźne sprzężenie, hermetyzację i kod, który można łatwo zweryfikować przy użyciu testów jednostkowych.

Kiedy należy zastosować DDD

DDD doskonale nadaje się do dużych aplikacji ze znaczną złożonością biznesową (nie tylko techniczną). Aplikacja powinna wymagać wiedzy ekspertów w dziedzinie. W samym modelu domeny powinno istnieć znaczące zachowanie, reprezentując reguły biznesowe i interakcje poza zwykłe przechowywanie i pobieranie bieżącego stanu różnych rekordów z magazynów danych.

Kiedy nie należy stosować DDD

DDD obejmuje inwestycje w modelowanie, architekturę i komunikację, które mogą nie być uzasadnione dla mniejszych aplikacji lub aplikacji, które sprowadzają się do wykonywania podstawowych operacji CRUD, takich jak tworzenie, odczytywanie, aktualizowanie i usuwanie danych. Jeśli zdecydujesz się podejść do aplikacji według wzorca DDD, ale okaże się, że domena ma anemiczny model pozbawiony zachowania, może być konieczne przemyślenie na nowo podejścia. Aplikacja może nie potrzebować DDD lub może być potrzebna pomoc w refaktoryzacji aplikacji w celu hermetyzacji logiki biznesowej w modelu domeny, a nie w bazie danych lub interfejsie użytkownika.

Podejście hybrydowe polegałoby na używaniu DDD tylko dla transakcyjnych lub bardziej złożonych obszarów aplikacji, ale nie dla prostszych części CRUD ani obszarów przeznaczonych tylko do odczytu aplikacji. Na przykład nie potrzebujesz ograniczeń agregacji, jeśli wysyłasz zapytania dotyczące danych w celu wyświetlenia raportu lub wizualizacji danych dla pulpitu nawigacyjnego. Jest to całkowicie dopuszczalne, aby mieć oddzielny, prostszy model odczytu dla takich wymagań.

Referencje – projekt Domain-Driven

Wdrożenie

Istnieje kilka kroków związanych z procesem wdrażania aplikacji ASP.NET Core, niezależnie od tego, gdzie będzie hostowana. Pierwszym krokiem jest opublikowanie aplikacji, co można zrobić za pomocą polecenia dotnet publish CLI. Ten krok spowoduje skompilowanie aplikacji i umieszczenie wszystkich plików potrzebnych do uruchomienia aplikacji w wyznaczonym folderze. Podczas wdrażania z poziomu programu Visual Studio ten krok jest wykonywany automatycznie. Folder publikowania zawiera pliki .exe i .dll dla aplikacji i jej zależności. Samodzielna aplikacja będzie również zawierać wersję środowiska uruchomieniowego platformy .NET. aplikacje ASP.NET Core będą również obejmować pliki konfiguracji, statyczne zasoby klienta i widoki MVC.

aplikacje ASP.NET Core to aplikacje konsolowe, które należy uruchomić po uruchomieniu serwera i ponownym uruchomieniu, jeśli aplikacja (lub serwer) ulegnie awarii. Menedżer procesów może służyć do automatyzacji tego procesu. Najbardziej typowymi menedżerami procesów dla platformy ASP.NET Core są serwery Nginx i Apache w systemach Linux i IIS lub Windows Service w systemie Windows.

Oprócz menedżera procesów aplikacje ASP.NET Core mogą używać zwrotnego serwera proxy. Zwrotny serwer proxy odbiera żądania HTTP z Internetu i przekazuje je do usługi Kestrel po wstępnej obsłudze. Odwrotne serwery proxy zapewniają warstwę zabezpieczeń aplikacji. Kestrel nie obsługuje również hostowania wielu aplikacji na tym samym porcie, więc techniki takie jak nagłówki hostów nie mogą być używane do hostowania wielu aplikacji na tym samym porcie i adresie IP.

Kestrel do Internetu

Rysunek 7–5. ASP.NET hostowane przez Kestrel za serwerem proxy typu reverse

Innym scenariuszem, w którym zwrotny serwer proxy może być przydatny, jest zabezpieczenie wielu aplikacji przy użyciu protokołu SSL/HTTPS. W takim przypadku tylko zwrotny serwer proxy musi mieć skonfigurowany protokół SSL. Komunikacja między zwrotnym serwerem proxy a serwerem Kestrel może odbywać się za pośrednictwem protokołu HTTP, jak pokazano na rysunku 7-6.

ASP.NET hostowane za zabezpieczonym za protokołem HTTPS zwrotnym serwerem proxy

Rysunek 7–6. ASP.NET jest hostowany za pomocą zabezpieczonego protokołem HTTPS serwera odwrotnego proxy.

Coraz bardziej popularnym podejściem jest hostowanie aplikacji ASP.NET Core w kontenerze platformy Docker, który następnie może być hostowany lokalnie lub wdrożony na platformie Azure na potrzeby hostingu opartego na chmurze. Kontener platformy Docker może zawierać kod aplikacji działający na Kestrel i będzie wdrożony za serwerem proxy typu reverse, jak pokazano na powyższym schemacie.

Jeśli hostujesz aplikację na platformie Azure, możesz użyć usługi Microsoft Azure Application Gateway jako dedykowanego urządzenia wirtualnego, aby zapewnić kilka usług. Oprócz działania jako zwrotny serwer proxy dla poszczególnych aplikacji usługa Application Gateway może również oferować następujące funkcje:

  • Równoważenie obciążenia HTTP

  • Odciążanie protokołu SSL (tylko protokół SSL do Internetu)

  • Kompleksowa łączność SSL

  • Routing obejmujący wiele lokacji (konsolidowanie do 20 lokacji w jednej usłudze Application Gateway)

  • Zapora aplikacji internetowej

  • Obsługa protokołu Websocket

  • Zaawansowana diagnostyka

Dowiedz się więcej o opcjach wdrażania platformy Azure w rozdziale 10.

Referencje — wdrażanie