Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swagger not working correctly with multiple versions of ASP.NET WebApi app

Please help me with this, it looked easy at first, now I'm late in the project:

I'm trying to setup API versioning for a ASP.NET WebApi project, along with Swagger. The API versioning works fine, calling different versions returns the correct results (see below).

On the contrary, Swagger fails to serve both versions. While debugging, I noticed that when c.MultipleApiVersions(...) gets called in SwaggerConfig.cs, the controller reported by apiDesc.ActionDescriptor.ControllerDescriptor is always PingController and never Ping11Controller.

Can somebody point out what needs to be done to solve this and have Swagger also work for both versions?

Below, the code and proof of API versioning working fine while Swagger working only for v1.0.

Thank you!

Calling API v1.0 works: enter image description here

Calling API v1.1 works too: enter image description here

Swagger for v1.0 is good: (http://localhost:50884/v1.0/swagger)

{
   "swagger":"2.0",
   "info":{
      "version":"v1.0",
      "title":"My API v1.0"
   },
   "host":"localhost:50884",
   "schemes":[
      "http"
   ],
   "paths":{
      "/api/ping":{
         "get":{
            "tags":[
               "Ping"
            ],
            "summary":"Get a pong.",
            "operationId":"GetAPong",
            "consumes":[
            ],
            "produces":[
               "application/json",
               "text/json",
               "application/xml",
               "text/xml"
            ],
            "responses":{
               "200":{
                  "description":"OK"
               },
               "404":{
                  "description":"NotFound"
               }
            }
         }
      }
   },
   "definitions":{
   }
}

Swagger for v1.1 is empty: (http://localhost:50884/v1.1/swagger)

{
   "swagger":"2.0",
   "info":{
      "version":"v1.1",
      "title":"My API v1.1"
   },
   "host":"localhost:50884",
   "schemes":[
      "http"
   ],
   "paths":{
   },
   "definitions":{
   }
}

THE CODE

App_Start\WebApiConfig.cs:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.AddApiVersioning(options => {
            options.ReportApiVersions = true;
        });

        var constraintResolver = new System.Web.Http.Routing.DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("apiVersion", typeof(Microsoft.Web.Http.Routing.ApiVersionRouteConstraint));
        config.MapHttpAttributeRoutes(constraintResolver);

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

App_Start\SwaggerConfig.cs:

public class SwaggerConfig
{
    static string XmlCommentsFilePath
    {
        get
        {
            var basePath = System.AppDomain.CurrentDomain.RelativeSearchPath;
            var fileName = typeof(SwaggerConfig).GetTypeInfo().Assembly.GetName().Name + ".xml";
            return Path.Combine(basePath, fileName);
        }
    }

    public static void Register()
    {
        var configuration = GlobalConfiguration.Configuration;
        GlobalConfiguration.Configuration.EnableSwagger("{apiVersion}/swagger", c => {
                c.OperationFilter<SwaggerDefaultValues>();
                c.MultipleApiVersions((System.Web.Http.Description.ApiDescription apiDesc, string targetApiVersion) =>
                {
                    var attr = apiDesc.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<Microsoft.Web.Http.ApiVersionAttribute>().FirstOrDefault();
                    if (attr == null && (targetApiVersion == "v1" || targetApiVersion == "v1.0")) return true;
                    var match = (attr != null) && (attr.Versions.FirstOrDefault(v => "v" + v.ToString() == targetApiVersion) != null);
                    return match;
                },
                (vc) =>
                {
                    vc.Version("v1.1", "My API v1.1");
                    vc.Version("v1.0", "My API v1.0");
                });

                c.IncludeXmlComments(SwaggerConfig.XmlCommentsFilePath);
            })
            .EnableSwaggerUi(c => {
                c.DocExpansion(DocExpansion.List);
                c.EnableDiscoveryUrlSelector();
            });
    }
}

Controllers for v1.0 and v1.1 (sitting in the same namespace)

[ApiVersion("1.0")]
[RoutePrefix("api")]
[ControllerName("Ping")]
public class PingController : ApiController
{
    [HttpGet]
    [Route("ping")]
    [SwaggerOperation("GetAPong")]
    [SwaggerResponse(HttpStatusCode.OK)]
    [SwaggerResponse(HttpStatusCode.NotFound)]
    public string Get()
    {
        return "Pong v1.0";
    }
}

[ApiVersion("1.1")]
[RoutePrefix("api")]
[ControllerName("Ping")]
public class Ping11Controller : ApiController
{
    [HttpGet]
    [Route("ping")]
    [SwaggerOperation("GetAPong")]
    [SwaggerResponse(HttpStatusCode.OK)]
    [SwaggerResponse(HttpStatusCode.NotFound)]
    public string Get()
    {
        return "Pong v1.1";
    }
}

PACKAGES

<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net46" />
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net46" />
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net46" />
<package id="Microsoft.AspNet.WebApi.Versioning" version="2.1.0" targetFramework="net46" />
<package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net46" />
<package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.7" targetFramework="net46" />
<package id="Microsoft.IdentityModel.Logging" version="1.1.4" targetFramework="net46" />
<package id="Microsoft.IdentityModel.Tokens" version="5.1.4" targetFramework="net46" />
<package id="Microsoft.Net.Compilers" version="2.3.2" targetFramework="net46" developmentDependency="true" />
<package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net46" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net46" />
<package id="NLog" version="4.4.12" targetFramework="net46" />
<package id="Swashbuckle" version="5.6.0" targetFramework="net46" />
<package id="Swashbuckle.Core" version="5.6.0" targetFramework="net46" />
<package id="System.IdentityModel.Tokens.Jwt" version="5.1.4" targetFramework="net46" />
<package id="WebActivatorEx" version="2.2.0" targetFramework="net46" />
</packages>
like image 423
Sorin Comanescu Avatar asked Sep 05 '17 15:09

Sorin Comanescu


2 Answers

Solved it by:

  1. Adding the Microsoft.AspNet.WebApi.Versioning.ApiExplorer package
  2. Using the versioned API explorer as below (note that I had to move the code from SwaggerConfig.cs in WebApiConfig.cs due to initialization issues):

        var apiExplorer = config.AddVersionedApiExplorer(options => {
            options.GroupNameFormat = "'v'VVV";
        });
    
        var versionSupportResolver = new Func<ApiDescription, string, bool>((apiDescription, version) => apiDescription.GetGroupName() == version);
    
        var versionInfoBuilder = new Action<VersionInfoBuilder>(info => {
            foreach (var group in apiExplorer.ApiDescriptions)
            {
                info.Version(group.Name, $"MyAPI v{group.ApiVersion}");
            }
        });
    
        config
            .EnableSwagger("{apiVersion}/swagger", swagger => {
                swagger.OperationFilter<SwaggerDefaultValues>();
                swagger.MultipleApiVersions(versionSupportResolver, versionInfoBuilder);
                swagger.IncludeXmlComments(WebApiConfig.XmlCommentsFilePath);
            })
            .EnableSwaggerUi(swaggerUi => {
                swaggerUi.EnableDiscoveryUrlSelector();
                swaggerUi.DocExpansion(DocExpansion.List);
            });
    
like image 79
Sorin Comanescu Avatar answered Nov 13 '22 21:11

Sorin Comanescu


enter image description here

ValueV1Controller.cs

[RoutePrefix("api/v1/value")]
public class ValueV1Controller : ApiController
{
    [Route("get")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

ValueV2Controller.cs

[RoutePrefix("api/v2/value")]
public class ValueV2Controller : ApiController
{
    [Route("get")]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1.2", "value2.2" };
    }
}

SwaggerConfig.cs

public class SwaggerConfig
{
    public static void Register()
    {
        var thisAssembly = typeof(SwaggerConfig).Assembly;

        GlobalConfiguration.Configuration
            .EnableSwagger(c =>
            {
                c.MultipleApiVersions(
                    (apiDesc, version) =>
                    {
                        var path = apiDesc.RelativePath.Split('/');
                        var pathVersion = path[1];

                        return CultureInfo.InvariantCulture.CompareInfo.IndexOf(pathVersion, version, CompareOptions.IgnoreCase) >= 0;
                    },
                    (vc) =>
                    {
                        vc.Version("v2", "Swashbuckle Dummy API V2");
                        vc.Version("v1", "Swashbuckle Dummy API V1");
                    });
            })
            .EnableSwaggerUi(c =>
            {
                c.EnableDiscoveryUrlSelector();
            });
    }
}
like image 26
Narottam Goyal Avatar answered Nov 13 '22 21:11

Narottam Goyal