Tworzenie aplikacji mvC platformy ASP.NET Core

Napiwek

Ta zawartość jest fragmentem książki eBook, architekta nowoczesnych aplikacji internetowych z platformą ASP.NET Core i platformą Azure, dostępnym na platformie .NET Docs lub jako bezpłatny plik PDF do pobrania, który można odczytać w trybie offline.

Tworzenie architektury nowoczesnych aplikacji internetowych za pomocą miniatury ASP.NET Core i książki eBook platformy Azure.

"Nie ważne jest, aby to dobrze po raz pierwszy. Niezwykle ważne jest, aby to było w porządku po raz ostatni. - 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 internetowych interfejsów API oprócz aplikacji opartych na widoku, ASP.NET Core to zaawansowana struktura umożliwiająca tworzenie 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 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. i przy użyciu routingu opartego na atrybutach strony Razor są umieszczane w jednym folderze ("/Pages"), trasy opartej na ich względnej lokalizacji w tym folderze i obsługiwać żądania z procedurami obsługi zamiast akcji kontrolera. W związku z tym podczas pracy ze stronami Razor wszystkie potrzebne pliki i klasy są zwykle kolokowane, a nie 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 w środowisku IDE lub przy użyciu polecenia interfejsu dotnet new wiersza polecenia wybierzesz kilka 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 internetowego interfejsu API używa standardowych kontrolerów Model-View-Controller — domyślnie brakuje 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. Projekty internetowego interfejsu API i kontrolera widoku-modelu nie zawierają domyślnie folderu Pages, ale można dodać go później, aby obsługiwać zachowanie 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żywanie kontrolerów i widoków często zdarzało się, że aplikacje mają bardzo duże kontrolery, które współpracowały z wieloma różnymi zależnościami i wyświetlały modele i 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ść innych niż trywialnych stron Razor będzie mieć skojarzona klasa 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 obowiązki kontrolera MVC i modelu widoku. Zamiast obsługiwać żądania za pomocą metod akcji kontrolera, programy obsługi modelu strony, takie jak "OnGet()", są wykonywane, renderowanie skojarzonej strony domyślnie. 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 internetowe interfejsy API, wzorzec MVC ma większe znaczenie 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 ocenić, czy warto wdrożyć strony Razor dla nowych funkcji, a nawet jako migrację hurtową. 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.

Mapowanie żądań na odpowiedzi

W centrum aplikacji ASP.NET Core mapuje 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. Symbole controller zastępcze i action mają określony domyślny (Home i Index, odpowiednio), a id symbol zastępczy 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ą zwykle definiowane w jednym miejscu dla aplikacji, na przykład w Program.cs, w którym skonfigurowano potok oprogramowania pośredniczącego żądania.

Trasy atrybutów są stosowane bezpośrednio do kontrolerów i akcji, a nie do określonych 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. Na 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 atrybutów. Dodatkowe informacje o szablonie trasy dla strony Razor można określić w ramach jej @page dyrektywy:

@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. ASP.NET podstawowe filtry MVC oferują sposób przechwytywania grup żądań, dzięki czemu można zastosować typowe zasady i kwestie obejmujące wiele cięć. 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 platforma Razor Pages doskonale sprawdza się, czy kontrolery nie są zbyt duże. Każda strona ma własne pliki i klasy przeznaczone tylko dla jej procedur obsługi. Przed wprowadzeniem stron Razor wiele aplikacji zorientowanych na widok ma duże klasy kontrolerów odpowiedzialne za wiele różnych akcji i widoków. Klasy te naturalnie rosną, aby mieć 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 procedur obsługi do wykonywania pracy 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órych zwykle korzysta kontroler, jest wystąpienie 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
}

MyOrders W akcji wywołanie Send komunikatu 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, aby kontrolery były znacznie mniejsze i koncentruje się głównie na powiązaniu routingu i modelu, podczas gdy poszczególne programy obsługi są odpowiedzialne za określone zadania wymagane przez dany punkt końcowy. Takie podejście można również osiągnąć bez usługi MediatR przy użyciu pakietu NuGet ApiEndpoints, który próbuje przenieść do kontrolerów interfejsu API te same korzyści, jakie zapewnia platforma Razor Pages do wyświetlania kontrolerów.

Odwołania — mapowanie żądań na odpowiedzi

Praca z zależnościami

ASP.NET Core ma wbudowaną obsługę i wewnętrznie wykorzystuje technikę znaną jako wstrzykiwanie zależności. 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 otwierania/zamykania. 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 przylgnięcie występuje, gdy klasy tworzą wywołania metod statycznych lub uzyskują dostęp do właściwości statycznych, które mają skutki uboczne lub zależności w infrastrukturze. 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 takich metod jest notorycznie trudne, ponieważ takie testy wymagają komercyjnego pozorowania bibliotek do pozorowania 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 wywołania, które są całkowicie bezstanowe, są w porządku do wywoływania i nie mają wpływu na sprzęganie ani możliwość testowania (poza kodem sprzęgania do samego wywołania statycznego).

Wielu deweloperów rozumie ryzyko statycznego przylgnięcia i stanu globalnego, ale nadal ściśle połączy swój kod z konkretnymi implementacjami za pomocą bezpośredniego wystąpienia. "Nowy jest klej" ma być przypomnieniem tego sprzężenia, a nie ogólne potępienie 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 wystąpi wystąpienie klasy, pośmiń chwilę, aby zastanowić się, czy ma to sens, aby zakodować to konkretne wystąpienie w tej konkretnej lokalizacji, czy też lepszym rozwiązaniem byłoby zażądanie tego wystąpienia jako zależności.

Deklarowanie zależności

ASP.NET Core bazuje na uzyskaniu metod i klas deklarujących ich zależności, żądając ich jako argumentów. ASP.NET aplikacje są zwykle konfigurowane w Program.cs lub w Startup klasie.

Uwaga

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 polecenia 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 programie 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 podczas tworzenia wystąpienia uruchamiania. 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 naprawdę nie musi obsługiwać wstrzykiwania zależności, ponieważ z jednej strony jest odpowiedzialny za dodawanie obiektów do kontenera usług, a drugi ma dostęp do wszystkich aktualnie skonfigurowanych usług za pośrednictwem parametru IServiceCollection . W związku z tym można pracować z zależnościami zdefiniowanymi w kolekcji usług ASP.NET Core w każdej części Startup klasy, żądając wymaganej usługi jako parametru lub pracując z elementem IServiceCollection w ConfigureServicespliku .

Uwaga

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

Klasa Startup to model, w którym należy strukturę innych części aplikacji ASP.NET Core, od kontrolerów do oprogramowania pośredniczącego po filtry do własnych usług. 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 tworzy się wystąpienia implementacji, zwłaszcza usług i obiektów, które współpracują z infrastrukturą lub mają skutki uboczne. Preferuj pracę z abstrakcjami zdefiniowanymi w rdzeniu aplikacji i przekazywanym jako argumenty do odwołujących się do określonych 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 jego implementacje trwałości są odpowiedzialne za pobieranie i przechowywanie jednostek zdefiniowanych w rdzeniach aplikacji.

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 od wykorzystania di między dwoma projektami, ale ma dodatkową złożoność.

Organizacja funkcji

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 Eksplorator 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 Obszar. 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ę obszaru do 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 ConfigureServices w 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 zastąpić ją konwencją niestandardową, tak aby widoki znajdowały się w folderach funkcji (przy użyciu nazwy funkcji podanej przez funkcjęConvention powyżej). Aby dowiedzieć się więcej na temat tego podejścia, możesz pobrać przykład pracy z artykułu MSDN Magazine , Feature Slices for ASP.NET Core MVC (Fragmentacje funkcji dla ASP.NET Core MVC).

Interfejsy API i Blazor aplikacje

Jeśli aplikacja zawiera zestaw internetowych interfejsów API, które muszą być zabezpieczone, te interfejsy API powinny być najlepiej skonfigurowane jako oddzielny projekt z poziomu aplikacji Widok 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. Są one również bardzo prawdopodobne, aby przyjąć różne mechanizmy zabezpieczeń, przy użyciu standardowych aplikacji opartych na formularzach korzystających z uwierzytelniania opartego na plikach cookie i interfejsów API najprawdopodobniej przy użyciu uwierzytelniania opartego na tokenach.

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 BlazorWebAssembly projekt, BlazorAdmin. 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 typy udostępnione używane przez oba te projekty są przechowywane w nowym BlazorShared projekcie.

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 dowolnego typu wymaganego zarówno przez i BlazorAdminPublicApi ? Odpowiedź polega na tym, że ten projekt obejmuje całą logikę biznesową aplikacji i jest w ten sposób znacznie większy niż jest to konieczne, a także znacznie bardziej prawdopodobne, aby trzeba było zachować bezpieczeństwo 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ć swoich typów 100% 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 ogólne

W miarę zwiększania się aplikacji coraz ważniejsze staje się uwzględnianie zagadnień związanych z przecięciem w celu wyeliminowania duplikacji i utrzymania spójności. 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 podstawowe filtry MVC umożliwiają uruchamianie kodu przed lub po pewnych krokach w potoku przetwarzania żą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, konwersji wykonania akcji i wyniku akcji, filtrów wyjątków, filtrów wyników i wykonywania wyników. W drodze żądanie jest przetwarzane tylko przez filtry wyników i filtry zasobów przed uzyskaniem odpowiedzi wysłanej 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 typowymi zasadami interfejsu API jest zwrócenie odpowiedzi NotFound na żądania odwołujące się do kluczy, które nie istnieją, oraz odpowiedź, jeśli weryfikacja modelu zakończy się niepowodzeniem BadRequest . 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();
}

Nie zezwalaj metodom akcji na zaśmiecanie kodem warunkowym w następujący sposób. Zamiast tego należy ściągnąć zasady do filtrów, które można zastosować zgodnie z potrzebami. 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ć wartość 404 przed wykonaniem akcji, eliminując konieczność wykonania tych kontroli w 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 w świecie rzeczywistym).

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.

Odwołania — tworzenie struktur aplikacji

Zabezpieczenia

Zabezpieczanie aplikacji internetowych to duży temat, z wieloma zagadnieniami. Na najbardziej podstawowym poziomie zabezpieczenia obejmują zapewnienie, kto pochodzi z danego żądania, a następnie zapewnienie, że żądanie ma dostęp tylko do zasobów, z których powinien. 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 z zabezpieczeniami chroni żądania przed podsłuchiwaniem przez osoby trzecie, dla których należy przynajmniej upewnić się, że protokół SSL jest używany przez aplikację.

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 ona konta użytkowników lokalnych, a także zewnętrzną pomoc techniczną dostawcy logowania od dostawców, takich jak konto Microsoft, 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 Tożsamość podstawowa jest uwzględniana w nowych szablonach projektów, jeśli wybrano opcję Indywidualne konta 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 pozycję Indywidualne konta użytkowników, aby mieć wstępnie skonfigurowaną tożsamość.

Obsługa tożsamości jest konfigurowana 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 skonfigurujesz usługi z wystąpienia, a następnie po utworzeniu WebHostBuilder aplikacji skonfigurujesz jej oprogramowanie pośredniczące. Najważniejsze kwestie, które należy zwrócić uwagę, to wywołanie AddDefaultIdentity wymaganych usług i UseAuthenticationUseAuthorization wywołań, które dodają wymagane oprogramowanie pośredniczące.

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 UseAuthenticationUseAuthorization i pojawić 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 zamiast tego odwołuje się do dostawców, którzy tworzą monity, które mogą być wysyłane do użytkowników za pośrednictwem wiadomości SMS lub poczty e-mail w celu potwierdzenia tożsamości.

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 zastąpić te ustawienia ustawione przez właściwość AddIdentity. Jeśli używasz tożsamości, konfiguruje uwierzytelnianie oparte na plikach cookie jako schemat domyślny.

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.
  • Zabraniają. Poinformuj klienta, że nie mogą wykonywać akcji.
  • Logowanie. Utrwalić istniejącego klienta w jakiś sposób.
  • Wyrejestrowywania. Usuń klienta z trwałości.

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-Połączenie (OIDC) nie obsługuje logowania ani wylogowania, ale jest często skonfigurowany do używania uwierzytelniania plików cookie dla tej trwałości.

W aplikacji ASP.NET Core można skonfigurować schematy DefaultAuthenticateScheme , a także opcjonalne 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 używają innej formy uwierzytelniania, takiej jak tokeny elementu nośnego JWT.

Internetowe interfejsy API są używane przez kod, na przykład HttpClient w aplikacjach platformy .NET i równoważnych typach w innych strukturach. Ci klienci oczekują użytecznej odpowiedzi z wywołania interfejsu API lub kodu stanu wskazującego, co, jeśli istnieje, wystąpił problem. 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 od własnego projektu , PublicApiniezależnie od głównego Web projektu, 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. BlazorWebAssembly aplikacje nie mogą jednak używać wbudowanych dostawców tożsamości i uwierzytelniania, ponieważ działają w przeglądarce. BlazorWebAssembly 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.

Odwołania — 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 HRManager ról lub Finance (lub obu) będą mieli dostęp do elementu 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 jako ciągów na 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 wysyłane za pomocą _httpClient tokenu będą miały token osadzony w nagłówkach żądania, umożliwiając interfejsowi API po stronie serwera uwierzytelnianie i autoryzację żądania.

Zabezpieczenia niestandardowe

Uwaga

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.

Odwołania — zabezpieczenia

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 używana bezpośrednio, czy też w innych technikach, jest przydatna w różnych scenariuszach aplikacji. Przykłady obejmują:

  • Aplikacje pokoju rozmów na żywo

  • Monitorowanie aplikacji

  • Aktualizacje postępu zadania

  • Notifications

  • 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 klienta

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

Projektowanie oparte na domenie (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ą odnosić się do deweloperów, jak działa rzeczywisty system. 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 osoby biorące udział w projekcie nietechnicznych i współautorów) powinien opracować wszechobecny język dla przestrzeni 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 hermetyzować złożone zachowanie w modelu. Jednostki, w szczególności, nie powinny być tylko kolekcjami 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 w celu hermetyzacji tworzenia złożonych obiektów.

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

  • Polecenie w celu oddzielenia poleceń wystawiających i wykonywania samego polecenia.

  • Specyfikacja w celu hermetyzacji 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 są zasadniczo tylko CRUD (tworzenie/odczytywanie/aktualizowanie/usuwanie). Jeśli zdecydujesz się podejść do aplikacji po DDD, ale okaże się, że domena ma model anemiczny bez zachowania, może być konieczne ponowne przemyślenie 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ściem hybrydowym byłoby użycie tylko DDD dla transakcyjnych lub bardziej złożonych obszarów aplikacji, ale nie w przypadku prostszych fragmentów CRUD lub 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ń.

Odwołania — projekt oparty na domenie

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, którą można wykonać za pomocą polecenia interfejsu dotnet publish wiersza polecenia. 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 w usłudze Kestrel za zwrotnym serwerem proxy

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 hostowane za zabezpieczonym za protokołem HTTPS zwrotnym serwerem 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 uruchomiony w usłudze Kestrel i zostanie wdrożony za zwrotnym serwerem proxy, jak pokazano powyżej.

Jeśli hostujesz aplikację na platformie Azure, możesz użyć usługi Microsoft aplikacja systemu Azure 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.

Odwołania — wdrażanie