40

Using ASP.NET Web API. Is there a way to automatically return a status code 400 if a parameter is null? I found this question but that is a global solution that is applied to all methods, I want to do this on a per method per parameter basis.

So, for example, this is what I am currently doing:

public HttpResponseMessage SomeMethod(SomeNullableParameter parameter)
{
    if (parameter == null)
        throw new HttpResponseException(HttpStatusCode.BadRequest);

    // Otherwise do more stuff.
}

I would really just like to do something like this (notice the required attribute):

public HttpResponseMessage SomeMethod([Required] SomeNullableParameter parameter)
{
    // Do stuff.
}
Community
  • 1
  • 1
Jason Boyd
  • 6,839
  • 4
  • 29
  • 47

5 Answers5

24

The approach I ended up using was to create a custom filter that I registered globally. The filter checks all request parameters for the RequiredAttribute. If the attribute is found then it checks if the parameter was passed with the request (not null) and returns status code 400 if it was null. I also added a cache to the filter to store the required parameters for each request to avoid the reflection hit on future calls. I was pleasantly surprised to find that this works for value types as well since the action context stores the parameters as objects.

EDIT - Updated solution based on tecfield's comment

public class RequiredParametersFilter : ActionFilterAttribute
{
    // Cache used to store the required parameters for each request based on the
    // request's http method and local path.
    private readonly ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>> _Cache =
        new ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>>();

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        // Get the request's required parameters.
        List<string> requiredParameters = this.GetRequiredParameters(actionContext);     

        // If the required parameters are valid then continue with the request.
        // Otherwise, return status code 400.
        if(this.ValidateParameters(actionContext, requiredParameters))
        {
            base.OnActionExecuting(actionContext);
        }
        else
        {
            throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
    }

    private bool ValidateParameters(HttpActionContext actionContext, List<string> requiredParameters)
    {
        // If the list of required parameters is null or containst no parameters 
        // then there is nothing to validate.  
        // Return true.
        if (requiredParameters == null || requiredParameters.Count == 0)
        {
            return true;
        }

        // Attempt to find at least one required parameter that is null.
        bool hasNullParameter = 
            actionContext
            .ActionArguments
            .Any(a => requiredParameters.Contains(a.Key) && a.Value == null);

        // If a null required paramter was found then return false.  
        // Otherwise, return true.
        return !hasNullParameter;
    }

    private List<string> GetRequiredParameters(HttpActionContext actionContext)
    {
        // Instantiate a list of strings to store the required parameters.
        List<string> result = null;

        // Instantiate a tuple using the request's http method and the local path.
        // This will be used to add/lookup the required parameters in the cache.
        Tuple<HttpMethod, string> request =
            new Tuple<HttpMethod, string>(
                actionContext.Request.Method,
                actionContext.Request.RequestUri.LocalPath);

        // Attempt to find the required parameters in the cache.
        if (!this._Cache.TryGetValue(request, out result))
        {
            // If the required parameters were not found in the cache then get all
            // parameters decorated with the 'RequiredAttribute' from the action context.
            result = 
                actionContext
                .ActionDescriptor
                .GetParameters()
                .Where(p => p.GetCustomAttributes<RequiredAttribute>().Any())
                .Select(p => p.ParameterName)
                .ToList();

            // Add the required parameters to the cache.
            this._Cache.TryAdd(request, result);
        }

        // Return the required parameters.
        return result;
    }

}
Liam
  • 27,717
  • 28
  • 128
  • 190
Jason Boyd
  • 6,839
  • 4
  • 29
  • 47
  • 7
    Be careful about your cache. you may want to use a thread-safe `ConcurrentDictionary` instead of a normal `Dictionary` which is not thread-safe! – tecfield Oct 08 '15 at 17:01
  • Does this work for nested fields / `POST` models? I.e. where the parameter is a class of some kind which has fields that are `[Required]`. – Zero3 Apr 07 '17 at 10:57
5

Set [Required] on a property in your model and then check the ModelState to see if it IsValid.

This will allow all the required properties to be tested at the same time.

See the "Under-Posting" section @ Model validation in WebAPI

Liam
  • 27,717
  • 28
  • 128
  • 190
Timothy Lee Russell
  • 3,719
  • 1
  • 35
  • 43
  • 2
    I had concerns with this approach because I may want to handle an invalid model different than a null parameter. I did give it a try though to see if it would work and it did not. Because the object was null it was never added to the model so validation never occurred. – Jason Boyd Oct 11 '13 at 16:21
  • Did you declare the optional parameter type as nullable in your model? [Required] on non-nullable primitives return the default value. Also, ordering of the parameters matters. All required parameters must precede optional parameters. Just curious, since this is working for me. This is all irrelevant, of course, if you want to differentiate between invalid model and null parameters. You still have to check for null at some point anyway. – Timothy Lee Russell Oct 11 '13 at 18:46
  • I did declare the optional type as nullable. I did not have the required parameter before the optional parameters so that must have been the issue. – Jason Boyd Oct 11 '13 at 20:46
  • Your solution looks like a good generic method for serving out the 400. – Timothy Lee Russell Oct 12 '13 at 01:01
  • 3
    From Asp.Net Core 2.1 there is a buit-in validation. See my response stackoverflow.com/a/54533218/245460 – Karel Kral Feb 05 '19 at 11:29
  • @KarelKral Please post that answer here, and I'll upvote it :) – codeMonkey Apr 25 '19 at 16:13
  • 1
    @codeMonkey: I postedit before but it was delete as duplicate. SO is strange place – Karel Kral Apr 25 '19 at 16:37
3

we can use the BindRequired, which is from Microsoft.AspNetCore.Mvc.ModelBinding namespace.

public async Task<ActionResult<IEnumerable<Numbers>>> GetAll([BindRequired, FromQuery]string[] numbers)
        {
            var result = await _service.GetAllDetails(numbers);
            return Ok(result);
        }

after that your swagger will look like below.

enter image description here

Jaydeep Shil
  • 1,894
  • 22
  • 21
2

A solution for asp.net core...

[AttributeUsage(AttributeTargets.Method)]
public sealed class CheckRequiredModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var requiredParameters = context.ActionDescriptor.Parameters.Where(
            p => ((ControllerParameterDescriptor)p).ParameterInfo.GetCustomAttribute<RequiredModelAttribute>() != null).Select(p => p.Name);

        foreach (var argument in context.ActionArguments.Where(a => requiredParameters.Contains(a.Key, StringComparer.Ordinal)))
        {
            if (argument.Value == null)
            {
                context.ModelState.AddModelError(argument.Key, $"The argument '{argument.Key}' cannot be null.");
            }
        }

        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
            context.Result = new BadRequestObjectResult(errors);
            return;
        }

        base.OnActionExecuting(context);
    }
}

[AttributeUsage(AttributeTargets.Parameter)]
public sealed class RequiredModelAttribute : Attribute
{
}

services.AddMvc(options =>
{
    options.Filters.Add(typeof(CheckRequiredModelAttribute));
});

public async Task<IActionResult> CreateAsync([FromBody][RequiredModel]RequestModel request, CancellationToken cancellationToken)
{
    //...
}
James Law
  • 6,067
  • 4
  • 36
  • 49
  • From Asp.Net Core 2.1 there is a buit-in validation. See my response https://stackoverflow.com/a/54533218/245460 – Karel Kral Apr 26 '19 at 06:55
0

The accepted solution takes it upon itself to report back any errors. A more appropriate approach for MVC5 is to let the controller handle (via model validation) the reporting of any errors, aka something like this:

using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class ValidateParametersAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext context)
    {
        var descriptor = context.ActionDescriptor;
        if (descriptor != null)
        {
            var modelState = context.ModelState;
            foreach (var parameterDescriptor in descriptor.GetParameters())
            {
                EvaluateValidationAttributes(
                    suppliedValue: context.ActionArguments[parameterDescriptor.ParameterName],
                    modelState: modelState,
                    parameterDescriptor: parameterDescriptor
                );
            }
        }

        base.OnActionExecuting(context);
    }

    static private void EvaluateValidationAttributes(HttpParameterDescriptor parameterDescriptor, object suppliedValue, ModelStateDictionary modelState)
    {
        var parameterName = parameterDescriptor.ParameterName;

        parameterDescriptor
            .GetCustomAttributes<object>()
            .OfType<ValidationAttribute>()
            .Where(x => !x.IsValid(suppliedValue))
            .ForEach(x => modelState.AddModelError(parameterName, x.FormatErrorMessage(parameterName)));
    }
}

You may then plug it in universally via WebApiConfig.cs:

config.Filters.Add(new ValidateParametersAttribute());
XDS
  • 3,786
  • 2
  • 36
  • 56