asp.net core swashbuckle how to use versioning in same controller

Viewed 1031

When using versioning in asp.net core with swashbuckle for swagger, you cannot have by default multiple methods in same controller like that:

[Route("v{version:apiVersion}/[controller]")]
[ApiVersion("1")]
[ApiVersion("2")]
[ApiController]
public class TestController : ControllerBase
{
    /// <summary>
    /// test ver 1
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [MapToApiVersion("1")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public string Get()
    {
        return "Version 1";
    }

    /// <summary>
    /// test ver 2
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [MapToApiVersion("2")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public string Get2()
    {
        return "Version 2";
    }
}

By default, you will have error message

Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Conflicting method/path combination "GET v{version}/Test" for actions

  • ...Controllers.TestController.Get,...TestController.Get2

because swagger-ui recognized both methods by same route

localhost/v{version:apiVersion}/Test

Basic swashbuckle solution is to have two separate controllers for each version of method. That is bad solution for my use-case.

1 Answers

Solution is manual check for version in swagger setter. In my case, I also added supported version list.

So you have to add to Startup.cs this:

private readonly string[] supportedVersions = new string[] { "1", "2" };

public void ConfigureServices(IServiceCollection services)
{
...
services.AddApiVersioning();
services.AddSwaggerGen(c =>
{
            
    c.DocInclusionPredicate((docName, apiDesc) =>
    {
        var metadata = apiDesc.ActionDescriptor.EndpointMetadata;
        var apiVersionModel = metadata.Where(x => x.GetType() == typeof(ApiVersionAttribute)).Cast<ApiVersionAttribute>();
        var supportedVersions = metadata.Where(x => x.GetType() == typeof(MapToApiVersionAttribute)).Cast<MapToApiVersionAttribute>();
        if (apiVersionModel.IsNullOrEmpty() || supportedVersions.IsNullOrEmpty())
        {
            return false;
        }

        var apiVersions = apiVersionModel.SelectMany(x => x.Versions.Select(y => y.MajorVersion));
        var versions = supportedVersions.SelectMany(x => x.Versions.Select(y => y.MajorVersion));

        return apiVersions.Any(v => $"v{v.ToString()}" == docName) && versions.Any(v => $"v{v.ToString()}" == docName);
    });
    c.DocumentFilter<ReplaceVersionWithExactValueInPathFilter>();
    c.OperationFilter<RemoveVersionParameterFilter>();
    c.EnableAnnotations();
    supportedVersions.ForEach(sv =>
    {
        c.SwaggerDoc($"v{sv}", new OpenApiInfo()
        {
            Version = $"v{sv}",
            Title = $"API v{ sv }",
            Description = "My web api",
        });
    });
    var baseDirectory = AppContext.BaseDirectory;
    var xmlFiles = Directory.EnumerateFiles(baseDirectory, "*.xml");

    xmlFiles.ForEach(x => c.IncludeXmlComments(x));
});
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
            app.UseSwaggerUI(c =>
        {
            supportedVersions.ForEach(sv =>
            {
                c.SwaggerEndpoint($"/swagger/v{sv}/swagger.json", $"My api v{sv}");
            });
        });
    }

and to separate file two methods to avoid showing version as parameter in UI. Otherwise you will see endpoints with version parameter instead of version itself.

public class RemoveVersionParameterFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters.FirstOrDefault(p => p.Name == "version");
        if (versionParameter == default(OpenApiParameter))  return;
        var parameters = operation.Parameters.ToList();
        operation.Parameters.Remove(versionParameter);
    }
}

public class ReplaceVersionWithExactValueInPathFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var paths = new OpenApiPaths();
        foreach (var path in swaggerDoc.Paths)
        {
            
            paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value);
        }
        swaggerDoc.Paths = paths;
    }
}
Related