Optional Route Parameters with Swagger and ASP.NET Core

According to OpenAPI 3.0 it isn’t possible. But what if you really want it to be? Do you have to just settle and allow your Swagger documentation to be wrong? I’m here to show you how to make optional route parameters with Swagger and ASP.NET Core.

Before we begin let’s evaluate the scenario. ASP.NET Core allows us to define optional route parameters by adding a question-mark at the end of the key. I don’t want to debate the merits or technicalities of this functionality. Let’s also not argue whether or not we’re breaking OpenAPI specs by doing it. The fact is, it’s possible in ASP.NET Core and it’d be nice to support it in Swagger. Code for today’s post is located on my GitHub. All pertinent examples are also inline images.

Wait, what’s Swagger?

On the off-chance you navigated to this post and don’t know what Swagger is, how about I give a quick introduction? “Swagger is a set of rules for a format describing REST apis… as a result, it can be used to share documentation among product managers, testers and developers…” – Getting Started with Swagger.

One common usage of Swagger is to also provide an interface via Swagger UI. Swagger UI allows you to visualize and interact with the API’s resources. It is similar to yet more directed than Postman or like tooling.

Setting up the scenario

**DISCLAIMER** – what I’m showing below fails OpenAPI 3.0 validation. This is because they don’t allow or support optional route parameters. If you want your validation to succeed then you can’t do this.

Now that we have a basic idea what Swagger is, I’m going to set up a contrived scenario. Let us pretend we have an API that serves up blog summaries. This API can return the summaries based on a number of criteria, all of which are served up from the URL. In this particular use-case I’m running Swashbuckle.AspNetCore v5.3.3. This version uses OpenApi v3.x. It is my understanding that things work different in 2.x. You can read up on Swashbuckle on MSDN or GitHub.

Contrived BlogSummaryController with a Get operation that has an optional route parameter for {day}
Contrived BlogSummaryController with a Get operation that has an optional route parameter for {day}

Let’s go ahead and run the site and see what Swagger creates for us.

Default Swagger generation
Default Swagger generation

You’ll notice that even though I defined {day?} with the optional route selector, Swagger is telling us it is required.

Ok, so how do I make Optional Route Parameters?

I imagine there are several ways to approach this. That said, the approach I found seems to work just fine and can be applied both globally and individually as necessary.

So without further ado lets look at IOperationFilter as a starting point. Operation filters allow us to post-modify operation documentation. This is exactly what we need since we need to undo the restrictions around our optional route parameters.

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.HttpMethodAttribute>();

		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.Description = "Must check \"Send empty value\" or Swagger passes a comma for empty values otherwise";
				parameter.Required = false;
				//parameter.Schema.Default = new OpenApiString(string.Empty);
				parameter.Schema.Nullable = true;
			}
		}
	}
}

In my ReApplyOptionalRouteParameterOperationFilter class we first test to see if the method has a “Route” attribute. If so, we then check to see if it has an optional route parameter in the url. If not, we don’t bother applying any changes. On the other hand if we do have one, I make use of a little regex to extract the key. Now that we have the key we find a matching parameter on the operation and finally apply some changes to make optional. Phew!

Applying the IOperationFilter

So now that we have an OperationFilter we need to actually apply it. There are two ways you can go about doing so. The first way is apply it globally in the SwaggerConfiguration. Please note that doing it globally will either require some logic in the Apply method (as my example) to skip applying it where not needed. Failing to do so might cause you some pain otherwise.

Applying an OperationFilter globally
Applying an OperationFilter globally

The other way is to apply an OperationFilter individually per action you wish to modify. In order to apply it locally you simply use SwaggerOperationFilter attribute and specify the type. Please note that you need to use the Swashbuckle.AspNetCore.Annotations nuget package for that attribute. If you apply it globally you should not also apply it locally.

Using SwaggerOperationFilter to apply an OperationFilter locally
Using SwaggerOperationFilter to apply an OperationFilter locally

Using either approach, we can now see that Swagger no longer requires that optional parameter. The world is a better place.

New Swagger documentation with optional route parameters
New Swagger documentation with optional route parameters

Additional Reading

Earlier I dropped a line about how optional route parameters aren’t supported by OpenAPI 3. Rather than just leave you hanging I should probably give some sources. While I was Googling how to fix this problem I came across a couple of “issues” on the Swashbuckle GitHub. The first one had a reference deep in the comments which pointed to an OpenAPI specifiation document. The gist of it is that a variable in the path *must* be required.

The second one I came across actually references the first and gave me the idea how to resolve it. A comment by the repo owner (domaindrivendev) reiterates that the OpenAPI spec does not allow for it.

So there you have it. I show you a way to force it to work even though the specification flat out says don’t do it man. You can do with this knowledge whatsoever your little heart desires. I chose to use it.

Conclusion

Swagger (and Swagger UI) are really neat ways to document and visualize your APIs. Swashbuckle.AspNetCore is a great way to generate that documentation with .NET Core. OpenAPI specifications flat out disallow optional values in your path even though ASP.NET Core allows optional route parameters. I showed you one way to get around that and have your documentation match your implementation. We did that using an IOperationFilter. All code from this example is found on GitHub.

Credits

Photo by Jamie Street on Unsplash