Search code examples
c#asp.net-core.net-5fluentvalidation

Issue with custom clientside validation for a PropertyValidator


FluentValidation 10.3.3, .NET 5

I have a custom validator with the following signature:

public class FileSizeValidator<T> : PropertyValidator<T, IFormFile>

and I tried to implement the client-side portion of it, so that it has parity with attribute-based validations where the client-side portion already worked. There's precious few leads on how would I ever start doing it, but I found this comment on an issue which seemed not even all that outdated. Certainly more recent than Codeplex where my search led me.

So I made a simple client-side validator like so:

public class FileSizeClientValidator : ClientValidatorBase
{
    public FileSizeClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component)
    { }

    public override void AddValidation(ClientModelValidationContext context)
    {
        MergeAttribute(context.Attributes, "test-attribute", "test message");
    }
}

and registered it with

services
    .AddFluentValidation(options =>
    {
        options.ConfigureClientsideValidation(clientside =>
        {
            clientside.ClientValidatorFactories[typeof(FileSizeValidator<>)] = (_, rule, component) =>
                new FileSizeClientValidator(rule, component);
        });
    });

which seems to mostly follow what was contained in the comment linked above. Can't exactly get the generic parameter for my validator to cast it, because there seems to be nowhere to get it from, and some signatures changed since then, so I decided on a simpler test. Just a hardcoded attribute and message.

Alas, nothing shows up in the form. Worse still, neither the constructor nor the AddValidation method get hit at any point, the breakpoints placed there never trigger.


Solution

  • The solution was using a non-generic interface:

    public interface IFileSizeValidator : IPropertyValidator
    {
        public uint Max { get; }
    }
    

    implemented by the validator

    public class FileSizeValidator<T> : PropertyValidator<T, IFormFile>, IFileSizeValidator
    

    so that it can be used by the client-side validator:

    public class FileSizeClientValidator : ClientValidatorBase
    {
        public FileSizeClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component)
        { }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            var validator = (IFileSizeValidator)Validator;
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-filesize-max", validator.Max.ToString());
        }
    }
    

    and also used to register it properly:

    services.AddFluentValidation(options =>
    {
        options.RegisterValidatorsFromAssemblyContaining<Startup>();
        options.ConfigureClientsideValidation(clientside =>
        {
            clientside.ClientValidatorFactories[typeof(IFileSizeValidator)] = (_, rule, component) =>
                new FileSizeClientValidator(rule, component);
        });
    })
    

    Full credit to this comment on Github that led me to the answer. I also described the whole process on dev.to for posterity.