How to set up Swashbuckle vs Microsoft.AspNetCore.Mvc.Versioning

15,933

Solution 1

At the moment Swashbuckle and Microsoft.AspNetCore.Mvc.Versioning are friends. It works good. I just created test project in VS2017 and checked how it works.

First include these two nuget packages:

<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="1.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />

Configure everything in Startup.cs (read my comments):

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();


        // Configure versions 
        services.AddApiVersioning(o =>
        {
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.DefaultApiVersion = new ApiVersion(1, 0);
        });

        // Configure swagger
        services.AddSwaggerGen(options =>
        {
            // Specify two versions 
            options.SwaggerDoc("v1", 
                new Info()
                {
                    Version = "v1",
                    Title = "v1 API",
                    Description = "v1 API Description",
                    TermsOfService = "Terms of usage v1"
                });

            options.SwaggerDoc("v2",
                new Info()
                {
                    Version = "v2",
                    Title = "v2 API",
                    Description = "v2 API Description",
                    TermsOfService = "Terms of usage v2"
                });

            // This call remove version from parameter, without it we will have version as parameter 
            // for all endpoints in swagger UI
            options.OperationFilter<RemoveVersionFromParameter>();

            // This make replacement of v{version:apiVersion} to real version of corresponding swagger doc.
            options.DocumentFilter<ReplaceVersionWithExactValueInPath>();

            // This on used to exclude endpoint mapped to not specified in swagger version.
            // In this particular example we exclude 'GET /api/v2/Values/otherget/three' endpoint,
            // because it was mapped to v3 with attribute: MapToApiVersion("3")
            options.DocInclusionPredicate((version, desc) =>
            {
                var versions = desc.ControllerAttributes()
                    .OfType<ApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions);

                var maps = desc.ActionAttributes()
                    .OfType<MapToApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions)
                    .ToArray();

                return versions.Any(v => $"v{v.ToString()}" == version) && (maps.Length == 0 || maps.Any(v => $"v{v.ToString()}" == version));
            });

        });

    }

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"v2");
            c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"v1");
        });
        app.UseMvc();
    }

There two classes that make the trick:

public class RemoveVersionFromParameter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters.Single(p => p.Name == "version");
        operation.Parameters.Remove(versionParameter);
    }
}

public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        swaggerDoc.Paths = swaggerDoc.Paths
            .ToDictionary(
                path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
                path => path.Value
            );
    }
}

The RemoveVersionFromParameter removes from swagger UI this textbox:

enter image description here

The ReplaceVersionWithExactValueInPath change this:

enter image description here

to this:

enter image description here

Controller class looks now as follows:

[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1")]
[ApiVersion("2")]
public class ValuesController : Controller
{
    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public string Get(int id)
    {
        return "value";
    }

    // POST api/values
    [HttpPost]
    public void Post([FromBody]string value)
    {
    }

    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }


    [HttpGet("otherget/one")]
    [MapToApiVersion("2")]
    public IEnumerable<string> Get2()
    {
        return new string[] { "value1", "value2" };
    }

    /// <summary>
    /// THIS ONE WILL BE EXCLUDED FROM SWAGGER Ui, BECAUSE v3 IS NOT SPECIFIED. 'DocInclusionPredicate' MAKES THE
    /// TRICK 
    /// </summary>
    /// <returns></returns>
    [HttpGet("otherget/three")]
    [MapToApiVersion("3")]
    public IEnumerable<string> Get3()
    {
        return new string[] { "value1", "value2" };
    }
}

Code: https://gist.github.com/Alezis/bab8b559d0d8800c994d065db03ab53e

Solution 2

If working with .Net Core 3, Basically I have taken @Alezis's solution and updated it to work with .Net core 3:

public void ConfigureServices(IServiceCollection services)
    {
     ....
        services.AddSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo() { Title = "My API", Version = "v1" });
            options.OperationFilter<RemoveVersionFromParameter>();

            options.DocumentFilter<ReplaceVersionWithExactValueInPath>();

        });
      ...
    }

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
   ...
}

public class RemoveVersionFromParameter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters.Single(p => p.Name == "version");
        operation.Parameters.Remove(versionParameter);
    }
}

public class ReplaceVersionWithExactValueInPath : 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;
    }
}

Solution 3

Instead of tweaking the OpenAPI document, you can use the library provided by Microsoft that adds versions to the API Explorer. That way the versions are provided before Swashbuckle (or another toolchain) needs it and allows you to avoid custom code.

Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

I was able to get versions configured correctly after adding the package and this block of code.

services.AddVersionedApiExplorer(
    options =>
    {
    // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
    // note: the specified format code will format the version as "'v'major[.minor][-status]"
    options.GroupNameFormat = "'v'VVV";

    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
    // can also be used to control the format of the API version in route templates
    options.SubstituteApiVersionInUrl = true;
    }
);

Solution 4

In Asp.core 2.+ Add this class:

public class ApiVersionOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var actionApiVersionModel = context.ApiDescription.ActionDescriptor?.GetApiVersion();
            if (actionApiVersionModel == null)
            {
                return;
            }

            if (actionApiVersionModel.DeclaredApiVersions.Any())
            {
                operation.Produces = operation.Produces
                    .SelectMany(p => actionApiVersionModel.DeclaredApiVersions
                        .Select(version => $"{p};v={version.ToString()}")).ToList();
            }
            else
            {
                operation.Produces = operation.Produces
                    .SelectMany(p => actionApiVersionModel.ImplementedApiVersions.OrderByDescending(v => v)
                        .Select(version => $"{p};v={version.ToString()}")).ToList();
            }
        }
   }

next add below codes in configureServices method in startup:

services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "Versioned Api v1", Version = "v1" });

                c.OperationFilter<ApiVersionOperationFilter>();
        });

then add below codes in configure method in startup:

            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {                
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned Api v1");
                    c.RoutePrefix = string.Empty;

in Asp.core 3.+ add these classes:

public class RemoveVersionFromParameter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
                if (!operation.Parameters.Any())
                    return;

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

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

 public class ReplaceVersionWithExactValueInPath : IDocumentFilter
    {
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            if (swaggerDoc == null)
                throw new ArgumentNullException(nameof(swaggerDoc));

            var replacements = new OpenApiPaths();

            foreach (var (key, value) in swaggerDoc.Paths)
            {
                replacements.Add(key.Replace("v{version}", swaggerDoc.Info.Version,
                        StringComparison.InvariantCulture), value);
            }

            swaggerDoc.Paths = replacements;
        }
    }

next add below codes in ConfigureServices method in startup:

protected virtual IEnumerable<int> Versions => new[] {1};

 services.AddSwaggerGen(options =>
            {
                Versions.ToList()
                    .ForEach(v =>
                        options.SwaggerDoc($"v{v}",
                            new OpenApiInfo
                            {
                                Title = $"Versioned Api:v{v}", Version = $"v{v}"
                            }));

                options.OperationFilter<RemoveVersionFromParameter>();
                options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
                options.RoutePrefix = string.Empty;
            });

then add below codes in configure method in startup:

            app.UseSwagger();

            app.UseSwaggerUI(options =>
           {
               Versions.ToList()
                   .ForEach(v => options.SwaggerEndpoint($"/swagger/v{v}/swagger.json", $"Versioned Api:v{v}"));

               options.RoutePrefix = string.Empty;
           });

Solution 5

@Alezis Nice approach, but if you are using the latest version of Microsoft.AspNetCore.Mvc.Versioning (2.3.0) library, ControllerAttributes() and ActionAttributes() are deprecated, you can update DocInclusionPredicate as follows:

options.DocInclusionPredicate((version, desc) =>
{
    if (!desc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
    var versions = methodInfo.DeclaringType
        .GetCustomAttributes(true)
        .OfType<ApiVersionAttribute>()
        .SelectMany(attr => attr.Versions);
     return versions.Any(v => $"v{v.ToString()}" == version);
});

Swashbuckle.AspNetCore github project helps me a lot.

Share:
15,933
Alezis
Author by

Alezis

Updated on June 28, 2022

Comments

  • Alezis
    Alezis almost 2 years

    We have asp.net core webapi. We added Microsoft.AspNetCore.Mvc.Versioning and Swashbuckle to have swagger UI. We specified controllers as this:

    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class ContactController : Controller
    {
    

    When we run swagger ui we get version as parameter in routes: enter image description here

    How to set-up default "v1" for route ? If version 2 come to the stage how support swagger ui for both versions ?

  • Mohan Gopi
    Mohan Gopi over 5 years
    I am using swashbuckle.aspnetcore v3.0.0 and I changed version reading code in the ConfigureServices method. But i am getting the following error "Failed to load API definition. Fetch errorInternal Server Error /swagger/v1/swagger.json". What could be the problem?
  • Major
    Major about 4 years
    Cool much simpler solution and works perfectly :) Thanks.
  • Akin_Glen
    Akin_Glen almost 4 years
    This answer is outdated. You should update it, or edit your answer to state that it will work for only Swashbuckle 4.x. Anyone installing Swashbuckle now will get the latest 5.x, and this solution won't work: github.com/domaindrivendev/Swashbuckle.AspNetCore/releases/t‌​ag/…
  • Jason Coyne
    Jason Coyne about 3 years
    Such a better overall solution, wish it was up higher.
  • Dasith Wijes
    Dasith Wijes over 2 years
    Note: If you use this method, you will need to decorate your API methods with "MapToApiVersion" attribute. If you use the IDocumentFilter and IOperationFilter to write the swagger doc then you don't need that. I use both AddVersionedApiExplorer and the rewriting of swagger file to so I don't have to add the required method attribute to every method in every class.
  • Dasith Wijes
    Dasith Wijes over 2 years
    You also need this in addition if you are using the api explorer. ``` services.AddVersionedApiExplorer(setup => { setup.GroupNameFormat = "'v'VVV"; setup.SubstituteApiVersionInUrl = true; }); ```