Structed
Structed

Reputation: 314

NSwag: Generate C# Client from multiple Versions of an API

We are versioning our API and generating the Swagger specification using Swashbuckle in ASP.NET Core 1.1. We can generate two API docs based on those JSON specification files:

<!-- language: c# -->
services.AddSwaggerGen(setupAction =>
{
    setupAction.SwaggerDoc("0.1", new Info { Title = "Api", Version = "0.1", Description = "API v0.1" });
    setupAction.SwaggerDoc("0.2", new Info { Title = "Api", Version = "0.2", Description = "API v0.2" });

    // more configuration omitted
}

We are including all actions in both spec files, unless it is mapped to a specific version using the [MapToApiVersion] and ApiExplorerSettings(GroupName ="<version>")] attributes. Methods belonging to an older version only are also decorated with the [Obsolete] attribute:

<!-- language: c# -->
[MapToApiVersion("0.1")]
[ApiExplorerSettings(GroupName = "0.1")]
[Obsolete]

However, we want to have only one C# Client generated from the Union of both spec files, where all methods are included in the Client, 0.1 as well as 0.2, but all obsolete methods marked, in fact, as obsolete.

I have looked into both NSwag (which we are using for quite some time now) as well as AutoRest. AutoRest seems to support a merging scenario, but I could not get it to work because of schema validation errors (and I am more than unsure whether our specific scenario would be actually supported).

My last idea as of now to get this sorted is to somehow JSON-merge the specs into one and then feed it to NSwag.

Do we miss anything here? Is this somehow possible to realize with NSwag?

Upvotes: 3

Views: 8648

Answers (3)

Renat Sungatullin
Renat Sungatullin

Reputation: 106

I wrote an article about similar problem https://medium.com/dev-genius/nswag-charp-client-from-multiple-api-versions-7c79a3de4622

First of all, create a schema. As I see, there are two approaches:

  • one schema where multiple versions are living
  • own schema for each version

Next, create clients for each supported version and wrap them under the wrapper client:

public class AppApiClient
{
    public IV1Client V1 { get; }
    public IV2Client V2 { get; }

    public AppApiClient(HttpClient httpClient)
    {
        V1 = new V1Client(httpClient);
        V2 = new V2Client(httpClient);
    }
}

Upvotes: 3

Narottam Goyal
Narottam Goyal

Reputation: 3652

Packages:

Install-Package Swashbuckle.AspNetCore

Install-Package Microsoft.AspNetCore.Mvc.Versioning

enter image description here

ValueV1Controller.cs

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

ValueV2Controller.cs

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

Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddApiVersioning();
        // Register the Swagger generator, defining 1 or more Swagger documents
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new Info { Title = "My API - V1", Version = "v1" });
            c.SwaggerDoc("v2", new Info { Title = "My API - V2", Version = "v2" });

            c.DocInclusionPredicate((docName, apiDesc) =>
            {
                var versions = apiDesc.ControllerAttributes()
                    .OfType<ApiVersionAttribute>()
                    .SelectMany(attr => attr.Versions);

                return versions.Any(v => $"v{v.ToString()}" == docName);
            });

            c.OperationFilter<RemoveVersionParameters>();
            c.DocumentFilter<SetVersionInPaths>();
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger();

        // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 
        // specifying the Swagger JSON endpoint.
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
        });

        app.UseMvc();
    }
}

public class RemoveVersionParameters : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        var versionParameter = operation.Parameters?.SingleOrDefault(p => p.Name == "version");
        if (versionParameter != null)
            operation.Parameters.Remove(versionParameter);
    }
}

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

Upvotes: 0

Helder Sepulveda
Helder Sepulveda

Reputation: 17574

Here is my idea, expanding from the comments:

With swashbuckle you can generate as many SwaggerDoc as you like, the idea on this case is to generate 3 keep the same 2 versions that you have and add one more that will have everything.

c.MultipleApiVersions(
    (apiDesc, targetApiVersion) => 
      targetApiVersion.Equals("default") || // Include everything by default
      apiDesc.Route.RouteTemplate.StartsWith(targetApiVersion), // Only include matching routes for other versions
    (vc) =>
    {
        vc.Version("default", "Swagger_Test");
        vc.Version("v1_0", "Swagger_Test V1_0");
        vc.Version("v2_0", "Swagger_Test V2_0");
    });

Here is a working sample:
http://swagger-net-test-multiapiversions.azurewebsites.net/swagger/ui/index?filter=Api

And the entire code for that project is on GitHub:
https://github.com/heldersepu/Swagger-Net-Test/tree/MultiApiVersions

Upvotes: 0

Related Questions