Passing an object to an inheritance validator does not emit FluentValidation rules on property
icnocop opened this issue · 15 comments
Hi.
When I create a rule for a property which sets and passes an object to an inheritance validator, the FluentValidation rules for that property's type are not emitted in swagger.json.
For example, when not passing an object to an inheritance validator, the NotEmpty and MaxLength rules are emitted in swagger.json:
using FluentValidation;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace SampleWebApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IssueA : Controller
{
public class TenantA
{
public PersonA Owner { get; set; }
[UsedImplicitly]
public class TenantAValidator : AbstractValidator<TenantA>
{
public TenantAValidator()
{
RuleFor(x => x.Owner)
.SetValidator(x => new PersonA.PersonAValidator(/*x*/));
}
}
}
public class PersonA
{
public AccountA Account { get; set; }
public class PersonAValidator : AbstractValidator<PersonA>
{
public PersonAValidator(/*TenantA tenant*/)
{
RuleFor(x => x.Account)
.SetInheritanceValidator(x =>
{
x.Add(new StrictAccountA.StrictAccountAValidator(/*tenant*/));
});
}
}
}
public class AccountA
{
public string UserName { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Password { get; set; }
}
public class StrictAccountA : AccountA
{
public class StrictAccountAValidator : AbstractValidator<AccountA>
{
public StrictAccountAValidator(/*TenantA tenant*/)
{
this.RuleFor(x => x.UserName)
.NotEmpty()
.MaximumLength(50);
this.RuleFor(x => x.Password)
.MaximumLength(256);
}
}
}
[HttpPost("[action]")]
public ActionResult<TenantA> AddTenantA(TenantA tenant)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return tenant;
}
}
}
However, when passing an object to an inheritance validator, the NotEmpty and MaxLength rules are not emitted in swagger.json:
using FluentValidation;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace SampleWebApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class IssueB : Controller
{
public class TenantB
{
public PersonB Owner { get; set; }
[UsedImplicitly]
public class TenantBValidator : AbstractValidator<TenantB>
{
public TenantBValidator()
{
RuleFor(x => x.Owner)
.SetValidator(x => new PersonB.PersonBValidator(x));
}
}
}
public class PersonB
{
public AccountB Account { get; set; }
public class PersonBValidator : AbstractValidator<PersonB>
{
public PersonBValidator(TenantB tenant)
{
RuleFor(x => x.Account)
.SetInheritanceValidator(x =>
{
x.Add(new StrictAccountB.StrictAccountBValidator(tenant));
});
}
}
}
public class AccountB
{
public string UserName { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Password { get; set; }
}
public class StrictAccountB : AccountB
{
public class StrictAccountBValidator : AbstractValidator<AccountB>
{
public StrictAccountBValidator(TenantB tenant)
{
this.RuleFor(x => x.UserName)
.NotEmpty()
.MaximumLength(50);
this.RuleFor(x => x.Password)
.MaximumLength(256);
}
}
}
[HttpPost("[action]")]
public ActionResult<TenantB> AddTenantB(TenantB tenant)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return tenant;
}
}
}
Any ideas?
Thank you.
When debugging, the following exception is thrown and I see it in the Debug Output window:
MicroElements.Swashbuckle.FluentValidation.FluentValidationRules: Warning: GetValidator for type 'SampleWebApi.Controllers.IssueB+AccountB' fails.
System.InvalidOperationException: Unable to resolve service for type 'SampleWebApi.Controllers.IssueB+TenantB' while attempting to activate 'SampleWebApi.Controllers.IssueB+StrictAccountB+StrictAccountBValidator'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, 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.TryCreateExact(Type serviceType, CallSiteChain callSiteChain)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.<>c__DisplayClass7_0.<GetCallSite>b__0(Type type)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(Type serviceType, CallSiteChain callSiteChain)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor(Type serviceType)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at MicroElements.Swashbuckle.FluentValidation.HttpContextServiceProviderValidatorFactory.CreateInstance(Type validatorType) in C:\github\icnocop\MicroElements.Swashbuckle.FluentValidation\src\MicroElements.Swashbuckle.FluentValidation\AspNetCore\HttpContextServiceProviderValidatorFactory.cs:line 30
at FluentValidation.ValidatorFactoryBase.GetValidator(Type type) in C:\Projects\FluentValidation\src\FluentValidation\ValidatorFactoryBase.cs:line 41
at MicroElements.Swashbuckle.FluentValidation.FluentValidationRules.Apply(OpenApiSchema schema, SchemaFilterContext context) in C:\github\icnocop\MicroElements.Swashbuckle.FluentValidation\src\MicroElements.Swashbuckle.FluentValidation\Swashbuckle\FluentValidationRules.cs:line 92
Exception thrown: 'System.InvalidOperationException' in Microsoft.Extensions.DependencyInjection.dll
As a work-around, I have to add this to ConfigureServices
in Startup.cs
:
services.AddScoped(c => new Controllers.IssueB.TenantB());
services.AddScoped(c => new Controllers.IssueB.TenantB());
is needed registration.
All validators resolves in runtime so tenant
should be registered.
The validators themselves work during runtime without issues even without tenant
being registered because tenant
is a dynamic object passed to the validators at the time of validation.
tenant
shouldn't be registered as part of the same service provider as the ASP.NET Core pipeline because it's created during runtime with property values provided from the HTTP request and/or retrieved from the database, so an instance of the object cannot be registered in Startup.cs
for example.
The problem occurs when generating swagger.json because HttpContextServiceProviderValidatorFactory
doesn't know how to create an instance of tenant
, needed to create an instance of the validator, because tenant
hasn't been registered in the service collection.
I will provide some samples that resolves this issue.
Can you provide more datailed sample of using tenant in validator?
see branch with workarounds: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/tree/feature/issue96
One example is validating an account password based on a password policy.
An account password must adhere to the password policy defined on the tenant
.
The application must support multiple tenants with different password policies, so the account password validation is dynamic.
So the validator uses the password policy defined in the tenant
to determine if the password is valid or not.
Thank you for the sample work-around.
In the work-around example you provided, if I try to use tenant
to validate the password, then the maximum length rule on the password is not emitted in swagger.json.
For example:
this.RuleFor(x => x.Password)
.MaximumLength(256)
.Must(x => x.Contains("!"))
.When(x => tenant.Owner.Account.UserName == "admin");
Hello!
MicroElements.Swashbuckle.FluentValidation is a plugin for Swashbuckle.AspNetCore that generates swagger specification on application startup. Conditional validation logic is skipped because swagger did not support it (before OpenAPI 3.0).
So MicroElements.Swashbuckle.FluentValidation cant generate swagger depending user input (swagger is static document).
I think that some conditional logic can be done with oneOf
see also: https://swagger.io/specification/
and this: https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
Thank you! 🙏
When I added ApplyConditionTo.CurrentValidator
to When
it works as expected.
This works in an MVC Controller
, but the OnActionExecutionAsync
method isn't available to override in a web API controller deriving from ControllerBase
.
You can do the same with IAsyncActionFilter
Thank you.
I've tried using an action filter, but FluentValidation runs during the model binding process so the action filter doesn't have a chance to run before validation.
From FluentValidation/FluentValidation#1607 (comment):
FluentValidation runs during ASP.NET's model binding process. You'd need to write a custom model binder if you want to customize this process (not a filter).
From https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1:
yes it's true
I will try to create a sample.
Other opportunity is to disable auto validation and validate in own filter after context is built
Thank you.
I was able to resolve the issue as follows:
- Set
fluentValidationMvcConfiguration.ImplicitlyValidateChildProperties = false;
inStartup.cs
. - Implement
IValidatorInterceptor
in the top-level validator:
public ValidationResult AfterAspNetValidation(ActionContext actionContext, IValidationContext validationContext, ValidationResult result)
{
return result;
}
public IValidationContext BeforeAspNetValidation(ActionContext actionContext, IValidationContext commonContext)
{
// get model
MyModel model = commonContext.InstanceToValidate as MyModel;
// set model in accessor
IMyAccessor myAccessor = actionContext.HttpContext.RequestServices.GetService<IMyAccessor>();
myAccessor.MyModel = model;
return commonContext;
}
- Manually call
SetValidator
for child properties.