4

I'm using Asp.Net Core 3.1 to build my API. I'm using swagger to generate document for my API. I decided to do grouping on my swagger document based on controller. So I ended up doing like this,

Startup - ConfigureServices:

options.SwaggerDoc(
    "LibraryOpenAPISpecificationCategories",
    ...

Startup - Configure:

options.SwaggerEndpoint(
    "/swagger/LibraryOpenAPISpecificationCategories/swagger.json",
    "Library API (Categories)");

Controller:

[Route("api/categories")]
[ApiController]
[ApiExplorerSettings(GroupName = "LibraryOpenAPISpecificationCategories")]
public class CategoriesController : ControllerBase

Until this point everything was working fine. When I added versioning the Swagger document stopped displaying the methods in the controller. I was trying to bring grouping inside version so that each version will have the groups like,

V1 -> LibraryOpenAPISpecificationCategories

V1 -> LibraryOpenAPISpecificationItems

V2 -> LibraryOpenAPISpecificationCategories

V2 -> LibraryOpenAPISpecificationItems

Here is what I did,

Startup - ConfigureServices:

services.AddVersionedApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VV";
});

services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
});

var apiVersionDescriptionProvider =
    services.BuildServiceProvider().GetService<IApiVersionDescriptionProvider>();

services.AddSwaggerGen(options =>
{
    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        options.SwaggerDoc(
            $"LibraryOpenAPISpecificationCategories{description.GroupName}",
            ...

Startup - Configure:

app.UseSwaggerUI(options =>
{
    foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint(
            $"/swagger/LibraryOpenAPISpecificationCategories{description.GroupName}/swagger.json",
            $"Library API (Categories) {description.GroupName.ToUpperInvariant()}");

Controller:

[Route("api/categories")]
[ApiController]
[ApiExplorerSettings(GroupName = "LibraryOpenAPISpecificationCategories")]
public class CategoriesController : ControllerBase

No error is displayed in swagger document. Please assist me on where I'm going wrong. Am I missing anything?

fingers10
  • 6,675
  • 10
  • 49
  • 87

2 Answers2

6

After some analysis, I figured out that I missed DocInclusionPredicate in AddSwaggerGen in my ConfigureServices.

Here is how I resolved,

options.DocInclusionPredicate((documentName, apiDescription) =>
{
    var actionApiVersionModel = apiDescription.ActionDescriptor
    .GetApiVersionModel(ApiVersionMapping.Explicit | ApiVersionMapping.Implicit);

    var apiExplorerSettingsAttribute = (ApiExplorerSettingsAttribute)apiDescription.ActionDescriptor.EndpointMetadata.First(x => x.GetType().Equals(typeof(ApiExplorerSettingsAttribute)));

    if (actionApiVersionModel == null)
    {
        return true;
    }

    if (actionApiVersionModel.DeclaredApiVersions.Any())
    {
        return actionApiVersionModel.DeclaredApiVersions.Any(v =>
        $"{apiExplorerSettingsAttribute.GroupName}v{v.ToString()}" == documentName);
    }
    return actionApiVersionModel.ImplementedApiVersions.Any(v =>
        $"{apiExplorerSettingsAttribute.GroupName}v{v.ToString()}" == documentName);
});

Hope this helps someone out there.

fingers10
  • 6,675
  • 10
  • 49
  • 87
  • thanks for your addition, it helped me to get a working solution with multiple swagger docs! – EricBDev Apr 27 '21 at 18:14
  • This will work, but I would recommend using a custom **IApiDescriptionProvider** to recollate the **ApiDescription** instances. The API Explorer only supports a single level of organization (e.g. `GroupName`). It logically makes sense that this should be the API version by default. Until API Versioning 5.0+, any explicit any explicit value you put in `ApiExplorerSettings.GroupName` was overwritten. While you can (now) provide your own group name, putting those all in a single OpenAPI document will depend on how you are versioning. You typically need 1 document per API version. – Chris Martinez Apr 28 '21 at 18:29
  • Ultimately, API Versioning's own **IApiDescriptionProvider** only provides to basic functions. **1.** Add a parameter descriptor for the API version parameter. **2.** Set and collate API descriptions by API version. Creating your own provider that reorganizes things at the end doesn't take much work. – Chris Martinez Apr 28 '21 at 18:31
0

Since a few people asked for this in various places, here's how you'd implement a custom IApiDescriptionProvider. It simply updates the ApiDescription.GroupName at the end of processing. This will work completely independent of Swashbuckle or any other OpenAPI/Swagger document generator:

public class CollateApiDescriptionProvider : IApiDescriptionProvider
{
    readonly IOptions<ApiExplorerOptions> options;

    public CollateApiDescriptionProvider( IOptions<ApiExplorerOptions> options ) =>
        this.options = options;

    public int Order => 0;

    public void OnProvidersExecuting( ApiDescriptionProviderContext context ) { }

    public void OnProvidersExecuted( ApiDescriptionProviderContext context )
    {
        var results = context.Results;
        var format = options.Value.GroupNameFormat;
        var text = new StringBuilder();

        for ( var i = 0; i < results.Count; i++ )
        {
            var result = results[i];
            var action = result.ActionDescriptor;
            var version = result.GetApiVersion();
            var groupName = action.GetProperty<ApiDescriptionActionData>()?.GroupName;

            text.Clear();

            // add the formatted API version according to the configuration
            text.Append( version.ToString( format, null ) )

            // if there's a group name, prepend it
            if ( !string.IsNullOrEmpty( groupName ) )
            {
                text.Insert( 0, ' ' );
                text.Insert( 0, groupName );
            }

            result.GroupName = text.ToString();
        }
    }
}

To register your new provider, add it to the service collection:

services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApiDescriptionProvider, CollateApiDescriptionProvider>() );

NOTE: this should occur after services.AddApiVersioning()

You can get as creative as you want with the group names, but be aware that you cannot create multiple levels of grouping. It simply isn't supported out-of-the-box. In most cases, you can only have a single OpenAPI/Swagger document per API version. This is because URLs must be unique within the document.

It is technically possible to group on multiple levels, but this would require a number of changes to the UI and document generation process. I've only ever seen a handful of people willing to put that much effort in. They effectively created their own UIs and documentation generation backend.

Chris Martinez
  • 3,185
  • 12
  • 28