November 25th, 2024

OpenAPI document generation in .NET 9

Mike Kistler
Principal Program Manager

ASP.NET Core in .NET 9 streamlines the process of creating OpenAPI documents for your API endpoints with new built-in support for OpenAPI document generation. This new feature is designed to simplify the development workflow and improve the integration of OpenAPI definitions in your ASP.NET applications. And OpenAPI’s broad adoption has fostered a rich ecosystem of tools and services that can help you build, test, and document your APIs more effectively. Some examples are Swagger UI, the Kiota client library generator, and Redoc, but there are many, many more.

Why OpenAPI?

OpenAPI is a powerful tool for defining and documenting HTTP APIs. It provides a standard way to describe your API’s endpoints, request and response formats, authentication schemes, and other essential details. This standardization makes it easier for developers to understand and interact with APIs, leading to better collaboration and more robust applications. And

In addition, many large language models (LLMs) have been trained on OpenAPI documents, enabling them to generate code, test cases, and other artifacts automatically. By producing OpenAPI documents for your APIs, you can take advantage of these LLMs to accelerate your development process.

What’s New in .NET 9?

With .NET 9, we are introducing built-in support for OpenAPI document generation that provides a more integrated and seamless experience for .NET developers. This feature can be used in both Minimal APIs and controller-based applications. Here are some of the key highlights:

  • Support for generating OpenAPI documents at run time and accessing them via an endpoint on the application, or generating them at build time.
  • Attributes and extension methods for adding metadata to API methods and data.
  • Support for “transformer” APIs that allow modifying the generated document in a variety of ways.
  • Support for generating multiple OpenAPI documents from a single app.
  • Takes advantage of JSON schema support provided by System.Text.Json.
  • Is compatible with native AoT when used in conjunction with Minimal APIs.

How to get started

Getting started with the new OpenAPI document generation feature in .NET 9 is straightforward. Here’s a quick guide to help you begin.

Update to .NET 9

Ensure that your project is using .NET 9, which was released earlier this month. You can download the latest version from the official .NET website.

If you are adding OpenAPI support to an existing project, you will need to update your project to target .NET 9. There’s a detailed migration guide for this in the ASP.NET Core docs.

Enable OpenAPI support

If you are starting a new project, OpenAPI support is already built in to the .NET 9 webapi template.

To enable OpenAPI document in an existing project, you just need to add the Microsoft.AspNetCore.OpenApi package and add a few lines of code to your main application file.

You can add the package with the dotnet add package command:

dotnet add package Microsoft.AspNetCore.OpenApi

After that, in your Program.cs file, you need to add the OpenAPI services to the WebApplicationBuilder:

builder.Services.AddOpenApi();

There are various configuration options available for the OpenAPI feature, such as setting the document title, version, and other metadata. You can find more information on these options in the ASP.NET Core docs.

Then add the endpoint to your app to serve the OpenAPI document with the MapOpenApi extension method, like this:

app.MapOpenApi();

Now you can run your application and access the generated OpenAPI document at the /openapi/v1.json endpoint.

What you will see there is an OpenAPI document with paths, operations, and schemas based on the code for your application, but maybe not important details like descriptions and examples. To get these elements, you will need to add metadata as described in the next section.

Add OpenAPI metadata

Descriptions, tags, examples, and other metadata can be added to your API methods and data to give meaning to the generated OpenAPI document. This metadata can be added using attributes or extension methods.

You can add a summary and description for each endpoint in your application using the WithSummary and WithDescription extension methods:

app.MapGet("/hello", () => "Hello, World!")
    .WithSummary("Get a greeting")
    .WithDescription("This endpoint returns a friendly greeting.");

Endpoint summaries and descriptions are very important because they tell users (and LLMs) what things they can accomplish with your API.

You may also want to group related endpoints together in documentation, and this is usually done with tags. You can add tags to your endpoints using the WithTag extension method:

app.MapGet("/hello", () => "Hello, World!")
    .WithTag("Greetings");

When an endpoint has parameters, it is important to include a description on each parameter to explain its meaning and how it is used by the endpoint. Use the [Description] attribute to add a description to a parameter:

app.MapGet("/hello/{name}",
(
    [Description("The name of the person to greet.")] string name
) => $"Hello, {name}!")
    .WithSummary("Get a personalized greeting")
    .WithDescription("This endpoint returns a personalized greeting.")
    .WithTag("Greetings");

You can also use the [Description] attribute to add descriptions to properties in your data models:

public record Person
{
    [Description("The person's name.")]
    public string Name { get; init; }

    [Description("The person's age.")]
    public int Age { get; init; }
}

There are many other metadata attributes for describing parameters and properties, including [MaxLength], [Range], [RegularExpression], and [DefaultValue]. Note that in controller-based apps, these attributes trigger validations that are performed during model binding, but in Minimal APIs, they are used only for documentation.

See the Include OpenAPI metadata topic in the docs to learn more about adding metadata to your API methods and data.

Customize your documents

ASP.NET also provides a way to customize the generated OpenAPI document using “transformers”, which can operate on the entire document, on operations, or on schemas. Transformers are classes that implement the IOpenApiDocumentTransformer, IOpenApiOperationTransformer, or IOpenApiSchemaTransformer interfaces. Each of these interfaces has a single async method that receives the document, operation, or schema to be transformed along with a context object that provides additional information. The OpenAPI document, operation, or schema passed to a transformer is a strongly-typed object using the types from the Microsoft.OpenApi.Models namespace. The method performs the transformation “in place” by modifying the object it receives.

Transformers are added by the configureOptions delegate parameter of the AddOpenApi call, and can specified as an instance of a class, as a DI-activated class, or as a delegate method.

builder.Services.AddOpenApi(options =>
{
    // document transformer added as an instance of a class
    options.AddDocumentTransformer(new MyDocumentTransformer());
    // operation transformer added as a DI-activated class
    options.AddOperationTransformer<MyOperationTransformer>();
    // schema transformer added as a delegate method
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
});

One application of document transformers is to modify portions of the OpenAPI document outside the paths and components.schemas sections. For example, you could add a contact in the info element of the document like this:

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info.Contact = new OpenApiContact
        {
            Name = "Contoso Support",
            Email = "support@contoso.com"
        };
        return Task.CompletedTask;
    }
});

Operation transformers can be used to modify individual operations in the document. An operation transformer is invoked on every operation in the app, and it can choose to modify the operation or not. For example, you could add a security requirement to all operations that require authorization like this:

    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        if (context.Description.ActionDescriptor.EndpointMetadata.OfType<IAuthorizeData>().Any())
        {
            operation.Security = [new() { ["Bearer"] = [] }];
        }
        return Task.CompletedTask;
    });

Schema transformers can be used to modify the schemas for the application. Schemas describe the request or response bodies of operations. Complex properties within a request or response body may have their own schemas. Schema transformers can be used to modify any or all of these schemas.

It is important to know that all transformers, including schema transformers, are invoked before schemas are converted to “$ref” references — a process discussed in the next section.

The following example shows a simple schema transformer that sets the format field to decimal for any schema representing a C# decmial value.

    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            // default schema for decimal is just type: number.  Add format: decimal
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });

Customize schema reuse

After all transformers have been applied, the framework makes a pass over the document transferring certain schemas to the components.schemas section, replacing them with $ref references to the transferred schema. This reduces the size of the document and makes it easier to read.

The details of this processing are a bit complicated, and might change in future versions of .NET, but in general:

  • Schemas for class/record/struct types are replaced with a $ref to a schema in components.schemas if they appear more than once in the document.
  • Schemas for primitive types and standard collections are left “inline”.
  • Schemas for enum types are always replaced with a $ref to a schema in to components.schemas.

Typically the name of the schema in components.schemas is the name of the class/record/struct type, but in some circumstances a different name must be used.

ASP.NET Core lets you customize which schemas are replaced with a $ref to a schema in components.schemas using the CreateSchemaReferenceId property of OpenApiOptions. This property is a delegate that takes a JsonTypeInfo object and returns the name of the schema in components.schemas that should be used for that type. The framework provides a default implementation of this delegate, OpenApiOptions.CreateDefaultSchemaReferenceId, that uses the name of the type, but you can replace it with your own implementation.

As a simple example of this customization, you might choose to always inline enum schemas. This is done by setting CreateSchemaReferenceId to a delegate that always returns null for enum types, and otherwise returns value from the default implementation. The following code shows how to do this:

builder.Services.AddOpenApi(options =>
{
    // Always inline enum schemas
    options.CreateSchemaReferenceId = (type) => type.Type.IsEnum ? null : OpenApiOptions.CreateDefaultSchemaReferenceId(type);
});

Generating OpenAPI documents at build time

A feature that I think many .NET developers will find appealing is the option to generate the OpenAPI document at build time. Generating the OpenAPI document as part of the build process makes it much easier to integrate with tools in your local development workflow or CI pipeline. For example, you can run a linter on the generated document to ensure it meets your organization’s standards, or you can use the document to generate client code or tests.

Generating the OpenAPI document at build time is simple. Just add the Microsoft.Extensions.ApiDescription.Server package to your project. By default, the OpenAPI document is generated into the obj directory of your project, but you can customize the location of the generated document with the OpenApiDocumentsDirectory property. For example, to generate the document into the root directory of your project, add the following to your project file:

<PropertyGroup>
  <OpenApiDocumentsDirectory>./</OpenApiDocumentsDirectory>
</PropertyGroup>

Note that build-time OpenAPI document generation works by launching the application’s entrypoint with an inert server implementation. This allows the framework to incorporate metadata that is only available at runtime, but could require some changes in your application to work properly in certain build scenarios.

See the Generating OpenAPI at Build Time topic in the documentation for more information.

Conclusion

The new OpenAPI document generation feature in .NET 9 provides developers with a new path to create and maintain API documentation for their ASP.NET apps. By integrating this functionality directly into ASP.NET Core, developers can now generate OpenAPI documents either at build time or run time, customize them as needed, and ensure they stay in sync with their code. And in Minimal API apps, the feature is fully compatible with native AoT compilation.

We’d love to hear your feedback on this new feature. Please try it out and let us know what you think.

Happy coding!

Author

Mike Kistler
Principal Program Manager

9 comments

  • ChrisTorng .

    I found an error. For this:

        options.AddOperationTransformer((operation, context, cancellationToken) =>
        {
            if (context.Description.ActionDescriptor.EndpointMetadata.OfType().Any())
            {
                operation.Security = [new() { ["Bearer"] = [] }];
            }
            return Task.CompletedTask;
        });

    should be this:

        options.AddOperationTransformer((operation, context, cancellationToken) =>
        {
            if (context.Description.ActionDescriptor.EndpointMetadata.OfType().Any())
            {
                operation.Security = [new() { [new() { Scheme = "Bearer" } ] = [] }];
            }
            return Task.CompletedTask;
        });
    • Mike KistlerMicrosoft employee Author 5 days ago

      @ChrisTorng thanks for pointing this out. The code in the blog post is indeed wrong, I tried the fix you suggested and it didn't work for me. What worked for me is
      ```csharp
      var scheme = new OpenApiSecurityScheme()
      {
      Type = SecuritySchemeType.Http,
      ...

      Read more
  • Dan Colgan

    Hi everyone. This might be a "newbie" question in regards to why its happening but I upgraded a .NET 6 application to .NET 9 using the upgrade assistant; then attempted to add OpenAPI to it. Prior to adding the package I was building and running successfully. Now after adding the package I get:
    "There was no runtime pack for Microsoft.AspNetCore.App available for the specified RuntimeIdentifier 'iossimulator-x64'."
    and
    "There was no runtime pack...

    Read more
  • Luca SpolidoroMicrosoft employee

    Why endpoint summary and description are added via extension methods, while descriptions of parameters and entity’s properties is done via attributes?
    Seems a very inconsistent approach.
    I was expecting something like:

    [Summary("Get a greeting")]
    [Description("This endpoint returns a friendly greeting.")]
    app.MapGet("/hello", () => "Hello, World!");
    • Mike KistlerMicrosoft employee Author

      You can use attributes to add summary, description, and operationId to operations in Minimal APIs, but the syntax is non-obvious. You have to put the attributes on the delegate method (the second parameter to MapGet), like this:

      <code>

      The extension methods are a little cleaner and more discoverable because of intellisense, but use whichever mechanism you prefer.

      Read more
    • Michael Taylor

      is a method call. You cannot add attributes to method calls. Attributes are metadata applied to information baked at compile time such as types and type members. A function call doesn't occur until runtime so attributes cannot be applied. The syntax you gave is not supported.

      What you'd have to do instead is define a type that represents the call, such as a controller method or endpoint class. Then you can add attributes to...

      Read more
  • Mark Adamson

    It looks great, but unfortunately we will need to wait a year for .net 10 before we can adopt it because it doesn’t support extracting information from XML comments yet. I know it’s in the works, and I really hope you can get that into a version before v10.