Search code examples
c#fluentvalidation

fluentvalidation cascade stop fails to halt


I have the following :

internal sealed class LabelValidator : AbstractValidator<string>
{
   public LabelValidator()
   {    
        RuleFor(label => label)
           .NotNull()
           .NotEmpty()
           .MaximumLength(120);
    }
}
internal static class ValidatorExtensions
{
  public static IRuleBuilderOptions<T, string> MustHaveValidLabel<T>(this IRuleBuilder<T, string> ruleBuilder)
  {
     return ruleBuilder.SetValidator(new LabelValidator());
  }
}
internal sealed class SharedFolderValidator : ResourceValidator<SharedFolderDto>
{
   public SharedFolderValidator(string labelPrefix, string groupPrefixFullControl, string groupPrefixReadOnly)
    {   
       RuleFor(resource => resource.Label)
           .Cascade(CascadeMode.Stop)
           .MustHaveValidLabel()
           .Must(l => l.StartsWith(labelPrefix, StringComparison.Ordinal))               
    }
    
    protected override bool PreValidate(ValidationContext<SharedFolderDto> context, ValidationResult result)
    {
        base.PreValidate(context, result);
    
        [Some other tests]
    
        return !result.Errors.Any();
   }
}
public sealed class ValidatorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
`    private async Task<TResponse> Validate(TRequest request, RequestHandlerDelegate<TResponse> next)
    {
        List<ValidationFailure> failures = _validators
            .Select(v => v.Validate(request))
            .SelectMany(result => result.Errors)
            .Where(error => error != null)
            .ToList();

        if (failures.Any())
        {
            _logger.LogValidationResult(typeName, JsonSerializer.Serialize(request), failures);
            throw new ResourceDomainException();
        }

        return await next();
    }
}

MustHaveValidLabel is used on other usecases.

The code fails on .Must(l => l.StartsWith(labelPrefix, StringComparison.Ordinal)) when label is empty / null but CascadeMode.Stop should have had stopped it.

I'm not looking for workarounds.

I'd like to understand why CascadeMode.Stop won't work in that usecase.

I added PipelineBehavior and static class for the extension.


Solution

  • According to my attempts to reproduce - your validator is not executed at all (not sure why though, maybe string is a special type for fluent validation). One workaround is to change the validator to be a PropertyValidator<T, string>:

    internal sealed class LabelValidator<T> : PropertyValidator<T, string>
    {
        public LabelValidator()
        {    
        }
    
        public override bool IsValid(ValidationContext<T> context, string value)
        {
            return !string.IsNullOrEmpty(value) && value.Length <= 120;
        }
    
        protected override string GetDefaultMessageTemplate(string errorCode) => "label is not valid"; // TODO
        public override string Name => "Label validator";
    }
    
    internal static class exts
    {
        public static IRuleBuilderOptions<T, string> MustHaveValidLabel<T>(this IRuleBuilder<T, string> ruleBuilder)
        {
            return ruleBuilder.SetValidator(new LabelValidator<T>());
        }
    }
    

    Or move the the rules to the extension method:

    internal static class exts
    {
        public static IRuleBuilderOptions<T, string> MustHaveValidLabel<T>(this IRuleBuilder<T, string> ruleBuilder)
        {
            return ruleBuilder.NotNull()
                .NotEmpty()
                .MaximumLength(120);
        }
    }