Walidacja danych w nHibernate – ewolucja postępowania w czasie

Niedawno byłem zmuszony do powrotu do kodu, który kiedyś współtworzyłem. Musiałem dołożyć pewną w gruncie rzeczy drobną funkcjonalność. Nie sprawiło to mi większych problemów, ale samo obcowanie z tym kodem skłoniło mnie do refleksji. Dopiero porównując tamten kod (z którego byłem przecież dumny jak paw) z kodem dzisiejszym widzę różnicę (ciekawe co powiem za kilka lat o obecnym kodzie). Różnicę w sobie samym. Widzę jak przez ostatnie dwa lata się zmieniłem (ale to temat na inny post). Moją szczególną uwagę wzbudził sposób walidacji danych.

Początki

Z perspektywy czasu każdy jest mądry (nawet ja) więc krytykować jest łatwo no, ale cóż za błędy “młodości” trzeba płacić. Gdy patrzę teraz na kod, który kiedyś napisałem to widzę jedno – spaghetti. Ma jednak jedną niewątpliwą zaletę – działa. Aby nie być gołosłowny mały przykład spaghetti a’la Wojtek.

if(string.IsNullOrEmpty(person.FirstName))
{
    //DoSomethingWithIt
}
else if (string.IsNullOrEmpty(person.LastName))
{
    //DoSomethingWithIt;
}
else if (person.Age >18)
{
    //DoSomethingWithIt
}
//Next rules
session.Save(person);

Jeśli ktoś się z takiego kodu śmieje to jego prawo. Pisałem tak jak umiałem (inna sprawa, że dość szybko przestało mi się to podobać) więc przyjmuję to z pokorą.

Trochę później – FluentValidation

FluentValidation
Widząc, że walidacja w ten sposób prowadzi donikąd (albo poza ekran przy bardzo rozbudowanych if-elsach) zacząłem szukać rozwiązania. Znalazłem takie
FluentValidation i muszę się przyznać, że się w nim zakochałem. Jej użycie było banalne. Wystarczyło stworzyć walidatora dziedziczącego po AbstractValidator a następnie opisać reguły walidacyjne w konstruktorze.

public class PersonValidator : AbstractValidator<Preson>
{
    public PersonValidator()
    {
        RuleFor(p => p.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(p => p.LastName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(p => p.Age).GreaterThanOrEqualTo(18).WithMessage("Person must be at least 18 years old");
        //Next rules
    }
}

(oczywiście to tylko bardzo proste reguły, ale nic nie stoi na przeszkodzie aby stworzyć naprawdę skomplikowane reguły biblioteka ta ma naprawdę wiele możliwości)
Następnie wystarczy wywołać takiego walidatora przy zapisie.

PersonValidator validator = new PersonValidator();
ValidationResult results = validator.Validate(person);
bool validationSucceeded = results.IsValid;
IList<ValidationFailure> failures = results.Errors;

Znowu byłem z siebie zadowolony (tylko, że do czasu).

Chwila obecna – NHibernate.Validator

Dlaczego byłem niezadowolony z FluentValidation? No cóż może dlatego, że czasami mimo ewidentnego złamania reguł biznesowych można było wykonać zapis do bazy, Wystarczyło, że ktoś (najczęściej ja) zapomniał dopisać kodu uruchamiającego walidatora, albo pominął jego wynik. Choć może lepszą odpowiedzią jest ta, którą znalazłem swego czasu na blogu Leva Gorodinskiego

Validation frameworks are best used at specific application layers, not across all layers

FluentValidation jest wspaniały i co do tego nie mam wątpliwości, ale stosuje go już tylko do walidacji danych wprowadzanych przez użytkownika. Jeśli chce walidować moje encje przed zapisem (a możemy spokojnie przyjąć, że muszę) to stosuje w tej chwili NHibernate.Validator. Rozwiązanie to polecał kiedyś na swoim blogu Ayende, ale wtedy mi się ono nie podobało. Pewnie dlatego, że mam jakąś awersję do klas przesadnie obłożonych atrybutami (źle mi się czyta jak do jednej property jest wiele atrybutów 😉 )

public class Preson
{
    public virtual int Id{ get; set; }

    [NotNullNotEmpty]
    [Length(25)]
    public virtual string FirstName { get; set; }

    [NotNullNotEmpty]
    [Length(25)]
    public virtual string LastName { get; set; }

    [Min(18)]
    public virtual int Age { get; set; }

    //Next property
}

Na szczęście (jak się później okazało) jest inny sposób zapisywania reguł biznesowych

public class PersonDef : ValidationDef&lt;Preson&gt;
{
    public PersonDef()
    {
        Define(p => p.FirstName).NotNullableAndNotEmpty().WithMessage("Please specify a first name");
        Define(p => p.LastName).NotNullableAndNotEmpty().WithMessage("Please specify a first name");
        Define(p => p.Age).GreaterThanOrEqualTo(18).WithMessage("Person must be at least 18 years old");
        //Next rules
    }
}

To sposób bardzo podobny do rozwiązania z FluentValidation (użyłbym wręcz słowa bliźniaczy). Tak samo możemy napisać bardziej skomplikowane reguły (np. Jan Nowak musi mieć co najmniej 25 lat). Gdzie więc zysk ktoś się zapyta?
Otóż budując sessionFactory wystarczy że dodam taką linijkę

ExposeConfiguration(ConfigureNhibernateValidator)

a następnie

private static void ConfigureNhibernateValidator(Configuration config)
{
    var provider = new NHibernateSharedEngineProvider();
    NHibernate.Validator.Cfg.Environment.SharedEngineProvider = provider;

    var nhvConfiguration = new NHibernate.Validator.Cfg.Loquacious.FluentConfiguration();
    nhvConfiguration
       .Register(typeof(PersonDef).Assembly.ValidationDefinitions())
       .SetDefaultValidatorMode(ValidatorMode.UseExternal);

    ValidatorEngine validatorEngine = NHibernate.Validator.Cfg.Environment.SharedEngineProvider.GetEngine();
    validatorEngine.Configure(nhvConfiguration);
    ValidatorInitializer.Initialize(config, validatorEngine);
}

Od tej pory wszystkie walidatory są rejestrowane przy starcie aplikacji więc nie muszę się przejmować, że ktoś(najczęściej ja) pozwoli zapisać do bazy złe dane. Jeśli ktoś chce poczytać o możliwości customizacji komunikatów przekazywanych przez walidator to polecam wpisMarcina Wolańskiego(trochę stary ale zawsze można się czegoś ciekawego dowiedzieć).

Podsumowanie

Czy to już wszystkie możliwości walidacji? Oczywiście, że nie no bo co zrobić np. ze sprawdzeniem unikalności danej property? Moim zdaniem takie rzeczy powinny być sprawdzane, ale przez serwis(tylko co ja tam mogę wiedzieć).

3 thoughts on “Walidacja danych w nHibernate – ewolucja postępowania w czasie

  1. Pingback: dotnetomaniak.pl

Comments are closed.