Making Enum Parameters Required in ASP.NET Core MVC

aspnet Jul 24, 2019

Model binding is such a finicky beast sometimes. ASP.NET Core MVC does a lot of magic by default, but somehow I keep running into complex requirements wherein I have to do a lot of digging.

Scenario

Suppose we have an API that has a GetSummary endpoint and the URI should look like this:

/api/customers/{id}/summary?timePeriod={timePeriod}

The variable id is a GUID string, but timePeriod is a list of values such as "last 7 days" and "last 4 weeks".

/api/customers/9a3a86b0-3aca-460a-8b0d-270021a89c28/summary?timePeriod=Last7Days

I created a TimePeriod enum with the following values defined:


public enum TimePeriod
{
	Last7Days,
    Last4Weeks,
    Last3Months
}

And this is the controller method:


[Route("/api/[controller]")]
public class CustomersController : ControllerBase {

	[HttpGet]
    [Route("{id}/summary")]
    public async Task<ActionResult<CustomerSummary>> GetSummary(string id, TimePeriod timePeriod)
    {  
        // code logic here
    }
}

This works - however, we can enter invalid values into the timePeriod parameter and still get a valid result. Somehow, timePeriod gets bound with the first enum value. For invalid values, we should be returning a bad request result.


[HttpGet]
[Route("{id}/summary")]
public async Task<ActionResult<CustomerSummary>> GetSummary(string id, TimePeriod timePeriod)
{  
    if (!ModelState.IsValid)
    	return BadRequest(ModelState);
        
   	// code logic here
}

Checking the model state now throws an error when an invalid value is passed in the request.

{
"timePeriod": [
	"The value 'blah' is not valid for TimePeriod."
	]
}

However, now we want to make sure that the timePeriod parameter is always present. If we go to this URI:

/api/customers/CF16C694-619B-4105-BD7B-A9AFBACA6320/summary

Once again timePeriod gets bound with the first value of the enum, which is not what we want.

I thought making the timePeriod parameter nullable then adding the BindRequired attribute would resolve this issue. When the query parameter is not present in the request, the timePeriod is null. However, the model state is still valid.


[HttpGet]
[Route("{id}/summary")]
public async Task<ActionResult<CustomerSummary>> GetSummary(string id, [BindRequired]TimePeriod? timePeriod)
{  
	// Does not work - ModelState.IsValid is true even if timePeriod is null
    if (!ModelState.IsValid)
    	return BadRequest(ModelState);
        
   	// code logic here
}

We need to trigger the error manually by checking if timePeriod has a value and adding it to the model state error collection if it doesn't.


[HttpGet]
[Route("{id}/summary")]
public async Task<ActionResult<CustomerSummary>> GetSummary(string id, TimePeriod? timePeriod)
{  
	if (!timePeriod.HasValue)
		ModelState.AddModelError("timePeriod", "timePeriod is required");
                
    if (!ModelState.IsValid)
    	return BadRequest(ModelState);
        
   	// code logic here
}

A cleaner solution for me is to wrap the parameters in a separate class then add a Required attribute to the required fields. This way, the model state validation automatically kicks in.


using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

public class QueryParameters {
	[Required]
  	[FromQuery(Name="timePeriod")]
    public TimePeriod? TimePeriod { get; set; }
}

(Note: the FromQuery attribute is needed to make the query parameter lowercase.)


[HttpGet]
[Route("{id}/summary")]
public async Task<ActionResult<CustomerSummary>> GetSummary(string id, QueryParameters queryParams)
{           
    if (!ModelState.IsValid)
    	return BadRequest(ModelState);
        
   	// code logic here
}

Now when we try to go to the /summary endpoint without a timePeriod query parameter, we get this error:

{
	"timePeriod": [
		"The TimePeriod field is required."
	]
}

Hope this helps anyone who needs to make enum query parameters required!

unsplash-logoLouis Hansel

Kristina Alberto

Software engineer from Sydney. I work at Domain Group.