Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

In FluentValidation is there an extension or some other way to defer the selection of a child validator depending on the type/value of the property being validated?

My situation is that I have a Notification class that I want to validate. This class has a Payload property, which can be one of a number of Payload types e.g. SmsPayload, EmailPayload etc. Each of these Payload subclasses has their own associated validator e.g. SmsPayloadValidator and EmailPayloadValidator respectively. In addition to the above, there is no references from the core library(ies) to the individual notification providers. Essentially, this means I can add providers on an as needed basis and wire everything up 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 the NotificationValidator is does not reference the assemblies where the individual Payload validator classes live. All the wiring is taken care of by Ioc (Simple-Injector for this project).

Basically I want to do something like the following - first by registering a 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;
    }
}));

Such that 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'm proposing? If none, I'll fork the FluentValidation repository and submit a PR.

like image 654
Ryan.Bartsch Avatar asked Feb 27 '15 00:02

Ryan.Bartsch


Video Answer


1 Answers

You might make your intentions a little more clear by avoiding the factory. While the end result is probably the same with this approach, you can at least end up injecting IValidator<Payload> directly instead of Func<Payload, IValidator<Payload>>.

Create a class called PolymorphicValidator. This will allow you to repeat this pattern in a consistent manner, as well as provide a fallback base validator if you so desire. This is essentially the recommended "composite 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));
    }
}

You can then register your validator like this (optionally providing a base class fallback and additional child class validators):

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 which contains singleton child validators (Singletons are recommended by the FluentValidation team). You can now inject IValidator<Payload> as shown in your 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);
    }
}
like image 74
Taylor Buchanan Avatar answered Oct 14 '22 11:10

Taylor Buchanan