Eli Weinstock-Herman

Filtered Swagger docs for ASP.Net Core 2.0

August 21, 2018 ▪ technical posts ▪ 3 min read

I have a set of APIs that I'm publishing with an application. Some of these API endpoints are designed to be publicly accessible, while others are internal API endpoints or specifically designed for a front-end application to access. I want to easily include the public endpoints in my API docs without publishing details on the internal ones.

This post uses the following dependencies/versions:
  • ASP.Net Core 2.0
  • Swashbuckle.AspNetCore 2.4.0 Nuget package

The goal...

The end result is that I can opt any Controller's Actions into the Swagger documentation simply by adding a single new IncludeInDocumentation attribute:

[IncludeInDocumentation]
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/events")]
public class EventsController : Controller
{
    //... all the Actions, with additional markup for expected status codes, output, etc
}

And any endpoints without the IncludeInDocumentation attribute are excluded from the generated ocumentation automatically.

Implementation

The biggest chunk of logic is the piece we add to the Startup config. The Swagger options provides a DocInclusionPredicate function to evaluate each of the Document Name and Action combinations.

Official Link: SwashBuckle: Customize the Action Selection Process

Startup.cs

services.AddSwaggerGen(o =>
{
    // ... additional swagger config ...

    // Configuring the Inclusion predicate/filter
    o.DocInclusionPredicate((docName, apiDesc) =>
    {
        // must be opted into documentation and match version (GroupName below)
        return apiDesc.ControllerAttributes()
            .OfType<IncludeInDocumentationAttribute>()
            .Any()
            &&
            apiDesc.ControllerAttributes()
                .OfType<ApiVersionAttribute>()
                .SelectMany(v => v.Versions)
                .Any(v => $"v{v.ToString()}" == docName);
    });

    // Document configs (ApiVersion GroupName becomes the Swagger DocumentName)
    var provider = services.BuildServiceProvider()
                        .GetRequiredService<IApiVersionDescriptionProvider>();

    foreach (var description in provider.ApiVersionDescriptions)
    {
        o.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
    }
});

Because I'm using the Microsoft.AspnetCore.Mvc.Versioning package, I've looped through the versions and configured a SwaggerDoc for each. As the documents are created, they run the DocInclusionPredicate against all of the API Actions to determine if they should be included. If it's decorated with an ApiVersion attribute and my custom IncludeInDocumentationAttribute, then it's included in the public docs.

The custom attribute is an empty class, without any additional markup:

IncludeInDocumentationAttribute.cs

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class IncludeInDocumentationAttribute : Attribute
{
}

And that's all it takes!

Why Opt-In instead of Opt-Out? Safety

Creating an opt-in attribute was an explicit choice, one that fits with the pit of success style of thinking.

With an opt-in attribute, the failure mode if I forget to add the attribute is that no new endpoints are added to the documentation. This could be embarassing if I had promised some new endpoints to a customer, but quickly fixed.

With an opt-out attribute, the failure mode if I forget to add the attribute is that many new endpoints are added to the public documentation. In the worst case, this case bleed information about the internal API's security mechanisms. In the slightly less worse case, it exposes API details to end users that could start trying to use an endpoint before it was stable and lead to me breaking something before I expected it to be usable.

So we opt-in and choose the safer course for customers by default.

Share:
Related Posts
TEST TEST
August 21, 2018 ▪ technical posts
banner for 'TEST TEST'