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śleniaRuleSet
? W takim przypadku Fluent Validation wykona wszystkie reguły walidacji, które nie są przypisane do żadnego konkretnegoRuleSet
. To oznacza, że jeśli zdefiniowaliśmy reguły pozaRuleSet
, 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żdymRuleSet
.
Nazewnictwo
RuleSet
powinno być intuicyjne i opisowe, odzwierciedlając konkretny kontekst lub akcję, np.AccountCreation
zamiastRuleSet1
. Stosuj jednolitą konwencję, taką jakCamelCase
, 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żdyRuleSet
, 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ższaValidate
i wymagają zaimportowania przestrzeni nazwFluentValidation
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.