Grupowanie i ponowne użycie reguł walidacji w FluentValidation

W poprzednich postach o FluentValidation przyjrzeliśmy się podstawowym i złożonym regułom walidacji, które pozwalają na precyzyjne sprawdzanie danych wejściowych w naszych aplikacjach. Jak już omówiliśmy, zaawansowane walidacje są kluczowe w złożonych systemach, gdzie dane są ściśle powiązane i wymagają dokładnej weryfikacji.

Dziś przyjrzymy się dwóm istotnym mechanizmom w FluentValidation, które pomagają jeszcze bardziej usprawnić proces walidacji — RuleSet i Include. Mechanizm RuleSet umożliwia grupowanie reguł walidacji, co jest niezwykle przydatne, gdy chcemy stosować różne zestawy reguł w zależności od kontekstu. Natomiast mechanizm Include pozwala na ponowne użycie istniejących reguł walidacji, co znacząco upraszcza kod i zwiększa jego czytelność.

Zarówno RuleSet, jak i Include, są narzędziami, które pomagają w zarządzaniu złożonością walidacji, umożliwiając bardziej modularne i wielokrotne wykorzystywanie kodu. Dzięki nim możemy tworzyć bardziej elastyczne i skalowalne rozwiązania, które łatwiej dostosować do zmieniających się wymagań biznesowych.

Grupowanie reguł za pomocą RuleSet

RuleSet w FluentValidation to mechanizm, który pozwala na grupowanie i organizację reguł walidacji w logiczne zestawy. Dzięki temu można łatwiej zarządzać walidacją, szczególnie w sytuacjach, gdy dla różnych operacji na tym samym obiekcie są wymagane różne reguły walidacji. Użycie RuleSet jest kluczowe w utrzymaniu czytelności i organizacji kodu, a także w zapewnieniu elastyczności w procesie walidacji. Użycie RuleSet przynosi wiele korzyści, takich jak:

  • Modularność: Reguły są grupowane w logiczne zestawy, co ułatwia zarządzanie nimi i zwiększa czytelność kodu.
  • Elastyczność: Można łatwo zastosować różne reguły walidacji dla różnych scenariuszy bez potrzeby duplikowania kodu.
  • Utrzymanie: Zmiany w regułach walidacji dla konkretnego scenariusza są izolowane, co ułatwia utrzymanie i testowanie kodu.

W FluentValidation, grupowanie reguł walidacji definiuje się za pomocą metody RuleSet, w której określa się nazwę zestawu oraz zestaw reguł, które mają być zastosowane. Na przykład, można mieć osobny zestaw dla tworzenia ("Creation") i edycji ("Edition") obiektu. Wewnątrz każdego RuleSet, definiuje się reguły walidacji tak, jak w standardowym przypadku, ale są one uruchamiane tylko wtedy, gdy wywoła się walidację z użyciem odpowiedniego RuleSet.

Gdy RuleSet-y są zdefiniowane, możemy je stosować do walidacji obiektów. To robimy przez wywołanie metody Validate na instancji walidatora, przekazując jej obiekt do walidacji oraz nazwę RuleSet, którego chcemy użyć. W naszym przykładzie, dla nowego klienta używamy RuleSet "Creation", a dla aktualizacji istniejącego klienta - "Update".

Co się stanie, gdy wywołamy metodę Validate bez określenia RuleSet? W takim przypadku Fluent Validation wykona wszystkie reguły walidacji, które nie są przypisane do żadnego konkretnego RuleSet. To oznacza, że jeśli zdefiniowaliśmy reguły poza RuleSet, to te reguły zostaną zastosowane. Jest to przydatne, gdy mamy zestaw reguł, które są wspólne dla różnych operacji i nie chcemy ich duplikować w każdym RuleSet.

Nazewnictwo RuleSet powinno być intuicyjne i opisowe, odzwierciedlając konkretny kontekst lub akcję, np. AccountCreation zamiast RuleSet1. Stosuj jednolitą konwencję, taką jak CamelCase, unikaj skrótów, chyba że są powszechnie rozpoznawane. W przypadku zestawów reguł związanych z określoną funkcjonalnością używaj spójnych prefiksów, np. ProductCreation, ProductUpdate. Pamiętaj, aby dokumentować każdy RuleSet, wyjaśniając jego zastosowanie i kontekst, oraz regularnie przeglądać i aktualizować nazwy, aby utrzymać klarowność i odpowiedniość w miarę ewolucji aplikacji.

Załóżmy, że pracujemy nad systemem zarządzania klientami i musimy zapewnić, że dane wprowadzane przez użytkowników są poprawne. W naszym systemie mamy różne wymagania walidacyjne w zależności od tego, czy klient jest nowo dodawany, czy aktualizowany.

Naszym punktem wyjścia jest klasa Customer, która reprezentuje klienta w naszym systemie:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Każdy klient ma identyfikator (Id), imię (Name) oraz adres e-mail (Email).

Następnie, tworzymy walidator CustomerValidator, który określa reguły walidacji. Z pomocą RuleSet, definiujemy różne zestawy reguł dla różnych scenariuszy:

using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        // RuleSet for creating a new customer
        RuleSet("Creation", () =>
        {
            RuleFor(customer => customer.Name)
                .NotEmpty()
                .WithMessage("First name is required.");

            RuleFor(customer => customer.Email)
                .NotEmpty()
                .EmailAddress()
                .WithMessage("A valid email address is required.");
        });

        // RuleSet for updating an existing customer
        RuleSet("Update", () =>
        {
            RuleFor(customer => customer.Id)
                .NotEmpty()
                .WithMessage("Customer ID is required.");

            RuleFor(customer => customer.Name)
                .NotEmpty()
                .WithMessage("First name is required.");

            RuleFor(customer => customer.Email)
                .NotEmpty()
                .EmailAddress()
                .WithMessage("A valid email address is required.");
        });
    }
}

W Creation sprawdzamy, czy imię i email nie są puste, a email jest poprawny. W Update musimy dodatkowo upewnić się, że klient ma określone Id.

Kiedy przychodzi czas na walidację, łatwo możemy zastosować odpowiedni RuleSet. Przykład poniżej pokazuje, jak to zrobić:

public void ValidateCustomer()
{
    var customerValidator = new CustomerValidator();

    var newCustomer = new Customer
    {
        Name = "John Doe",
        Email = "john.doe@example.com"
    };
    var creationResults = customerValidator.Validate(newCustomer, strategy => strategy.IncludeRuleSets("Creation"));

    var existingCustomer = new Customer
    {
        Id = 1,
        Name = "Jane Doe",
        Email = "jane.doe@example.com"
    };
    var updateResults = customerValidator.Validate(existingCustomer, strategy => strategy.IncludeRuleSets("Update"));

    if (!creationResults.IsValid || !updateResults.IsValid)
    {
        foreach (var failure in creationResults.Errors)
        {
            Console.WriteLine($"Property {failure.PropertyName} failed validation. Error: {failure.ErrorMessage}");
        }
        foreach (var failure in updateResults.Errors)
        {
            Console.WriteLine($"Property {failure.PropertyName} failed validation. Error: {failure.ErrorMessage}");
        }
    }
    else
    {
        Console.WriteLine("Both customers are valid!");
    }
}

Wiele metod w FluentValidation to metody rozszerzające, takie jak powyższa Validate i wymagają zaimportowania przestrzeni nazw FluentValidation za pomocą instrukcji using, np.using FluentValidation;.

W tym przykładzie najpierw tworzymy nowego klienta i walidujemy go używając RuleSet "Creation". Następnie, bierzemy istniejącego klienta i walidujemy go używając RuleSet "Update". Jeśli walidacja się nie powiedzie, wyświetlamy błędy.

Przyglądając się naszym regułom grupowania, możemy zauważyć, że w regułach grupowania "Creation" i "Update" mamy powtarzające się reguły walidacji dla Name oraz Email. Czy można to zrefaktoryzować? Oczywiście poniżej przykład po refaktoryzacji:

using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(customer => customer.Name)
            .NotEmpty()
            .WithMessage("First name is required.");

        RuleFor(customer => customer.Email)
            .NotEmpty()
            .EmailAddress()
            .WithMessage("A valid email address is required.");

        RuleSet("Creation", () =>
        {
            // Specific rules for the process of creating a new customer
            // You can place rules here that are unique to this process
            // I encourage the reader to introduce their own rules.
        });

        RuleSet("Update", () =>
        {
            RuleFor(customer => customer.Id).NotEmpty().WithMessage("ID klienta jest wymagane.");
        });
    }
}

W powyższym kodzie, reguły walidacji dla Name i Email zostały umieszczone w sekcji globalnej, co oznacza, że będą one zawsze stosowane, niezależnie od tego, czy wywołujemy walidację z RuleSet dla tworzenia ("Creation") czy aktualizacji ("Update"). Dzięki temu unikamy duplikacji reguł w różnych RuleSet.

Jednocześnie, dzięki zastosowaniu RuleSet, zachowujemy możliwość definiowania reguł specyficznych dla danego kontekstu – w tym przypadku wymagamy, aby w procesie aktualizacji ("Update") pole Id było wypełnione.

Podsumowując, RuleSet w FluentValidation jest potężnym narzędziem, które pozwala na efektywne grupowanie i zarządzanie regułami walidacji. Jego zastosowanie znacząco ułatwia utrzymanie porządku i klarowności w projektach, w których walidacja danych jest kluczowym elementem.

Ponowne użycie reguł

Mechanizm Include w FluentValidation jest wykorzystywany do włączania jednego walidatora do drugiego, co pozwala na tworzenie modularnych i łatwych do ponownego użycia komponentów walidacji. Umożliwia to skomponowanie bardziej złożonych walidatorów z prostszych, dedykowanych walidatorów, co prowadzi do lepszej organizacji kodu i unikania duplikacji logiki walidacji.

Include jest używane, gdy chcesz, aby reguły zdefiniowane w jednym walidatorze były częścią innego walidatora. Można to rozumieć jako "dołącz te reguły walidacji do mojego obecnego zestawu reguł". To szczególnie przydatne, gdy masz wspólne reguły walidacji, które chcesz zastosować w wielu różnych kontekstach walidacji.

W przykładzie walidator PersonValidator, który dołącza reguły z PersonAgeValidator i PersonNameValidator:

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        Include(new PersonAgeValidator());
        Include(new PersonNameValidator());
    }
}

Załóżmy, że PersonAgeValidator i PersonNameValidator są zdefiniowane następująco:

public class PersonAgeValidator : AbstractValidator<Person>
{
    public PersonAgeValidator()
    {
        RuleFor(person => person.Age)
            .GreaterThan(0).WithMessage("Age must be greater than 0");
    }
}

public class PersonNameValidator : AbstractValidator<Person>
{
    public PersonNameValidator()
    {
        RuleFor(person => person.Name)
            .NotEmpty().WithMessage("Name is required");
    }
}

W tym scenariuszu:

  • PersonAgeValidator definiuje reguły walidacji dla wieku osoby.
  • PersonNameValidator definiuje reguły walidacji dla imienia osoby.
  • PersonValidator dołącza te dwa walidatory, tworząc złożony walidator, który sprawdza zarówno wiek, jak i imię osoby.

Użycie Include przynosi liczne korzyści:

  • Modularność: Możesz zdefiniować dedykowane walidatory dla poszczególnych aspektów modelu i łatwo je łączyć.
  • Ponowne Użycie: Reguły walidacji zdefiniowane w jednym miejscu mogą być wykorzystane w wielu walidatorach, co zmniejsza duplikację kodu.
  • Łatwość Zarządzania Zmianami: Aktualizacje w dedykowanych walidatorach automatycznie odzwierciedlają się we wszystkich walidatorach, które je dołączają.

Podsumowując, mechanizm Include w FluentValidation to ciekawe narzędzie do budowania złożonych walidatorów w sposób modularny i łatwy do zarządzania, co jest kluczowe w utrzymaniu czystego i efektywnego kodu walidacji w większych projektach.

W tym artykule przyjrzeliśmy się, jak korzystać z mechanizmów RuleSet i Include w FluentValidation, aby efektywnie grupować i ponownie używać reguł walidacji. Dzięki tym narzędziom możemy tworzyć bardziej modularne, elastyczne i łatwe do zarządzania rozwiązania walidacyjne. Co więcej, możemy je łączyć, aby tworzyć kompleksowe reguły walidacyjne, które są zarówno wielokrotnego użytku, jak i dostosowane do specyficznych scenariuszy.

Do zobaczenia w kolejnych postach.