Search code examples
c#.netfluentvalidation

FluentValidation - pre-validation / conditional validation with no code duplication


I'm trying to create Validation which is able to have two groups and block second validation if first fail (it contains many rules).

For now I did create a private 'BasicValidation' class inside and in 'main validator' do sth like this:

RuleFor(m => m).SetValidator(new BasicValidation()).DependentRules(() => {
//Complex validation
RuleFor(m => m.IdOfSthInDb)
    .MustAsync(ItemMustExists)
    .WithMessage("Item does not exist.");
});            

It does the trick but I would like to avoid creating that 'BasicValidation' for each model.


Solution

  • In my previous answer I misunderstood the question. The main goal is to avoid code duplication in different validators. After some investigation I found solution that matches your requirements. Suppose you have models:

    public abstract class BaseModel
    {
        public string BaseProperty1 { get; set; }
        public string BaseProperty2 { get; set; }
    }
    
    public class ChildModel : BaseModel
    {
        public int IdOfSthInDb { get; set; }
    }
    

    You have to create validator for base model (it would be used further):

    class InternalBaseModelValidator : AbstractValidator<BaseModel>
    {
        public InternalBaseModelValidator()
        {
            RuleFor(x => x.BaseProperty1).NotEmpty().WithMessage("Property 1 is empty");
            RuleFor(x => x.BaseProperty2).NotEmpty().WithMessage("Property 2 is empty");
        }
    }
    

    Then you can use new feature of FluentValidation, called PreValidate:

    public class BaseModelValidator<T>: AbstractValidator<T> where T : BaseModel
    {
        // necessary for reusing base rules
        private readonly InternalBaseModelValidator preValidator; 
    
        protected BaseModelValidator()
        {
            preValidator = new InternalBaseModelValidator();
        }
    
        protected override bool PreValidate(ValidationContext<T> context, ValidationResult result)
        {
            var preValidationResult = preValidator.Validate(context.InstanceToValidate);
            if (preValidationResult.IsValid)
            {
                return true;
            }
    
            foreach(var error in preValidationResult.Errors)
            {
                result.Errors.Add(new ValidationFailure(error.PropertyName, error.ErrorMessage, error.AttemptedValue));
            }
    
            return false;
        }
    }
    

    After creating validator for all base models you can inherit from it for ChildModel validation:

    public class ChildModelValidator : BaseModelValidator<ChildModel>
    {
        public ChildModelValidator() 
            : base()
        {
            RuleFor(x => x.IdOfSthInDb)
                .MustAsync(ItemMustExists)
                .WithMessage("Item does not exist.");
        }
    
        private Task<bool> ItemMustExists(int arg1, CancellationToken arg2)
        {
            return Task.FromResult(false); // some logic here
        }
    }
    

    That's it!