Search code examples
c#fluentvalidation

Best practice to validate properties based on other properties


Imagine you have a class like :

public enum Kind { Kind1, Kind2 }
public class MyForm
{
    public string Kind { get; set; }
    public ACustomClass1 Custom1 { get; set; }
    public ACustomClass2 Custom2 { get; set; }
}

And you want to validate Custom1 with Custom1Validator when Kind == Kind1 (and Custom2 with Custom2Validator when Kind == Kind2, obviously)

What is the best way to proceed with version 8.6.0 ?

At the moment, I've done like this (but I find it is awkward):

public class MyFormValidator : AbstractValidator<MyForm>
{
    public MyFormValidator (IStringLocalizer<Strings> localizer, Custom1Validator validator1, Custom2Validator validator2)
    {            
        //validate Kind and then, in function of Kind, use correct validator
        RuleFor(x => x).Custom((f, context) => {
            if (!Enum.TryParse<Kind>(f.Kind, out var kind))
            {
                context.AddFailure(localizer["Invalid Kind"]);
                return;
            }
            switch (kind)
            {
                case Kind.Kind1:
                    if (f.Custom1 == null)
                    {
                        context.AddFailure(localizer["Invalid Kind"]);
                    }
                    else if (! validator1.Validate(f.Custom1, out var firstError))
                    {
                        context.AddFailure(firstError);
                    }
                    break;
                case Kind.Kind2:
                    if (f.Custom2 == null)
                    {
                        context.AddFailure(localizer["Invalid Kind"]);
                    }
                    else if (!validator2.Validate(f.Custom2, out var firstError))
                    {
                        context.AddFailure(firstError);
                    }
                    break;
            }
        });
    }
}

Note that I am using asp.net core with dependency injection (this is why there is IStringLocalizer and I can not use SetValidator for Custom1 and Custom2)

What I'd like instead is something like

RuleFor(x => x.Kind).NotEmpty().IsEnumName(typeof(Kind)).withMessage(_ => localizer["Invalid Kind"]);
RuleFor(x => x.Custom1).NotEmptyWhen(f => f.Kind == Kind.Custom1.ToString()).withMessage(_ => localizer["Invalid Kind"])
RuleFor(x => x.Custom1).SetValidator(validator1); //would be executed only when custom1 is not null

//same for custom2

The problem is that I do not see how to do code the NotEmptyWhen method


Solution

  • Restructure?

    By the looks of your posted code snippets, I presume that MyForm will never have a populated Custom1 and Custom2 property in the same request. So, instead of having a parent model that holds both payload kinds, I would encourage you to directly use the model that represents the payload being validated. Then you won't run into this nasty pattern of checking the kind wherever necessary.

    One of your form endpoints accepts a Custom1, which has an associated Custom1Validator. Another one of your form endpoints accepts a Custom2, which has an associated Custom2Validator. They are decoupled. You can safely change one without affecting the other.

    Use Fluent Validation Conditions (When/Unless)

    If you're dead set on having one model responsible for representing the payload of multiple requests (please don't), you can use the When() method provided by the library. Take a look at their documentation on conditional rules.