Ćwiczenia aplikacje konsolowe i MVC

Ćwiczenie I - Serwer

Stwórz katalog roboczy, w którym wykonasz ćwiczenie. Wejdź do niego następującą komendą:

$ mkdir cw1
$ cd cw1

Zacznijmy od stworzenia pierwszej aplikacji konsolowej. Robimy to za pomocą komendy:

$ dotnet new console

Następnie, za pomocą dotnet CLI, zainstaluj paczkę nuget-a Microsoft.AspNetCore

$ dotnet add package Microsoft.AspNetCore

Kolejnym krokiem jest dodanie w pliku Program.cs publicznej, statycznej metody CreateWebHostBuilder

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();

Pozostając w tym samym pliku skasuj zawartość funkcji Main i wywołaj w niej świeżo stworzoną funkcję. Następnie, na zwróconym obiekcie, wywołaj metody Build i Run.

        static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

Możesz przejść do utworzenia klasy Startup. Musi ona zawierać minimum jedną metodę Configure(IApplicationBuilder app).

    public class Startup
    {
        public void Configure(IApplicationBuilder app)
        {
            // ...
        }
    }

W metodzie Configure stwórz middleware, który, dla każdego request-u, będzie zwracał Hello .NET Core!

            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello .NET Core!");
            });

Ćwiczenie 2 - MVC

Przygotowanie aplikacji MVC

W ćwiczeniu numer 2 zaczynamy korzystać z .NET Core MVC. Konieczne jest stworzenie modelu i kontrolera. Ponieważ nie będziemy pisać zwykłej aplikacji MVC, możemy się obejść bez widoków. Zacznijmy od stworzenia nowego katalogu, gdzie będziemy przechowywać kod źródłowy naszej aplikacji. Za pomocą konsoli możesz to zrobić w następujący sposób.

$ mkdir cw2
$ cd cw2

Pamiętaj, aby od razu przejść do katalogu, w którym ma się znaleźć aplikacja. Następnie, stwórz pustą aplikacje .NET Core MVC. Możesz to zrobić za pomocą polecenia w konsoli

$ dotnet new mvc -n cw2

Zobaczenie pełnej listy folderów i katalogów wymaga wykorzystania polecenia:

$ ls -la

W konsoli powinna pojawić się następująca lista plików i katalogów:

➜ ls -la
total 40
drwxr-xr-x  13 dante  staff   416 Feb 22 15:52 .
drwxr-xr-x   6 dante  staff   192 Feb 22 15:52 ..
drwxr-xr-x   3 dante  staff    96 Feb 22 15:51 Controllers
drwxr-xr-x   3 dante  staff    96 Feb 22 15:51 Models
-rw-r--r--   1 dante  staff   631 Feb 22 15:51 Program.cs
drwxr-xr-x   3 dante  staff    96 Feb 22 15:51 Properties
-rw-r--r--   1 dante  staff  2199 Feb 22 15:51 Startup.cs
drwxr-xr-x   6 dante  staff   192 Feb 22 15:51 Views
-rw-r--r--   1 dante  staff   146 Feb 22 15:51 appsettings.Development.json
-rw-r--r--   1 dante  staff   105 Feb 22 15:51 appsettings.json
-rw-r--r--   1 dante  staff   460 Feb 22 15:51 cw2.csproj
drwxr-xr-x   6 dante  staff   192 Feb 22 15:52 obj
drwxr-xr-x   6 dante  staff   192 Feb 22 15:51 wwwroot

Po wygenerowaniu projektu zobacz jak wyglądają pliki Program.cs i Startup.cs. Widzisz pewne podobieństwa między wersjami plików, a tymi, które stworzyliśmy w poprzednim ćwiczeniu? Uruchom projekt za pomocą swojego IDE albo za przy użyciu polecenia konsolowego

$ dotnet restore
$ dotnet run

Kiedy zobaczy na konsoli, że projekt wystartował powinien się pojawić napis:

Hosting environment: Development
Content root path: /Users/dante/workspace/__LINECODE/workshops/net-core-ver-3/src/cw2
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Będziesz mógł wejść na aplikację za pomocą przeglądarki internetowej. Użyj do tego adresu https://localhost:5001 (Uwaga: Jeżeli wygenerowałeś projekt za pomocą Visual Studio to najprawdopodobniej aplikacja będzie uruchomiona na innym porcie), aby sprawdzić, czy na Twoim komputerze są poprawnie zainstalowane certyfikaty.

Stworzenie modelu User

Jeżeli wszystko działa poprawnie przechodzimy do implementacji modelu użytkownika naszej aplikacji. Przejdź do katalogu Models i stwórz plik User.cs. Stwórz klasę, która będzie składa z następujących propertis-ów:

  • Id o typie Guid
  • Email o typie string
  • Role o typie string
  • PasswordHash o typie string
  • CreatedAt o typie DateTime
  • UpdatedAt o typie DateTime Zachęcam by samemu pisać cały kod. Jakby jednak nie brzmiało to aż tak dobrze, możesz wykorzystać następujący snippet:
    public class User
    {
        public Guid Id { get; set; }
        public string Email { get; set; }
        public string Role { get; set; }
        public string PasswordHash { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime UpdatedAt { get; set; }
    }

Do tej klasy jeszcze będziemy wracać przy kolejnych ćwiczeniach. Na chwile obecną taka najprostsza implementacja całkowicie nam wystarczy.

Stworzenie kontrolera UsersController

Teraz przejdź do katalogu Controllers. Utwórz nowy plik o nazwie UsersController.cs. W tym pliku utwórz klasę UsersController, która musi dziedziczyć po klasie bazowej Controller.

    public class UsersController : Controller
    {
        // ...
    }

W klasie UsersController utwórz metodę o nazwie Index, która będzie zwracała typ danych IActionResult. W tej metodzie wykorzystaj metodę pomocniczą do zwrócenia status 200 (Ok).

    public IActionResult Index()
    {
        return Ok();
    }

Uruchom projekt i sprawdź czy wszystko działa poprawnie. (adres w przeglądarce: https://localhost:5001/users/

Dodaj do kontrolera atrybut [ApiController] , aby w sposób deklaratywny określić, że ten kontroler nie zwraca widoków. Po dodaniu atrybutu trzeba będzie deklaratywnie wskazać routing w naszym kontrolerze. Wykorzystuje się do tego atrybut [Route], który można wywołać na poziomie całej klasy jak i pojedynczej metody. Jednocześnie, do metody Index, dodajemy atrybut [HttpGet] aby jednoznacznie określić, ze ta akcja kontrolera jest dostępna tylko i wyłącznie za pomocą zadania metodą GET.

    [ApiController]
    [Route("[controller]")]
    public class UsersController : Controller
    {
        [HttpGet]
        [Route("")]
        public IActionResult Index()
        {
            return Ok();
        }
    }

W tym momencie , samym routingiem nie przejmuj się za bardzo. Opowiem o tym w późniejszej części warsztatów.

Przechodzimy do zwrócenia danych przez kontroler. W metodzie Index utwórz obiekt klasy User i wykorzystując metodę pomocniczą Json zwróć utworzony obiekt w notacji JSON. Zamiast hashu hasła ustaw mu na razie wartość "none".

    public IActionResult Index()
    {
        var user = new User
        {
            Id = Guid.NewGuid(),
            Role = "User",
            Email = "test@test.pl",
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow,
            PasswordHash = "none"
        };
        return Json(user);
    }

Ponownie uruchom projekt i zobacz co zostanie zwrócone pod adresem:https://localhost:5001/users/ Przeglądanie odpowiedzi JSON w czytelniejszej formie wymaga zainstalowania dodatku do Chrome-a o nazwie JSON Viewer

W tym momencie wiesz jak zwracać dane za pomocą .NET Core MVC. Wypróbuj teraz trudniejszy scenariusz. Postarajmy się otrzymać dane użytkownika za pomocą request HTTP.

Zacznijmy od Zdefiniowania nowej metody w kontrolerze UsersController. Nazwijmy ją Create. Niech, tak samo jak poprzednia metoda, zwraca typ danych IActionResult. Tym razem będzie zwracać metodę pomocniczą dostępną w kontrolerach, mianowicie Accepted(). Oznacza to, że status odpowiedzi serwera zostanie ustawiony na 202.

    public IActionResult Create()
    {
        return Accepted();
    }

W obecnej sytuacji akcja kontrolera nadal będzie wywoływana przez metodę GET. To nie jest poprawna sytuacja. Za pomocą atrybutu [HttpPost] zmień metodę żądania HTTP na POST.

    [HttpPost]
    public IActionResult Create()
    {
        return Accepted();
    }

Czas na dodanie argumentu User user do metody Create , tak aby pipeline .NET Core MVC mógł zbindowac przesłane dane do tego argumentu.

    [HttpPost]
    [Route("create")]
    public IActionResult Create(User user)
    {
        return Accepted();
    }

Po wykonaniu tego uruchamiamy aplikacje ponownie, aby zobaczyć czy wszystko działa poprawnie. Próbując wejść na https://localhost:5001/users/create otrzymasz błąd HTTP 404 NOT FOUND. Akcja kontrolera obsługuje żądania tylko za pomocą metody POST, natomiast przeglądarka, usiłując pobrać stronę internetową, wysyła żądanie typu GET. Aby przetestować implementację będzie potrzebny Postman. Zatrzymaj się debuggerem wewnątrz metody Create i wyślij zadanie na podany powyżej adres używając poniższych ustawień:

POST /users/create HTTP/1.1
Host: localhost:5001
Content-Type: application/json
cache-control: no-cache
Postman-Token: e8b6b140-754b-4fda-b725-4167ab9fbe35
{
    "Role": "User",
    "Email": "test@test.pl",
    "PasswordHash": "none"
}------WebKitFormBoundary7MA4YWxkTrZu0gW--

Gratulacje! Właśnie wykonałeś wszystkie zadania z ćwiczenia 2!! Jeżeli ukończyłeś je jako pierwszy, zawsze możesz pomóc innym mającym problemy. Jeżeli nikt nie zgłasza, możesz zrobić sobie krótką przerwę lub kilka dodatkowych zadań, które przygotowałem dla Ciebie:

  • Zwróć listę kilku użytkowników, najlepiej w oddzielnej akcji kontrolera. Nazwij ją GetAll
  • Utwórz listę użytkowników na poziomie konstruktora kontrolera. Następnie napisz logikę, która doda użytkownika do listy podczas wywołania akcji Create
  • Spróbuj wykorzystać bibliotekę Json.NET, aby kluczę obiektu user zwracał w json-ie, w postaci camelCase

Ćwiczenie 3 - Wstrzykiwanie Zależności

W tym ćwiczeniu wykorzystamy wcześniej utworzony projekt. Mam nadzieje, że jeszcze go masz 😉

Stworzenie Users Repository

Zacznijmy od stworzenia nowego katalogu do projektu. Niech nazwy się Persistence. Wewnątrz niego utwórz nową klasę UsersRepository. Będziemy tutaj implementować wzorzec repozytorium do przechowywania instancji użytkowników. Wszystko zrobimy za pomocą słownika w pamięci, tak abyśmy dali radę zrobić wszystkie ćwiczenia bez potrzeby łączenia się z bazą danych. Klasa UsersRepository musi implementować dwie metody. Pierwsza z nich dodaje obiekt użytkownika do słownika. Natomiast druga musi pobierać i zwracać obiekt użytkownika wyszukując go po adresie email. Jak zwykle zachęcam Ciebie do samodzielnej implementacji. Natomiast, gdyby Ci nie szło, podpowiedź znajdziesz pod spodem.

    public class UsersRepository
    {
        private Dictionary<string, User> _users = new Dictionary<string, User>();

        public User GetByEmail(string email)
        {
            _users.TryGetValue(email, out var user);
            return user;
        }

        public void Add(User user)
        {
            _users.Add(user.Email, user);
        }
    }

Modifikacja modelu uzytkownika

Będąc przy UserRepository nic nie stoi na przeszkodzie, żeby lekko zmodyfikować stworzony przez nas model użytkownika. Zmieńmy wszystkie propertisy umożliwiając modyfikację tylko z wnętrza klasy (trzeba do tego użyć akcesora dostępu private lub protected). Następnie, przenieśmy tworzenie unikalnego Id, sczytywanie dat utworzenia i modyfikacji do konstruktora modelu. Pozwoli nam to lepiej hermetyzować przechowywane dane. Nie ustawiajmy hasła w konstruktorze. Za chwile stworzymy do tego oddzielna metodę. Wprowadzone zmiany możesz zobaczyć poniżej.

    public class User
    {
        public Guid Id { get; private set; }
        public string Email { get; private set; }
        public string Role { get; private set; }
        public string PasswordHash { get; private set; }
        public DateTime CreatedAt { get; private set; }
        public DateTime UpdatedAt { get; private set; }

        public User(Guid id, string email, string role)
        {     
            Id = id;
            Email = email.ToLowerInvariant();
            Role = role.ToLowerInvariant();
            CreatedAt = DateTime.UtcNow;
            UpdatedAt = DateTime.UtcNow;
        }
    }

Przejdzmy do implementacji metody odpowiedzialnej za tworzenie hash-u hasła. Z racji, że nie chcemy przechowywać haseł Plain Textem, wykorzystamy do tego wbudowany mechanizm PasswordHasher. Jest to klasa generyczna, która jako swój typ przyjmie nasz model użytkownika. Na obiekcie PasswordHasher wywołąj metodę HashPassword przekazując do niej jako argumenty this i password.

    public void SetPassword(string password, IPasswordHasher<User> passwordHasher)
    {        
        PasswordHash = passwordHasher.HashPassword(this, password);
    }

Dostosuj klasę UsersController. Zmieniliśmy publiczne API klasy User. Wystarczy, że w metodzie Index uaktualnisz inicjalizacje obiektu do postaci

    [HttpGet]
    public IActionResult Index()
    {
        return Json(new User(Guid.NewGuid(), "test@test.pl", "user"));
    }

Stworzenie serwisu do uwierzytelniania

W ramach ćwiczenia stwórz kolejne dwa katalogi. Tym razem, na poziomie projektu, utwórz katalog Services. W środku tego katalogu zrób kolejny o nazwie Auth. To właśnie tutaj, w późniejszym czasie, będziemy pisać naszą implementację uwierzytelniania i autoryzacji użytkowników za pomocą tokenów JWT.

Później, w katalogu Auth, utwórz plik AuthService.cs. W środku stwórz klasę AuthService.

public class AuthService
{
    // ... TODO
}

W powyższej klasie utwórz konstruktor, do którego zostaną przekazane dwa argumenty: instancja klasy generycznej PasswordHasher<User> (najlepiej zrobić to za pomocą interfejsu tej klasy IPasswordHasher<User>) oraz wcześniej utworzone repozytorium użytkowników UsersRepository.

        public AuthService(IPasswordHasher<User> passwordHasher,
            UsersRepository usersRepository)
        {
            _passwordHasher = passwordHasher;
            _usersRepository = usersRepository;
        }

Instancje obiektów zostaną przekazane do klasy za pomocą kontenera DI, który domyślnie jest dodawany do aplikacji MVC. Dodajmy jeszcze jedną metodę, abyśmy mogli zarejestrować użytkownika (logowaniem zajmiemy się w sekcji dotyczącej tokenów JWT). Tworzymy to poprzez metodę, do które przekażemy takie dane jak:

  • Id (Guid)
  • Email (string)
  • Password (string)
  • Role (string)

Niech metoda nazywa się zgodnie z tym, jak podobna metoda znajduje się w paczce Microsoft.AspNetCore.Identity, `SignUpAsyc.

        public Task SignUpAsync(Guid id, string email, string password, string role)
        {
            var user = new User(id, email, role);
            user.SetPassword(password, _passwordHasher);
            _usersRepository.Add(user);
            return Task.CompletedTask;
        }

Metoda może zarówno zwracać typ Task jak i void. W tym przypadku robimy metodę asynchroniczną, ponieważ w większości przypadków utworzenie nowego użytkownika w systemie będzie wiązało się z zapytaniem do bazy danych.

Następnie wygeneruj, lub po prostu stwórz samemu, interfejs tej klasy. Jeżeli Twoje IDE na to pozwala, lepszą decyzją będzie wygenerowanie interfejsu.

    public interface IAuthService
    {
        Task SignUpAsync(Guid id, string email, string password, string role);
    }

Pamiętaj aby serwis AuthService implementował interfejs IAuthService. Inaczej dodanie go do kontenera DI może być utrudnione.

Utworzenie klasy UserSignUpResource

Przejdźmy do stworzenia obiektu DTO, który posłuży nam do obsługi request-u rejestracji użytkownika. Na potrzeby takich obiektów utwórzmy kolejny katalog w projekcie i nazwijmy go Dtos. W jego wnętrzu utwórz kolejną klasę o nazwie UserSignUpResource. Klasa ta będzie zawierać tylko credentiale oraz role, z jakimi użytkownik chce założyć konto w naszym systemie.

  • Email
  • Password
  • Role
    public class UserSignUpResource
    {
        public string Email { get; set; }
        public string Password { get; set; }
        public string Role { get; set; }
    }

Konfiguracja DI

Po wykonaniu wszystkich zmian możesz przejść do pliku Startup.cs i dodać wszystkie stworzone klasy do kontenera DI.

  • Klasę UsersRepository dodaj jako Singleton
  • Klasę AuthService dodaj jako Scoped, który implementuje interfejs IAuthService
  • Klasę PasswordHasher<User> dodaj jako Transient, który implementuje interfejs IPasswordHasher<User> Spróbuj to zrobić samemu, korzystając ze slajdów w prezentacji.

Jeżeli jednak nie udało Ci się poprawnie dodać powyższych klas do kontenera DI, poniżej masz wyjaśnienie jak to zrobić.

W pliku Startup.cs w metodzie ConfigureServices dodaj poniższy kod przed linijka services.AddMvc().

            services.AddTransient<IPasswordHasher<User>, PasswordHasher<User>>();
            services.AddSingleton(_ => new UsersRepository());
            services.AddScoped<IAuthService, AuthService>();

Stworzenie kontrolera IdentityController

Kiedy już dodałeś wszystkie potrzebne klasy do kontenera DI przechodzimy do wykorzystania ich w celu wystawienia endpoint - a, dzięki któremu użytkownik będzie mógł się zarejestrować do systemu. Przejdź do katalogu Controllers i utwórz plik IdentityController. W nim stwórz nowy kontroller dekorując go przy okazji atrybutami ApiController i Route("api/[controller]"). Coś takiego robiliśmy już wcześniej, dlatego tym razem nie otrzymasz rozwiązania na tacy 😉

W konstruktorze tego kontrolera wstrzyknij wcześniej stworzony AuthService i przypisz do zmiennej prywatnej.

        public IdentityController(IAuthService authService)
        {
            _authService = authService;
        }

Stwórz endpointa odpowiedzialnego za rejestrowanie nowych użytkowników. Ta akcja musi być wywoływana za pomocą metody POST zadania HTTP i w jej argumencie otrzymać wcześniej utworzony obiekt klasy UserSignUpResource. Wewnątrz tej akcji powinniśmy wygenerować nowe Id użytkownika i przekazać wszystkie potrzebne dane do instancji AuthService. W celu potwierdzenia, ze wszystko działa poprawnie, możesz zwrócić Id nowego użytkownika.

[HttpPost]
[Route("signUp")]
public IActionResult SignUp(UserSignUpResource resource)
{
    var userId = Guid.NewGuid();
    _authService.SignUpAsync(userId, resource.Email, resource.Password, resource.Role);
    return Ok(new { userId });
}

Testowanie rozwiązania

W tym ćwiczeniu napisaliśmy trochę kodu, dlatego warto odpalić serwer i zobaczyć, czy wszytko działa tak jak powinno. Wysyłamy przez postmana zapytanie HTTP, zgodne z poniższa specyfikacja

POST /api/identity/signup HTTP/1.1
Host: localhost:5001
Content-Type: application/json
cache-control: no-cache
Postman-Token: bd99bf1c-9f7f-4559-b048-28efebbe18de
{
	"email": "kamil.m.kielbasa@gmail.com",
	"password": "Test1234",
	"role": "Admin"
}------WebKitFormBoundary7MA4YWxkTrZu0gW--

Jeżeli w odpowiedzi otrzymałeś id nowo utworzonego użytkownika to gratulacje, właśnie ukończyłeś ćwiczenie nr 3. Jeżeli zrobiłeś jako pierwszy i nadal czekasz na grupę to mam dla Ciebie zadania dodatkowe.

  • Spróbuj obsłużyć błąd, kiedy użytkownik próbuje się zarejestrować na adres email, który już istnieje w naszej kolekcji użytkowników.
  • Spróbuj dopisać obsługę zmiany hasła przez użytkownika.

Ćwiczenie 4 - Middleware

Tym razem ćwiczeniu będziemy pracować na nowym projekcie. Ewentualnie, jeżeli nie chcesz tworzyć kolejnego projektu, użyj projektu z ćwiczenia pierwszego. Skupimy się teraz na tworzeniu Middleware-ów. Nie wiem, czy wiesz, że już miałeś okazje stworzyć swój pierwszy middleware na tych warsztatach. Tak, to było w ćwiczeniu pierwszym. Przyjrzyjmy się mu jeszcze raz:

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello .NET Core!");
    });

Jest to middleware z wykorzystaniem funkcji Run na obiekcie IApplicationBuilder. Tak jak to było powiedziane podczas prezentacji, wykonanie dodanego middleware kończy pipeline procesujący zadania HTTP na platformie .NET Core.

Przejdźmy zatem do poćwiczenia innych sposobów dodawania middleware-ów.

App.Use

Zacznij od bardzo prostego przypadku. Na podstawie parametru z QueryString-a potrzebujmy ustawić CultureInfo w naszej aplikacji. Oczywiście, nie chcemy tego robić w każdej akcji kontrolera, ponieważ wtedy duplikacja kodu byłaby olbrzymia. Dlatego tez middleware wydaje się być odpowiednim rozwiązaniem.

Przejdź do pliku Startup.cs, w nim do metody Configure. To właśnie w niej odbywa się konfiguracja pipelin-a aplikacji. Najprostszym sposobem dodania własnego middleware jest wywołanie metody app.Use(argument), która w swoim parametrze przyjmie wyrażenie lambda. Najprostsze takie wyrażenie wygląda następująco:

app.Use((context, next) => { return next(); })

Taki middleware nic nie robi, przekazuje tylko wywołanie do kolejnego middlewar-a, aż w końcu zostanie wywołany ten middleware, zarejestrowany za pomocą metody app.Run.

Wracając do sedna problemu: zakładając, że otrzymamy informacje o kulturze w QueryString-u w kluczu culture, spróbuj napisać middleware, który ustawi odpowiednie propertisy w obiekcie CultureInfo. W formie przypomnienia, krótki snippet tworzący obiekt CultureInfo

var culture = new CultureInfo("en");

Mam nadzieje, że udało Ci się. Jjeżeli nie, to rozwiązanie znajdziesz poniżej:

app.Use((context, next) =>
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);

            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }

        return next();
    });

Jeszcze, abyś mógł przetestować to rozwiązanie, dopisz kolejnego middleware-a, który wypisze DisplayName z cultureInfo.

 app.Run(async (context) =>
    {
        await context.Response.WriteAsync(
            $"Hello {CultureInfo.CurrentCulture.DisplayName}");
    });

Teraz uruchom aplikację i sprawdź jak reaguje na różne przypadki wartości w kluczu culture. Możesz przetestować następujące kombinacje:

  • en
  • pl_PL
  • en_US
  • en_EN
  • none
  • null

App.Map

W pliku Startup.cs dodaj nową, prywatną, statyczna metodę HandleMultiSeg, która przyjmie jako parametr IApplicationBuilder app. W metodzie wywołaj poniższy kod:

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map multiple segments.");
    });

Teraz wróć do metody Configure i dodaj wywołanie tego middleware na adresie /mapped. Wykorzystaj do tego metodę app.Map.

app.Map("/mapped", HandleMultiSeg);

Sprawdź, czy znajdziesz taki adres url, aby wywołały się wszystkie zdefiniowane middleware-y.

Enkapsulacja Middleware-a w oddzielnej klasie

Jako, że taka implementacja spowodowałaby bardzo duży rozrost pliku Startup.cs. Konwencją jest, aby zamykać middleware-y w osobnych klasach. Dlatego w projekcie utwórz katalog Middlewares, a w nim stwórz klasę RequestCultureMiddleware. Spróbuj, na podstawie slajdów, samemu napisać logikę tego middleware-a.

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);

            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;

        }

        // Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

Inną, dobra praktyką, jest tworzenie extension method do rejestracji Middleware. Pozwoli to utrzymać czystość w pliku Startup.cs w metodzie Configure. Z wykorzystaniem extension method ta metoda będzie wyglądać następująco:

app.UseMiddleware1();
app.UseMiddleware2();
app.UseMiddleware3();

Ewentualnie, będzie można to jeszcze bardziej skrócić, wykorzystując wzorzec chain of resposibility.

app.UseMiddleware1()
    .UseMiddleware2()
    .UseMiddleware3();

W tym przypadku wystarczy Ci poniższa implementacja extension method:

public static class RequestCultureMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestCulture(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestCultureMiddleware>();
    }
}

Teraz, w pliku Startup.cs, dodaj poniższą linijkę do metody Configure (przed linia app.UseMvc)

app.UseRequestCulture();

Ćwiczenie 5 - Routing

W aplikacji wykorzystywanej w ćwiczeniu 4 stwórz nowy kontroler. Nazwij go jakkolwiek chcesz, może być nawet TestController. Udekoruj go za pomocą atrybutu Route tak, aby automatycznie używał nazwy kontrolera i akcji do definiowania endpointów. Następnie dodaj do niego prefix api. Potem stwórz akcję kontrolera, gdzie w atrybucie Route zdefiniujesz minimum 2 parametry. Do parametrów dołóż przykładowe constraint-y. Dla przykładu:

  • Dla parametru Id dodaj constraint int ({id:int})
  • Dla parametru Name dodaj constraint alpha ({name:alpha})