Life After NSwag
Buried in the FastEndpoints package props is a comment marking NSwag for deprecation - and a whole second OpenAPI stack built on Microsoft.AspNetCore.OpenApi already ships beside it. Transformers instead of processors, AOT export via MSBuild tricks, and a golden-master test suite. Part 8 of FastEndpoints — The Missing Chapters.
The parent series' Swagger part was called Teaching NSwag What Your Endpoints Mean, and everything in it remains true. It just stopped being the whole truth. Sitting next to Src/Swagger in the repository is Src/OpenApi - a complete, independent, second implementation of document generation, built not on NSwag but on Microsoft.AspNetCore.OpenApi, the document generator that ships in the platform since .NET 9. And in Directory.Packages.props, the NSwag package references carry a one-line eulogy: "deprecate at next FE major version jump."
So this part is about a migration in progress - what the successor stack looks like, what it does differently, and the genuinely clever way it solves the one problem that made the old stack impossible under Native AOT.
Same knowledge, new host
The entry point will feel familiar:
bld.Services
.AddFastEndpoints()
.OpenApiDocument(o =>
{
o.DocumentName = "v1";
o.Title = "Orders API";
o.MaxEndpointVersion = 1;
});
var app = bld.Build();
app.UseFastEndpoints();
app.MapOpenApi(); // the built-in ASP.NET Core endpoint, not a package route
app.Run();
Two differences from SwaggerDocument() matter immediately. The serving side is the platform's own MapOpenApi() - FastEndpoints no longer owns the pipeline that renders the document, only the knowledge poured into it. And the package targets net10.0 only, with IsAotCompatible set - it is the forward-looking stack by construction, while the NSwag package (which multi-targets back to net8.0 and no-ops under AOT) is the compatibility one.
The architectural translation is almost mechanical. NSwag extends via IOperationProcessor/IDocumentProcessor; Microsoft's generator extends via IOpenApiOperationTransformer, IOpenApiDocumentTransformer and IOpenApiSchemaTransformer. OpenApiDocument() registers one operation transformer, one document transformer, and a stack of small schema transformers (XML docs, enum handling, [ToHeader] stripping, hidden properties, unique-items, numeric cleanup, optional polymorphism). Everything the parent series said NSwag had to be taught - which DTO properties are route parameters, which validator rules are schema constraints, which versions belong in which document - had to be taught again to a new student.
The operation transformer, re-taught
The heart is OperationTransformer, and its first move explains the whole integration: it pulls the EndpointDefinition out of context.Description.ActionDescriptor.EndpointMetadata. The definition object that rides every route as metadata is, once again, the single source of truth - the same seam serving its fourth consumer in this series.
From there, per operation: the endpoint's Summary class supplies summary/description/response examples, falling back to XML doc comments on the endpoint type; [Obsolete] becomes deprecated; GET and HEAD request DTOs are decomposed into query parameters and their request body removed; [FromHeader], [FromCookie], [FromClaim] and [HasPermission] properties are lifted out of the body schema into parameters - with reserved names like Authorization and Content-Type stripped, because a parameter row for those helps no one. Non-FastEndpoints endpoints in the same app are either passed through or excluded wholesale (ExcludeNonFastEndpoints).
Two touches show how current the stack is: idempotency headers and even the x402 payment headers are first-class operation metadata. And FluentValidation still projects into schema constraints - NotEmpty to required, Length to minLength/maxLength, Matches to pattern, comparisons to minimum/maximum - via a rule catalog whose header politely credits the zymlabs/nswag-fluentvalidation project it descends from. The validator is instantiated through DI (fresh scope, so scoped dependencies work) and its rules cached per validator type, including Include()d child validators resolved recursively.
The subtlest machinery is what I'd call schema localization. Component schemas are shared; FastEndpoints frequently needs to mutate a schema for one operation only - remove a route-bound property here, a claim-bound one there. The document-level SharedContext tracks per-operation schema variants, deduplicates identical ones, and collapses them back into shared components when the differences wash out. That is bookkeeping NSwag's model made implicit, done explicitly - and a finally block resets the per-document state, because the transformer instances live across document regenerations.
Versioning survives too: MaxEndpointVersion/MinEndpointVersion and ReleaseVersion produce doc-per-release grouping exactly as the parent versioning part described for NSwag - and the DocumentVersionFilter throws if you combine the two strategies, rather than guessing which you meant.
The AOT export trick
Here is the problem the old stack couldn't solve: document generation is reflection-heavy, so it cannot run inside a Native AOT binary - but an AOT-deployed API still wants to ship its own OpenAPI JSON. The FastEndpoints.OpenApi answer lives not in C# but in an MSBuild .targets file that NuGet installs into your project, and it is worth spelling out because I have not seen the pattern elsewhere:
- A target that runs before compilation writes a generated
[ModuleInitializer]into your project, carrying the MSBuild-configured export path into runtime code. - A second target, hooked before the IL trimmer runs on
PublishAotbuilds, quietly performs a nested JIT build of your same project (PublishAot=false), then executes it with--export-openapi-docs true. - In that mode,
ExportOpenApiDocsAndExitAsyncboots the app, resolves the platform'sIOpenApiDocumentProviderfor each document, serializes the JSON towwwroot/openapi/, and callsEnvironment.Exit- so the “run” is really a build step. - The emitted files are copied into the AOT publish output as static assets.
Your reflection-free production binary serves a document that was generated by a reflection-capable twin of itself, at build time. It is the same philosophy as the source generators - move the dynamic work to build time - implemented with build orchestration instead of Roslyn.
Reading the test suite as documentation
How do you trust a from-scratch reimplementation of a thousand lines of translation logic? The repo's answer: golden masters. The Int.OpenApi integration tests boot the shared harness, fetch each document over HTTP from MapOpenApi(), and compare the JSON against checked-in snapshot files - one per release document, one per versioning strategy - with a single _updateSnapshots flag to regenerate them deliberately. Any accidental drift in the emitted spec fails CI as a diff you can read. For a document generator, whose entire job is a large deterministic output, snapshot testing is exactly the right tool, and it doubles as the best documentation of the stack's actual behaviour: the expected outputs are sitting in the repo.
What I'd do with this today
New project on .NET 10: start on OpenApiDocument() - it is where the maintainers' one-way door points, it is AOT-ready, and the platform document pipeline gets Microsoft's ongoing investment. Existing projects with NSwag customizations: no urgency, but inventory your IOperationProcessor usage now, because each will need a transformer equivalent, and the extension seam - ConfigureOpenApi(o => ...) exposes the raw platform options - is where they'll land.
One gap remains, and it is deliberate: this package generates documents, not clients. The four-way story of turning those documents into typed C#, TypeScript, and Kiota clients - including the version-pin drama the migration caused - is next week's chapter.