Time to upgrade your extension to Umbraco 18?
A friendly heads up, the OpenAPI generation got an overhaul.
Microsoft.AspNetCore.OpenApi is Umbraco's new library of choice for OpenAPI document generation, replacing Swashbuckle.AspNetCore. It ships with ASP.NET Core and is maintained by Microsoft alongside the framework, which means one fewer third-party dependency to worry about.
The trade-off: if your extension wired up its own OpenAPI document on v17, it won't compile against v18. The Swashbuckle types it relied on are gone.
Two paths forward:
-
Migrate to Microsoft OpenAPI. Two options:
-
Use the
AddBackOfficeOpenApiDocumentextension method: Umbraco's new helper. What the v18 template uses. Covers most extensions. -
Use Microsoft's
AddOpenApimethod directly: callMicrosoft.AspNetCore.OpenApiyourself. For full control.
-
Use the
-
Keep using Swashbuckle (not recommended). Install
Swashbuckle.AspNetCorein your own extension, wire it up, then plug your document into Umbraco's Swagger UI viaAddOpenApiDocumentToUi(...). When rewriting isn't an option right now.
ℹ️ Note: the OpenAPI spec URL also changed from
/umbraco/swagger/{documentName}/swagger.jsonto/umbraco/openapi/{documentName}.json. If you generate a client from the spec (e.g. via the v17 extension template'spackage.jsongenerate-clientscript), update the URL before regenerating.
Use the AddBackOfficeOpenApiDocument extension method
We added AddBackOfficeOpenApiDocument() in v18 to take the boilerplate out of registering an OpenAPI document. It's an extension method on IUmbracoBuilder that wires up Umbraco's sensible defaults so you don't have to.
Out of the box, the helper registers your document, scopes it to endpoints decorated with [MapToApi(documentName)], applies Umbraco's naming conventions for schemas and operation IDs, and adds the document to the Swagger UI dropdown at /umbraco/openapi/.
⚠️ Watch out: the first time you call
AddBackOfficeOpenApiDocument(orAddOpenApidirectly) and build, you'll hiterror CS9137: The 'interceptors' feature is not enabled in this namespace. TheMicrosoft.AspNetCore.OpenApisource generator (which surfaces your XML doc comments as descriptions in the OpenAPI document, among other compile-time enrichments) propagates through Umbraco's project reference, but the property that enables it ships only with the Web SDK. Class-library projects don't get it automatically, so add it to your.csproj:<PropertyGroup> <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated</InterceptorsNamespaces> </PropertyGroup>
Minimal usage
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddBackOfficeOpenApiDocument("my-api");
}
}
This will add and configure a "my-api" OpenAPI document that will display the controllers/endpoints that have the attribute [MapToApi("my-api")].
Typical usage
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddBackOfficeOpenApiDocument("my-api", document => document
.WithTitle("My API")
.WithBackOfficeAuthentication());
}
}
Besides adding a "my-api" OpenAPI document, this will set the document title to "My API" and declare that the document's operations require backoffice authentication (which is what enables the "Authorize" flow in the Swagger UI).
Builder method reference
| Method | What it does |
|---|---|
WithTitle(string) |
Sets the document's Info.Title. Also used as the UI dropdown label unless WithUiTitle is set. |
WithUiTitle(string) |
Overrides only the UI dropdown label, leaving Info.Title untouched. |
ExcludeFromUi() |
Suppresses registration in the Swagger UI document dropdown. Document is still reachable at its JSON URL. |
WithBackOfficeAuthentication() |
Registers the backoffice OAuth2 security scheme on the document and marks operations as requiring authentication. Swagger UI uses this to prompt for login; generated clients use it to send the token. |
ConfigureOpenApiOptions(Action<OpenApiOptions>) |
Adds a configuration callback for the underlying OpenApiOptions. Multiple calls will run in order after Umbraco's defaults. |
WithJsonOptions(...) |
Sets the JsonOptions used when generating the document's schema. See Aligning schema serialization below. |
Aligning schema serialization
Schema generation needs to use the same JsonOptions your API uses at runtime, otherwise the generated SDKs and the actual payloads will not match. By default, Microsoft.AspNetCore.OpenApi generates schemas using the global HTTP JsonOptions, which means whoever hosts your extension can change them and that affects your schema.
For most backoffice extensions you want the BackOffice named JsonOptions (the same ones Umbraco's Management API uses). Point the document at them with WithJsonOptions:
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddBackOfficeOpenApiDocument("my-api", document => document
.WithJsonOptions(Constants.JsonOptionsNames.BackOffice));
}
}
For this to be meaningful at runtime, your controllers also need to serialize with those same options. The v17 extension template inherits from a plain ControllerBase, which uses the default options instead. Fix that by adding [JsonOptionsName(Constants.JsonOptionsNames.BackOffice)] to your own base controller or to each controller directly.
Without it, the schema reflects backoffice serialization but the actual responses are serialized with the default options, and you're back to the schema-vs-runtime mismatch.
Going beyond the defaults
The helper is not a closed box. Anything you'd normally do on OpenApiOptions (custom operation/schema/document transformers, your own CreateSchemaReferenceId, a replacement operation ID transformer, etc.) is still available through ConfigureOpenApiOptions. It just runs after Umbraco's defaults, so you compose on top of them rather than starting from scratch.
For example, registering your own schema transformer:
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddBackOfficeOpenApiDocument("my-api", document => document
.WithTitle("My API")
.ConfigureOpenApiOptions(options =>
options.AddSchemaTransformer<MyExtensionSchemaTransformer>()));
}
}
Migrating an existing composer
If your extension followed the shape of the v17 dotnet template, the migration is: delete the old composer body and write the AddBackOfficeOpenApiDocument(...) call. Below is the same extension's composer before and after.
v17 (Swashbuckle)
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IOperationIdHandler, CustomOperationHandler>();
builder.Services.Configure<SwaggerGenOptions>(opt =>
{
opt.SwaggerDoc(Constants.ApiName, new OpenApiInfo
{
Title = "My Test Extension Backoffice API",
});
opt.OperationFilter<UmbracoExtensionOperationSecurityFilter>();
});
}
public class UmbracoExtensionOperationSecurityFilter : BackOfficeSecurityRequirementsOperationFilterBase
{
protected override string ApiName => Constants.ApiName;
}
public class CustomOperationHandler : OperationIdHandler
{
// ...
}
}
v18 (Microsoft OpenAPI + new builder)
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder) =>
builder.AddBackOfficeOpenApiDocument(
Constants.ApiName,
document => document
.WithTitle("My Test Extension Backoffice API")
.WithBackOfficeAuthentication());
}
The helper registers UmbracoOperationIdTransformer by default, so the old IOperationIdHandler is no longer needed. If you want custom operation ID logic, register your own through ConfigureOpenApiOptions:
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddBackOfficeOpenApiDocument("my-api", document => document
.ConfigureOpenApiOptions(options =>
options.AddOperationTransformer((operation, context, _) =>
{
operation.OperationId = $"{context.Description.ActionDescriptor.RouteValues["action"]}";
return Task.CompletedTask;
})));
}
}
Use Microsoft's AddOpenApi method directly
If you want to register a document that isn't a backoffice document, or you want to start from a clean slate without Umbraco's defaults, call AddOpenApi yourself. The helper is just a thin wrapper around that method. Umbraco already wires the OpenAPI routing and Swagger UI hosting, so you don't need to call MapOpenApi or configure endpoints. You only need to register your documents.
For the transformer model itself (how operation/schema/document transformers work), see Microsoft's Customize OpenAPI documents docs.
For Umbraco's specifics ([MapToApi] filtering via options.ShouldInclude, schema/operation ID conventions, the public UmbracoSchemaIdGenerator.Generate(type) helper), see the API versioning and OpenAPI docs.
A few things worth remembering:
-
Backoffice authentication: call
options.AddBackofficeSecurityRequirements()(fromUmbraco.Cms.Api.Management.OpenApi) instead of subclassingBackOfficeSecurityRequirementsOperationFilterBase. -
Swagger UI registration: documents are no longer auto-listed. Call
services.AddOpenApiDocumentToUi(documentName, uiTitle)to surface yours in the dropdown. -
InterceptorsNamespacesopt-in: same as the helper path. See the Watch out callout at the top of the helper section.
Keep using Swashbuckle
Not recommended. Consider this only if rewriting a complex Swashbuckle setup isn't feasible right now. Trade-offs:
- Extra dependency on
Swashbuckle.AspNetCore. - OpenAPI 3.0 output, whereas the rest of Umbraco's documents emit 3.1.
- Extra wiring you maintain yourself.
- Both libraries depend on
Microsoft.OpenApi. A breaking change there that Swashbuckle doesn't follow leaves your extension incompatible with the rest of the stack.
If you still want to go this route, see the Swashbuckle.AspNetCore README for general setup. The rest of this section is the Umbraco-specific wiring.
1. Install the NuGet package
Add a PackageReference to Swashbuckle.AspNetCore in your extension's .csproj (or dotnet add package Swashbuckle.AspNetCore).
2. Register Swashbuckle and your document in a composer
In v17, Umbraco called AddSwaggerGen for you and your extension only configured SwaggerGenOptions. In v18 you have to call it yourself:
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSwaggerGen(opt =>
{
opt.SwaggerDoc(Constants.ApiName, new OpenApiInfo
{
Title = "My Test Extension Backoffice API",
Version = "1.0",
});
var previousDocInclusion = opt.SwaggerGeneratorOptions.DocInclusionPredicate;
opt.DocInclusionPredicate((docName, apiDesc) =>
docName == Constants.ApiName
? apiDesc.ActionDescriptor.HasMapToApiAttribute(docName)
: previousDocInclusion?.Invoke(docName, apiDesc) ?? true);
// any operation filters, schema filters, or other v17 setup you had...
});
}
}
3. Host the Swagger JSON middleware via an IUmbracoPipelineFilter
Umbraco no longer calls app.UseSwagger(), so the middleware that serves the Swagger JSON isn't running. Add it through Umbraco's pipeline filter, and set the RouteTemplate to match UmbracoOpenApiOptions.RouteTemplate (default umbraco/openapi/{documentName}.json) so the dropdown link resolves.
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.Configure<UmbracoPipelineOptions>(options =>
{
options.AddFilter(new UmbracoPipelineFilter("Swashbuckle")
{
PostPipeline = app =>
{
var openApiOptions = app.ApplicationServices.GetRequiredService<IOptions<UmbracoOpenApiOptions>>().Value;
if (!openApiOptions.Enabled)
{
return;
}
var routeTemplate = openApiOptions.RouteTemplate;
app.UseWhen(
ctx => ctx.Request.Path == routeTemplate.Replace("{documentName}", Constants.ApiName).EnsureStartsWith('/'),
branch => branch.UseSwagger(c => c.RouteTemplate = routeTemplate));
},
});
});
}
}
Scope the middleware to your own document's URL. Otherwise Swashbuckle intercepts requests for Umbraco's own documents (Management, Delivery) and returns 404 because they aren't registered with AddSwaggerGen.
4. Register the document in the Swagger UI dropdown
Register the document in Umbraco's Swagger UI dropdown:
public class UmbracoExtensionApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddOpenApiDocumentToUi(Constants.ApiName, "My API");
}
}
TL;DR
The helper is the simplest option for most extensions. AddBackOfficeOpenApiDocument() is what the v18 template uses, gives you Umbraco's sensible defaults out of the box (naming conventions, Swagger UI registration, JsonOptions alignment), and lets you register an OpenAPI document in a single call.
Calling AddOpenApi yourself is the standard ASP.NET Core approach and works fine. It means more wiring and code to maintain, so reach for it when you need full control (non-backoffice documents, or skipping the Umbraco defaults).
If rewriting really isn't an option right now, keeping Swashbuckle as your own dependency and plugging into the dropdown via AddOpenApiDocumentToUi(...) will get you through, but plan to revisit when you can.
Further reading
- Announcements #32: the official announcement, with the full set of changes and reasoning
- Custom Backoffice API: end-to-end backoffice API setup
- API versioning and OpenAPI: schema IDs, operation IDs, route configuration, endpoint filtering
- Umbraco 18 version-specific upgrade docs: official migration reference
Top comments (0)