Własna walidacja w .NetCore – czy użytkownik wyraził zgodę na regulamin?

Własna walidacja w .NetCore – czy użytkownik wyraził zgodę na regulamin?

Pokażę Ci jak zrobić własną walidację na bardzo użytecznym przykładzie. Zrobimy mechanizm sprawdzenia, czy użytkownik wyraził zgodę na regulamin. Jeśli nie wiesz, jak działa walidacja w .NetCore, sprawdź najpierw ten artykuł.

Stworzenie własnej walidacji składa się z dwóch kroków.

  • utworzenie własnego atrybutu
  • dodanie sprawdzenia po stronie JavaScript.

Zasadniczo jest to dość proste, ale trzeba pamiętać o pewnej rzeczy… o tym później

Walidacja po stronie klienta

Required nie wystarczy

W .NET można tworzyć własne atrybuty. To wiadomo. Atrybut to po prostu klasa, która dziedziczy po specyficznej klasie i zawiera… konkretne atrybuty 🙂

Z jakiegoś powodu w .NetCore nie ma domyślnie możliwości sprawdzenia, czy checkbox został zaznaczony. Wydawać by się mogło, że wystarczy:

class RegisterUserViewModel
{
	[Required]
	public bool TermsAndConditions { get; set; }
}

Widziałem też takie przykłady:

class RegisterUserViewModel
{
	[Range(typeof(bool), "true", "true")]
	public bool TermsAndConditions { get; set; }
}

niestety z checkboxem i jego wartością jest nieco inaczej. I powinien zostać do tego stworzony nowy atrybut. Rozwiązanie, które tutaj podaję zaproponowałem do Microsoftu. Być może w kolejnych wersjach wdrożą coś analogicznego.

Dopisane: W Blazor można walidować checkbox za pomocą atrybutu Required

Tworzenie własnego atrybutu

Zatem stwórzmy własny atrybut walidacyjny. Takie atrybuty powinny dziedziczyć po klasie ValidationAttribute. Jest to klasa abstrakcyjna, która zawiera pewną wspólną logikę dla wszystkich walidacji.

ValidationAttribute zapewnia walidację po stronie serwera. Jeśli chcesz mieć zapewnioną dodatkowo walidację po stronie klienta, musisz zaimplementować interfejs IClientModelValidator.

IClientModelValidator

Tutaj na chwilę się zatrzymamy. Walidacja po stronie klienta zawsze odbywa się za pomocą JavaScript (lub podobnych). .NetCore to znacznie ułatwia, gdyż wprowadza własny prosty mechanizm. Spójrz na ten prosty formularz:

<form asp-action="Register" method="Post">
	<div class="form-group">
		<label for="email">Podaj swój email:</label>
		<input class="form-control" type="email" asp-for="UserName" />
		<span asp-validation-for="UserName" class="text-danger"></span>
	</div>
</form>

Widzisz tutaj pomocnicze tagi: asp-for i asp-validation-for. W wynikowym HTMLu będzie to wyglądało mniej-więcej tak:

<div class="form-group">
	<label for="email">Podaj swój email:</label>
	<input class="form-control" type="email" data-val="true" data-val-email="The Email field is not a valid e-mail address." data-val-required="The UserName field is required." id="UserName" name="UserName" value="">
	<span class="text-danger field-validation-valid" data-valmsg-for="UserName" data-valmsg-replace="true"></span>
</div>

Jak widzisz, tagi pomocnicze trochę tutaj nagrzebały w kodzie. Nagrzebały pod kątem Microsoftowej biblioteki jQuery Unobtrusive Validation. Ta biblioteka, można powiedzieć, „szuka” atrybutów data-val (skrót od „data validation”), których wartość jest ustawiona na true. Oznacza to, że takie pole podlega walidacji. Następnie tworzone są odpowiednie komunikaty, gdy walidacja się nie powiedzie:

  • data-val-email – komunikat dla atrybutu [EmailAddress]
  • data-val-required – komunikat dla atrybutu [Required]

Te komunikaty są później umieszczane w elemencie, który ma data-valmsg-for i data-valmsg-replace (true).

To się dzieje automatycznie – Microsoft porobił takie mechanizmy. Tutaj wchodzi interfejs IClientModelValidator. Zajmijmy się najpierw nim:

Implementacja IClientModelValidator

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class CheckBoxCheckedAttribute : ValidationAttribute, IClientModelValidator
{
	public void AddValidation(ClientModelValidationContext context)
	{
		if (context == null)
		{
			throw new ArgumentNullException(nameof(context));
		}

		MergeAttribute(context.Attributes, "data-val", "true");
		MergeAttribute(context.Attributes, "data-val-chboxreq", "Nie zaznaczyłeś checkboxa!");
	}
	
	private static void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
	{
		if (!attributes.ContainsKey(key))
		{
			attributes.Add(key, value);
		}
	}
}

context.Attributes to słownik, który zawiera atrybuty dodane już do pola

Użycie atrybutu

Utworzyliśmy klasę CheckBoxCheckedAttribute, implementując interfejs IClientModelValidator.
Teraz tak. Tag pomocniczy asp-for sprawdza wszystkie walidacje na danym polu, czyli jeśli mamy model:

class RegisterUserViewModel
{
	[CheckBoxChecked]
	public bool TermsAndConditions { get; set; }
}

wtedy asp-for weźmie wszystkie klasy atrybutów dla danego pola, które implementują interfejs IClientModelValidator i uruchomi metodę AddValidation.

To, co „zwróci” ta metoda, będzie dopisane do pola . Jak widzisz, zadaniem metody AddValidation jest właściwie tylko odpowiednie dodanie atrybutów do pola input, a więc:

  • data-val usatwione na true
  • data-val-chboxreq – którego wartością jest odpowiedni komunikat. Czyli teraz dopiszemy checkboxa w formularzu:
<form asp-action="Register" method="Post">
	<div class="form-group">
	    <div class="form-check">
	        <input type="checkbox" class="form-check-input" asp-for="TermsAndConditions" />
	        <label class="form-check-label" asp-for="TermsAndConditions">Akceptuję warunki regulaminu(acceptLabel)</label>
	    </div>
	    <span asp-validation-for="TermsAndConditions" class="text-danger"></span>
	</div>	
</form>

A ostatecznie pole input będzie wyglądało podobnie do tego:

<input type="checkbox" class="form-check-input" data-val="true" data-val-chboxreq="Nie zaznaczyłeś checkboxa!" />

Zauważ, że klasa CheckBoxCheckedAttribute dodała w metodzie AddValidation te dwa atrybuty (data-val i data-val-chboxreq), które są wymagane do poprawnego działania walidacji po stronie klienta.

Oczywiście nie musisz pisać nazywać tego data-val-chboxreq. Ważne, żeby było data-val- i w miarę unikalna, jednoznaczna końcówka. Równie dobrze mógłbyś napisać: data-val-checkbox-zaznaczony i sprawdzać później wartość tego atrybutu.

Czyli podsumowując tę część:

Tag asp-for powoduje wywołanie metody AddValidation z interfejsu IClientModelValidator, która jest odpowiedzialna za dodanie atrybutów walidacyjnych do pola . Oczywiście nic nie stoi na przeszkodzie, żebyś dodał tam jeszcze inne atrybuty. Pamiętaj, że musisz atrybut implementujący IClientModelValidator dodać do odpowiedniego pola w swoim modelu (viewmodelu).

OK, skoro już wiesz do czego służy IClientModelValidator, to teraz polecimy dalej. Pierwsza część walidacji jest zrobiona. Teraz druga. JavaScript.

Walidacja w JavaScript

Jak już mówiłem, Microsoft ma tą swoją bibliotekę jQuery Unobtrusive Validation, która działa razem z jQuery Validation.

Ten fragment kodu możesz dodać albo tylko na stronie z formularzem, albo lepiej – w widoku _ValidationScriptsPartial. Wtedy będzie dostępny zawsze, gdy dodajesz jakąś walidację:

<script>
	$.validator.addMethod("chboxreq", function (value, element, param) {
	    if (element.type != "checkbox")
	        return false;
	
	    return element.checked;
	});
	
	$.validator.unobtrusive.adapters.addBool("chboxreq");	
</script>
  • najpierw dodajemy metodę do walidatora jQuery.
  • metoda ma zostać uruchomiona, gdy trafi na atrybut data-val-chboxreq. Zauważ, że w parametrze podajemy tylko tę ostatnią część – chboxreq.
  • funkcja przyjmuje 3 parametry – wartość (atrybut value elementu HTML), element – czyli element HTML i param – dodatkowy parametr.

Jak widzisz funkcja walidująca jest bardzo prosta – najpierw jest sprawdzenie, czy element jest checkboxem, potem funkcja sprawdza, czy checkbox jest zaznaczony.

Aby to wszystko zadziałało, trzeba jeszcze dodać ten „adapter”, używając $.validator.unobtrusive.adapters
Nie będę tutaj opisywał walidatorów w jQuery, bo:

  1. Mógłbym zaciemnić obraz
  2. Nie znam się na nich
  3. Możesz sam wyszukać w dokumentacjach, jeśli będziesz chciał robić jakieś dziwne rzeczy.

I teraz pewna uwaga, o której pisałem na początku. Ten cały kod nie chciał mi kiedyś zadziałać i męczyłem się chyba 2 godziny zanim wpadłem dlaczego (oczywiście nie byłbym sobą, gdybym nie zgłosił tego do Microsoftu :)). Ten kod nie zadziała z poziomu TypeScript. Nie wiem, czemu. Po prostu MUSI być bezpośrednio w JavaScript.

Walidacja po stronie serwera

Aby wszystko zadziałało poprawnie, musimy dodać jeszcze walidację po stronie serwera. Zauważ, że walidacja kliencka została zrobiona w JavaScript. Walidacja po stronie serwera odbywa się już bezpośrednio w klasie CheckBoxCheckedAttribute. Wystarczy przesłonić metodę IsValid:

public override bool IsValid(object value)
{
	return (value is bool && (bool)value);
}

To tyle.

Komunikaty o błędach

Teraz mała uwaga na koniec. Klasa ValidationAttribute zawiera już pewne mechanizmy, które pomagają.
Jak już wiesz, każdy atrybut walidacyjny z .NET ma pewne właściwości: ErrorMessage, ErrorMessageResourceName, ErrorMessageResourceType… Bierze się to właśnie z klasy ValidationAttribute. Zatem klasa CheckBoxCheckedAttribute też ma te właściwości. Więc możesz napisać:

class RegisterUserViewModel
{
	[CheckBoxChecked(ErrorMessage = "Musisz zaakceptować warunki")]
	public bool TermsAndConditions { get; set; }
}

lub (w aplikacji wielojęzycznej):

class RegisterUserViewModel
{
	[CheckBoxChecked(ErrorMessageResourceName = nameof(LangRes.ValidationError_AcceptTerms), ErrorMessageResourceType = typeof(LangRes))]
	public bool TermsAndConditions { get; set; }
}

Jeśli nie wiesz, jak zrobić lokalizację do swojego programu, sprawdź ten artykuł

wOczywiście musisz pamiętać, żeby w metodzie AddValidation zwrócić poprawny komunikat. I teraz uwaga. Niezależnie od tego, czy posłużysz się ErrorMessage, czy ErrorMessageResourceName i ErrorMessageResourceType, klasa ValidationAttribute przechowa już dla Ciebie konkretny komunikat. Trzyma to we właściwości ErrorMessageString, a więc zamiast komunikatu bezpośrednio w kodzie powinieneś się posłużyć tą właściwością:

public void AddValidation(ClientModelValidationContext context)
{
	if (context == null)
	{
		throw new ArgumentNullException(nameof(context));
	}

	MergeAttribute(context.Attributes, "data-val", "true");
	MergeAttribute(context.Attributes, "data-val-chboxreq", ErrorMessageString);
}

To tyle.
Jeśli masz jakieś wątpliwości lub znalazłeś błąd w artykule, podziel się w komentarzu.

Podziel się artykułem na:
Walidacja danych w asp mvc .netcore 3

Walidacja danych w asp mvc .netcore 3

Wstęp

Ten artykuł opisuje czym jest walidacja danych i jak ją zastosować poprawnie w .Net. Jeśli trafiłeś na ten artykuł, szukając, jak w razor zrobić wymaganego checkboxa, to sprawdź ten artykuł.

Na szybko

Atrybuty walidacyjne w modelu:

  • Porównanie dwóch pól – [Compare(„InnePole”)]
  • Maksymalna ilość znaków – [MaxLength(50)]
  • Minimalna ilość znaków – [MinLength(10)]
  • Sprawdzenie wieku – [Range(Minimum = 18)]
  • Pole wymagane – [Required]

Trochę teorii

Czym jest walidacja danych

Walidacja to po prostu sprawdzenie poprawności danych podanych przez użytkownika. Walidacja jest ściśle powiązana z jakimś formularzem, który użytkownik wypełnia. Może być to po prostu rejestracja nowego konta. Wtedy taka walidacja polega na sprawdzeniu, czy użytkownik wypełnił wszystkie wymagane pola, czy jego hasło i nazwa użytkownika spełniają nasze założenia (np. musi być wielka litera albo nazwa użytkownika musi być adresem e-mail) no i oczywiście, czy zaakceptował naszą politykę prywatności i regulamin 🙂

Walidacja danych jest potrzebna żeby nie dopuścić do sytuacji, w której w bazie danych znajdują się głupoty. Zapewnia też większą spójność danych, a także chroni przed pewnymi atakami.

Także każdy formularz wypełniany przez użytkownika powinien być zwalidowany. Pamiętaj, że użytkownik może wpisać wszystko, co mu się podoba. To na Tobie w pewnym sensie leży odpowiedzialność sprawdzenia, czy to co wpisał ma sens.

Są dwa „tryby” walidacji. Po stronie serwera i po stronie klienta.

Walidacja danych po stronie klienta

Walidacja po stronie klienta następuje przed wysłaniem danych do serwera. Czyli np. w przeglądarce internetowej lub aplikacji. Mamy formularz rejestracji, użytkownik wciska guzik „Rejestruj” i w tym momencie musimy sprawdzić poprawność danych. Jeśli dane nie są poprawne, wtedy pokazujemy komunikat. Jeśli uznamy, że są ok – wysyłamy je do serwera. W aplikacjach internetowych takim sprawdzeniem zajmuje się np. JavaScript. Czyli musimy napisać odpowiedni kod w tym… bleee… języku, który sprawdzi nam poprawność wpisanych danych.

Walidacja danych po stronie serwera

No i tutaj mamy to, co backendowcy lubią najbardziej. Czyli dostajemy dane od klienta i za chwilę wrzucimy je do bazy danych. Ale, ale… Jeden z moich profesorów na studiach mawiał:

„Kto szybko daje, ten dwa razy daje”

Zatem nie możemy do końca ufać danym, które otrzymaliśmy. Musimy sprawdzić je drugi raz. I tu wchodzi walidacja danych po stronie serwera – dopiero jeśli tutaj upewnimy się, że wszystko jest ok, możemy wbić dane do bazy lub zrobić z nimi coś innego.

Czyli krótko podsumowując, proces powinien wyglądać tak:

  • Użytkownik wklepuje dane i wciska guzik „OK”
  • Następuje walidacja po stronie klienta (JavaScript)
  • Jeśli walidacja jest ok, to wysyłamy dane do serwera, jeśli nie, to mówimy użytkownikowi, że coś zje… źle wprowadził
  • Po stronie serwera odbieramy dane i SPRAWDZAMY JE JESZCZE RAZ
  • Jeśli dane są ok, to wbijamy je do bazy danych i odpowiadamy klientowi: „Ok, wszystko się udało”. Jeśli dane są złe, odpowiadamy: „Hola hola, coś tu źle wprowadziłeś”.

Taka podwójna walidacja danych nie jest w prawdzie konieczna. Możesz tworzyć systemy jak Ci się podoba. Ale jeśli chcesz ograniczyć dziury, błędy i podatności na ataki w swoim systemie, podwójna walidacja jest obowiązkiem. Pamiętaj, że nie zawsze dostaniesz dane z własnej aplikacji. Czasem ktoś po prostu wyśle „na pałę”. Dlatego walidacja po stronie serwera jest konieczna. Nie opłaca się też nikomu wysyłać na serwer danych, które wiadomo, że są niepoprawne. Bardziej opłaca się sprawdzić je po stronie klienta przed wysłaniem.

Trochę praktyki

Walidacja danych w .NetCore

Na szczęście .NetCore ma pewne mechanizmy do walidacji danych, które teraz pokrótce Ci pokażę. Walidacja w .NetCore składa się z trzech etapów:

  • odpowiednie przygotowanie modelu
  • wywołanie walidacji po stronie klienta
  • wywołanie walidacji po stronie serwera

Przygotowanie modelu

Załóżmy, że mamy klasę, która przechowuje dane rejestracyjne użytkownika (model), możemy jej poszczególne właściwości ubrać w konkretne atrybuty. To jest tzw. „annotation validation„, czyli
walidacja obsługiwana za pomocą „adnotacji”.

Spójrzmy na tę „gołą” klasę:

class RegisterUserViewModel
{
    public string UserName { get; set; }
	public string Password { get; set; }
	public string RepeatedPassword { get; set; }
}

Musimy się upewnić, że:

  • wypełniona jest nazwa użytkownika
  • wypełnione jest hasło
  • podane hasła są identyczne

Za pierwsze 2 założenia odpowiada atrybut Required

//using System.ComponentModel.DataAnnotations;

class RegisterUserViewModel
{
    [Required]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	public string RepeatedPassword { get; set; }
}

Teraz, gdy uruchomimy proces walidacji, sprawdzi on te pola. To wygląda mniej więcej tak:

Walidator: Cześć obiekcie, jakie masz pola?
Obiekt: No hej, mam UserName, Password i PasswordRepeated
Walidator: Ok, jakie masz atrybuty walidacyjne na polu UserName?
Obiekt: Required
Walidator: Hej wielki walidatorze Required! Czy pole UserName w danym obiekcie spełnia Twoje założenia?

Wtedy walidator Required sprawdza. Taki walidator mógłby wyglądać w taki sposób (zakładając, że byłby tylko dla stringa, ale jest dla innych typów też):

return !string.IsNullOrWhitespace(value);

Walidator zrobi analogiczne sprawdzenie przy pozostałych polach.

Ok, teraz w drugim kroku chcemy, żeby pole UserName było poprawnym adresem e-mail. Można się do tego posłużyć atrybutem…. EmailAddress:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	public string RepeatedPassword { get; set; }
}

(jak widzisz, jedno pole może mieć wiele atrybutów walidacyjnych)

Walidacja adresu e-mail

Poświęciłem na to osobny akapit (chociaż zastanawiam się nad artykułem). Zasadniczo wiemy, jakie są poprawne adresy e-mail:

  • a@b.c
  • jkowalski@gmail.com
  • user@serwer.poczta.pl

Natomiast niepoprawnymi mogą być:

  • @@@@
  • jkowalski(at)gmail.com
  • pitu-pitu@pl

Po staremu

Do wersji .NetFramework 4.7.2 atrybut EmailAddress działał tak, że sprawdzał adres e-mail za pomocą wyrażeń regularnych. Wyrażenia regularne mają jedną mała wadę – są stosunkowo drogie, jeśli chodzi o zasoby – wolne. To jest pewna furtka dla ataku DoS (denial of service). Atak ten polega na przeciążeniu serwera, żeby nie służył już innym użytkownikom.

Okazało się, że duża ilość stringów, a co gorsza dużych stringów, przepychana przez ten walidator, może właśnie mieć takie działanie. Wyrażenia regularne żrą sporo, więc duża ilość dużych stringów może
zablokować serwer. Dlatego Microsoft zmienił trochę sposób działania tego walidatora.

Po nowemu

każdy string, który ma tylko jedną małpę i nie jest ona ani na końcu, ani na początku jest poprawnym adresem e-mail, czyli:

  • a@b – poprawny
  • ja@cie.krece – poprawny
  • @blbla – niepoprawny
  • abc@ – niepoprawny

No i tutaj właśnie zapala się lampka: „a@b” ma być poprawnym e-mailem? No właśnie nie do końca. Ale to sprawdzenie miało być szybkie. Jest szybkie. Ale skoro jest szybkie, to jest też proste. Zasadniczo sprawdza czy podany string MOŻE być poprawnym adresem e-mail.

Można to jednak zmienić. W pliku appsettings trzeba dodać taką linijkę:

<add key="dataAnnotations:dataTypeAttribute:disableRegEx" value="false"/>

Wtedy sprawdzenie adresu e-mail będzie po staremu za pomocą wyrażeń regularnych. Stosuj tylko wtedy, kiedy wiesz co robisz. W gruncie rzeczy to użytkownik powinien podać Ci swój właściwy adres e-mail.

Sprawdzenie hasła

Ok, skoro załatwiliśmy już e-mail, sprawdźmy teraz czy użytkownik podał dwa razy to samo hasło, czy może coś znowu schrzanił:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
}

Jak widzisz odpowiada za to atrybut Compare. W parametrze (otherProperty) podajemy nazwę pola z tej klasy, z którą ma zostać to pole porównane. Tutaj „Password”. Czyli porównamy pole „RepeatedPassword” z polem „Password”. Tylko tutaj też uwaga. Jeśli chodzi o porównanie dwóch stringów, to wykorzystywana jest tu metoda Equals z klasy string. Ta metoda nie jest wrażliwa na ustawienia językowe. Tzn. że w pewnych językach i w pewnych warunkach może stwierdzić, że dwa różne stringi są takie same lub dwa takie same stringi są różne.

Sprawdzenie wieku

Teraz dodatkowo możemy upewnić się, czy użytkownik jest pełnoletni. Do tego może posłużyć atrybut Range, który oznacza, że wartość powinna być z konkretnego zakresu. A więc użytkownik może na przykład podać swój wiek:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(18, 50)]
	public int Age { get; set; }
}

W powyższym przykładzie sprawdzamy, czy wiek użytkownika jest między 18 a 50 lat. Atrybut Range musi być zastosowany z typem int lub datą. Przy czym stosowanie go z datą nie ma raczej uzasadnienia,
ponieważ musielibyśmy wklepać ją na sztywno, co może przysporzyć sporych problemów w przyszłości. Dlatego stosuj Range raczej do zmiennych int. Możesz podać też samo minimum lub maksimum:

class RegisterUserViewModel
{
    [Required]
	[EmailAddress]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(Minimum = 18)]
	public int Age { get; set; }
}

(analogicznie istnieje właściwość Maximum)

Pewnie chciałoby się zapytać – jak atrybut range traktuje swoje minimum i maksimum. Czy od minimum, czy minimum jest wykluczone? Możesz sam to sprawdzić, pisząc odpowiedni kod, do czego Cię zachęcam 🙂 Jeśli jednak trafiłeś tutaj tylko po to, żeby dowiedzieć się tej konkretnie rzeczy, to już mówię – minimum i maksimum są dopuszczone. Czyli minimum jest pierwszą liczbą, która przechodzi sprawdzenie, a maksimum – ostatnią.

Jest jeszcze kilka atrybutów, które mogą Ci się przydać. Każdy z nich jest opisany na stronach Microsoftu:

https://docs.microsoft.com/pl-pl/dotnet/api/system.componentmodel.dataannotations.validationattribute?view=net-5.0

Walidacja danych po stronie serwera – jak?

Generalnie w .NetCore nie ma chyba nic prostszego. Robi się to w kontrolerze (to jedna z możliwości) – niezależnie od tego, czy pracujesz nad WebApi, czy nad stroną (Razor Views). Działa to tak:

//przykład dla RazorView, dla WebAPI jest to analogicznie
public class MyController: Controller
{
	[HttpPost]
	public async Task<IActionResult> RegisterUser(RegisterUserViewModel model)
	{
	    if(!ModelState.IsValid)
			return BadRequest();
	}
}

I w zasadzie to wszystko. Kontroler ma taki obiekt ModelState, który przechowuje informacje na temat poprawności przekazanego modelu. Właściwość IsValid określa, czy model jest poprawnie wypełniony (true), czy nie (false). Możesz też poznać wszystkie błędy obecne w modelu, ale uwaga. Na tym etapie (tuż przed dodaniem na serwer) raczej nie informowałbym użytkownika o szczegółowych błędach (chociaż to zależy od Ciebie – Ty wiesz co klient usiłuje zrobić i jak istotne i tajne powinny być dane w każdym przypadku).

Jesteśmy w końcu na serwerze, a ktoś te dane na serwer musiał wysłać. Więc albo zrobił to źle klient – wtedy musimy poprawić klienta (bo np. zabrakło walidacji po stronie klienta), albo ktoś próbuje nam w jakiś sposób zaszkodzić. Przy WebAPI jest jeszcze inna opcja – ktoś po prostu tworzy aplikację i nie poradził sobie z poprawnym wysłaniem danych… No cóż… Musi doczytać w dokumentacji.

Jeśli chcesz dowiedzieć się, jak automatycznie walidować ModelState, sprawdź ten artykuł.

Walidacja w RazorPages wygląda też analogicznie. Tutaj obiekt ModelState też istnieje z tym, że w klasie PageModel, np:

public class MyPage: PageModel
{
	public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
			return BadRequest();
			
        return Page();
    }
}

Walidacja danych po stronie klienta

Tutaj kwestia jest też zasadniczo prosta, jeśli chodzi przynajmniej o walidację typową – dostępną w .NetCore.
Gdzieś tam na początku mówiłem, że walidacja po stronie klienta wymaga JavaScriptu. I to niestety prawda. Na szczęście Microsoft stworzył taką bibliotekę jQuery unobtrusive validation.
Ona jest stworzona w taki sposób, żeby współdziałać z widokami dzięki TagHelperom.

Jeśli nie wiesz, czym są TagHelpers, to dosłownie „Tagi pomocnicze” – tagi w sensie tagi html. Przeczytaj ten artykuł, żeby dowiedzieć się więcej.

Przygotowanie

UWAGA! Żeby to zadziałało, musisz dodać do strony 3 skrypty:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>

(pamiętaj o odpowiednich wersjach bibliotek :))

Pierwszy z nich masz raczej na „dzień dobry” w widoku _Layout.cshtml, pozostałe w _ValidationScriptsPartial.cshtml. Więc domyślnie wystarczyłoby, żebyś dodał na początku strony:

<partial name="_ValidationScriptsPartial" />

Walidacja formularza

Na początek przekazać do widoku konkretny model:

@model RegisterUserViewModel

Następnie musimy zwalidować konkretne pola w formularzu, np:

<form asp-action="Register" method="Post">
	<div class="form-group">
		<label for="email">Podaj swój email:</label>
		<input class="form-control" type="email" asp-for="UserName" />
		<span asp-validation-for="UserName" class="text-danger"></span>
	</div>
</form>

Powyżej masz fragment formularza rejestracyjnego. Jeśli nie znasz bootstrap, to tłumaczę pokrótce:
pierwszy div – tworzy „grupę” kontrolek – w tym przypadku label, input i jakiś span (Etykietę, pole do wprowadzenia danych i jakiś span).

Etykieta to po prostu tekst zachęty: „Podaj swój email:”.

asp-for

Teraz pole do wprowadzenia danych – input. Tutaj pojawiła się nowa rzecz – tag pomocniczy „asp-for”. Jeśli wpiszesz sobie asp-for, to Intellisense pokaże Ci wszystkie pola w Twoim modelu. Skąd wie, co jest Twoim modelem? No przecież mu pokazałeś na początku widoku:

@model RegisterUserViewModel

asp-for tworzy pewnego rodzaju powiązanie między kontrolką HTML, a polem w Twoim modelu. Czyli to, co użytkownik wpisze do tej kontrolki, AUTOMAGICZNIE trafi do pola UserName w Twoim modelu. Niczego nie musisz przepisywać. No złoto…

Ale to nie wszytko. Zapewnia to też walidację. Czyli automagicznie zostanie sprawdzone Twoje pole pod kątem poprawności (w tym wypadku Required i EmailAddress).

Komunikaty o błędach

Jeśli walidacja przejdzie, to formularz zostanie wysłany, jeśli nie, no to jakoś użytkownikowi wypadałoby powiedzieć, że znowu coś schrzanił. I od tego mamy ten tajemniczy SPAN.

Zauważ na początek jedną rzecz – span ma tag otwarcia i zamknięcia. Nie możesz napisać tak:

<span asp-validation-for... />

bez tagu zamknięcia coś może nie zadziałać (może być różnie na różnych wersjach). Więc musi być tag zamknięcia.

Ten SPAN wyświetli informacje, jeśli pole zostanie błędnie wypełnione (nie przejdzie walidacji). Tag „asp-validation-for” mówi po prostu dla jakiego pola ma pokazać się informacja o błędzie. Żeby nie zrobić użytkownikowi mindfucka, podaliśmy tutaj pole UserName. Klasa text-danger to jest po prostu bootstrapowa klasa, która powoduje, że wyświetlony tekst będzie w kolorze czerwonym.


Czyli podsumowując:

  • label – etykieta dla pola, mówiąca użytkownikowi co ma wpisać
  • input z tagiem asp-for – pole do wpisania
  • span z tagiem asp-validation-for – informacja w przypadku błędu.

No właśnie, ale jaka informacja? .NetCore pokaże po prostu domyślne info takie, jakie zaprogramowali w Microsofcie. Ale MOŻESZ ustawić własne powiadomienia. Wróćmy do modelu:

class RegisterUserViewModel
{
    [Required(ErrorMessage="Nie, nie, nie. To pole MUSISZ wypełnić")]
	[EmailAddress(ErrorMessage="A takiego! Nie podałeś prawidłowego e-maila")]
	public string UserName { get; set; }
	
	[Required]
	public string Password { get; set; }
	
	[Required]
	[Compare("Password")]
	public string RepeatedPassword { get; set; }
	
	[Required]
	[Range(Minimum = 18)]
	public int Age { get; set; }
}

WSZYSTKIE atrybuty walidacyjne mają właściwość ErrorMessage, do której możesz wpisać komunikat błędu. Komunikaty mogą być też lokalizowane, np:

[Required(ErrorMessageResourceName = nameof(LangRes.Validation_RequiredField), ErrorMessageResourceType = typeof(LangRes))]

I teraz tak. ErrorMessageResourceType to jest Twój typ z zasobami językowymi, który posiada klucz Validation_RequiredField – ten klucz, który używasz.
Jeśli nie wiesz, jak tworzyć wersje językowe, przeczytaj ten artykuł.

Jest jeszcze jeden sposób na pokazanie błędów w widoku. Możesz pokazać wszystkie błędy jeden po drugim, zamiast konkretnych błędów pod konkretnymi kontrolkami. Wystarczy zrobić to:

<form asp-action="Register" method="Post">
	<div asp-validation-summary="All"></div>
	<div class="form-group">
		<label for="email">Podaj swój email:</label>
		<input class="form-control" type="email" asp-for="UserName" />
		<span asp-validation-for="UserName" class="text-danger"></span>
	</div>
</form>

Wtedy komunikaty o błędach pojawią się na tym dodatkowym divie w postaci listy.

To właściwie już tyle jeśli chodzi o podstawy walidacji w .NetCore.
Gratulacje, dotarłeś do końca 🙂

Jeśli masz jakieś wątpliwości lub znalazłeś błąd w artykule, podziel się w komentarzu.

Akceptacja regulaminu – wymagany checkbox

W każdym portalu, wymagana jest akceptacja regulaminu. I sprawa wydaje się prosta, ale z jakiegoś powodu nie jest (osobiście zgłosiłem to do MS wraz z rozwiązaniem). Przeczytaj ten artykuł, żeby dowiedzieć się, jak wymusić zaznaczenie checkboxa przez użytkownika w .NetCore

Podziel się artykułem na: