How can I change ValidationVisitor.MaxValidationDepth
locally for one Razor Page to prevent validation of sub objects?
I don't want to change it globally with MvcOptions.MaxValidationDepth
like it is described in the Docs: Maximum recursion
This is a follow question. Please check my original question to see the object structure here:
Razor Page validation error with a complex object, TryValidateModel() does not work
We can start from the IObjectModelValidator
. The default implementation is internal (DefaultObjectValidator
) and inherits from ObjectModelValidator
. The base (and abstract) class requires the derived class to implement only one method called GetValidationVisitor
. That's one extensibility point for you to modify the MaxValidationDepth
of the ValidationVisitor
before it runs. The default implementation DefaultObjectValidator
just sets the MaxValidationDepth
to the value obtained from MvcOptions
. So it's applied globally as you said.
In your custom implementation for IObjectModelValidator
(of course we let it inherit from ObjectModelValidator
), you can obtain the MaxValidationDepth
from the current context instead.
Before executing each page handler, you can set the MaxValidationDepth
of your choice. To make it standard (used like a cross-cutting concern), we can create an IAsyncPageFilter
as an attribute that can be applied on any page model class you want.
Here's the implementation code:
//the custom IObjectModelValidator (which is much like what from the source code
//of DefaultObjectValidator)
public class ContextBasedObjectModelValidator : ObjectModelValidator
{
readonly MvcOptions _mvcOptions;
public ContextBasedObjectModelValidator(IModelMetadataProvider modelMetadataProvider,
IList<IModelValidatorProvider> validatorProviders,
MvcOptions mvcOptions) : base(modelMetadataProvider, validatorProviders)
{
_mvcOptions = mvcOptions;
}
public override ValidationVisitor GetValidationVisitor(ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
{
var visitor = new ValidationVisitor(
actionContext,
validatorProvider,
validatorCache,
metadataProvider,
validationState)
{
MaxValidationDepth = actionContext.HttpContext.Features.Get<IContextBasedMaxValidationDepthFeature>()?.MaxValidationDepth ?? _mvcOptions.MaxValidationDepth,
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
};
return visitor;
}
}
//we need a class for the custom request feature to hold your context-based MaxValidationDepth
public interface IContextBasedMaxValidationDepthFeature
{
int MaxValidationDepth { get; }
}
public class ContextBasedMaxValidationDepthFeature : IContextBasedMaxValidationDepthFeature
{
public ContextBasedMaxValidationDepthFeature(int maxValidationDepth)
{
MaxValidationDepth = maxValidationDepth;
}
public int MaxValidationDepth { get; }
}
//here the page filter to help set your context-based MaxValidationDepth
[AttributeUsage(AttributeTargets.Class)]
public class MaxValidationDepthAttribute : Attribute, IAsyncPageFilter
{
public MaxValidationDepthAttribute(int maxValidationDepth)
{
MaxValidationDepth = maxValidationDepth;
}
public int MaxValidationDepth { get; }
public Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
//set the max validation depth from the predefined value (via the attribute)
context.HttpContext.Features
.Set<IContextBasedMaxValidationDepthFeature>(new ContextBasedMaxValidationDepthFeature(MaxValidationDepth));
return next();
}
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
{
return Task.CompletedTask;
}
}
Finally we need to register your custom IObjectModelValidator
inside the Startup.ConfigureServices
:
services.Replace(new ServiceDescriptor(typeof(IObjectModelValidator), sp => {
var options = sp.GetRequiredService<IOptions<MvcOptions>>().Value;
var metadataProvider = sp.GetRequiredService<IModelMetadataProvider>();
return new ContextBasedObjectModelValidator(metadataProvider, options.ModelValidatorProviders, options);
}, ServiceLifetime.Singleton));
Use it:
//suppose you want it to be 1
//for this specific page
[MaxValidationDepth(1)]
public class YourPageModel : PageModel {
//...
}