Disable model validation for single .NET Core API action

Viewed 4518

I have an API controller for performing autosaves on an application I am developing. It uses the same viewmodel as the view, which has a number of required fields. The autosave controller may need to save a model that is not considered valid if the user has not completed the form when it is saved. By default, an .NET Core controller declared with the [ApiController] attribute will automatically force validation. I know I can disable this like so in Startup.cs:

services.Configure<ApiBehaviorOptions>(options =>
{
     options.SuppressModelStateInvalidFilter = true;
});

But this will apply to all API controllers in the project. Is it possible to disable this default validation for only one controller or action? Everything I've found so far has directed me to use the code above, but that doesn't accomplish what I'm looking for.

2 Answers

You can override the default InvalidModelStateResponseFactory:

            services.Configure<ApiBehaviorOptions>(options =>
            {
                options.InvalidModelStateResponseFactory =
                    AllowingServerSideValidationToBeDisabledInvalidModelStateResponseFactoryHelper.InvalidModelStateResponseFactory;
            });

The InvalidModelStateResponseFactory below checks for OptionalValidationAttribute on a controller action, and searches for a form/query parameter flag which controls the validation is enabled/disabled:

    // Code taken from https://github.com/dotnet/aspnetcore/blob/5747cb36f2040d12e75c4b5b3f49580ef7aac5fa/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs#L23
    // and is modified to optionally disable validation for controller action methods decorated with OptionalValidationAttribute
    public static class AllowingServerSideValidationToBeDisabledInvalidModelStateResponseFactoryHelper
    {
        public static Func<ActionContext, IActionResult> InvalidModelStateResponseFactory => actionContext =>
        {
            var shouldEnableDataValidationarameterName = ((OptionalValidationAttribute)((ControllerActionDescriptor)actionContext.ActionDescriptor)
                .MethodInfo.GetCustomAttributes(typeof(OptionalValidationAttribute), true)
                .SingleOrDefault())?.ShouldEnableDataValidationParameterName;

            var isValidationEnabled = true;

            if (shouldEnableDataValidationarameterName != null)
            {
                var httpContextRequest = actionContext.HttpContext.Request;
                var shouldEnableDataValidationValue = httpContextRequest.Form[shouldEnableDataValidationarameterName]
                    .Union(httpContextRequest.Query[shouldEnableDataValidationarameterName]).FirstOrDefault();
                isValidationEnabled = shouldEnableDataValidationValue?.ToLower() == bool.TrueString.ToLower();
            }

            if (!isValidationEnabled)
            {
                return null;
            }

            var problemDetailsFactory = actionContext.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
            var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(actionContext.HttpContext, actionContext.ModelState);
            ObjectResult result;
            if (problemDetails.Status == 400)
            {
                // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400.
                result = new BadRequestObjectResult(problemDetails);
            }
            else
            {
                result = new ObjectResult(problemDetails)
                {
                    StatusCode = problemDetails.Status,
                };
            }
            result.ContentTypes.Add("application/problem+json");
            result.ContentTypes.Add("application/problem+xml");

            return result;
        };
    }

The OptionalValidationAttribute:

    [AttributeUsage(AttributeTargets.Method)]
    public class OptionalValidationAttribute : Attribute
    {
        public OptionalValidationAttribute(string shouldEnableDataValidationParameterName)
        {
            ShouldEnableDataValidationParameterName = shouldEnableDataValidationParameterName;
        }

        public string ShouldEnableDataValidationParameterName { get; }
    }

Example usage on an controller action:

[HttpPost]
[OptionalValidation(shouldEnableDataValidationParameterName: nameof(shouldEnableDataValidation))] // C# 11 needed to use nameof for a method parameter 
public async Task<IActionResult> Update(
    [FromForm] int id,
    [FromForm] string name,
    [FromForm] bool shouldEnableDataValidation
)
{
...
}

I would suggest you to approach this differently: Disabling the model validation will mean that there isn’t any validation for this action; not now, not later. Just because you do not require validation right now that doesn’t mean that you won’t need some kind of validation later on.

If you used some custom handling to disable the validation altogether for that action, then all you are doing is creating an actual exception into your application which will make it more complex. Developers looking at this later might not expect this behavior and could spend a good amount of time trying to figure out why the validation isn’t running when it’s working for every other action.

So instead, consider just duplicating the model so that each action has its own model: Action A has the original model with the validation attributes, requiring the values to be filled. And action B has a copy of that model without any validation attributes.

While this may seem wasteful, this does give you a few more benefits:

  • If you later require validation for some fields on action B, you could just add some validation attributes back. You didn’t have to disable the automatic validation completely, so individual validation attributes will just continue to work if you add them to the model.
  • Having separate models allows both actions to evolve independently. There’s already a good indicator that the actions do two different things: One requires the values, the other doesn’t. So it’s not unlikely that the models might need to diverge further in the future. For example, you might want to add a property to only one model but not the other.
  • As already mentioned above, you can stick to the default behavior and keep a consistent development experience.
Related