FluentValidation – wstrzykiwanie zależności

Przedstawione przeze mnie do tej pory przykłady walidacji były zdecydowanie prostymi walidacjami – wszystkie dane zawierały się w modelu walidowanym. Niekiedy jednak zachodzi potrzeba sięgnięcia do zewnętrznych zasobów – bazy danych / cache / serwisów zewnętrznych. Mając w pamięci 5 zasadę SOLID’a warto by takie zależności wstrzykiwać do walidatora, zamiast tworzyć je bezpośrednio w konstruktorze. Kod będzie lepiej utrzymywalny i testowalny. Ja w moim przykładzie chciałbym wam pokazać wstrzykiwanie zależności do walidatorów dla ASP.NET MVC i ASP.NET WebAPI.

Na samym początku stwórzmy repozytorium użytkowników, które będziemy wstrzykiwać do walidatora. Ponieważ w przykładzie nie używam bazy danych, repozytorium będzie zwracało domyślną listę obiektów:

public class UserRepository:IUserRepository
{
    public IEnumerable<User> GetAll()
    {
        return new User[]
        {
            new User() {Id = 1, Email = "john@gmail.com", UserName = "john", Password = "123456"},
            new User() {Id = 2, Email = "rick@gmail.com", UserName = "rick", Password = "654321"},
        };
    }
}

public interface IUserRepository
{
    IEnumerable<User> GetAll();
}

Następnie do walidatora UserViewModel wstrzyknijmy powyższe repozytorium i dodajmy regułę walidacji o unikalnym emailu.

public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
    private IUserRepository userRepository;

    public UserViewModelValidator(IUserRepository userRepository)
    {
        this.userRepository = userRepository;

        this.RuleFor(r => r.UserName).NotEmpty().Length(0, 50);

        this.RuleFor(r => r.Email).NotEmpty().EmailAddress().Length(0, 100)
            .Must(BeUnique).WithMessage("Email must be unique.");

        this.RuleFor(r => r.Password).NotEmpty().Length(6, 50);
    }

    private bool BeUnique(string email)
    {
        var emailFound = userRepository.GetAll().Any(u => u.Email == email);
        return !emailFound;
    }
}

Teraz część najważniejsza – połączenie naszego kontenera z FluentValidation. Ja mój przykład oparłem o kontener Autofac, ale analogiczny kod można stworzyć dla każdego używanego przez nas kontenera. Poniższe 2 listy kodu znajdują się w konfiguracji kontenera, u mnie w pliku Global.asax.cs.

Najpierw przeskanujmy nasz projekt w poszukiwaniu walidatorów,aby połączyć je z interfejsami.

AssemblyScanner.FindValidatorsInAssembly(Assembly.GetExecutingAssembly())
    .ForEach(match =>
    {
        builder.RegisterType(match.ValidatorType).As(match.InterfaceType);
    });

Kolejnym krokiem będzie poinformowanie FluentValidation, że walidatory powinny być instancjonowane przez Autofac. Robi się to przez rozwinięcie istniejącej już konfiguracji o informacje o fabryce walidatorów. Taką konfigurację trzeba dokonać osobno dla ASP.NET MVC i ASP.NET WebAPI.

FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(
    p => p.ValidatorFactory = new AutofacValidatorFactory(container));

FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(GlobalConfiguration.Configuration, 
    p => p.ValidatorFactory = new AutofacValidatorFactory(container));

Poniżej przedstawiam przykładowy kod fabryki walidatorów. Wyszukuje ona w kontenerze walidator wymaganego typu.

public class AutofacValidatorFactory : ValidatorFactoryBase
{
    private readonly IContainer container;

    public AutofacValidatorFactory(IContainer container)
    {
        this.container = container;
    }

    public override IValidator CreateInstance(Type validatorType)
    {
        IValidator validator = (IValidator)container.Resolve(validatorType);
        return validator;
    }
}

I to wszystko – wstrzykiwanie zależności do walidatorów powinno działać. Nie potrzeba dodatkowej konfiguracji w samym MVC / WebAPI FluentValidation przez konfigurację fabryki wszystkim się zajmuje. By być całkowicie pewnym dokonajmy prostych testów tej funkcjonalności dokonując zapytań z poziomu przeglądarki i Postmana.
chrome_2016-04-18_23-26-54
chrome_2016-04-18_23-27-12

Widzimy, że zarówno dla ASP.NET MVC jak i ASP.NET WebAPI walidacja zadziałała i dostajemy komunikat o wymaganym unikalnym mailu.

Jeśli macie jeszcze jakieś pomysły na post o FluentValidation to będę wdzięczny za podrzucenie ich – na razie mam jeszcze pomysł na posty o lokalizacji komunikatów, warunkowej walidacji i walidacji dynamicznego modelu 🙂

Standardowo, wszystkie pokazane tutaj przykłady są na GitHubie.

  • Pingback: dotnetomaniak.pl()

  • Dariusz Lenartowicz

    Mała uwaga!
    Walidatory w Web API są buforowane przez samo Web API w związku z czym kolejnym razem ten sam walidator będzie miał tą samą instancję wszystkich zależności co może łatwo doprowadzić do katastrofy. Rozwiązaniem jest użycie Func lub własna mała infrastrukturka, która zanim wykona cokolwiek w akcji kontrolera wykona najpierw walidację modelu.

    • Radosław Maziarka

      Oczywiście tak jest – możliwym rozwiązaniem jest tak jak podałeś powyżej wstrzykiwanie Func zamiast IService, dzięki czemu w kontrolerach będziemy mieli pewność, że dostajemy nową instancję serwisu.

      Można się również pokusić o usunięcie cachowania / buforowania walidatorów przez lekki hak na WebAPI:

      Assembly httpAssembly = Assembly.Load(„System.Web.Http”);
      Type cacheType = httpAssembly.GetType(„System.Web.Http.Validation.IModelValidatorCache”);
      GlobalConfiguration.Configuration.Services.Clear(cacheType));

      Co usunie nam mechanizm buforowania i sprawi że zawsze będzie wywoływane tworzenie walidatora. Niestety interfejs IModelValidatorCache jest internal, przez wymagane jest wsparcie się refleksją.