How to use API versioning and OpenAPI (Swagger) for your own APIs.
Umbraco ships with Swagger to document the Content Delivery API. Swagger and the Swagger UI is available at {yourdomain}/umbraco/swagger. For security reasons, both are disabled in production environments.
Due to the way OpenAPI works within ASP.NET Core, we have to apply some configurations in a global scope. If your Umbraco site used Swagger previous to Umbraco 12, these global configurations may interfere with your setup.
In this article we'll explore concrete solutions to overcome challenges with the global configurations.
If you have been using NSwag previous to Umbraco 12, chances are your Swagger setup will continue to work in Umbraco 12+ without any changes. Swashbuckle.AspNetCore and NSwag can coexist within the same site, as long as there are no conflicting routes between the two.
That being said, it would be sensible to consider migrating your API documentation to Swashbuckle.AspNetCore. This way you can avoid having multiple dependencies that perform the same tasks.
API versioning
The Umbraco APIs rely on having the requested API version as part of the URL. If you prefer a different versioning for your own APIs, you can setup alternatives while still preserving the functionality of the Umbraco API.
The following code sample illustrates how you can use a custom header to pass the requested API version to your own APIs.
MyConfigureApiVersioningOptions.cs
usingAsp.Versioning;usingMicrosoft.Extensions.Options;namespaceMy.Custom.Swagger;publicclassMyConfigureApiVersioningOptions:IConfigureOptions<ApiVersioningOptions>{publicvoidConfigure(ApiVersioningOptions options)=>options.ApiVersionReader=ApiVersionReader.Combine( // the URL segment version reader is required for the Umbraco APIsnewUrlSegmentApiVersionReader(), // here you can add additional version readers to suit your needsnewHeaderApiVersionReader("my-api-version"));}publicstaticclassMyConfigureApiVersioningUmbracoBuilderExtensions{ // call this from Program.cs, i.e.: // builder.CreateUmbracoBuilder() // ... // .ConfigureMyApiVersioning() // .Build();publicstaticIUmbracoBuilderConfigureMyApiVersioning(thisIUmbracoBuilder builder) {builder.Services.ConfigureOptions<MyConfigureApiVersioningOptions>();return builder; }}
Swagger route and/or availability
As mentioned in the beginning of this article, Umbraco exposes Swagger and the Swagger UI at {yourdomain}/umbraco/swagger. Both are disabled when the site is in production mode.
The code sample below shows how to change the Swagger route and availability.
MySwaggerRouteTemplatePipelineFilter.cs
usingUmbraco.Cms.Api.Common.OpenApi;usingUmbraco.Cms.Web.Common.ApplicationBuilder;namespaceMy.Custom.Swagger;publicclassMySwaggerRouteTemplatePipelineFilter:SwaggerRouteTemplatePipelineFilter{publicMySwaggerRouteTemplatePipelineFilter(string name) : base(name) { } /// <summary> /// This is how you change the route template for the Swagger docs. /// </summary>protectedoverridestringSwaggerRouteTemplate(IApplicationBuilder applicationBuilder) =>"swagger/{documentName}/swagger.json"; /// <summary> /// This is how you change the route for the Swagger UI. /// </summary>protectedoverridestringSwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder) =>"swagger"; /// <summary> /// This is how you configure Swagger to be available always. /// Please note that this is NOT recommended. /// </summary>protectedoverrideboolSwaggerIsEnabled(IApplicationBuilder applicationBuilder) =>true;}publicstaticclassMyConfigureSwaggerRouteUmbracoBuilderExtensions{ // call this from Program.cs, i.e.: // CreateUmbracoBuilder() // ... // .ConfigureMySwaggerRoute() // .Build();publicstaticIUmbracoBuilderConfigureMySwaggerRoute(thisIUmbracoBuilder builder) {builder.Services.Configure<UmbracoPipelineOptions>(options => { // include this line if you do NOT want the Swagger docs at /umbraco/swaggeroptions.PipelineFilters.RemoveAll(filter => filter isSwaggerRouteTemplatePipelineFilter); // setup your own Swagger routesoptions.AddFilter(newMySwaggerRouteTemplatePipelineFilter("MyApi")); });return builder; }}
Adding custom operation IDs
Custom operation IDs can be a great way to make your API easier to use. Especially for consumers that generate API contracts from your Swagger documents.
The Umbraco APIs use custom operation IDs for that exact reason. In order to remain as un-intrusive as possible, these custom operation IDs are not applied to your APIs.
If you want to apply custom operation IDs to your APIs, you must ensure that the Umbraco APIs retain their custom operation IDs. The following code sample illustrates how this can be done.
MyOperationIdSelector.cs
usingAsp.Versioning;usingMicrosoft.AspNetCore.Mvc.ApiExplorer;usingMicrosoft.AspNetCore.Mvc.Controllers;usingUmbraco.Cms.Api.Common.OpenApi;namespaceMy.Custom.Swagger;publicclassMyOperationIdSelector:OperationIdSelector{publicoverridestring?OperationId(ApiDescription apiDescription,ApiVersioningOptions apiVersioningOptions) { // use this if you want to opt into the default Umbraco operation IDs: // return UmbracoOperationId(apiDescription, apiVersioningOptions); // only handle your own APIs here - make sure to let the base class handle the Umbraco APIsif (apiDescription.ActionDescriptorisnotControllerActionDescriptor controllerActionDescriptor||controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("My.Custom.Api") isfalse) {return base.OperationId(apiDescription, apiVersioningOptions); } // build your own logic to generate operation IDs herereturnapiDescription.RelativePathisnull?null:string.Join(string.Empty,apiDescription.RelativePath.Split(new[] { '/','-' }).Select(segment =>segment.ToFirstUpperInvariant())); }}publicstaticclassMyOperationIdUmbracoBuilderExtensions{publicstaticIUmbracoBuilderConfigureMyOperationId(thisIUmbracoBuilder builder) { // call this from Program.cs, i.e.: // CreateUmbracoBuilder() // ... // .ConfigureMyOperationId() // .Build();builder.Services.AddSingleton<IOperationIdSelector,MyOperationIdSelector>();return builder; }}
Adding custom schema IDs
Custom schema IDs can also make it easier for your API consumers to understand and work with your APIs. To that same end, Umbraco applies custom schema IDs to the Umbraco APIs - but not to your APIs.
If you want to create custom schema IDs for your APIs, you must ensure that the Umbraco APIs retain their custom schema IDs. The following code sample illustrates how that can be done.
MySchemaIdSelector.cs
usingUmbraco.Cms.Api.Common.OpenApi;namespaceMy.Custom.Swagger;publicclassMySchemaIdSelector:SchemaIdSelector{publicoverridestringSchemaId(Type type) { // use this if you want to opt into the default Umbraco schema IDs: // return UmbracoSchemaId(type); // only handle your own types here - make sure to let the base class handle the Umbraco typesif (type.Namespace?.StartsWith("My.Custom.Api") isfalse) {return base.SchemaId(type); } // build your own logic to generate schema IDs herereturnstring.Join(string.Empty,type.FullName!.Replace("My.Custom.Api",string.Empty).Split('.').Reverse()); }}publicstaticclassMySchemaIdUmbracoBuilderExtensions{publicstaticIUmbracoBuilderConfigureMySchemaId(thisIUmbracoBuilder builder) { // call this from Program.cs, i.e.: // builder.CreateUmbracoBuilder() // ... // .ConfigureMySchemaId() // .Build();builder.Services.AddSingleton<ISchemaIdSelector,MySchemaIdSelector>();return builder; }}
Adding your own Swagger documents
Umbraco automatically adds a "default" Swagger document to contain all APIs that are not explicitly mapped to a named Swagger document. This means that your custom APIs will automatically appear in the "default" Swagger document.
If you want to exercise more control over where your APIs show up in Swagger, you can do so by adding your own Swagger documents.
Umbraco imposes no limitations on adding Swagger documents, and the code below is a simplistic example.
A common use case for this is when you maintain multiple versions of the same API. Often you want to have separate Swagger documents for each version. The following code sample creates two Swagger documents - "My API v1" and "My API v2".
MyConfigureSwaggerGenOptions.cs
usingMicrosoft.Extensions.Options;usingMicrosoft.OpenApi.Models;usingSwashbuckle.AspNetCore.SwaggerGen;namespaceMy.Custom.Swagger;publicclassMyConfigureSwaggerGenOptions:IConfigureOptions<SwaggerGenOptions>{publicvoidConfigure(SwaggerGenOptions options) {options.SwaggerDoc("my-api-v1",newOpenApiInfo { Title ="My API v1", Version ="1.0", });options.SwaggerDoc("my-api-v2",newOpenApiInfo { Title ="My API v2", Version ="2.0", }); }}publicstaticclassMyConfigureSwaggerGenUmbracoBuilderExtensions{publicstaticIUmbracoBuilderConfigureMySwaggerGen(thisIUmbracoBuilder builder) { // call this from Program.cs, i.e.: // builder.CreateUmbracoBuilder() // ... // .ConfigureMySwaggerGen() // .Build();builder.Services.ConfigureOptions<MyConfigureSwaggerGenOptions>();return builder; }}
With these Swagger documents in place, you can now assign the different versions of your API controllers to their respective documents using the MapToApi annotation.
MyApiController.cs
usingAsp.Versioning;usingMicrosoft.AspNetCore.Mvc;usingUmbraco.Cms.Api.Common.Attributes;namespaceMy.Custom.Api.V1;[Route("api/v{version:apiVersion}/my")][ApiController][ApiVersion("1.0")][MapToApi("my-api-v1")]publicclassMyApiController:Controller{ [HttpGet] [Route("do-something")] [ProducesResponseType(typeof(MyDoSomethingViewModel),StatusCodes.Status200OK)]publicIActionResultDoSomething(string value)=>Ok(newMyDoSomethingViewModel(value));}publicclassMyDoSomethingViewModel{publicMyDoSomethingViewModel(string value)=> Value = value;publicstring Value { get; }}