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).

UWAGA! .NET8 automatycznie sprawdza poprawność modelu w klasie kontrolera dziedziczącej po ControllerBase.

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: