Home > Mobile >  Issue with IsApiVersionNeutral in asp.net minimal api versioning
Issue with IsApiVersionNeutral in asp.net minimal api versioning

Time:12-31

Hi I am trying to implement versioning in asp.net core minimal api (net7.0). But I am seeing some url access issues with certain api versions. Hope someone can give me some clarity about why the urls are throwing 404 below.

I am trying to build below urls

/GetMessage - to support only version 2.0

/GetText - to support no version or 1.0 or 2.0 or 3.0 (like this is version neutral)

Below is the code for the same

Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => c.OperationFilter<SwaggerParameterFilters>());
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
                       new HeaderApiVersionReader("x-api-version"),
                       new QueryStringApiVersionReader("api-version"));
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();

var vs = app.NewApiVersionSet()
            .HasApiVersion(new ApiVersion(1.0))
            .HasApiVersion(new ApiVersion(2.0))
            .HasApiVersion(new ApiVersion(3.0))
            .ReportApiVersions().Build();
app.MapGet("/GetMessage", () => "This is GetMessage API").WithApiVersionSet(vs).HasApiVersion(new ApiVersion(2, 0));
app.MapGet("/GetText", () => "This is GetText API").WithApiVersionSet(vs).IsApiVersionNeutral();
app.Run();

public class SwaggerParameterFilters : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        operation.Parameters.Add(
            new OpenApiParameter
            {
                Name = "api-version",
                In = ParameterLocation.Query,
                Schema = new OpenApiSchema { Type = "String" }
            });


        var versionParameter = operation.Parameters.SingleOrDefault(p => p.Name == "version");

        if (versionParameter != null)
        {
            operation.Parameters.Remove(versionParameter);
        }
    }
}

csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include=".usings" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    <PackageReference Include="Asp.Versioning.Http" Version="7.0.0" />
  </ItemGroup>

</Project>

Added SwaggerParameterFilters just to pass the version easily from swagger UI, couldn't find any better way to implement the same for specific urls where we need versioning like only for /GetMessage.

Now I tested below urls:

1: /GetMessage works with v2.0 only - expected, BUT the response headers say 'api-supported-versions: 1.0,2.0,3.0', why? Is it reporting for the whole application & not for this api only?

2: /GetMessage without any version query param or with 1.0 or 3.0 - fails in all 3 cases as expected with http 400

3: /GetText works with no api version as query parameter - expected as this is version neutral

4: /GetText works with 2.0 version BUT not with 1.0 or 3.0 - why is this? It is supposed to work with all versions as this api is version neutral

I am not able to figure out why #4 is happening, seems I am missing some concept. Thanks for your help.

CodePudding user response:

To be honest, this is a bit of a strange setup. I was able to take the information you've provided and all of the scenarios work as expected, including #4. A version-neutral API can match any API version defined in the application, including none at all.

URL Status
/GetText 200
/GetText?api-version=1.0 200
/GetText?api-version=2.0 200
/GetText?api-version=3.0 200
/GetText?api-version=4.0 400
/GetMessage 400
/GetMessage?api-version=1.0 400
/GetMessage?api-version=2.0 200
/GetMessage?api-version=3.0 400

An API version set is a logical collection of API version information that you can attach to an endpoint. The surface area of this API is really left over from .NET 6 where there was no grouping concept. I never liked it, but the alternative was to go completely off the tracks and create a custom grouping feature. Fortunately, grouping was added to .NET 7. API version sets are still there under the hood, but it's a lot more natural to use.

While it's true that any endpoint is an API, most developers create a logical group of endpoints that make up an API. For example, consider GET /order/{id} and POST /order are two distinct endpoints, but most people would logically collate these together in the Orders API. In the classic controller-based model, it therefore made sense to pivot on a controller. For Minimal APIs, we need a different way to collate associated endpoints; this is facilitated by an API version set.

You should expect to have an API version set for each API. It's not entirely clear from your example, but I'm going go out on a limb and guess that /GetMessage and /GetMessage are meant to actually be different APIs. This means that these should be defined in separate API version sets. The reported API versions are based on all supported API versions in the version set, not just a specific endpoint. The meaning of API and endpoint are a bit conflated, but the general idea is the version of the whole API, not just a singular endpoint.

Using the grouping features in .NET 7, this can be reorganized in a much more natural way like this:

var message = app.NewVersionedApi( "Message" ); // ← why are we giving these names?
var text = app.NewVersionedApi( "Text" );       // more on that down below

message.MapGet( "/GetMessage", () => "This is GetMessage API" ).HasApiVersion( 2.0 );
text.MapGet( "/GetText", () => "This is GetText API" ).IsApiVersionNeutral();

app.Run();

Defining an API version is not the same thing as mapping an API version to an endpoint. Interleaving API versions is supported in Minimal APIs, but makes a lot less sense.

// declare that 1.0 and 2.0 for all endpoints
var message = app.NewVersionedApi()
                 .HasApiVersion( 1.0 );
                 .HasApiVersion( 2.0 );

message.MapGet( "/GetMessage", () => "This is v1" ).MapToApiVersion( 1.0 );
message.MapGet( "/GetMessage", () => "This is v2" ).MapToApiVersion( 2.0 );

Interleaving API versions an mapping them to specific endpoints

// define a logical, versioned API which is backed by a version set
var message = app.NewVersionedApi();

// all endpoints in this group have 1.0
var v1 = message.MapGroup( "/GetMessage" ).HasApiVersion( 1.0 );

// all endpoints in this group have 2.0
var v2 = message.MapGroup( "/GetMessage" ).HasApiVersion( 2.0 );

v1.MapGet( "/", () => "This is v1" );
v2.MapGet( "/", () => "This is v2" );

Using groups to define API versions at various levels

I also noticed that you used ReportApiVersions at the API version set level. This is not necessary since you've already enabled it for the entire application via ApiVersioningOptions.ReportApiVersions = true. This capability exists so you can opt to report API versions on some APIs, but not others.

Finally, OpenAPI is fully supported for Minimal APIs. You might have wondered why you specified a name with the API version sets. This is the logical name of the API. This information is used by the API Explorer extensions so when they are used in a context such as OpenAPI, that is the name/group you see in the UI. You need to reference the Asp.Versioning.Mvc.ApiExplorer package. For Swashbuckle, you will also need to configure its options so that it picks up the discovered API versions.

public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider provider;

    public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider;

    public void Configure( SwaggerGenOptions options )
    {
        // add a swagger document for each discovered API version
        // note: you might choose to skip or document deprecated API versions differently
        foreach ( var description in provider.ApiVersionDescriptions )
        {
            options.SwaggerDoc(
                description.GroupName,
                new OpenApiInfo()
                {
                    Title = "My API",
                    Description = "An example API",
                    Version = description.ApiVersion.ToString(),
                } );
        }
    }
}

Your configuration will then look something like:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
       .AddApiVersioning(
        options =>
        {
            // this is already ApiVersion.Default which is new ApiVersion(1, 0)
            options.DefaultApiVersion = new ApiVersion(1, 0);

            options.ReportApiVersions = true;
            
            // this is meant for backward compatibility of existing services ONLY.
            // be sure this is really, really what you want. it's not necessary
            // for version-neutral APIs to work
            options.AssumeDefaultVersionWhenUnspecified = true;

            options.ApiVersionReader = ApiVersionReader.Combine(
                            new HeaderApiVersionReader("x-api-version"),
                            new QueryStringApiVersionReader("api-version"));
        })
       .AddApiExplorer(
        options =>
        {
           // note: the specified format code will format the version as "'v'major[.minor][-status]"
           options.GroupNameFormat = "'v'VVV";
        });

// connect discovered API versions to Swashbuckle
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

The last part of your setup should be:

app.UseSwagger();
app.UseSwaggerUI(
    options =>
    {
        // build a swagger endpoint for each discovered API version
        foreach ( var description in app.DescribeApiVersions() )
        {
            options.SwaggerEndpoint(
                $"/swagger/{description.GroupName}/swagger.json",
                description.GroupName );
        }
    } );
app.Run();

This is just scratching surface. The Minimal OpenAPI Example has a complete end-to-end example project that demonstrates how to wire everything all together.

  • Related