DEV Community

Laura Neto
Laura Neto

Posted on

Umbraco 18 and OpenAPI: a heads-up for extension developers

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:

  1. Migrate to Microsoft OpenAPI. Two options:
  2. Keep using Swashbuckle (not recommended). Install Swashbuckle.AspNetCore in your own extension, wire it up, then plug your document into Umbraco's Swagger UI via AddOpenApiDocumentToUi(...). When rewriting isn't an option right now.

ℹ️ Note: the OpenAPI spec URL also changed from /umbraco/swagger/{documentName}/swagger.json to /umbraco/openapi/{documentName}.json. If you generate a client from the spec (e.g. via the v17 extension template's package.json generate-client script), 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 (or AddOpenApi directly) and build, you'll hit error CS9137: The 'interceptors' feature is not enabled in this namespace. The Microsoft.AspNetCore.OpenApi source 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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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>()));
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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;
                })));
    }
}
Enter fullscreen mode Exit fullscreen mode

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() (from Umbraco.Cms.Api.Management.OpenApi) instead of subclassing BackOfficeSecurityRequirementsOperationFilterBase.
  • Swagger UI registration: documents are no longer auto-listed. Call services.AddOpenApiDocumentToUi(documentName, uiTitle) to surface yours in the dropdown.
  • InterceptorsNamespaces opt-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...
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
                },
            });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)