Search code examples
c#asp.net-corerazor-pages

How can I set `ValidationVisitor.MaxValidationDepth` = 1 only for one Razor Page?


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


Solution

  • 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 {
         //...
    }