Autoryzacja oparta na zasobach

Autoryzacja oparta na zasobach

Wstęp – opisanie problemu

Często zdarza się, że użytkownik musi mieć pewne uprawnienia, żeby móc pracować z pewnymi zasobami. Żeby to nie brzmiało aż tak enigmatycznie, posłużę się przykładem.

Załóżmy, że jest aplikacja do fakturowania (np. Fakturownia, której sam używam :)). Załóżmy też, że aplikacja ma kilka ról – administrator, użytkownik i super admin.

Administrator – może tworzyć organizacje, dodawać do niej użytkowników i zarządzać wszystkimi rekordami w swojej organizacji. Użytkownik może tylko przeglądać i dodawać nowe faktury. A superadmin jest „ponad to” i może zupełnie wszystko. Super adminem byłby w tym przypadku właściciel takiego serwisu.

I teraz tak. Administrator zakłada konto „Piękny Lolo”. Dodaje rekordy związane z wydatkami (dla swojej organizacji), usuwa i git. Dodaje też użytkowników pod swoje konto.

Ale nagle rejestruje się nowy administrator (z innej organizacji) – zakłada konto: „Wąsaty Jan”. I teraz jakby wyglądała sytuacja, gdybyś posługiwał się autoryzacją opartą o role? Zarówno Wąsaty Jan, jak i Piękny Lolo mają uprawnienia administratora. Więc teoretycznie Wąsaty Jan może pracować na rekordach Pięknego i vice versa. Nie chcemy tego.

Trzeba zatem ograniczyć ich działanie tylko do ich „organizacji”. W innym przypadku mamy podatność bezpieczeństwa (jest to jedna z podatności, o których piszę w swojej książce – „Zabezpieczanie aplikacji internetowych”).

Tutaj z pomocą przychodzi tzw. autoryzacja oparta na zasobach (resource based authorization).

Przykładowy projekt

Dla ułatwienia posłużymy się prostszym problemem – zrobimy standardową aplikację do zadań. Nie zaciemni to obrazu, a postępowanie jest dokładnie takie samo.

Przygotowałem już gotowe rozwiązanie, które możesz pobrać z GitHuba: https://github.com/AdamJachocki/ResourceBasedAuth

Najpierw przyjrzyjmy się mu.

To jest zwykły projekt RazorPages z ustawionym uwierzytelnianiem (Authentication Type) na Individual Accounts. Dla ułatwienia wszystko zawarłem w jednym projekcie. Pamiętaj, że w prawdziwym życiu powinieneś rozdzielić ten projekt na kilka innych.

UWAGA! To jest bardzo prosta aplikacja bez żadnych walidacji. Pokazuje właściwie najprostszy uporządkowany kod, żeby bez sensu nie zaciemniać obrazu.

Potrafi utworzyć zadanie (TodoItem), zmodyfikować i usunąć je.

Zanim uruchomisz projekt, musisz utworzyć bazę danych. W katalogu z projektem uruchom polecenie:

dotnet ef database update

Namespacey projektu

Abstractions

Zawiera interfejs ITodoItemService, który jest wstrzykiwany do RazorPages. On obsługuje wszystkie operacje na bazie danych. Są dwa serwisy, które implementują ten interfejs: SecureTodoItemService – który pokazuje operowanie na zasobach w sposób bezpieczny, a także InsecureTodoItemService – ten pokazuje działania bez żadnych zabezpieczeń.

Domyślnie działającym jest InsecureTodoItemService. Możesz to zmienić w pliku Program.cs.

Areas

To domyślna obsługa .Net Identity – zakładanie kont, logowanie itp.

Data

Głównym jej elementem jest model bazodanowy TodoItem. Poza tym zawiera migracje EfCore, a także DbContext.

Pages

Zawiera strony i komponenty – zgodnie z nomenklaturą RazorPages

Services

Zawiera potrzebne serwisy.

Działanie niezabezpieczone

Spójrz na serwis InsecureTodoItemService. Jak widzisz nie ma on żadnych zabezpieczeń ani sprawdzeń. Przykładowa metoda usuwająca zadanie wygląda tak:

public async Task RemoveItem(int id)
{
    var model = new TodoItem { Id = id };
    _db.TodoItems.Remove(model);
    await _db.SaveChangesAsync();
}

To znaczy, że właściwie każdy, kto ma konto może usunąć dowolne itemy. Wystarczy poznać ID. Nie jest to, coś co byśmy chcieli uzyskać.

Więc zajmijmy się tym.

Zabezpieczamy program

Zabezpieczenie w tym przypadku polega na sprawdzeniu, czy użytkownik, który wykonuje operację ma prawo do wykonania tej operacji na danym zasobie. Czyli w przypadku tej aplikacji – czy jest właścicielem danego zasobu.

Oczywiście można to zrobić na kilka sposobów, jednak pokażę Ci tutaj standardowy mechanizm .NET, który to zadanie ułatwia.

Krok 1 – dodawanie wymagań

Pierwszy krok jest zarówno najprostszy, jak i najcięższy do zrozumienia. Musimy dodać wymaganie (requirement). To wymaganie musi zostać spełnione, żeby użytkownik mógł przeprowadzić operację.

To wymaganie może wyglądać tak:

public class TodoItemOwnerOrSuperAdminRequirement: IAuthorizationRequirement
{

}

Zapytasz się teraz – dlaczego ta klasa jest pusta? Jaki jest jej sens? To wytłumaczyć najtrudniej. Generalnie interfejs IAuthorizationRequirement nie ma w sobie żadnych metod, właściwości… zupełnie niczego. Jest pusty. Służy głównie tylko do opisania wymagania. Samego zaznaczenia odpowiedniej klasy. Oczywiście nikt Ci nie zabroni dodać do tej klasy jakiejś logiki. Możesz też ją wstrzykiwać do swoich serwisów.

Krok 2 – dodawanie AuthorizationHandler

Drugim krokiem jest dodanie handlera, który sprawdzi, czy użytkownik może wykonać daną operację. Prosty przykład w naszej aplikacji:

public class TodoItemAuthHandler : AuthorizationHandler<TodoItemOwnerOrSuperAdminRequirement, TodoItem>
{
    private readonly LoggedUserProvider _loggedUserProvider;

    public TodoItemAuthHandler(LoggedUserProvider loggedUserProvider)
    {
        _loggedUserProvider = loggedUserProvider;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, 
        TodoItemOwnerOrSuperAdminRequirement requirement, 
        TodoItem resource)
    {
        var loggedUser = await _loggedUserProvider.GetLoggedUser();
        if(resource.OwnerId == loggedUser.Id)
            context.Succeed(requirement);
        else
            context.Fail();
    }
}

Twoja klasa musi dziedziczyć po AuthorizationHandler. AuthorizationHandler jest abstrakcyjną klasą generyczną. W parametrach generycznych przyjmuje typ wymagania, a także typ resource’a. Jest jeszcze druga jej postać, która przyjmuje tylko typ wymagania.

Musisz przesłonić tylko jedną metodę – HandleRequirementAsync. W parametrze AuthorizationHandlerContext dostajesz m.in. zalogowanego użytkownika (ClaimsPrincipal). Ja się posługuję swoim serwisem LoggedUserProvider ze względu na prostotę (w przeciwnym razie musiałbym jakoś odczytywać i zapisywać claimsy). W parametrze dostajesz również obiekt, o który pytasz.

I jeśli spojrzysz teraz do ciała tej metody, zobaczysz że sprawdzam, czy zalogowany użytkownik jest właścicielem danego zasobu. Normalnie sprawdzałbym, czy zalogowany użytkownik jest superadminem lub właścicielem zasobu. Ze względu na prostotę, pominęliśmy tutaj aspekt ról i superadmina.

I teraz, jeśli użytkownik jest superadminem lub właścicielem zasobu, przekazuję do kontekstu sukces. W przeciwnym razie blokuję.

Krok 3 – użycie AuthorizationHandler

W pierwszej kolejności musimy zarejestrować naszą klasę AuthorizationHandler, żeby móc jej używać. Rejestrujemy to oczywiście podczas rejestracji serwisów:

services.AddScoped<IAuthorizationHandler, TodoItemAuthHandler>(); //może być jako singleton, jeśli Twój serwis nie wykorzystuje innych scoped serwisów

A potem już tylko mały zastrzyk do serwisu (plik SecuredTodoItemService.cs). Wstrzykujemy interfejs IAuthorizationService:

private readonly IAuthorizationService _authService;

public SecuredTodoItemService(IAuthorizationService authService)
{
    _authService = authService;
}

I spójrz na przykładowe użycie:

public async Task ModifyItem(TodoItem item)
{
    var authResult = await _authService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, item,
        new TodoItemOwnerOrSuperAdminRequirement());
    if(authResult.Succeeded)
    {
        _db.TodoItems.Update(item);
        await _db.SaveChangesAsync();
    }
}

_httpContextAccessor to oczywiście wstrzyknięty IHttpContextAccessor, bo metoda AuthorizeAsync niestety wymaga od nas przekazania zalogowanego użytkownika (on się później znajdzie w kontekście HandleRequirementAsync z klasy dziedziczącej po AuthorizationHandler).

Generalnie klasa implementująca IAuthorizationService została zarejestrowana automatycznie podczas rejestrowania autoryzacji (AddAuthorization()). Gdy wywołujesz AuthorizeAsync, ona sprawdza typ zasobu i wymaganie, o które pytasz. Na tej podstawie wywołuje metodę HandleRequirementAsync z odpowiedniej klasy dziedziczącej po AuthorizationHandler. A ich możesz mieć wiele. Dla różnych zasobów i różnych wymagań.

Jaki z tego wniosek? Wystarczy, że napiszesz jedną klasę pod konkretny typ zasobu, który chcesz chronić.

Dodatkowe uproszczenie

Oczywiście to można jeszcze bardziej ukryć/uprościć, tworząc przykładową klasę ResourceGuard, np:

public class ResourceGuard
{
    private readonly IAuthorizationService _authService;
    private readonly IHttpContextAccessor _httpCtx;

    public ResourceGuard(IAuthorizationService authService, IHttpContextAccessor httpCtx)
    {
        _authService = authService;
        _httpCtx = httpCtx;
    }

    public async Task<AuthorizationResult> LoggedIsAuthorized<T>(object resource)
        where T: IAuthorizationRequirement, new()
    {
        var requirement = new T();
        var user = _httpCtx.HttpContext.User;

        //tu możesz sprawdzić, czy user jest super adminem albo pójść dalej:

        return await _authService.AuthorizeAsync(user, resource, requirement);
    }
}

Wykorzystanie takiej klasy byłoby już dużo łatwiejsze:

public async Task DeleteItem(TodoItem item)
{
    var authResult = await _guard.LoggedIsAuthorized<TodoItemOwnerOrSuperAdminRequirement>(item);
    if (!authResult.Succeeded)
        return;
    else
    {
        //todo: usuń
    }
}

Gdzie wstrzykujesz już tylko ResourceGuard'a.

Moim zdaniem, jeśli masz dużo zasobów do chronienia, pomysł z ResourceGuardem jest lepszy, ale to oczywiście wszystko zależy od konkretnego problemu.

Pobieranie danych

A jak sprawdzić autoryzację przy pobieraniu danych? Tutaj trzeba odwrócić kolejność. Do tej pory najpierw sprawdzaliśmy autoryzację, a potem robiliśmy operacje na danych.

W przypadku pobierania musisz najpierw pobrać żądane dane, a dopiero potem sprawdzić autoryzację, np.:

public async Task<TodoItem> GetItemById(int id)
{
    var result = await _db.TodoItems.SingleOrDefaultAsync(x => x.Id == id);
    if (result == null)
        return null;

    var authResult = await _authService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, result,
        new TodoItemOwnerOrSuperAdminRequirement());
    if (authResult.Succeeded)
        return result;
    else
        return null;
}

Jeśli musisz pobrać całą listę, to być może będziesz musiał sprawdzić każdy rekord z osobna. I potem w zależności od analizy biznesowej – albo zwracasz tylko te rekordy, do których użytkownik może mieć dostęp, albo gdy nie ma dostępu przynajmniej do jednego – nie zwracasz niczego.

UWAGA!

Zwróć uwagę, że w przykładowym projekcie, jeśli użytkownik nie ma uprawnień do wykonania operacji to albo jej nie wykonuję, albo zwracam null. W rzeczywistym projekcie dobrze jednak jest w jakiś sposób poinformować kontroler, żeby odpowiedział błędem 403 - Forbidden.


Dzięki za przeczytanie artykułu. Jeśli czegoś nie rozumiesz lub znalazłeś błąd w tekście, koniecznie daj znać w komentarzu 🙂

Daj znać w komentarzu jaką formę artykułów wolisz – taką z gotowym projektem na GitHub, który omawiam – tak jak tutaj, czy klasyczną, w której tworzymy projekt od nowa z pominięciem GitHuba.

Obrazek z artykułu: Tkanina plik wektorowy utworzone przez storyset – pl.freepik.com

Podziel się artykułem na:
Walidacja w BLAZOR!

Walidacja w BLAZOR!

Jeśli trafiłeś tu tylko po to, żeby dowiedzieć się, jak walidować złożony obiekt, zobacz końcówkę artykułu.

Kiedyś napisałem co nieco o walidacji w .NetCore MVC. Możesz te artykuły przeczytać tu i tu. Powinieneś je przeczytać, ponieważ mówią również o podstawach walidacji jako takiej (w tym o data annotation validation, którym będziemy się tu posługiwać).

Wchodząc w świat Blazor, bardzo szybko będziesz chciał tworzyć formularze i je walidować. Na szczęście w Blazor jest to chyba jeszcze prostsze niż w Razor.

Pierwszy formularz

Blazor ma wbudowany komponent, który nazywa się EditForm. Jak można się domyślić, jest to po prostu formularz. Ma on kilka ciekawych możliwości, w tym artykule skupimy się jednak tylko na walidacji.

Na początek stwórzmy bardzo prosty model – Customer. Tę klasę utworzymy oczywiście w osobnym pliku Customer.cs.

public class Customer
{
	[Required]
	[StringLength(50, MinimumLength = 5)]
	public string Name { get; set; }
}

Jest klasa z jednym polem i dodanymi adnotacjami:

  • Pole jest wymagane
  • Musi mieć długość minimum 5 znaków i maksimum 50.

Teraz utwórzmy w pliku .razor stronę z formularzem:

@page "/"
@using Models

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name" />
    </div>
    <div>
        <button type="submit">Zapisz</button>
    </div>    
</EditForm>

@code
{
    Customer Customer = new Customer();

    async Task SubmitForm()
	{

	}
}

Spójrz, co mamy w sekcji code. Tworzymy nowy obiekt dla naszego modelu. To jest ważne, ponieważ ten obiekt powiążesz z formularzem.

Teraz spójrz wyżej na komponent EditForm. To jest właściwie najbardziej podstawowa konstrukcja formularza. Mamy właściwość Model, która określa OBIEKT powiązany (zbindowany) z formularzem. Pamiętaj, że to jest obiekt utworzony w sekcji code, a nie nazwa klasy.

Dalej mamy zdarzenie OnValidSubmit. Przypisujemy tutaj metodę, która wykona się po wysłaniu formularza, gdy będzie on prawidłowy. Z tego wynika, że ta metoda nie zostanie wykonana, jeśli formularz będzie zawierał błędy (są inne zdarzenia, które do tego służą).

Dalej w formularzu masz komponent InputText, który określa powiązanie z odpowiednim polem modelu – za pomocą @bind-value – czyli typowy binding z Blazor.

No i na koniec mamy guzik do wysłania formularza – pamiętaj, że w formularzu możesz mieć TYLKO JEDEN guzik typu submit.

Dodajemy walidację do strony

Przede wszystkim trzeba powiedzieć formularzowi, żeby walidował model za pomocą DataAnnotations (można to zrobić inaczej, ale w tym artykule skupiamy się na prostych podstawach). W tym celu trzeba mu dorzucić komponent DataAnnotationsValidator:

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator/>
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name" />
    </div>
    <div>
        <button type="submit">Zapisz</button>
    </div>    
</EditForm>

Właściwie to załatwia sprawę, formularz może być już walidowany. Sprawdź to. Jeśli spróbujesz wysłać pusty formularz, wymagane pole zaświeci się na czerwono.

Komunikaty błędów

Pewnie chciałbyś uzyskać jakiś komunikat błędu? Można to zrobić na kilka sposobów. Najprościej – dodaj komponent ValidationSummary – czyli podsumowanie walidacji.

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name" />
    </div>
    <div>
        <button type="submit">Zapisz</button>
    </div>
</EditForm>

Możesz go dodać w dowolnym miejscu formularza – tutaj pojawią się po prostu komunikaty o błędach. Uruchom teraz i zobacz, co się stanie, gdy podasz nieprawidłowe dane:

Widzimy opis błędu i podświetlone konkretne pole. Przy czym zwróć uwagę, że opis błędu wskazuje konkretnie na pole o nazwie „Name” – to zostało wzięte z modelu. Można to zmienić. Wszystkie rodzaje inputów (a jest ich kilka) w EditForm mają właściwość DisplayName. Dodaj ją:

<InputText id="name" @bind-Value="Customer.Name" DisplayName="imię i nazwisko"/>

To jest po prostu przyjazna nazwa pola dla walidacji. Niestety w mojej wersji Blazor (wrzesień 2021) ta właściwość nie chce działać. Ale nic to. I tak w prawdziwym świecie posłużymy się odpowiednią adnotacją w modelu:

public class Customer
{
	[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
	public string Name { get; set; }
}

Pisałem już o tych mechanizmach w artykule o własnej walidacji w .NetCore, więc nie będę się tutaj powtarzał.

Wymagany checkbox

W artykule o własnej walidacji w .NetCore pisałem też o tym, jak zrobić wymagany checkbox. Np do akceptacji regulaminu. Wtedy trzeba było się nieźle nakombinować. Dzisiaj jest dużo prościej i zdecydowanie bardziej przyjaźnie. Spójrz na model:

public class Customer
{
	[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
	public string Name { get; set; }

	[Required]
	[Range(typeof(bool), "true", "true", ErrorMessage = "Musisz zaakceptować plitykę prywatności")]
	public bool PrivacyAgreed { get; set; }
}

I pole w formularzu:

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name"/>
    </div>
    <div>
        <InputCheckbox id="agreement" @bind-Value="Customer.PrivacyAgreed" />
        <label for="agreement">Zgoda na politykę prywatności</label>
    </div>
    <div>
        <button type="submit">Zapisz</button>
    </div>
</EditForm>

I już. Wszystko załatwione adnotacjami.

Nie wiem jak Ty, ale ja nie lubię widzieć całej listy rzeczy, które zostały źle wprowadzone. Lubię, jak każdy błąd jest wyświetlony przy konkretnym polu. Można to zrobić bardzo prosto.

Błędy przy konkretnych polach

W tym momencie możesz pozbyć się komponentu ValidationSummary. To on właśnie pokazuje pełną listę błędów. Pamiętaj jednak, żeby zostawić DataAnnotasionsValidator, który odpowiada za walidację.

Teraz do każdego walidowanego pola możesz dodać komponent ValidationMessage:

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator />
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name"/>
        <ValidationMessage For="() => Customer.Name" />
    </div>
    <div>
        <InputCheckbox id="agreement" @bind-Value="Customer.PrivacyAgreed" />
        <label for="agreement">Zgoda na politykę prywatności</label>
        <ValidationMessage For="() => Customer.PrivacyAgreed"/>
    </div>
    <div>
        <button type="submit">Zapisz</button>
    </div>
</EditForm>

Oczywiście te komponenty możesz umieścić gdziekolwiek w formularzu. Za pomocą właściwości For określasz, do którego pola się odnoszą. Zwróć uwagę, że przekazujesz tam lambdę, a nie nazwę pola

Wszystko fajnie, tylko że brzydko. A co, jeśli chcielibyśmy trochę popracować nad wyglądem komunikatów? Można to zrobić. W standardowym projekcie znajduje się plik site.css (w katalogu wwwroot/css). Jak sobie popatrzysz do środka, zobaczysz taki styl:

.validation-message {
    color: red;
}

To właśnie odpowiada za wygląd komunikatów. Możemy to zmienić, np:

.validation-message {
    color: #8a0000;
    font-size: smaller;
    font-weight: bold;
    margin-bottom: 20px;
}

Teraz, to wygląda tak:

Walidacja obiektu złożonego

Co ma każdy klient? Każdy klient ma swój adres. Stwórzmy teraz bardzo prosty model adresu:

public class Address
{
	[Required(ErrorMessage = "Musisz podać nazwę ulicy")]
	public string StreetName { get; set; }
	[Required(ErrorMessage = "Musisz podać numer ulicy")]
	public string StreetNumber { get; set; }
	[Required(ErrorMessage = "Musisz podać miasto")]
	public string City { get; set; }
}

I dodajmy go do modelu klienta, a także do formularza:

public class Customer
{
	[Required(ErrorMessage = "Musisz wypełnić imię i nazwisko")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "Imię i nazwisko musi być dłuższe niż 5 znaków i krótsze niż 50")]
	public string Name { get; set; }

	[Required]
	[Range(typeof(bool), "true", "true", ErrorMessage = "Musisz zaakceptować plitykę prywatności")]
	public bool PrivacyAgreed { get; set; }

    public Address Address { get; set; } = new Address();
}

Formularz:

<EditForm Model="Customer" OnValidSubmit="SubmitForm">
    <DataAnnotationsValidator />
    <div>
        <label for="name">Imię i nazwisko:</label>
        <InputText id="name" @bind-Value="Customer.Name" />
        <ValidationMessage For="() => Customer.Name" />
    </div>
    <div>
        <InputCheckbox id="agreement" @bind-Value="Customer.PrivacyAgreed" />
        <label for="agreement">Zgoda na politykę prywatności</label>
        <ValidationMessage For="() => Customer.PrivacyAgreed" />
    </div>

    <div>
        <label for="street">Ulica:</label>
        <InputText id="street" @bind-Value="Customer.Address.StreetName" />
        <ValidationMessage For="() => Customer.Address.StreetName" />
    </div>

    <div>
        <label for="street_no">Nr ulicy:</label>
        <InputText id="street_no" @bind-Value="Customer.Address.StreetNumber" />
        <ValidationMessage For="() => Customer.Address.StreetNumber" />
    </div>

    <div>
        <label for="city">Miasto:</label>
        <InputText id="city" @bind-Value="Customer.Address.City" />
        <ValidationMessage For="() => Customer.Address.City" />
    </div>

    <div>
        <button type="submit">Zapisz</button>
    </div>
</EditForm>

I co? I nie działa…

Okazuje się, że Blazor ma pewne problemy z takimi polami, chociaż być może jest to celowe działanie. Jeszcze dosłownie niedawno trzeba było tworzyć obejścia. Jednak teraz mamy to PRAWIE w standardzie:

  1. Pobierz NuGet: Microsoft.AspNetCore.Components.DataAnnotations.Validation. UWAGA! Na wrzesień 2021 ten pakiet nie jest jeszcze oficjalnie wydany. Jest to prerelease. A więc możliwe, że będziesz musiał zaznaczyć opcję Include Prerelase w NuGet managerze:
  1. W swoim modelu Customer oznacz pole Address atrybutem: ValidateComplexType. Pamiętaj, że jest to składnik pobranego właśnie NuGeta. Jeśli więc masz modele w innym projekcie, musisz tam też pobrać ten pakiet.
  2. W razor zmień DataAnnotationsValidator na ObjectGraphDataAnnotationsValidator

I to na tyle. Teraz wszystko już działa. ObjectGraphAnnotationsValidator sprawdzi walidacje wszystkich zwykłych pól, jak i tych „kompleksowych”.

Wbudowane kontrolki

Powiem jeszcze na koniec o kontrolkach formularzy wbudowanych w Blazor. Oczywiście możemy tworzyć własne jeśli będzie taka potrzeba (czasami jest), jednak do dyspozycji na starcie mamy:

  • InputText
  • InputCheckbox
  • InputDate
  • InputFile
  • InputNumber
  • InputRadio
  • InputRadioGroup
  • InputSelect
  • InputTextArea

To tyle, jeśli chodzi o podstawy walidacji w Blazor. Jeśli masz jakieś pytania lub znalazłeś błąd w artykule, podziel się w komentarzu.

Podziel się artykułem na: