Defer selection of a child validator depending on the type or value of the property

Is there an extension in FluentValidation or any other way to defer the selection of the child validator depending on the type / value of the property being checked?

My situation is that I have a Notification class that I want to check. This class has a Payload property, which can be one of several types of payloads, for example. SmsPayload, EmailPayload, etc. Each of these Payload subclasses has its own associated validator, for example. SmsPayloadValidator and EmailPayloadValidator respectively. In addition to the above, there are no links from the main library (s) to individual notification providers. Essentially, this means that I can add providers as needed and connect everything using IoC.

Consider the following classes:

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; } } 

There is a notification validator and SmsPayloadValidator as follows:

 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."); } } 

As I mentioned the assembly, where NotificationValidator does not reference the assemblies in which the individual payload validator classes live. All wiring was taken care of by Ioc (Simple-Injector for this project).

Basically, I want to do something like the following - by first registering the factory callback in Simple 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; } })); 

Thus, I can select the appropriate validator as follows:

 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)); } } 

Any suggestions? or is there a better way to do what i suggest? If not, I will open the FluentValidation repository and submit the PR.

+6
source share
2 answers

You can make your intentions a little clearer by avoiding the factory. Although the end result probably matches this approach, you can at least insert an IValidator<Payload> injection directly instead of Func<Payload, IValidator<Payload>> .

Create a class called PolymorphicValidator . This will allow you to consistently repeat this pattern, as well as provide a backup database validator if you wish. This is essentially the recommended “compound pattern” described here in the Simple Injector documentation.

 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)); } } 

Then you can register your validator as follows (optionally providing backup and base classes for the base class):

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

This will create a singleton PolymorphicValidator that contains syntax checks for a single user (Singletons are recommended by the FluentValidation team). You can now enter the IValidator<Payload> , as shown in the first NotificationValidator example.

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

I agree with Taylor's answer about using Composite (so definitely +1 for this), but its implementation is not so practical. Therefore, I propose a slightly different implementation, but still use composite.

If I'm not mistaken, your composition should look like this:

 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); } } 

Registration should be as follows:

 // 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); 

Here the following happens:

  • Calling RegisterCollection ensures that all validators are registered as collections. This means that for each T there can be several validators in it. For example, if your system has PayloadValidator and SmsPayloadValidator , the GetAllInstances<IValidator<SmsPayload>> returns both validators because IValidator<in T> contains the in keyword (contravariantly).
  • Register Register register a CompositeValidator<T> , which will be returned for each requested IValidator<T> . Because Simple Injector distinguishes collection registrations with individual registrations , an IValidator<T> injection will always lead to the introduction of a composite validator. Since the composite validator depends only on the container, it can be registered as a single one.
  • Consumers are introduced using CompositeValidator<T> (when they depend on IValidator<T> ), and the composite validator will request a collection of validators based on the exact type. Therefore, if the consumer uses the IValidator<Payload> , the composite validator will determine the real type ( SmsPayload for example) and request all the assigned validators for this exact type and redirect checks to these types.
  • If there are no validators for a particular type, the composite validator will automatically return a valid ValidationResult .
+4
source

Source: https://habr.com/ru/post/983076/


All Articles