15

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>
German Casares
  • 309
  • 1
  • 10
Sorin Comanescu
  • 4,829
  • 3
  • 29
  • 38

3 Answers3

6

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);
            });
    
Sorin Comanescu
  • 4,829
  • 3
  • 29
  • 38
  • 1
    It is possible to keep the swaggerConfig file, you should just follow the 3 steps below: 1- delete this line from swaggerConfig [assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")] 2- Add this param (HttpConfiguration config) to Register method and use it unstead of globalconfiguration 3- Add this line in global.asax GlobalConfiguration.Configure(SwaggerConfig.Register); – gandalf Nov 11 '18 at 07:16
  • @gandalf thank you. You spared my nerves. You are a real magician. – Kate Dec 24 '20 at 18:35
2

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();
            });
    }
}
piet.t
  • 11,718
  • 21
  • 43
  • 52
Narottam Goyal
  • 3,534
  • 27
  • 26
  • 1
    It looks strange to hasrdcode version in the route attribute like this RoutePrefix("api/v2/value") – Kate Dec 24 '20 at 18:27
  • 1
    While this solution will work, it completely disregards the question. The question asked how to integrate Web API with API Versioning and Swashbuckle (Swagger). The provided setup was also versioning by query string. This answer proposes its own solution as to how API versioning should be implemented using a URL segment and integrated with Swashbuckle. Ultimately, this solution will not work or solve the original question. – Chris Martinez Apr 27 '21 at 19:28
0

I had the same error, but what resolved it for me is changing the templateRoute in the EnableSwagger extension method from swagger/docs/{apiVersion} to swagger/{apiVersion}/docs. I think there was a resolution problem for minor version (may be because it contains a '.') when the api version was at the end of the route.

Dharman
  • 30,962
  • 25
  • 85
  • 135
KSoze
  • 141
  • 3
  • That was _likely_ an issue with the host; I'm guessing IIS. IIS will treat paths that end with `*.x` like a file or an associated handler (ex: *.aspx, *.cshtml, etc). It _is_ possible to configure IIS and ASP.NET to not do that if you really want the version at the end. – Chris Martinez Apr 27 '21 at 19:13