.NET 9 brings a new library for documenting your .NET web APIs.
If you’ve built APIs with .NET, you’ve likely heard of Swagger. Swagger has been bundled as part of the project templates for .NET web apps for a number of years.
It enables you to document your API by automatically generated a spec for it. This spec outlines the available endpoints, and the shape of requests and responses for those endpoints.
You can use tools like Swagger UI to get a dynamic view of that spec (which you can use to explore and test the API).
But spin up a new .NET 9 web project and you’ll find all those references to Swagger have disappeared, replaced by mysterious calls to “OpenAPI.”
So what is OpenAPI, and how are you supposed to generate those handy Swagger specs now?
“Swagger” has become synonymous with “OpenAPI” (ever since Swagger was donated to the OpenAPI initiative in 2015), but the two terms are worth separating.
OpenAPI refers to the specification (that JSON file we saw earlier).
Swagger refers to the family of open-source and commercial products from SmartBear that can interact with an OpenAPI spec.
In .NET projects (.NET 8 and earlier), Swagger is used by default, for a couple of purposes.
As of .NET 9, Microsoft has bundled its own tool for generating the OpenAPI spec. Spin up a new .NET 9 project, and you’ll find the following already in place.
But if you’re adding this to an existing project you’ll need to perform a couple of steps.
First, install the new Microsoft OpenAPI package:
dotnet add package Microsoft.AspNetCore.OpenApi
Second, configure your app by adding two lines in your Program.cs file:
// add this
builder.Services.AddOpenApi();
var app = builder.Build();
// and this
app.MapOpenApi();
Run this app now, navigate to /openapi/v1.json
and you’ll find yourself staring at a JSON OpenAPI spec.
But what if you want to use that handy UI that Swagger provides for interacting with your spec?
That’s still possible.
The generated spec conforms to the OpenAPI standard, so we can use familiar tools (Swagger UI, Redoc, etc.) to explore that spec.
For Swagger UI, it’s a case of installing the Swagger UI package.
dotnet add package Swashbuckle.AspNetCore.SwaggerUi
Then configuring it via Program.cs:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "v1");
});
}
Now if you navigate to /swagger
in your app you’ll see the standard Swagger UI driven from your OpenAPI spec (which was generated by the MS OpenAPI package).
But what if you need to customize the output of that spec, for example by tweaking how endpoints are named and/or how data models are represented?
It pays to know how some classic Swagger options translate to the new MS library.
The default settings will get you so far, but occasionally you might need to tweak the output of your OpenAPI spec.
Take this example of a command
for placing orders.
public record CreateOrder
{
public record Command
{
public string UserId { get; set; }
public string ProductId { get; set; }
public class DeliveryDetails
{
public string Address { get; set; }
public string ContactNumber { get; set; }
}
}
public record Response
{
public string OrderId { get; set; }
}
}
Here we’ve opted to nest classes to represent the structure of a command.
The Command
class represents the data we need to create an order, so it’s nested within the CreateOrder
class.
We can expose this via a .NET endpoint to create an order.
private static IResult CreateOrder(HttpContext context, CreateOrder.Command createOrderCommand)
{
Console.WriteLine("Creating order");
Console.WriteLine(createOrderCommand);
return Results.Ok();
}
This works, but there’s a subtle issue in the generated OpenAPI spec.
In the spec the Command
model will be represented like this:
{
"components": {
"schemas": {
"Command": {
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"productId": {
"type": "string"
}
}
}
}
}
}
Note how the generated schema uses the name “Command” for this object.
Now let’s say we go ahead and create a second command, using a similar approach.
public record CancelOrder
{
public record Command
{
public string OrderId { get; set; }
public string Reason { get; set; }
}
}
This second command in the resulting spec is given a name (Command2
) to differentiate it from the first:
{
"components": {
"schemas": {
"Command": {
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"productId": {
"type": "string"
}
}
},
"Command2": {
"type": "object",
"properties": {
"orderId": {
"type": "string"
},
"reason": {
"type": "string"
}
}
}
}
}
}
This may not be what you want. Especially if you go on to use a tool that can generate client code from this OpenAPI Spec.
You may end up with name collisions (two models sharing the same name) and/or opaque names (like Command, Command2) which don’t mean much when you’re trying to interact with your API using the generated client.
Let’s say you’d prefer to have models like CreateOrderCommand
and CancelOrderCommand
instead of Command
and Command2
.
You can configure this in Swagger using the CustomSchemaIds
method, which can tweak how the schema Ids are generated.
services.AddSwaggerGen(c =>
{
c.CustomSchemaIds(type =>
{
if (type.IsNested)
{
// Combine declaring type name with nested type name
return $"{type.DeclaringType.Name}{type.Name}";
}
return type.Name;
});
});
Where a class is nested, the name will now be a combination of the “containing” class and the nested class’s names.
But there is no CustomSchemaIds
option in the new MS OpenApi library.
Instead, the library exposes a lower-level mechanism for transforming your schema as it’s being generated.
A schema in OpenAPI represents the structure of the data on which our API depends. In this case, we will end up with a schema for each of our request and response types.
To change how that schema is generated, we can define a schema transformer using this syntax:
Task Transformer(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken arg3)
{
return Task.CompletedTask;
}
And register it when we configure it when we add the OpenAPI
service:
builder.Services.AddOpenApi(options =>
{
options.AddSchemaTransformer(Transformer);
});
This will be invoked for every schema the MS library attempts to generate.
But we only want to modify how this works for nested classes, so let’s include a check for that.
Task Transformer(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken arg3)
{
var type = context.JsonTypeInfo.Type;
if (!type.IsNested)
return Task.CompletedTask;
// custom id logic will go here
return Task.CompletedTask;
}
Here we get the type information using context.JsonTypeInfo
, then check if the class is nested. If not, we’ll return early.
Otherwise, if the type is nested, we can apply our custom logic.
Now this is where, at the time of writing, we have to write a little bit of “lower level” code than the Swagger alternative.
const string schemaId = "x-schema-id";
schema.Annotations[schemaId] = $"{type.DeclaringType?.Name}{type.Name}";
OpenAPI uses annotations to control how the schema is ultimately represented in the generated OpenAPI spec.
In this case we want to change the id, which means tweaking the annotation with id x-schema-id
.
schema.Annotations
is a dictionary so we go ahead and set a value for the x-schema-id
key to the new type name we want to use (a concatenation of the parent and nested class names).
Here’s the complete schema transformer:
Task Transformer(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken arg3)
{
var type = context.JsonTypeInfo.Type;
if (!type.IsNested)
return Task.CompletedTask;
const string schemaId = "x-schema-id";
schema.Annotations[schemaId] = $"{type.DeclaringType?.Name}{type.Name}";
return Task.CompletedTask;
}
In some cases you don’t need to resort to setting annotations like this.
For example, you can add titles and descriptions to the generated schema using the relevant properties directly, like this:
schema.Title = $"{type.DeclaringType?.Name}{type.Name}";
schema.Description = $"Request/Response for {type.DeclaringType?.Name}{type.Name}";
Which will include this in the generated spec:
"components": {
"schemas": {
"CancelOrderCommand": {
"title": "CancelOrderCommand",
"type": "object",
"properties": {
"orderId": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"description": "Request/Response for CancelOrderCommand"
}
}
}
It’s early days for this library, and you may run into incompatibilities or behavior that is inconsistent with how Swagger generates OpenAPI documents.
If you find something isn’t working as you expect, check out the GitHub issues for ASP.NET.
For example, here’s a bug report for an issue where duplicate schema Ids are being generated (for the same object): https://github.com/dotnet/aspnetcore/issues/58968. In this case, the issue looks set to be fixed in service release 9.0.2.
We’ve touched on schema transformers, but there are other transformers you can use to modify your OpenAPI documents.
Document transformers enable you to make global modifications to the entire OpenAPI document.
Operation transformers apply to each individual operation (for example, our create order endpoint).
You can dig further into these, and other ways to customize the OpenAPI document via the official docs here: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/customize-openapi?view=aspnetcore-9.0.
If you create a new .NET 9 web app you’ll find it’s using Microsoft.AspNetCore.OpenApi
instead of Swagger for generating OpenAPI specs for your APIs.
For existing projects, and in the case that you’re using Swagger without custom configuration, you can typically switch to Microsoft.AspNetCore.OpenApi
as a direct replacement. If you have custom configuration, you will need to modify that to work with the new library.
For many operations, a custom schema transformer is the answer, enabling you to modify the generated schema definitions at the point the OpenAPI spec is being generated.
Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.