Walidacja złożonych struktur danych w FluentValidation

W świecie programowania, szczególnie kiedy mamy do czynienia z aplikacjami obsługującymi złożone dane, często spotykamy się z wyzwaniem zweryfikowania poprawności tych danych. FluentValidation, dzięki swojej elastyczności, stanowi potężne narzędzie, które pozwala nam na eleganckie zarządzanie procesem walidacji, nie tylko dla prostych typów danych, ale również złożonych struktur.

Złożone struktury danych w kontekście FluentValidation odnoszą się do tych części modelu danych, które same są obiektami. Nie jest to rzadkość — modele często składają się z innych modeli, tworząc warstwę złożoności, która może być trudna do zarządzania bez odpowiednich narzędzi. Na przykład, model Order może zawierać listę obiektów Product, a każdy Product może mieć swoje własne, specyficzne reguły walidacji.

Rozważmy sytuację, w której musimy zweryfikować zamówienie, które zawiera listę produktów. Każdy produkt ma swoją cenę i ilość, a zamówienie zawiera informacje o kliencie i datę zamówienia.

public class Order
{
    public Customer Customer { get; set; }
    public DateTime OrderDate { get; set; }
    public List<Product> Products { get; set; }
}

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

Chcemy upewnić się, że każde zamówienie zawiera datę oraz listę produktów, a każdy produkt ma nazwę, cenę większą od zera i ilość.

Tworzymy walidator dla Order oraz Product, wykorzystując FluentValidation do zapewnienia zgodności danych z naszymi oczekiwaniami.

public class OrderValidator : AbstractValidator<Order>
{
    public OrderValidator()
    {
        RuleFor(order => order.OrderDate).NotEmpty();
        RuleForEach(order => order.Products).SetValidator(new ProductValidator());
    }
}

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(product => product.Name).NotEmpty();
        RuleFor(product => product.Price).GreaterThan(0);
        RuleFor(product => product.Quantity).GreaterThan(0);
    }
}

W OrderValidator, sprawdzamy, czy data zamówienia nie jest pusta i ustawiamy walidator dla każdego produktu w zamówieniu. ProductValidator zajmuje się weryfikacją, czy każdy produkt spełnia określone kryteria — ma nazwę, cenę większą od zera i odpowiednią ilość. Wykorzystując metodę SetValidator w OrderValidator, wprowadzamy złożone reguły walidacji dla elementów kolekcji, co pozwala na zastosowanie specyficznych walidatorów dla indywidualnych składników zamówienia. Więcej informacji na temat walidacji kolekcji znajdziesz w dalszej części tekstu.

Za pomocą FluentValidation do sprawdzania poprawności złożonych struktur danych, jesteśmy w stanie efektywnie zarządzać nawet najbardziej zawiłymi strukturami, jednocześnie utrzymując przejrzystość i łatwość konserwacji naszego kodu.

Walidacja kolekcji

Walidacja kolekcji w FluentValidation jest kluczowym elementem zapewnienia poprawności danych w aplikacjach, które operują na złożonych strukturach danych. FluentValidation dostarcza mechanizmów pozwalających na efektywną i elastyczną walidację elementów kolekcji.

Załóżmy, że mamy kolekcję obiektów, które również wymagają walidacji. Bezpośrednie podejście może polegać na iterowaniu przez kolekcję i walidowaniu każdego elementu indywidualnie. Jednak FluentValidation oferuje wyrafinowane i wydajne rozwiązanie za pomocą metody RuleForEach(). Ta metoda pozwala na zastosowanie określonych reguł walidacji do każdego elementu w kolekcji, co sprawia, że proces jest nie tylko bardziej efektywny, ale również bardziej zorganizowany i łatwiejszy w utrzymaniu. Jest to szczególnie przydatne, gdy chcemy upewnić się, że wszystkie elementy kolekcji spełniają określone kryteria. Oto przykład zastosowania RuleForEach():

public class Customer
{
    public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public decimal TotalAmount { get; set; }
}


public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleForEach(customer => customer.Orders).Must(order => order.TotalAmount >= 100)
            .WithMessage("Each order should have a value of at least 100.");
    }
}

W tym przykładzie, każde zamówienie w kolekcji Orders klienta jest walidowane indywidualnie, aby upewnić się, że jego wartość TotalAmount jest co najmniej 100.

ChildRules jest kolejną zaawansowaną funkcją FluentValidation, która umożliwia definiowanie zasad walidacji dla zagnieżdżonych obiektów. Dzięki temu, zamiast tworzyć oddzielne walidatory dla każdego zagnieżdżonego typu i ręcznie zarządzać ich wywołaniami, można skorzystać z zintegrowanego podejścia, które FluentValidation oferuje poprzez ChildRules. To podejście nie tylko upraszcza proces walidacji, ale także zapewnia, że wszystkie reguły walidacji dla danego modelu są zawarte w jednym miejscu, co znacząco ułatwia zarządzanie i utrzymanie kodu.

Załóżmy, że mamy klasę Customer zawierającą kolekcję Addresses, a każdy adres ma swoje własne reguły walidacji.

public class Customer
{
    public List<Address> Addresses { get; set; } = new List<Address>();
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleForEach(customer => customer.Addresses).ChildRules(address =>
        {
            address.RuleFor(a => a.Street).NotEmpty().WithMessage("The street cannot be empty.");
            address.RuleFor(a => a.City).NotEmpty().WithMessage("The city cannot be empty.");
            address.RuleFor(a => a.PostalCode).Matches(@"^\d{2}-\d{3}$")
                .WithMessage("The postal code must be in the format 00-000.");
        });
    }
}

W tym przykładzie każdy adres w kolekcji Addresses klienta jest poddawany walidacji z wykorzystaniem ChildRules. Dzięki temu każdy adres musi mieć określoną ulicę, miasto i kod pocztowy zgodny z określonym wzorcem. Użycie ChildRules sprawia, że walidacja jest bardziej modularna i łatwiejsza w utrzymaniu, szczególnie gdy struktury danych są złożone i zawierają wiele poziomów zagnieżdżenia.

Podsumowując, w naszej serii poświęconej FluentValidation, omówiliśmy dwa kluczowe mechanizmy umożliwiające efektywną walidację kolekcji: RuleForEach() i ChildRules. Obydwa te narzędzia są niezwykle przydatne w zapewnianiu integralności danych w aplikacjach obsługujących złożone struktury danych.

RuleForEach() jest idealne do stosowania jednolitych zasad walidacji na każdym elemencie kolekcji. Jak pokazano w przykładzie z CustomerValidator, ta metoda pozwala na łatwą i skuteczną walidację wszystkich elementów w kolekcji, gwarantując spełnienie określonych wymagań, takich jak minimalna wartość zamówienia.

Z drugiej strony, ChildRules umożliwia zaawansowaną walidację zagnieżdżonych obiektów wewnątrz kolekcji. Przykład z CustomerValidator ilustruje, jak można walidować każdy adres w kolekcji Addresses, stosując indywidualne reguły dla różnych atrybutów adresu. To podejście nie tylko zwiększa precyzję i skuteczność walidacji, ale również sprawia, że kod jest bardziej zorganizowany i łatwiejszy w utrzymaniu.

Podsumowując, RuleForEach() i ChildRules w FluentValidation oferują programistom potężne narzędzia do tworzenia czystych, modularnych i wydajnych walidacji dla aplikacji zarządzających złożonymi danymi. Ich zastosowanie znacząco poprawia jakość kodu i zapewnia solidną obronę przed nieprawidłowymi danymi, co jest kluczowe dla stabilności i niezawodności każdej aplikacji.

Do zobaczenia w następnych postach!