Zaawansowane reguły walidacji

Złożone reguły walidacji w programowaniu są istotnym narzędziem, pozwalającym na dokładne i precyzyjne sprawdzanie danych wejściowych w aplikacji. Ich głównym celem jest zapewnienie, że dane wprowadzane przez użytkownika lub otrzymywane z innych źródeł spełniają określone kryteria, które są często bardziej skomplikowane niż proste sprawdzenia, jak na przykład obecność wartości czy dopasowanie do wzorca.

W bardziej złożonych aplikacjach, gdzie dane są skomplikowane i powiązane ze sobą w różnorodne sposoby, konieczne jest stosowanie złożonych reguł walidacji. Przykładowo, w aplikacji bankowej, gdzie przeprowadzasz transakcje, walidacja może wymagać sprawdzenia, czy stan konta użytkownika pozwala na wykonanie transakcji, czy też wprowadzone dane transakcji są zgodne z innymi wcześniej zdefiniowanymi warunkami. W takich przypadkach proste reguły, takie jak sprawdzenie, czy pole nie jest puste, są niewystarczające.

Złożone reguły walidacji mogą obejmować sprawdzanie wielu warunków jednocześnie, walidację zależną od innych wartości w systemie, a nawet wykorzystywanie zewnętrznych usług lub baz danych do potwierdzenia poprawności danych. Dzięki temu zapewniają one większe bezpieczeństwo i dokładność w zarządzaniu danymi, co jest kluczowe w aplikacjach o wysokim stopniu złożoności i znaczeniu.

Podstawowe reguły walidacji są zazwyczaj prostymi sprawdzeniami, takimi jak: czy pole tekstowe nie jest puste, czy liczba wprowadzona w pole jest w określonym zakresie, czy format adresu e-mail jest poprawny. Są one stosunkowo proste do zaimplementowania i zrozumienia, nawet dla początkujących programistów.

Złożone reguły walidacji idą o krok dalej. Mogą one wymagać sprawdzenia kilku warunków naraz, analizowania wzajemnych zależności między różnymi danymi, a nawet łączenia logiki biznesowej z walidacją danych. Na przykład, reguła może wymagać, aby wartość jednego pola była mniejsza niż wartość innego pola (np. data rozpoczęcia musi być wcześniejsza niż data zakończenia).

Takie złożone reguły często wymagają głębszego zrozumienia logiki biznesowej aplikacji oraz umiejętności programistycznych umożliwiających efektywne ich implementowanie. Często wykorzystują one bardziej zaawansowane techniki programowania, takie jak wyrażenia lambda, funkcje anonimowe, a także mogą angażować zewnętrzne źródła danych lub serwisy.

W praktyce złożone reguły walidacji stają się niezbędne w sytuacjach, gdzie prostota podstawowych reguł nie jest w stanie sprostać wymogom aplikacji. Na przykład, w systemach, które zarządzają dużymi zbiorami danych z wieloma wzajemnymi zależnościami, czy w aplikacjach, gdzie decyzje oparte na danych mają duże konsekwencje finansowe lub prawne, precyzja i zaawansowanie walidacji są kluczowe.

Podsumowując, zrozumienie różnicy między podstawowymi a złożonymi regułami walidacji oraz umiejętność ich stosowania, jest kluczowym elementem w arsenale każdego programisty pracującego nad bardziej skomplikowanymi projektami. Pozwala to na tworzenie aplikacji, które nie tylko efektywnie zarządzają danymi, ale także zapewniają ich spójność, poprawność i bezpieczeństwo.

Łączenie warunków walidacyjnych

Łączenie kilku warunków walidacyjnych dla jednego pola jest kluczowym elementem złożonych reguł walidacji, pozwalającym na bardziej szczegółowe sprawdzanie danych wejściowych. Wyobraźmy sobie sytuację, w której potrzebujemy zweryfikować pole tekstowe, takie jak adres e-mail. Nie tylko chcemy upewnić się, że pole to nie jest puste, ale także, że wprowadzony adres e-mail spełnia określony format.

Załóżmy, że mamy klasę UserRegistration, która zawiera pole EmailAddress. Chcemy zweryfikować, czy to pole nie jest puste i czy zawiera poprawny adres e-mail. Oto jak moglibyśmy to zaimplementować:

using FluentValidation;

public class UserRegistration
{
    public string EmailAddress { get; set; }
}

public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
    public UserRegistrationValidator()
    {
        RuleFor(user => user.EmailAddress)
            .NotEmpty().WithMessage("Email address is required.")
            .EmailAddress().WithMessage("Invalid email format.");
    }
}

W powyższym przykładzie pierwszym krokiem jest określenie, że nasza reguła dotyczy właśnie właściwości EmailAddress w klasie UserRegistration. Robimy to za pomocą metody RuleFor, przekazując do niej wyrażenie lambda user => user.EmailAddress. To wyrażenie wskazuje, że reguła walidacji, którą zaraz zdefiniujemy, będzie dotyczyła pola EmailAddress naszego obiektu user reprezentującego instancję klasy UserRegistration.

Następnie przechodzimy do definiowania właściwych warunków walidacji. Pierwszym z nich jest NotEmpty, który ma za zadanie sprawdzić, czy pole EmailAddress nie jest puste. Jest to podstawowy warunek walidacyjny, zapewniający, że pole nie zostało pozostawione bez wartości. W przypadku, gdy pole jest puste, mechanizm FluentValidation automatycznie przechwytuje tę sytuację i generuje komunikat o błędzie "Adres e-mail jest wymagany", informując użytkownika o potrzebie uzupełnienia tego pola.

Kolejnym warunkiem walidacji jest EmailAddress. Ten warunek sprawdza, czy wprowadzona wartość w polu EmailAddress jest zgodna z ogólnie przyjętym formatem adresu e-mail. Nie chodzi tu tylko o sprawdzenie, czy pole zawiera tekst, ale czy ten tekst jest strukturalnie poprawnym adresem e-mail. Jeżeli format wprowadzonej wartości jest nieprawidłowy, system ponownie reaguje, wyświetlając komunikat "Niepoprawny format adresu e-mail", co pomaga użytkownikowi zrozumieć, w jaki sposób należy poprawić wprowadzone dane.

Przez łączenie tych dwóch warunków w jednej sekwencji walidacyjnej, FluentValidation pozwala na budowanie złożonych, a zarazem precyzyjnych reguł sprawdzających poprawność danych wejściowych. Dzięki temu proces ten staje się nie tylko efektywny, ale także wyraźny i zrozumiały zarówno dla programistów, jak i użytkowników aplikacji.

Aby zilustrować praktyczne użycie walidatora UserRegistrationValidator, który został zdefiniowany wcześniej, załóżmy, że mamy prostą aplikację lub scenariusz, w którym użytkownik musi zarejestrować się, podając swój adres e-mail. Sprawdzimy, czy podany adres e-mail jest poprawny, zarówno pod względem bycia niepustym, jak i spełnienia standardowego formatu adresu e-mail.

using FluentValidation;
using System;

public class Program
{
    public static void Main(string[] args)
    {
        var userRegistration = new UserRegistration
        {
            EmailAddress = "email@example.com"
        };

        var validator = new UserRegistrationValidator();
        var validationResult = validator.Validate(userRegistration);

        if (validationResult.IsValid)
        {
            Console.WriteLine("Registration successful!");
        }
        else
        {
            foreach (var error in validationResult.Errors)
            {
                Console.WriteLine(error.ErrorMessage);
            }
        }
    }
}

W powyższym przykładzie rozpoczynamy od utworzenia instancji klasy UserRegistration, do której przypisujemy przykładowy adres e-mail, symulując tym samym proces wprowadzania danych przez użytkownika. Następnie, tworzymy instancję walidatora UserRegistrationValidator i wykorzystujemy jego metodę Validate, przekazując do niej nasz obiekt userRegistration w celu sprawdzenia poprawności danych. Metoda Validate zwraca obiekt validationResult, który zawiera informacje o wyniku walidacji. Sprawdzamy, czy walidacja zakończyła się sukcesem (czyli validationResult.IsValid jest true). W przypadku pozytywnego wyniku wyświetlamy komunikat o poprawnej rejestracji. W przeciwnym razie przechodzimy przez listę błędów walidacji, wypisując każdy z nich, aby użytkownik mógł odpowiednio zareagować na występujące problemy.

Metoda Must

W tworzeniu oprogramowania, szczególnie w kontekście walidacji danych, elastyczność i dynamika są kluczowe. Właśnie tu predykaty w FluentValidation pokazują swoją moc, umożliwiając tworzenie złożonych reguł walidacji, które mogą się dostosować do różnorodnych scenariuszy i danych wejściowych. Predykat to funkcja, która przyjmuje jeden lub więcej argumentów i zwraca wartość typu bool, wskazując, czy dany warunek jest spełniony.

W FluentValidation, metoda Must umożliwia włączenie własnych, niestandardowych predykatów do procesu walidacji. Dzięki niej możesz definiować własne logiczne warunki, które muszą być spełnione, aby dane były uznane za poprawne. Co więcej, te predykaty mogą korzystać z kontekstu zewnętrznego, umożliwiając tworzenie reguł, które dynamicznie dostosowują się do aktualnych danych lub stanu aplikacji.

Przykładem użycia predykatów może być walidacja modelu użytkownika, gdzie wymagania dotyczące siły hasła zależą od roli przypisanej użytkownikowi. W prostym modelu użytkownika możesz mieć pole Password i pole Role. W zależności od roli, wymagania co do hasła mogą się różnić – np. administratorzy muszą mieć hasła dłuższe i bardziej złożone niż zwykli użytkownicy.

Oto przykładowa implementacja takiej logiki:

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(user => user.Password)
            .Must((user, password) => BeAValidPassword(user, password))
            .WithMessage("The password does not meet security requirements.");
    }

    private bool BeAValidPassword(User user, string password)
    {
        if (user.Role == Role.Admin)
        {
            return password.Length > 10 && HasSpecialCharacters(password);
        }
        else
        {
            return password.Length > 6;
        }
    }

    private bool HasSpecialCharacters(string input)
    {
        string specialCharacters = "!@#$%^&*()_-+={[}]|:;<,>.?/";
        return input.Any(ch => specialCharacters.Contains(ch));
    }
}

W tym przypadku metoda BeAValidPassword jest predykatem używanym w metodzie Must. Dzięki dostępowi do całego obiektu User, predykat może dostosować swoje wymagania w zależności od roli użytkownika, co ilustruje dynamikę i elastyczność walidacji.

Predykaty w C# i w bibliotece FluentValidation oferują szerokie możliwości tworzenia złożonych i wyrafinowanych reguł walidacji. Dzięki swojej elastyczności mogą być stosowane w różnorodnych, skomplikowanych scenariuszach, w tym przy walidacji kolekcji.

W przypadku walidacji kolekcji predykaty mogą być nieocenionym narzędziem umożliwiającym sprawdzanie, czy elementy kolekcji spełniają określone kryteria. FluentValidation pozwala na łatwe wdrożenie takich warunków. Na przykład, jeśli masz kolekcję obiektów i chcesz upewnić się, że każdy obiekt spełnia określone warunki, możesz użyć metody Must w połączeniu z metodami LINQ, takimi jak All lub Any.

RuleFor(user => user.Tasks)
    .Must(tasks => tasks.All(task => task.IsCompleted))
    .WithMessage("All tasks must be completed.");

W tym przypadku, dla modelu User z kolekcją Tasks, sprawdzamy, czy wszystkie zadania (tasks) zostały zakończone. Metoda All zwróci true tylko wtedy, gdy każde zadanie w kolekcji spełni warunek task.IsCompleted.

Wykorzystanie predykatów w ten sposób nie tylko zwiększa możliwości walidacji, ale również czyni kod bardziej modularnym i łatwym w utrzymaniu, ponieważ logika walidacyjna jest wyraźnie oddzielona od innych części aplikacji.

Warunkowe definiowanie reguł

Metody When oraz Unless można wykorzystać do określenia warunkowego definiowania reguł, które sterują momentem wykonania reguły. Pozwalają one na warunkowe zastosowanie walidacji, co sprawia, że walidacja staje się nie tylko bardziej elastyczna, ale również bardziej zgodna z rzeczywistymi wymaganiami biznesowymi. Z drugiej strony, Otherwise oferuje sposób na zdefiniowanie alternatywnej ścieżki walidacji, która ma być zastosowana, gdy warunek określony w When nie jest spełniony. To podejście pozwala na stworzenie klarownej i zorganizowanej logiki walidacji, która jest w stanie obsłużyć różnorodne scenariusze i przypadki użycia. Użycie tych metod w FluentValidation umożliwia budowanie skomplikowanych reguł walidacji w sposób, który jest zarówno wydajny, jak i zrozumiały dla osób rozwijających i utrzymujących kod.

When używane w regule walidacji pozwala na warunkowe nałożenie tej reguły. Można to przyrównać do użycia instrukcji if w tradycyjnym kodzie programistycznym. Jeśli warunek określony w When jest spełniony (zwraca wartość true), wtedy reguła walidacji zostanie zastosowana. W przeciwnym razie reguła jest pomijana.

Przykład użycia When w regule:

RuleFor(user => user.Password)
    .MinimumLength(8)
    .When(user => user.IsPasswordChangeRequired);

W tym przykładzie reguła wymagająca minimum 8 znaków dla hasła użytkownika zostanie nałożona tylko wtedy, gdy użytkownik ma ustawioną flagę IsPasswordChangeRequired na true. To pozwala na elastyczną walidację, która dostosowuje się do specyficznych wymagań biznesowych.

When można również używać jako osobną instrukcję, niezależną od konkretnej reguły walidacji, definiującą warunkowy blog, w którym można umieścić wiele reguł. To podejście pozwala na grupowanie wielu reguł, które mają być stosowane tylko wtedy, gdy spełniony jest określony warunek.

Przykład użycia When jako osobnej instrukcji:

When(user => user.IsPasswordChangeRequired, () =>
{
    RuleFor(user => user.Password)
        .MinimumLength(8)
        .Matches("[A-Z]")
        .Matches("[0-9]");
});

W tym przykładzie, When definiuje blok warunkowy, w którym umieszczamy trzy reguły dotyczące hasła. Wszystkie te reguły – wymaganie minimum 8 znaków, przynajmniej jednej dużej litery oraz przynajmniej jednej cyfry – zostaną zastosowane tylko wtedy, gdy warunek user.IsPasswordChangeRequired jest spełniony. Jest to szczególnie przydatne, gdy mamy zbiór reguł, które chcemy stosować w określonych sytuacjach, unikając redundancji i zwiększając czytelność kodu.

Podobnie jak When, dyrektywa Unless w Fluent Validation dostarcza mechanizmu do warunkowego stosowania reguł walidacji. Jednakże, w przeciwieństwie do When, Unless stosuje regułę walidacji tylko wtedy, gdy określony warunek nie jest spełniony. Innymi słowy, Unless działa na zasadzie negacji warunku – jeśli warunek zwraca false, wtedy reguła walidacji zostaje zastosowana.

Przykład użycia Unless w regule:

RuleFor(user => user.DateOfBirth)
    .Must(BeAValidAge)
    .Unless(user => user.IsSpecialMember);

W tym przykładzie reguła walidacji, która wymaga, by wiek użytkownika był odpowiedni (np. pełnoletni), jest nałożona na wszystkich użytkowników, chyba że użytkownik jest oznaczony jako IsSpecialMember. Dzięki Unless, możemy łatwo wykluczyć pewne obiekty z ogólnych reguł, bez konieczności pisania skomplikowanej logiki.

Podobnie jak When, Unless może być również używane jako osobna instrukcja, definiująca blok, w którym określamy reguły, które mają być pominięte, jeśli warunek jest spełniony.

Przykład użycia Unless jako osobnej instrukcji:

Unless(user => user.IsSpecialMember, () =>
{
    RuleFor(user => user.DateOfBirth)
        .Must(BeAValidAge);
    RuleFor(user => user.AccountBalance)
        .GreaterThan(0);
});

W tym przykładzie, Unless definiuje blok warunkowy, w którym umieszczamy reguły dotyczące daty urodzenia i salda konta. Te reguły będą zastosowane tylko wtedy, gdy użytkownik nie jest oznaczony jako IsSpecialMember.

Otherwise w Fluent Validation pozwala na definiowanie reguł, które mają zostać zastosowane, gdy warunek określony w When nie jest spełniony. To pozwala na czytelną i zorganizowaną strukturę walidacji, gdzie łatwo jest zdefiniować, co się dzieje w różnych scenariuszach.

Przykład użycia When wraz z Otherwise:

When(customer => customer.IsPreferred, () => {
    // These rules are applied only for preferred customers
    RuleFor(customer => customer.CustomerDiscount).GreaterThan(0);
    RuleFor(customer => customer.CreditCardNumber).NotNull();
}).Otherwise(() => {
    // These rules are applied when the customer is not preferred
    RuleFor(customer => customer.CustomerDiscount).Equal(0);
});

W tym przykładzie, dla preferowanych klientów, sprawdzamy, czy ich zniżka jest większa od 0 i czy posiadają numer karty kredytowej. Jeśli jednak klient nie jest preferowany (czyli warunek customer.IsPreferred nie jest spełniony), wówczas zastosowana jest alternatywna reguła walidacji, która sprawdza, czy zniżka klienta jest równa 0.

Metody When, Unless i Otherwise w Fluent Validation oferują potężne i elastyczne narzędzia do tworzenia warunkowych reguł walidacji, umożliwiając precyzyjne dostosowanie procesu walidacji do konkretnych wymagań biznesowych. When pozwala na stosowanie reguł tylko wtedy, gdy spełnione są określone warunki, podczas gdy Unless działa odwrotnie, aplikując reguły tylko, gdy dany warunek nie jest spełniony. Z kolei Otherwise wprowadza alternatywną ścieżkę walidacji, która jest używana, gdy warunki określone w When nie zostają spełnione. Współpraca tych trzech mechanizmów pozwala na budowanie skomplikowanych i dobrze zorganizowanych procesów walidacji, które są w stanie efektywnie zarządzać różnorodnymi scenariuszami i wymaganiami, zapewniając zarazem wysoką czytelność i łatwość w utrzymaniu kodu.

Komunikaty walidacji w fluent validation

Nadpisywanie domyślnych komunikatów błędów w FluentValidation pozwala dostosować walidację do konkretnego kontekstu aplikacji, czyniąc komunikaty bardziej zrozumiałymi dla użytkownika. Użycie metody WithMessage umożliwia zdefiniowanie własnych komunikatów, co znacznie poprawia doświadczenie użytkownika.

RuleFor(user => user.Email)
    .EmailAddress()
    .WithMessage("Please provide a valid email address.");

FluentValidation oferuje także możliwość wykorzystania placeholderów. Nadpisanie domyślnych komunikatów błędów w FluentValidation z użyciem placeholderów umożliwia tworzenie dynamicznych i kontekstowych komunikatów, które są automatycznie dostosowywane do konkretnego przypadku walidacji. Placeholdery takie jak {PropertyName} czy {PropertyValue} pozwalają na wstawienie nazwy walidowanej właściwości lub jej wartości bezpośrednio do komunikatu o błędzie. Każdy walidator ma listę dostępnych placeholderów, co pozwala na precyzyjne dostosowanie komunikatów.

Przykładowo, w walidatorach porównujących (takich jak Equal, GreaterThan) można użyć {ComparisonValue} do wyświetlenia wartości, z którą porównywana jest właściwość, a w walidatorze długości (Length) dostępne są placeholdery takie jak {MinLength}, {MaxLength}, czy {TotalLength}.

Przykład z placeholder:

RuleFor(user => user.Age)
    .GreaterThan(18)
    .WithMessage("{PropertyName} must be greater than 18. Current value: {PropertyValue}");

W tym przypadku, jeśli walidacja się nie powiedzie, komunikat o błędzie będzie zawierał nazwę właściwości (Age) oraz wartość, która nie przeszła walidacji, co czyni komunikat bardziej precyzyjnym i informatywnym.

FluentValidation oferuje także możliwość tworzenia bardziej złożonych komunikatów błędów, które mogą zawierać wartości z samego modelu lub z kontekstu walidacji. Można to zrobić za pomocą metod takich jak WithMessage, podając jako argument funkcję, która zwraca string.

Przykład:

RuleFor(user => user.Age)
    .GreaterThan(18)
    .WithMessage(user => $"User must be older than 18 years. Provided age: {user.Age}");

W tym przypadku, jeśli walidacja wieku się nie powiedzie, użytkownik otrzyma szczegółowy komunikat, zawierający informację o tym, jaki wiek został faktycznie podany.

Dzięki możliwości nadpisywania komunikatów i użyciu placeholderów, FluentValidation pozwala na tworzenie klarownych i pomocnych komunikatów błędów, które ułatwiają użytkownikom zrozumienie, co poszło nie tak, i jak mogą naprawić błąd.

Złożone reguły walidacji są kluczowe w zaawansowanych aplikacjach, zapewniając dokładność, bezpieczeństwo i spójność danych. Dzięki nim programiści mogą skutecznie zarządzać złożonymi warunkami i zależnościami, co jest niezbędne w systemach o wysokim stopniu komplikacji. Stosowanie tych reguł pozwala na uniknięcie błędów i zapewnia wysoką jakość funkcjonowania aplikacji.

Do zobaczenia w kolejnych postach.