asp.net-coreswaggerasp.net-core-webapiswashbuckleapi-versioning

How can I populate the basepath of a swagger document when I have multiple versions of my API?


I am using Swashbuckle.AspNetCore to generate my swagger document, and I am using ApiVersion to version my controllers. My setup supports multiple versions, and this works fine. Meaning I can select a version in the Swagger UI, and I have a swagger document created for each of my versions, with only the relevant actions.

However, I would like to refactor the swagger documents in order to make better use of the basepath-property.

As an example, let me use the swagger document generated for version 1 of my API. In this document, all the paths begin with "/api/v1/...", and there is no basepath in the generated swagger document. However, what I would like, is for all my paths to simply begin with "/...", and that the generated document includes a basepath property with the value "/api/v1".

I have tried creating a DocumentFilter, which gets me close, but not all the way. True, I can access the GroupName property (which holds the version-string "v1"), but my predicate obviously fails, since the value of swaggerDoc.Info.Version is "1.0". This is how the class looks today:

public class SetBasePath : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        swaggerDoc.BasePath = $"/{context.ApiDescriptionsGroups.Items.Where(i => i.GroupName == swaggerDoc.Info.Version).Single().GroupName}";
    }
}

Then I tried another approach, using PreSerializeFilters to first add the basepath to the swagger document, and then removing it again from the paths in the document. This also got me very close, but failed since PreSerializeFiltersis not executed for each of the swagger documents, but once (and hence the last basepath specified will be used in all the generated documents). This is my code using PreSerializeFilters:

app.UseSwagger(c =>
{
    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        var basepath = $"/api/{description.GroupName}";

        c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
        {
            swaggerDoc.Host = httpReq.Host.Value;
            swaggerDoc.BasePath = basepath;
        });

        c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
        {
            IDictionary<string, PathItem> paths = new Dictionary<string, PathItem>();
            foreach (var path in swaggerDoc.Paths)
            {
                paths.Add(path.Key.Replace(basepath, string.Empty, StringComparison.InvariantCulture), path.Value);
            }
            swaggerDoc.Paths = paths;
        });
    }
});

Can anyone help me walk that last mile, and get this working the way I'd like?


Solution

  • There are a few ways you can make the ApiDescription objects match the Swagger document version. You cannot safely reverse the Swagger document version; however, since you are in control of creating it to begin with, it's not hard to match. You are likely using the example behavior, which looks like:

    new Info() { Version = description.ApiVersion.ToString() }
    

    Using the provided extension methods, you can match this in your document filter like so:

    public class SetBasePath : IDocumentFilter
    {
        public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
        {
            var docVersion = swaggerDoc.Info.Version;
            var groupName = (from descriptionGroup in context.ApiDescriptionGroups.Items
                             from description in descriptionGroup.Items
                             let apiVersion = description.GetApiVersion().ToString()
                             where apiVersion == docVersion
                             select descriptionGroup.GroupName).First();
    
            swaggerDoc.BasePath = "/api/" + groupName;
        }
    }
    

    I hope that helps.