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.
- 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.