fingers10
fingers10

Reputation: 7987

Grouping and Versioning not working well together in swagger in asp.net core 3.1 web api

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?

Upvotes: 5

Views: 5332

Answers (3)

builder.Services.AddApiVersioning( options =>
{
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion( 1, 0 );
} )
.AddMvc()
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
    options.FormatGroupName = ( group, version ) => $"{group} - {version}";
} );

try this one. "FormatGroupName" is the trick you need

Upvotes: 0

Chris Martinez
Chris Martinez

Reputation: 4438

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.

Upvotes: 1

fingers10
fingers10

Reputation: 7987

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.

Upvotes: 7

Related Questions