OdeToCode IC Logo

Model Binding in GET Requests

Tuesday, February 27, 2018

I've seen parameter validation code inside controller actions on an HTTP GET request. However, the model binder in ASP.NET Core will populate the ModelState data structure with information about all input parameters on any type of request.

In other words, model binding isn't just for POST requests with form values or JSON.

Take the following class, for example.

public class SearchParameters
{
    [Required]
    [Range(minimum:1, maximum:int.MaxValue)]
    public int? Page { get; set; }

    [Required]
    [Range(minimum:10, maximum:100)]
    public int? PageSize { get; set; }

    [Required]
    public string Term { get; set; }
}

We'll use the class in the following controller.

[Route("api/[controller]")]
public class SearchController : Controller
{
    [HttpGet]
    public IActionResult Get(SearchParameters parameters)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var model = // build the model ...
        return new OkObjectResult(model);
    }
}

Let's say a client sends a GET request to /api/search?pageSize=5000. We don't need to write validation code for the input in the action, all we need to do is check model state. For a request to /api/search?pageSize=5000, the above action code will return a 400 (bad request) error.

{ 
  "Page":["The Page field is required."],
  "PageSize":["The field PageSize must be between 10 and 100."],
  "Term":["The Term field is required."]
}

For the Required validation to activate for Page and PageSize, we need to make these int type properties nullable. Otherwise, the runtime assigns a default value 0 and the Range validation fails, which might be confusing to clients. 

Default Values

Give your input model a default constructor to provide default values and you won't need nullable properties or the Required attributes. Of course, this approach only works if you can provide sensible default values for the inputs. 

public class SearchParameters
{
    public SearchParameters()
    {
        Page = 1;
        PageSize = 10;
        Term = String.Empty;
    }
   
    [Range(minimum:1, maximum:int.MaxValue)]
    public int? Page { get; set; }

    [Range(minimum:10, maximum:100)]
    public int? PageSize { get; set; }

    public string Term { get; set; }
}


Comments
Gravatar Bertrand Tchoumkeu Tuesday, February 27, 2018
Thank you for the great trick, Scott. In addition to parameter validation, this can also be useful when you have a search form with multiple parameters. It saves you from creating an ugly method signature with dozens of parameters. Also, I wanted to see if this worked on "old" ASP.NET Web API, and it turns out all you need to do is add the [FromUri] hint to the SearchParameters and it works. public IHttpActionResult Get([FromUri]SearchParameters parameters) { ...... }
Gravatar Tyler Tuesday, February 27, 2018
Can we eliminate the constructor and modify to: [Range(minimum:1, maximum:int.MaxValue)] public int? Page { get; set; } = 1; [Range(minimum:10, maximum:100)] public int? PageSize { get; set; } = 10; public string Term { get; set; } = String.Empty;
Gravatar scott Tuesday, February 27, 2018
@Tyler - yes, it's a style preference.
Gravatar Dayo Adebayo Thursday, March 1, 2018
The explanation is straightforward. Thanks.
Comments are closed.