If you stumble upon this post, chances are, you were also having a hard time trying to figure out how to configure Azure API Management (APIM) to return the correct StatusCode when a required querystring parameter wasn't supplied. Well, the short answer is you can't. Atleast, based on my research 😛. In this post, we'll take a look at how we can make the required querystring parameters to work in APIM.

Background

The scenario that I have is that I have a simple ASP.NET Core API endpoint that looks something like in the following:

[HttpGet("foo/{id:long}")]
[ProducesResponseType(typeof(FooResponse), Status200OK)]
[ProducesResponseType(typeof(ApiProblemDetailsResponse), Status400BadRequest)]
[ProducesResponseType(typeof(ApiProblemDetailsResponse), Status401Unauthorized)]
[ProducesResponseType(typeof(ApiProblemDetailsResponse), Status404NotFound)]
[ProducesResponseType(typeof(ApiProblemDetailsExceptionResponse), Status500InternalServerError)]
public async Task<IActionResult> GetAsync(long id, [FromQuery] UrlQueryParameters queryParameters)
{
    if (!ModelState.IsValid)
    {
        throw new ApiProblemDetailsException(ModelState, Status400BadRequest);
    }

    var foo = await _someService.GetFooAsync(id, queryParameters.PropertyA, queryParameters.PropertyB);

    return (foo is null)
           ? throw new ApiProblemDetailsException("No record found.", Status404NotFound)
           : Ok(foo);
}

The important thing to notice from the preceding code snippet is the [FromQuery] parameter which is defined something like in the following:

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

public class UrlQueryParameters
{
    [BindRequired]
    public string PropertyA { get; set; }

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

The preceding code annotate/decorate the properties in a class to make them required using [BindRequired] and [Required] attributes. It was my intent to show both approaches as you might be using either of them. These validations ensure that the querystring parameters should be present and valid, otherwise we return a 400 Bad Request with details of which parameters are invalid/missing. This is done via !ModelState.IsValid call from the previous code above.

When you are using Swashbuckle.AspNetCore for generating Swagger definitions and UI, you'll notice that both fields are automatically shown as required fields from the Swagger UI page. Everything should look good until you deployed your application API in Azure and import the definitions to APIM.

The Problem

The main problem with this is how the way the OpenApi definition is converted into APIM operations. If you go to Azure portal, particularly in APIM, you'll notice that the required querystring parameters are added as template parameters and they are added to the operation URLs that looks something like this:

/api/foo/{id}/?propertyA={propertyA}&propertyB={propertyA}

Now, when you try to make an Http GET request for /api/foo/{id} without the required querystring parameters, you would expect to get a 400 StatusCode back with the message details. Well, unfortunately, that's not the case when your API endpoints are imported to APIM.

Since the /api/foo/{id} does not exist as an operation URL entry, APIM would not be able to match the the incoming request to an operation and would return a 404 Not Found to the caller instead rather than the 400 Bad Request that our API code would return. In other words, APIM intercepts the request and returns a 404 Not Found before it hits the downstream endpoint.

That is how the APIM works and I spent hours to figure out how to make them work - returning the correct StatusCode via APIM configurations but to no avail. 😫

The Dirty Hack!

I couldn't find a way to configure APIM to not converting querystring parameters to templated querystring. If you know a better way, please drop your suggestions in the comments below and I would truly appreciate it. 👍

Anyways, what I did was to configure it on the Application side and use FluentValidation to make the querystring parameters required. This way, I don't have to decorate each property with [BindRequired] or [Required] attributes, which makes my class/model cleaner, be able to define validation rules in a fluent way and ultimately won't affect  the Swagger OpenAPI defintions.

The first step is to modify the model to make use of FluentValidation as follows:

using FluentValidation;

public class UrlQueryParameters
{
    public string PropertyA { get; set; }
    public string PropertyB { get; set; }
}

public class UrlQueryParametersValidator : AbstractValidator<UrlQueryParameters>
{
    public UrlQueryParametersValidator()
    {
        RuleFor(o => o.PropertyA)
            .NotEmpty()
            .WithMessage("The 'PropertyA' parameter was missing or a value was not provided.");

        RuleFor(o => o.PropertyB)
            .NotEmpty()
            .WithMessage("The 'PropertyB' parameter was missing or a value was not provided.");
    }
}

The preceding code defines a couple of validation rules with custom messages for the PropertyA and PropertyB properties.

Now, enable FluentValidation as the default validation mechanism for our application by adding the following code at ConfigureServices method of Startup.cs file:

public void ConfigureServices(IServiceCollection services) {
    // Rest of the code omitted for brevity
    
    services
      .AddControllers()
      .AddFluentValidation(fv => 
      { 
          fv.DisableDataAnnotationsValidation = true;
          
          // The following line registers ALL public validators in the assembly containing UrlQueryParametersValidator
          // There is no need to register additional validators from this assembly.
          fv.RegisterValidatorsFromAssemblyContaining<UrlQueryParametersValidator>(lifetime: ServiceLifetime.Singleton);
      });
}

At this point, your API endpoints should validate the required parameters from the request and APIM should not short-circuit the request by throwing 404 Not Found when you try to access /api/foo/{id}.

The reason why this works because Swashbuckle doesn't automatically import validation rules from FluentValidation. Meaning, the properties PropertyA and PropertyB won't be marked as required when viewing them in the Swagger UI. This is the downside for this approach as the required querystring parameters from the Swagger UI will not be marked as required which could be confusing to consumers. But to me, returning the correct StatusCode with meaningful message to consumers is more important than returning the generic 404 error generated from APIM. This is why I will stick to this workaround for the time being. You could try using the MicroElements.Swashbuckle.FluentValidation to altleast set/marked the parameters as required in the Swagger UI schema. But that's just about it.

You could of course create  custom Swagger operation filter by implementing the IOperationFilter just like in the following:

public class CustomSwaggerOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation.Parameters == null)
        {
            return;
        }

        var parameters = context.ApiDescription.ParameterDescriptions;
        
        foreach (var parameter in parameters)
        {
            var param = operation
                .Parameters
                .FirstOrDefault(x => x.Name.ToLowerInvariant() == parameter.Name.ToLowerInvariant());

            if (param is null)
            {
                return;
            }

            var containeModelType = parameter.ModelMetadata.ContainerType;
            if (containeModelType is null)
            {
                continue;
            }

            if (containeModelType.Name == nameof(UrlQueryParameters))
            {
                if (parameter.Name.Trim().ToLower() == "propertya")
                {
                    param.Required = true;
                }

                if (parameter.Name.Trim().ToLower() == "propertyb")
                {
                    param.Required = true;
                }
            }
        }
    }
}

And then inject the custom filter during the swagger document generation like in the following:

services.AddSwaggerGen(options =>
{
    // Rest of the code omitted for brevity
    
    options.OperationFilter<CustomSwaggerOperationFilter>();
});

Now, you should see that the properties PropertyA and PropertyB are now marked as required in the Swagger UI.

However, doing this will result to getting the original problem we had. It appears to me that APIM automatically converts the required querystring parameters from the OpenAPI definition to templated querystring.

Again, if you know a better way to import the Swagger OpenAPI definitions in APIM without affecting the required querystring parameters, please do let me know. 😊

Thank you and I hope you find this post useful. 🍻