0

I have an ASP.NET Core Minimal API. I want to version an endpoint using request header X-Api-Version. Depending on the version I want to do different things.

Something like this:

app.MapPost("/save", (
    [FromHeader(Name = "X-Api-Version")] SaveVersion version,  // how to constraint this to v1?
    [FromBody] SaveRequestV1 request
    ) => "Do something");

app.MapPost("/save", (     // this is considered duplicate route
    [FromHeader(Name = "X-Api-Version")] SaveVersion version,
    [FromBody] SaveRequestV2 request
    ) => "Do another thing");

public enum SaveVersion
{
    v1 = 1,
    v2
};

public class SaveRequestV1
{
    public string Name { get; set; }
    public bool IsMember { get; set; }
}

public class SaveRequestV2
{
    public string Name { get; set; }
    public int MemberStatus { get; set; }
}

Is it possible to have different route handlers based on request header?

I was hoping I could do

[FromHeader(Name = "X-Api-Version:regex(^1$)")]

like a route constraint in the url but it is not recognized.

Can Swagger understand this versioning by request header?

2 Answers 2

1

Whilst you could use the ASP.NET API Versioning package to handle the versioning requirements, Swagger will not like the fact you have two endpoints with the same method/path.

Conflicting method/path combination "POST save" for actions - HTTP: POST index,HTTP: POST index. Actions require a unique method/path combination for Swagger/OpenAPI 3.0

This will not prevent the endpoints from working however will prevent Swagger from loading and instead displaying a Failed to load API definition - 500 error. The conflict can be ignored by configuring the Swagger options, but then only one endpoint would be visible.

If you provide the version in the URL either as part of the path or a query string parameter you can avoid the conflict and is also the recommended approach by Microsoft.

Sign up to request clarification or add additional context in comments.

Comments

1

Heavily based on the sample from the docs. Install Asp.Versioning.Http and Asp.Versioning.Mvc.ApiExplorer nugets, add following infrastrucutre classes:

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

    /// <summary>
    /// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
    /// </summary>
    /// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider ) => this.provider = provider;

    /// <inheritdoc />
    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, CreateInfoForApiVersion( description ) );
        }
    }

    private static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description )
    {
        var text = new StringBuilder( "An example application with OpenAPI, Swashbuckle, and API versioning." );
        var info = new OpenApiInfo()
        {
            Title = "Example API",
            Version = description.ApiVersion.ToString(),
        };

        info.Description = text.ToString();

        return info;
    }
}

and

public class SwaggerDefaultValues : IOperationFilter
{
    /// <inheritdoc />
    public void Apply( OpenApiOperation operation, OperationFilterContext context )
    {
        var apiDescription = context.ApiDescription;

        operation.Deprecated |= apiDescription.IsDeprecated();

        // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
        foreach ( var responseType in context.ApiDescription.SupportedResponseTypes )
        {
            // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
            var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
            var response = operation.Responses[responseKey];

            foreach ( var contentType in response.Content.Keys )
            {
                if ( !responseType.ApiResponseFormats.Any( x => x.MediaType == contentType ) )
                {
                    response.Content.Remove( contentType );
                }
            }
        }

        if ( operation.Parameters == null )
        {
            return;
        }
        
        // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
        // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
        foreach ( var parameter in operation.Parameters )
        {
            var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name );

            if ( parameter.Description == null )
            {
                parameter.Description = description.ModelMetadata?.Description;
            }

            if ( parameter.Schema.Default == null &&
                 description.DefaultValue != null &&
                 description.DefaultValue is not DBNull &&
                 description.ModelMetadata is ModelMetadata modelMetadata )
            {
                // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
                var json = JsonSerializer.Serialize( description.DefaultValue, modelMetadata.ModelType );
                parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json );
            }

            parameter.Required |= description.IsRequired;
        }
        operation.Parameters.Add(new OpenApiParameter
        {
            In = ParameterLocation.Header,
            Name = "X-Api-Version",
            Example = new OpenApiString(apiDescription.GroupName)
        });
    }
}

Add them to registration:

builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen(options => options.OperationFilter<SwaggerDefaultValues>());

Register versioning/api explorer infrastructure:

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
    })
    .AddApiExplorer()
    .EnableApiVersionBinding();

Add minimal API routes with versioning:

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1))
    .HasApiVersion(new ApiVersion(2))
    .ReportApiVersions()
    .Build();

app.MapGet("/weatherforecast", () =>
    {
        return "1.0";
    })
    .WithApiVersionSet(versionSet)
    .HasApiVersion(1)
    .MapToApiVersion(1)
    .WithOpenApi();

app.MapGet("/weatherforecast", () => "2.0")
    .WithApiVersionSet(versionSet)
    .HasApiVersion(2)
    .MapToApiVersion(2)
    .WithOpenApi();

add swagger AFTER routes:

app.UseSwagger();
app.UseSwaggerUI(options =>
{
    var descriptions = app.DescribeApiVersions();

    // build a swagger endpoint for each discovered API version
    foreach ( var description in descriptions )
    {
        var url = $"/swagger/{description.GroupName}/swagger.json";
        var name = description.GroupName.ToUpperInvariant();
        options.SwaggerEndpoint( url, name );
    }
});

Result:

enter image description here

Issues I was not able to figure out:

  1. Version header is required, without it endpoint is not reachable
  2. I use parameter with default value to make swagger pass the version (Example = new OpenApiString(apiDescription.GroupName)) which arguably is not the cleanest approach

Full code @github.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.