6

В FluentValidation есть расширение или какой-либо другой способ отложить выбор дочернего валидатора в зависимости от типа/значения проверяемого свойства?Отложить выбор дочернего валидатора в зависимости от типа или значения свойства

Моя ситуация в том, что у меня есть класс уведомлений, который я хочу проверить. Этот класс имеет свойство Payload, которое может быть одним из нескольких типов полезных нагрузок, например. SmsPayload, EmailPayload и т. Д. Каждый из этих подклассов Payload имеет свой собственный связанный валидатор, например. SmsPayloadValidator и EmailPayloadValidator соответственно. В дополнение к вышесказанному, нет ссылок от основной библиотеки (ов) на отдельных поставщиков уведомлений. По сути, это означает, что я могу добавить поставщиков по мере необходимости и подключить все, используя IoC.

Рассмотрим следующие классы:

public class Notification 
{ 
    public Payload Payload { get; set; } 
    public IEnumerable<string> Details { get; set; } 
} 

public abstract class Payload 
{ 
    public string Message { get; set; } 
    public abstract string Type { get; } 
} 

public class SmsPayload : Payload 
{ 
    public List<string> Numbers { get; set; } 
    public string Region { get; set; } 
    public string Provider { get; set; } 
} 

Существует уведомления валидатор и SmsPayloadValidator следующим образом:

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(IValidator<Payload> payloadValidator) 
    { 
     RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null."); 
     RuleFor(notification => notification.Payload).SetValidator(payloadValidator); 
    } 
} 

public class SmsPayloadValidator : AbstractValidator<SmsPayload> 
{ 
    public SmsPayloadValidator() 
    { 
     RuleFor(payload => payload.Provider) 
      .Must(s => !string.IsNullOrEmpty(s)) 
      .WithMessage("Provider is required."); 
     RuleFor(payload => payload.Numbers) 
      .Must(list => list != null && list.Any()) 
      .WithMessage("Sms has no phone numbers specified."); 
     RuleFor(payload => payload.Region) 
      .Must(s => !string.IsNullOrEmpty(s)) 
      .WithMessage("Region is required."); 
    } 
} 

Как я уже упоминал узел, где NotificationValidator это не ссылки на узлы, где человек Классы валидатора полезной нагрузки живут. Все проводки позаботились Ioc (Simple-Injector для этого проекта).

В основном я хочу сделать что-то вроде следующего - первый регистрирующим заводского обратного вызова в простом Injector:

container.Register<Func<Payload, IValidator<Payload>>>(() => (payload => 
{ 
    if (payload.GetType() == typeof(SmsPayload)) 
    { 
     return container.GetInstance<ISmsPayloadValidator>(); 
    } 
    else if (payload.GetType() == typeof(EmailPayload)) 
    { 
     return container.GetInstance<IEmailPayloadValidator>(); 
    } 
    else 
    { 
     //something else; 
    } 
})); 

Такое, что я могу выбрать подходящий валидатор следующим образом:

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(Func<Payload, IValidator<Payload>> factory) 
    { 
     RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null."); 
     RuleFor(notification => notification.Payload).SetValidator(payload => factory.Invoke(payload)); 
    } 
} 

Любого предложения? или есть лучший способ сделать то, что я предлагаю? Если нет, я открою репозиторий FluentValidation и отправлю PR.

ответ

5

Вы можете сделать свои намерения немного более ясными, избегая фабрики. Хотя конечный результат, вероятно, один и тот же при таком подходе, вы можете, как минимум, ввести IValidator<Payload> вместо Func<Payload, IValidator<Payload>>.

Создайте класс под названием PolymorphicValidator. Это позволит вам последовательно повторять этот шаблон, а также обеспечивать резервный валидатор базы, если вы этого желаете. Это, по существу, рекомендуемый «составной рисунок», описанный here в документации Simple Injector.

public class PolymorphicValidator<T> : AbstractValidator<T> where T : class 
{ 
    private readonly IValidator<T> _baseValidator; 
    private readonly Dictionary<Type, IValidator> _validatorMap = new Dictionary<Type,IValidator>(); 

    public PolymorphicValidator() { } 

    public PolymorphicValidator(IValidator<T> baseValidator) 
    { 
     _baseValidator = baseValidator; 
    } 

    public PolymorphicValidator<T> RegisterDerived<TDerived>(IValidator<TDerived> validator) where TDerived : T 
    { 
     _validatorMap.Add(typeof (TDerived), validator); 
     return this; 
    } 

    public override ValidationResult Validate(ValidationContext<T> context) 
    { 
     var instance = context.InstanceToValidate; 
     var actualType = instance == null ? typeof(T) : instance.GetType(); 
     IValidator validator; 
     if (_validatorMap.TryGetValue(actualType, out validator)) 
      return validator.Validate(context); 
     if (_baseValidator != null) 
      return _baseValidator.Validate(context); 
     throw new NotSupportedException(string.Format("Attempted to validate unsupported type '{0}'. " + 
      "Provide a base class validator if you wish to catch additional types implicitly.", actualType)); 
    } 
} 

Вы можете зарегистрировать свой валидатор, как это (возможно предоставление базового класса запасного варианта и дополнительные валидаторы класса ребенка):

container.RegisterSingle<SmsPayloadValidator>(); 
//container.RegisterSingle<EmailPayloadValidator>(); 
container.RegisterSingle<IValidator<Payload>>(() => 
    new PolymorphicValidator<Payload>(/*container.GetInstance<PayloadValidator>()*/) 
     .RegisterDerived(container.GetInstance<SmsPayloadValidator>()) 
     /*.RegisterDerived(container.GetInstance<EmailPayloadValidator>() */); 

Это создаст синглтон PolymorphicValidator, который содержит одноэлементный ребенок валидатор (Singletons являются рекомендованный командой FluentValidation). Теперь вы можете ввести IValidator<Payload>, как показано в первом примере NotificationValidator.

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(IValidator<Payload> payloadValidator) 
    { 
     RuleFor(notification => notification.Payload) 
      .NotNull().WithMessage("Payload cannot be null.") 
      .SetValidator(payloadValidator); 
    } 
} 
+0

+1 для использования композитного материала. Однако я считаю, что ваша реализация является более сложной, чем необходимо. См. Мой ответ для альтернативного подхода. – Steven

+0

Спасибо за upvote. Однако я бы сказал, что мой подход не более «сложный», чем ваш. Более утомительно, да. Возможно, непрактично, как вы говорите в своем ответе, но только в зависимости от намерений исполнителя. –

+1

Хотя немного сложнее, он немного более явный, и я работал. Благодаря! –

4

Я согласен с ответом Тейлора об использовании Composite (так определенно +1 для этого), но его реализация не такая практичная. Поэтому я предлагаю немного отличающуюся реализацию, все еще использую композит.

Если я не ошибаюсь, ваш композит должен выглядеть следующим образом:

public class CompositeValidator<T> : AbstractValidator<T> where T : class 
{ 
    private readonly Container container; 

    public CompositeValidator(Container container) 
    { 
     this.container = container; 
    } 

    public override ValidationResult Validate(T instance) 
    { 
     var validators = this.container.GetAllInstances(instance.GetType()); 

     return new ValidationResult(
      from IValidator validator in validators 
      from error in validator.Validate(instance).Errors 
      select error); 
    } 
} 

регистрации должно быть следующим:

// Simple Injector v3.x 
container.RegisterCollection(typeof(IValidator<>), 
    AppDomain.CurrentDomain.GetAssemblies()); 

container.Register(typeof(IValidator<>), 
    typeof(CompositeValidator<>), 
    Lifestyle.Singleton); 

// Simple Injector v2.x 
container.RegisterManyForOpenGeneric(
    typeof(IValidator<>), 
    container.RegisterAll, 
    AppDomain.CurrentDomain.GetAssemblies()); 

container.RegisterOpenGeneric(
    typeof(IValidator<>), 
    typeof(CompositeValidator<>), 
    Lifestyle.Singleton); 

Что здесь происходит следующее:

  • Звонок RegisterCollection гарантирует, что все валидаторы зарегистрированы как коллекции. Это означает, что для каждого T в нем может быть несколько валидаторов. Например, если ваша система имеет PayloadValidator и SmsPayloadValidator, разрешение GetAllInstances<IValidator<SmsPayload>> вернет оба валидатора, так как IValidator<in T> содержит ключевое слово in (контравариантно).
  • Регистрация Register будет зарегистрирована CompositeValidator<T> для возврата по запросу IValidator<T>. Поскольку Simple Injector differentiates registrations of collections with one-to-one registrations, впрыскивание IValidator<T> всегда приведет к введению составного валидатора. Поскольку составной валидатор зависит только от контейнера, он может быть зарегистрирован как одиночный.
  • Потребители вводятся с помощью CompositeValidator<T> (когда они зависят от IValidator<T>), а составной валидатор будет запрашивать коллекцию валидаторов на основе точного типа. Поэтому, если потребитель использует IValidator<Payload>, составной валидатор определит реальный тип (например, SmsPayload) и запросит все присваиваемые валидаторы для этого точного типа и перенаправит проверки на эти типы.
  • Если для определенного типа нет валидаторов, составной валидатор автоматически вернет действительный ValidationResult.
+1

+1 Для более общего решения. У вас может быть небольшое преимущество, поскольку вы являетесь разработчиком структуры, и я только что узнал об этом, когда прочитал этот вопрос ... lol. Одно замечание для наблюдателей заключается в том, что, хотя это решение менее сложное для реализации, оно также менее гибко, чем мое решение. Вывод предполагается, что разработчик всегда хочет зарегистрировать и использовать все валидаторы для определенного типа. Если это ваше намерение, то это определенно отличный подход. –

+1

@TaylorBuchanan: Очень примечательно, что вы сделали некоторый анализ, прочитав документы, просто чтобы ответить на этот вопрос. Не так много людей делают это на Stackoverflow. – Steven

 Смежные вопросы

  • Нет связанных вопросов^_^