DEV Community

Cover image for Versioning ASP.Net Core APIs with Swashbuckle - Making Space Potatoes V-x.x.x

Versioning ASP.Net Core APIs with Swashbuckle - Making Space Potatoes V-x.x.x

Henrick Tissink on August 26, 2019

Updated Path Versioning Post Here Creating a new API with ASP.NET Core is fun and easy; versioning that API is a bit harder. The cinch though is h...
Collapse
 
slavius profile image
Slavius

Hi Henrick,

isn't the solution a bit overly complex?

With .Net Core 2.2 I just did:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
        c.DisplayOperationId();
        c.DisplayRequestDuration();
    });
}

private class ApiExplorerGroupPerVersionConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        var controllerNamespace = controller.ControllerType.Namespace; // e.g. "Controllers.v1"
        var apiVersion = controllerNamespace?.Split('.').Last().ToLower();

        controller.ApiExplorer.GroupName = apiVersion;
    }
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(c =>
        c.Conventions.Add(
            new ApiExplorerGroupPerVersionConvention()) // decorate Controllers to distinguish SwaggerDoc (v1, v2, etc.)
        });

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "API v1", Version = "v1" });
        c.SwaggerDoc("v2", new Info { Title = "API v2", Version = "v2" });
        c.ExampleFilters();

        // Set the comments path for the Swagger JSON and UI.
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        c.IncludeXmlComments(xmlPath);
        // Uses full schema names to avoid v1/v2/v3 schema collisions
        // see: https://github.com/domaindrivendev/Swashbuckle/issues/442
        c.CustomSchemaIds(x => x.FullName);
    });
}

Collapse
 
htissink profile image
Henrick Tissink

That is extremely clean and simple. Thanks for showing me this - I was trying to figure out a better way to do this. Using proper namespacing with ApiExplorerGroupPerVersionConvention makes a big difference.

Collapse
 
slavius profile image
Slavius

You're welcome. I also spent some time trying to figure it out and this was the cleanest solution. Enjoy!

Thread Thread
 
argenis96 profile image
argenis96

Hello slavius, I have this implementation in an API, and without a doubt it is the simplest and cleanest thing there is. I have only identified one problem. the repetition of the endpoint code from version 1 in version 2. I have solved this by declaring my endpoints in an abstract class and inheriting from it in my controller version 1 and version 2. overwriting functionality of any of those at point if necessary and adding new ones. The problem that I keep seeing is that I will not always want all the endpoints of my version 1 to continue in version 2 of my controller. any suggestion for me

Thread Thread
 
slavius profile image
Slavius

Hi argenis96,

this seems to me more as an architectural problem. The controller should only contain domain isolated code required for it to work. All other code should be extracted to appropriate locations (repository, services, shared library) and be re-used in both controllers.

Have a look at the code from identical endpoints in v1 and v2 and try to extract as much as you can outside of the method as functions leaving only the logic. Don't forget to use interfaces for dependencies so you get good testability. Then your code should remain clean from duplicates and easy to maintain and test.

For example, if your controller queries data from a database but v2 of the same controller method uses more fields returned, you can create a function inside your repository to either accept LINQ expression with output fileds or even Where() clause or you can implement method that returns IQueryable() and materialize it inside the controller by defining SELECT columns and calling .ToList() or .ToArray().

Collapse
 
zxswordxz profile image
zxswordxz

I speak too soon, I'm having a problem calling the different versions. How do I decorate my controller to make them unique? I have something like the below but that throw an endpoints.MapControllers() error.

namespace API.Controllers.v1
{
[Route("api/v1/[controller]")]

Collapse
 
smartcodinghub profile image
Oscar

This was awesome. And you can use whatever you want to mark the versions (attributes, namespaces, class names). I like it.

Collapse
 
zxswordxz profile image
zxswordxz

Thank you, this is clean and nice and work for me in .net core 3.0.

Collapse
 
codeswayslay profile image
Akin Agbaje • Edited

This is a great tutorial. Thanks.

I did get stuck at some point.

With .NET Core 3.1, Swashbuckle has changed a number of things. The "Apply" method in the class that implements the IDocumentFilter should be updated to this:

public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            var toReplaceWith = new OpenApiPaths();

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

            swaggerDoc.Paths = toReplaceWith;
        }
Enter fullscreen mode Exit fullscreen mode

Also, the predicate that ensures endpoints are displayed in their appropriate swagger doc should be updated:

setup.DocInclusionPredicate((version, desc) => 
                {
                    if (!desc.TryGetMethodInfo(out MethodInfo methodInfo))
                        return false;

                    var versions = methodInfo.DeclaringType
                    .GetCustomAttributes(true)
                    .OfType<ApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions);

                    var maps = methodInfo
                    .GetCustomAttributes(true)
                    .OfType<MapToApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions)
                    .ToArray();

                    return versions.Any(v => $"v{v.ToString()}" == version)
                    && (!maps.Any() || maps.Any(v => $"v{v.ToString()}" == version));
                });
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mustafasalahuldin profile image
Mustafa Salaheldin

Saved my day.

Collapse
 
gabehunt profile image
Gabe Hunt

Thank you ... this is exactly what I needed!

Collapse
 
bernoitaliano profile image
BernoItaliano

Hi!!!

I follow allll the steps but i have this Error : "System.ArgumentException: 'An item with the same key has already been added. Key: v1'"

Can u help me???

Collapse
 
sobhan_1995 profile image
Sobhan

hi Henrik .thankful for article.
i have problem with Swashbuckle.AspNetCore version 5.0.0-rc4
when i want implement ReplaceVersionWithExactValueInPath class
i get error.
tnx you.