Search code examples
asp.net-coreswagger-uiswagger-2.0aspnetboilerplateswashbuckle

Unable to create multiple OpenApi specifications with Swashbuckle


I am building a solution with Asp.Net Boilerplate / Asp.Net Zero

I have created two OpenApi specifications (HostApiv1 and TenantApiv1) as follows in Startup.cs:

services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("HostApiv1", new Info { Title = "Host API v1", Version = "v1" });
    options.SwaggerDoc("TenantApiv1", new Info { Title = "Tenant API v1", Version = "v1" });

    options.DocInclusionPredicate((docName, description) => true);
    options.IgnoreObsoleteActions();
    options.IgnoreObsoleteProperties();
    options.OrderActionsBy((apiDesc) => $"{apiDesc.RelativePath}");
    options.DescribeAllEnumsAsStrings();
});
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint(_appConfiguration["App:HostApiSwaggerEndPoint"], "Host API v1");
    options.SwaggerEndpoint(_appConfiguration["App:TenantApiSwaggerEndPoint"], "Tenant API v1");
    //...
});

However, when I decorate my AppService classes with [ApiExplorerSettings(GroupName = "HostApiv1")], the grouping is ignored and the tag (AppService controller), along with all of its operations (actions / methods), still appear under both documents.

Any idea what is wrong, or how I can debug it?


Solution

  • Swashbuckle depends on ApiExplorer, and the use of the ApiExplorer attribute limits us to specifying only a single groupname per controller / action. ABP service proxies are generated via NSwag for angular project, and it seems that during this process a dependency is broken.

    The workaround is to create a custom Attribute for delimiting one-or-more groupnames for an appservice controller or action, and subsequently use reflection in the DocInclusionPredicateFunction option to retrieve the groupnames for an action or its containing controller.

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
    public class SwaggerDocAttribute: Attribute
    {
        public SwaggerDocAttribute(params string[] includeInDocuments)
        {
            IncludeInDocuments = includeInDocuments;
        }
    
        public string[] IncludeInDocuments { get; }
    }
    
    
    options.DocInclusionPredicate((docName, apiDesc) =>
    {
        if (!apiDesc.ActionDescriptor.IsControllerAction())
        {
            return false;
        }
    
        apiDesc.TryGetMethodInfo(out MethodInfo methodInfo);
    
        var actionDocs = methodInfo.GetCustomAttributes<SwaggerDocAttribute>()
            .SelectMany(a => a.IncludeInDocuments);
    
        var controllerDocs = methodInfo.DeclaringType.GetCustomAttributes<SwaggerDocAttribute>()
            .SelectMany(a => a.IncludeInDocuments);
    
        switch (docName)
        {
            case "HostApiv1":
                return apiDesc.GroupName == null || 
                actionDocs.Contains("HostApiv1") || 
                controllerDocs.Contains("HostApiv1");
            case "TenantApiv1":
                return apiDesc.GroupName == null ||
                actionDocs.Contains("TenantApiv1") || 
                controllerDocs.Contains("TenantApiv1");
            default:
                return true;
        }
    });
    
    

    Usage

    [DisableAuditing]
    [AbpAuthorize(AppPermissions.HostSpecific.Dashboard.Access)]
    //[ApiExplorerSettings(GroupName = "HostApiv1")] // <== Don't use this
    [SwaggerDoc("HostApiv1")] // <== Use this in stead
    public class MyDemoAppService : ZenDetectAppServiceBase, IHostDashboardAppService
    {
            //...
    }