ASP.NET Core nullable route params in Swagger

July 22nd 2022 OpenAPI ASP.NET Core

I recently had an issue with optional route parameters in ASP.NET Core Web API not showing as such in the Open API specification generated by Swashbuckle. The most comprehensive source of information I could find on this issue was a blog post, which I used as a basis for further research.

First, I tried using the operation filter from the blog post to mark the route parameter as not required in the Open API specification. In doing so, I had to modify the code slightly so that it worked even if the route parameter was specified with a Route attribute instead of HttpGet, HttpPost and similar attributes. It was a small fix: I had to filter the custom parameters by the IRouteTemplateProvider interface, which is implemented by all these attributes, including the Route attribute, instead of by HttpMethodAttribute, which is just a base class of HttpGet and other verb-based attributes.

Here is my final code:

public class ReApplyOptionalRouteParameterOperationFilter : IOperationFilter
{
  const string captureName = "routeParameter";

  public void Apply(OpenApiOperation operation, OperationFilterContext context)
  {
    var httpMethodAttributes = context.MethodInfo
      .GetCustomAttributes(true)
      .OfType<Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider>();

    var httpMethodWithOptional = httpMethodAttributes?.FirstOrDefault(m =>
      m.Template?.Contains("?") ?? false);
    if (httpMethodWithOptional == null)
      return;

    string regex = $"{{(?<{captureName}>\\w+)\\?}}";

    var matches = System.Text.RegularExpressions.Regex.Matches(
      httpMethodWithOptional.Template ?? "", regex);

    foreach (System.Text.RegularExpressions.Match match in matches)
    {
      var name = match.Groups[captureName].Value;

      var parameter = operation.Parameters.FirstOrDefault(p =>
        p.In == ParameterLocation.Path && p.Name == name);
      if (parameter != null)
      {
        parameter.AllowEmptyValue = true;
        parameter.Required = false;
        parameter.Schema.Nullable = true;
      }
    }
  }
}

This worked, but had the disadvantage of creating an invalid Open API specification:

Structural error at paths./WeatherForecast/{dayOffset}.get.parameters.0
should have required property 'required'
missingProperty: required

This was to be expected, since the official specification explicitly states that route parameters must be required:

required: Determines whether this parameter is mandatory. If the parameter is in "path", this property is required and its value MUST be true. Otherwise, the property MAY be included and its default value is false.

For some reason, the automatically created UI supported this property, albeit in a somewhat roundabout way - it was not enough not to specify the value of the parameter, but it was also necessary to check the checkbox below it to ensure that no value was sent:

Open API UI optional route parameter

However, it was still likely that other tools, such as code generators, would not handle this invalid Open API specification correctly. Therefore, I decided to take a different approach.

I refactored the single route with the optional route parameter into two routes:

  • one where the route parameter is required,
  • and the other without the route parameter.

This approach would still work identically for all supported URLs, but had the advantage of creating a valid Open API specification. It also did not require many code changes. Instead of a single action method, I now had two (one for each route), but both called the same internal method with the common implementation:

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
  return this.Get(1, 5);
}

[HttpGet("{dayOffset}", Name = "GetWeatherForecastForOneDay")]
public IEnumerable<WeatherForecast> Get(int dayOffset)
{
  return dayOffset > 0 ? this.Get(dayOffset, 1) : this.Get();
}

private IEnumerable<WeatherForecast> Get(int start, int count)
{
  return Enumerable.Range(start, count).Select(index => new WeatherForecast
  {
    Date = DateTime.Now.AddDays(index),
    TemperatureC = Random.Shared.Next(-20, 55),
    Summary = Summaries[Random.Shared.Next(Summaries.Length)]
  })
  .ToArray();
}

The auto-generated Open API UI still supported calling the API without the route parameter. It just represented it as a separate endpoint:

Open API UI two endpoints

I created a sample project and published the code to my GitHub repository. The last commit contains the code for the two action method approach. The commit before that contains the code for the approach with the optional route parameter.

ASP.NET Core Web API supports optional route parameters, but that is not the case with Open API 3. In this post, I described how to generate an invalid Open API specification for the optional route parameter, but arrived at a better solution by using two separate routes without instead of the optional route parameter.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License