wtorek, 16 listopada 2010

Fluent Validation i dziedziczenie

FluentValidation jest całkiem przyjemną biblioteką, która jak wskazuje nazwa dostarcza narzędzie do walidacji klas poprzez interfejs typu fluent. Co czyni ją bardzo prostą w użyciu (przykłady w dokumentacji). Poza standardowym przypadkiem tzn mamy klasę, mamy walidator i gotowe, wspiera również zagnieżdżenia klas oraz walidację kolekcji. Do pewnego czasu wydawało mi się, że więcej do szczęścia nie będzie mi potrzebne. Zawsze jest jednak jakieś ale... Biblioteka ze względu na sposób budowania walidatorów (dziedziczenie po klasie AbstractValidator<T>) nie wspiera dziedziczenia ich samych, co jednak czasem może się przydać. Cały problem najlepiej będzie przedstawić na prostym przykładzie (bardzo mocno niewyszukanym ;]). Załóżmy zatem istnienie klas dziedziczących po sobie:
public class User
{
    public int Id { get; set; }
    public string Login { get; set; }
    public string Password { get; set; }
}

public class Person : User
{
    public string FirstName { get; set; }
    public string Surname { get; set; }
}
Walidator dla klasy User może wyglądać następująco:
public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(p => p.Id)
            .GreaterThanOrEqualTo(0);
        RuleFor(p => p.Login)
            .NotEmpty()
            .Length(5, 20);
        RuleFor(p => p.Password)
            .NotEmpty()
            .Length(6, 30);
    }
}
Co w takim razie zrobić z walidatorem klasy Person? Możemy go stworzyć jako AbstractValidator<Person>, ale od nowa trzeba będzie definiować reguły dla odziedziczonych pól, czyli niefajnie. Pozornie lepszym wyjściem jest dziedziczenie po UserValidator. Reguły będą poprawnie działać, ale stracimy interfejs fluent. Dlaczego? Ponieważ w drzewie dziedziczenia PersonValidator będzie uznany jak AbstractValidator<User>. Stąd metoda RuleFor będzie potrafiła jedynie odwoływać się do części bazowej klasy Person. Tak i tak niedobrze.
Dlatego przyjrzyjmy się bliżej w jaki sposób działa sama biblioteka. Interesuje nas głównie AbstactValidator. Okazuje się, że żądania walidacji opierają się ostatecznie na wywołaniu metody Validate(ValidationContext<T> context)(#70). Z źródła wynika, że metoda ta sprawdza każdą regułę używając kontekstu walidacji, a następnie łączy wyniki w jedną całość. I tu pojawia się pomysł... A może by tak do zbioru wynikowego dodać rezultat pracy walidatora bazowego. Tak powstał InheritanceValidator<TBase, TThis> przeciążający metodę Validate.
public abstract class InheritanceValidator<TBase, TThis> : AbstractValidator<TThis>
    where TThis : TBase
{
    private AbstractValidator<TBase> _baseValidator;

    public InheritanceValidator(AbstractValidator<TBase> baseValidator)
    {
        _baseValidator = baseValidator;
    }

    public override ValidationResult Validate(ValidationContext<TThis> context)
    {
        //stworzenie kontekstu dla wywołania reguł walidatora bazowego
        ValidationContext<TBase> baseValidationContext = new ValidationContext<TBase>
            (context.InstanceToValidate, context.PropertyChain, context.Selector);

        //wywołanie reguł bazowego walidatora
        IEnumerable<ValidationFailure> baseFailures = _baseValidator.SelectMany((rule) => {
            return rule.Validate(baseValidationContext);
        });

        //wywołanie reguł walidatora klasy dziedziczącej i połączenie ich z wynikami bazowymi
        IEnumerable<ValidationFailure> failures = this.SelectMany((rule) => {
            return rule.Validate(context);
        }).Concat(baseFailures);

        //stworzenie klasy reprezentującej ostateczny wynik
        return new ValidationResult(failures.ToList<ValidationFailure>());
    }
}
W ten sposób jeżeli implementując PersonValidator wykorzystany zostanie InheritanceValidator, to podczas walidacji sprawdzone zostaną wszystkie dostępne reguły zarówno w walidatorze podstawowym jak i bazowym.
public class PersonValidator : InheritanceValidator<User, Person>
{
    public PersonValidator() :
        base(new UserValidator())
    {
        RuleFor(p => p.FirstName)
            .NotEmpty();
        RuleFor(p => p.Surname)
            .NotEmpty();
    }
}
Gdzie użycie gotowego walidatora pozostaje proste tak jak zawsze było:
Person p = new Person();
PersonValidator validator = new PersonValidator();
ValidationResult result = validator.Validate(p);
Na zakończenie dodam jeszcze jedno. Podany wyżej sposób sprawdza się i działa tak jak powinien. Istnieje jednak prawdopodobieństwo, że jest jakaś lepsza metoda o której najzwyczajniej w świecie nie wiem ;]
Dla zainteresowanych zamieszczam również przykładowy projekt wykorzystujący przedstawione rozwiązanie.

Brak komentarzy:

Prześlij komentarz