4

I need to pass a parameter from a parent validator to a child validator, by using .SetValidator() but having trouble with registering a validator to the DI container using FluentValidation's automatic registration, when the validator is parameterized.

Parent Validator:

public class FooValidator: AbstractValidator<Foo>
{
    public FooValidator()
    {
        RuleFor(foo => foo.Bar)
            .SetValidator(foo => new BarValidator(foo.SomeStringValue))
            .When(foo => foo.Bar != null);
    }
}

Child Validator:


public class BarValidator: AbstractValidator<Bar>
{
    public BarValidator(string someStringValue)
    {
        RuleFor(bar => bar.Baz)
            .Must(BeValid(bar.Baz, someStringValue)
            .When(bar => bar.Baz != null);
    }
    
    private static bool BeValid(Baz baz, string someStringValue)
    {
        return baz == someStringValue; 
    }

}

DI registration

services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly(), ServiceLifetime.Transient);

Error message

System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: FluentValidation.IValidator`1[Domain.ValueObjects.Bar] Lifetime: Transient ImplementationType: Application.Common.Validators.BarValidator': 
    Unable to resolve service for type 'System.String' while attempting to activate 'Application.Common.Validators.BarValidator'.)
 ---> System.InvalidOperationException: Unable to resolve service for type 'System.String' while attempting to activate 'Application.Common.Validators.BarValidator'.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache lifetime, Type serviceType, Type implementationType, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain, Int32 slot)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceDescriptor serviceDescriptor, CallSiteChain callSiteChain)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
   at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
   at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
   at Program.<Main>$(String[] args) in C:\Program.cs:line 9
  • .NET 7
  • FluentValidation v11.2.2
  • Microsoft.Extensions.DependencyInjection

Any ideas?

Have tried circumventing the use of automatic registration by filtering it out and registering it manually, but this changes nothing.

_ = services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly(), ServiceLifetime.Transient, filter => filter.ValidatorType != typeof(BarValidator));
_ = services.AddTransient<IValidator<Bar>>(_ => new BarValidator(""));
trymv
  • 175
  • 1
  • 14
  • You could try adding a parameterless ctor with no validation rules essentially saying that when Fluent validates Foo.Bar automatically, it shouldn't validate anything. – maxbeaudoin Nov 25 '22 at 18:54
  • Thank you for your suggestion, but sadly I've tried that to no avail. – trymv Nov 26 '22 at 14:33

3 Answers3

1

You can try using Fluent Validators .WithState method and it will pass an additional object:

     RuleFor(foo => foo.Bar)
        .SetValidator(foo => new BarValidator())
        .When(foo => foo.Bar != null)
        .WithState(foo => foo.SomeStringValue);

And then the child validator will look something like this where you won't be passing a parameter to BarValidator and we will use GetState method on the Validation Context object:

  public class BarValidator: AbstractValidator<Bar>
 {
       public BarValidator()
{
    RuleFor(bar => bar.Baz)
        .Must(BeValid)
        .When(bar => bar.Baz != null);
}

private bool BeValid(Baz baz, ValidationContext<Bar> context)
{
string someStringValue = context.GetState<string>();
return baz == someStringValue; 
 }

}

0

You've got yourself a chicken and egg problem. The AbstractValidator<T> can't have parameters in its ctor, without them also being added to the DI container. At the time the container validates itself, it doesn't know how to instantiate an instance of AbstractValidator<T> because it can't resolve its string dependency.

AbstractValidator<T> isn't the right tool for the job. Have a look at Reusable Property Validators. Using PropertyValidator<Foo, Bar> you can access SomeStringValue from ValidationContext<Foo>.

Child Validator:

public class BarValidator : PropertyValidator<Foo, Bar>
{
    public override bool IsValid(ValidationContext<Foo> context, Bar bar)
    {
        if (bar != null && bar.Baz != context.InstanceToValidate.SomeStringValue)
        {
            context.MessageFormatter.AppendArgument(nameof(Foo.SomeStringValue), context.InstanceToValidate.SomeStringValue);
            return false;
        }

        return true;
    }

    public override string Name => "BazValidator";

    protected override string GetDefaultMessageTemplate(string errorCode)
        => "Baz must be equal to {SomeStringValue}.";
}

Parent Validator:

public class FooValidator : AbstractValidator<Foo>
{
    public FooValidator()
    {
        RuleFor(foo => foo.Bar).SetValidator(new BarValidator());
    }
}
Funk
  • 10,976
  • 1
  • 17
  • 33
0

The easiest way to achieve this is to create a factory that will create the validator with the required parameters.

public class FooValidator: AbstractValidator<Foo>
{
    public FooValidator()
    {
        RuleFor(foo => foo.Bar)
            .SetValidator(foo => MyValidatorFactory.CreateBarValidator(foo.SomeStringValue))
            .When(foo => foo.Bar != null);
    }
}

public static class MyValidatorFactory
{
    public static IValidator<Bar> CreateBarValidator(string someStringValue)
    {
        return new BarValidator(someStringValue);
    }
}

public class BarValidator: AbstractValidator<Bar>
{
    public BarValidator(string someStringValue)
    {
        RuleFor(bar => bar.Baz)
            .Must(BeValid(bar.Baz, someStringValue)
            .When(bar => bar.Baz != null);
    }
    
    private static bool BeValid(Baz baz, string someStringValue)
    {
        return baz == someStringValue; 
    }

}

Finally, make sure you add the factory to the DI container.

   services.AddTransient<Func<string, IValidator<Bar>>>(MyValidatorFactory.CreateBarValidator);
DotNetRussell
  • 9,716
  • 10
  • 56
  • 111