Search code examples
asp.net-mvcasp.net-mvc-routingbad-request

MVC controller should return 400 when validation query string parameters


Say you have a simple method in a MVC controller...

[Route("{id}", Name = "GetById")]
[HttpGet]
[ProducesResponseType(typeof(SomeType), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.Forbidden)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.ServiceUnavailable)]
public async Task<IActionResult> Get(string id, bool? includeNonActive = false)
{
    // some stuff
}

Is there any way to get MVC to automagically return a HTTP 400 if invalid value (non-boolean) are passed to the includeNonActive query string parameter?

HTTP GET http://my-server/api/12321312?includeNonActive=thisIsNotABooleanValue

Sure, I can accept a string type for 'includeNonActive', use Boolean.TryParse() and return BadRequest("wtf"), but that looks ugly in Swagger.


Solution

  • Is there any way to get MVC to automagically return a HTTP 400 if invalid value (non-boolean) are passed to the 'includeNonActive' query string parameter?

    Sure, you could build an action filter for this.

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class EnsureBooleanQueryParameterAttribute : ActionFilterAttribute
    {
        public EnsureBooleanQueryParameterAttribute(string parameterName)
        {
            if (string.IsNullOrEmpty(parameterName))
                throw new ArgumentException($"'{nameof(parameterName)}' is required.");
            this.ParameterName = parameterName;
        }
    
        public string ParameterName { get; set; }
    
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var attribute = (EnsureBooleanQueryParameterAttribute)filterContext.ActionDescriptor
                .GetCustomAttributes(typeof(EnsureBooleanQueryParameterAttribute), true)
                .FirstOrDefault();
    
            // The attribute exists on the current action method
            if (attribute != null)
            {
                string param = filterContext.HttpContext.Request.QueryString[attribute.ParameterName];
                bool result;
                // If the query string value is present and not boolean
                if (!string.IsNullOrEmpty(param) && !bool.TryParse(param, out result))
                {
                    filterContext.Result = new HttpStatusCodeResult(400, 
                        "Invalid boolean query string value for '{attribute.ParameterName}'.");
                }
            }
        }
    }
    

    And then use the filter like:

    [Route("{id}", Name = "GetById")]
    [HttpGet]
    [ProducesResponseType(typeof(SomeType), (int)HttpStatusCode.OK)]
    [ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
    [ProducesResponseType(typeof(string), (int)HttpStatusCode.Forbidden)]
    [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
    [ProducesResponseType(typeof(string), (int)HttpStatusCode.ServiceUnavailable)]
    [EnsureBooleanQueryParameter("includeNonActive")]
    public async Task<IActionResult> Get(string id, bool? includeNonActive = false)
    {
        // some stuff
    }
    

    You could use this as a starting point and add an Enum parameter that you can use to specify the data type and a bool parameter to indicate whether or not the value is required to exist in the URL.