NSwag multiple document endpoint

Viewed 5210

It's possible to have multiple document endpoint like in swashbuckle?

options.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");

If Yes, how to decorate api calls so that some belongs to one versione and some to the other version?

So According to Rico Suter suggestion what I've done it's kinda like that:

ApiVersionAttribute.cs

public class ApiVersionAttribute:Attribute
    {
        private List<string> _versions = new List<string>();

        public List<string> Versions { get { return _versions; } }

        public ApiVersionAttribute(string version) {
            Versions.Add(version);
        }
    }

MyApiVersionProcessor.cs

        public string Version { get; set; }

        public MyApiVersionProcessor(string version)
        {
            this.Version = version;
        }

        public new Task<bool> ProcessAsync(OperationProcessorContext context)
        {
            bool returnValue = true;

            var versionAttributes = context.MethodInfo.GetCustomAttributes()
                .Concat(context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes())
                .Where(a => a.GetType()
                                .IsAssignableTo("MapToApiVersionAttribute", TypeNameStyle.Name) 
                            || a.GetType()
                                .IsAssignableTo("ApiVersionAttribute", TypeNameStyle.Name)
                       )
                .Select(a => (dynamic)a)
                .ToArray();

            var versionAttribute = versionAttributes.FirstOrDefault();

            if (null == versionAttribute)
            {
                returnValue = false;
            }
            else
            {
                if (ObjectExtensions.HasProperty(versionAttribute, "Versions")
                    && Version.Equals(versionAttribute.Versions[0].ToString()))
                {
                    ReplaceApiVersionInPath(context.OperationDescription, versionAttribute.Versions);
                }
                else {
                    returnValue = false;
                }
            }

            return Task.FromResult(returnValue);
        }

        private void ReplaceApiVersionInPath(SwaggerOperationDescription operationDescription,
            dynamic versions)
        {
            operationDescription.Path = operationDescription.Path.Replace("{version:apiVersion}",
                versions[0].ToString());
        }
    }

And in my Global.asax

                // NSwag
                // https://github.com/RSuter/NSwag/wiki/OwinGlobalAsax#integration                
                app.UseSwaggerUi(typeof(WebApiApplication).Assembly, new SwaggerUiSettings
                {                       
                    //TypeNameGenerator = new MyTypeNameGenerator(),
                    MiddlewareBasePath = "/swagger",
                    SwaggerRoute = "/swagger/v1/swagger.json",
                    Version = "1.0.0.0",
                    // https://github.com/RSuter/NSwag/wiki/Middlewares
                    OperationProcessors =
                    {
                        new MyApiVersionProcessor("v1")
                    },
                    PostProcess = document =>
                    {                        
                        document.BasePath = "/";
                        document.Produces
                            = new List<string> { "application/json"
                                                , "text/json"
                                                , "text/html"
                                                , "plain/text"
                                                , "application/xml"};
                        document.Consumes
                            = document.Produces;
                        document.Info.Title = "Document V1";                     
                    }

                });

                app.UseSwaggerUi(typeof(WebApiApplication).Assembly, new SwaggerUiSettings
                {
                    //TypeNameGenerator = new MyTypeNameGenerator(),
                    MiddlewareBasePath = "/swagger",
                    SwaggerRoute = "/swagger/v2/swagger.json",
                    Version = "2.0.0.0",
                    OperationProcessors =
                    {
                        new MyApiVersionProcessor("v2")
                    },
                    PostProcess = document =>
                    {
                        document.BasePath = "/";
                        document.Produces
                            = new List<string> { "application/json"
                                                , "text/json"
                                                , "text/html"};
                        document.Consumes
                            = document.Produces;
                        document.Info.Title = "Document V2";
                    }

                });

And decorated my Controllers' Methods with

[ApiVersion("v2")]

[ApiVersion("v1")]

etc.

3 Answers

There is an out of the box solution now. I propose it for ASP.NET WebAPI Owin and I guess it should be very similar in ASP.NET Core.

First: You need to install ASP.NET API Versioning (GitHub, Nuget)

Second: You need to decorate your action methods with the desired route and version. For example:

[Route("api/v{version:apiVersion}/get1")]
[ApiVersion("1")]

public IEnumerable<string> Get1()
{           
   return new string[] { "value1", "value2" };
}

[Route("api/v{version:apiVersion}/get2")]
[ApiVersion("2")]

public IEnumerable<string> Get2()
{           
   return new string[] { "value1", "value2" };
}

Third: You got to add the required configuration to the Startup.cs file to 1) Generate a separate OpenAPI Specification file for each API version 2) Ask NSwag to show the sepecification files through Swagger-UI

public class Startup
{
    [Obsolete]
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();

        // register versioning
        config.MapHttpAttributeRoutes(new DefaultInlineConstraintResolver
        {
            ConstraintMap = { ["apiVersion"] = typeof(ApiVersionRouteConstraint) }
        });

        config.AddApiVersioning(options =>
        {
            // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
            options.ReportApiVersions = true;

            // automatically applies an api version based on the name of the defining controller's namespace
            options.Conventions.Add(new VersionByNamespaceConvention());
        });

        // generate OpenAPI Sepecification for each version and assign a route to it

        var assembly = typeof(Startup).Assembly;

        app.UseSwagger(assembly ,s =>
        {
            s.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "1" };
            s.GeneratorSettings.SchemaType = SchemaType.OpenApi3;
            s.DocumentPath = "/swagger/v1.0.json";
        });

        app.UseSwagger(assembly , s =>
        {
            s.GeneratorSettings.OperationProcessors.TryGet<ApiVersionProcessor>().IncludedVersions = new[] { "2" };
            s.GeneratorSettings.SchemaType = SchemaType.OpenApi3;

            s.DocumentPath = "/swagger/v2.0.json";
        });

        // integrate Swagger-UI with the generated OpenAPI files generated before.
        _ = app.UseSwaggerUi3(assembly , s =>
          {
              s.SwaggerRoutes.Add(new SwaggerUi3Route("Version 1", "/swagger/v1.0.json"));
              s.SwaggerRoutes.Add(new SwaggerUi3Route("Version 2", "/swagger/v2.0.json"));

              s.GeneratorSettings.Title = "My API";
              s.GeneratorSettings.Description = "API functionalities.";
          });

        app.UseWebApi(config);

        config.EnsureInitialized();
    }
}

Go to the Swagger page. You'll See:

  1. There is a drop box on upper right that lets you select versions
  2. For each API Version, only the relevant action methods are listed.

We've run into this problem but we're planning on doing this the other way around.

  1. curl the swagger document
  2. rummage through it and split it based on tags
  3. create local swagger documents for each tag
  4. run nswag cli against each newly created swagger document

That way we don't have to alter the server just because the client needs something else.

(Will update if we were successful :D)


EDIT: So I've successfully broken down the schema to multiple schemas, one per controller, and generated new files through nswag. Not the prettiest code but it works.. Could publish it on github if any one is interested

Related