Dmytro Shved

Deep dive into OpenApi in ASP.NET Core

ยท Dmytro Shved

Have you ever looked at beautiful Scalar UI and being wondering: “How does it works?”, “How does the ASP.NET Core constructs this UI based on my endpoints?”, “What OpenAPI is?” and “Why should I use OpenAPI?”

By the end of this post, youโ€™ll feel more confident about documenting your API.

Well, lets dive in!


Table of Contents

  1. What OpenAPI is?
  2. What does it brings to the table?
  3. How OpenAPI was integrated in ASP.NET Core?
  4. OpenAPI structure and naming conventions
  5. Customize the OpenAPI document
  6. Use Document transformers
  7. Use Operation transformers
  8. Use Schema transformers
  9. Generate the UI using Scalar

What OpenAPI is?

Lets clarify one simple question - “What OpenAPI is?”. OpenAPI is just a way to document HTTP APIs so it can be understood by humans as well as computers, to be readable by both humans and computers we need a proper format - OpenAPI creators decided to use JSON or YAML since those formats are well-known and widely used.


What does it brings to the table?

OpenAPI is a standart, it can be compared to another standarts like Git for source control or HTTP protocol for web, it’s just a way to standardize an API documentation format so potentially every human and computer will understand it reducing issues with compatibility (imagine if there was 10 various API specification standarts - which one to use? Will this library for creating UI work with that specification? What if I need to swith to another library? Will it support my current document?).


How OpenAPI was integrated in ASP.NET Core?

There is three key aspects for integrating OpenAPI specification in ASP.NET Core:

To complete 1 and 2 aspects Microsoft introduced Microsoft.AspNetCore.OpenApi package, the last one was delegated to the third-party libraries like Scalar.

Let’s see a practical example to understand it more deeply, look at this simple Program.cs file:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi(); // ๐Ÿ‘ˆ๏ธ
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9    app.MapOpenApi(); // ๐Ÿ‘ˆ๏ธ
10}
11
12app.UseHttpsRedirection();
13
14app.MapGet("/posts", () => "get posts");
15
16app.Run();

Meaning:

builder.Services.AddOpenApi() - registers services required for OpenAPI document generation into the application’s DI container.

app.MapOpenApi(); - adds an endpoint into the application for viewing the OpenAPI document serialized into JSON (restricted to the Development environment unavailable in production).

If you run the application

dotnet run --launch-profile https

and open the https://localhost:5001/openapi/v1.json

(In this case I’m using port 5001, change the port if needed)

You’ll see this output in JSON format:

 1{
 2  "openapi": "3.1.1",
 3  "info": {
 4    "title": "demo | v1",
 5    "version": "1.0.0"
 6  },
 7  "servers": [
 8    {
 9      "url": "https://localhost:5001/"
10    }
11  ],
12  "paths": {
13    "/posts": {
14      "get": {
15        "tags": [
16          "demo"
17        ],
18        "responses": {
19          "200": {
20            "description": "OK",
21            "content": {
22              "text/plain": {
23                "schema": {
24                  "type": "string"
25                }
26              }
27            }
28          }
29        }
30      }
31    }
32  },
33  "tags": [
34    {
35      "name": "demo"
36    }
37  ]
38}

This is an OpenAPI documentation, third-party libraries like Scalar are utilizing it to render the UI, for the sake of simplicity it’s contains 1 GET /posts endpoint that returns "get posts".


OpenAPI structure and naming conventions

You can customize every section in the OpenAPI documentation as you prefer, however, before customizing the document, Iโ€™ll first give you a clear and deep explanation of every section inside the OpenAPI specification and what each one is responsible for.

OpenAPI version

1"openapi": "3.1.1"

The first key-value pair describes the version of the specification that is being used. It’s important to know, that by the time I’m typing this text the latest version of OpenAPI specification is v3.2.0.

The fact is that the existence of version v3.2.0+ does not guarantee that tools such as ASP.NET Core, libraries, Scalar, Swagger etc. are already supporting it. The point is that a standard like OpenAPI is slow-moving, and changes to the specification and its structure lead to inevitable breaking changes, which break libraries and UI generators, so don’t be surprised if a library, tool or some tutorial “doesn’t keep up” with specification versions.

For more details about available OpenAPI Specification versions see docs

OpenAPI information

1  "info": {
2    "title": "demo | v1",
3    "version": "1.0.0"
4  },

This value represents the "title" and "version" of your API documentation. The "title" shows a simple text and the "version" informs the consumer about the version of API he’s utilizisng.

OpenAPI servers

1  "servers": [
2    {
3      "url": "https://localhost:5001/"
4    }
5  ],

The "servers" key stores the URL where our API documentation is available. This URL is important as the tool for rendering the UI like Scalar will use that URL to send the requests to the endpoints, so if our documentation has 1 endpoint, and it’s structure looks like this:

1"/posts": {
2  "get": { ... }
3}

the resulting request will be sent to the

GET https://localhost:5001/posts

The main thing to know is that OpenAPI documentation is built during runtime by default. Because of this, the “servers” value is dynamic and the documentation becomes “request-aware”.

OpenAPI paths

 1"paths": {
 2    "/posts": {
 3      "get": {
 4        "tags": [
 5          "demo"
 6        ],
 7        "responses": {
 8          "200": {
 9            "description": "OK",
10            "content": {
11              "text/plain": {
12                "schema": {
13                  "type": "string"
14                }
15              }
16            }
17          }
18        }
19      }
20    }
21  },

This section describes all our API endpoints, each endpoint has a lot of values that are describing it in details: path, HTTP request method, tags and responses. Let’s analyze this section in details with examples.

Understand that OpenAPI specification organizes documentation by operations, which are grouped by paths (endpoints). Currently our application has GET https://localhost:5001/posts endpoint, let’s add POST endpoin to see the different OpenAPI documentation output:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi();
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9    app.MapOpenApi();
10}
11
12app.UseHttpsRedirection();
13app.MapGet("/posts", () => "get posts");
14+ app.MapGet("/users", () => "get users"); // ๐Ÿ‘ˆ๏ธ Add new GET endpoint
15
16app.Run();

After running the application the "paths" section will be updated, now it consits 2 different endpoints:

1  "paths": {
2    "/posts": { ... },
3+   "/users": { ... }
4  },

OpenAPI HTTP request methods

Now let’s analyze the details of the path in our OpenAPI document by taking the "/post" endpoint as an example:

1  "paths": {
2    "/posts": {
3      "get": { ... }
4    },
5  },

Each HTTP endpoint has its own method name, which determines what type of CRUD operation a particular endpoint belongs to: GET, POST, PUT, DELETE etc. If we add a new endpoint with the same path, but different method names we’ll get 2 endpoints within the same path "/posts".

Let’s add a new endpoint into our application to see that in action:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi();
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9    app.MapOpenApi();
10}
11
12app.UseHttpsRedirection();
13app.MapGet("/posts", () => "get posts");
14+ app.MapPost("/posts", () => "create post");
15- app.MapGet("/users", () => "get users");
16
17app.Run();

After the application re-run:

1  "paths": {
2    "/posts": {
3      "get": { ... },
4      "post": { ... }
5    }
6  },

Now we have 2 different endpoints within the same path "/posts", it shows us how OpenAPI groups endpoints by path with different HTTP request methods.

Learn more about HTTP request methods here

OpenAPI tags

Each path has it’s set of "tags"

1"/posts": {
2  "get": {
3    "tags": [
4      "demo"
5    ],
6    ...
7  },
8}

"tags" in OpenAPI are used to logically group endpoints within documentation. Tools for rendering UI like Scalar are using tags to group related operations into sections, such as posts, auth, etc., to make the documentation structured and more readable. We’ll see why tags are helpful when we’ll render the UI using OpenAPI document.

OpenAPI responses

Each endpoint in an OpenAPI document contains detailed information about the responses it can return:

 1"paths": {
 2  "/posts": {
 3    "get": {
 4      "tags": [
 5        "demo"
 6      ],
 7      "responses": {
 8        "200": {
 9          "description": "OK",
10          "content": {
11            "text/plain": {
12              "schema": {
13                "type": "string"
14              }
15            }
16          }
17        }
18      }
19    }
20  }
21}

Since an endpoint can return multiple HTTP status codes with different response bodies, OpenAPI represents "responses" as a collection of possible responses keyed by status code.

You should be familiar with the most common HTTP status codes:

Each status code contains details describing the response.

In order to see how OpenAPI specification documents different HTTP responses we’ll add those lines to our endpoint:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi();
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9    app.MapOpenApi();
10}
11
12app.UseHttpsRedirection();
13
14app.MapGet("/posts", () => "get posts")
15+   .Produces<string>(StatusCodes.Status200OK)
16+   .Produces<string>(StatusCodes.Status500InternalServerError);
17- app.MapPost("/posts", () => "create post");
18
19app.Run();

The resulting output will be:

 1  "paths": {
 2    "/posts": {
 3      "get": {
 4        "tags": [
 5          "demo"
 6        ],
 7        "responses": {
 8          "200": {
 9            ...
10          },
11          "500": {
12            ...
13          }
14        }
15      }
16    }
17  },

Now we see that one endpoint can return different status codes and different information. Now let’s analyze the details of the responses structure.

"description"

1"description": "OK"

A short human-readable explanation of the response. In this case, 200 OK means that the request was successfully processed.

"content"

1"content": {
2  "text/plain": {
3    "schema": {
4      "type": "string"
5    }
6  }
7}

Describes the actual response body returned by the endpoint.

The content section contains:

Let’s analyze "content" section in details:

"text/plain"

1"text/plain": { ... }

Specifies the MIME type (media type) of the response. In this example, the endpoint returns plain text. Commonly used media types are:

as you can see, we can also return the output in a JSON format.

"schema"

1"schema": {
2  "type": "string"
3}

Defines the structure and data type of the response body, in this case the response body is a primitive string value.

OpenAPI parameters

There is also a "parameters" section we didn’t see, to introduce it, define a new endpoint with the following code:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi();
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9    app.MapOpenApi();
10}
11
12app.UseHttpsRedirection();
13
14app.MapGet("/posts", () => "get posts");
15+ app.MapPut("/posts/{id}", (int id) => $"update {id} post");
16
17app.Run();

After re-running the application we’ll see that the structure of that new enpoint slightly differs from the previous endpoints.

 1  "paths": {
 2    "/posts/{id}": {
 3      "put": {
 4        "tags": [
 5          "demo"
 6        ],
 7+        "parameters": [
 8+          {
 9+            "name": "id",
10+            "in": "path",
11+            "required": true,
12+            "schema": {
13+              "pattern": "^-?(?:0|[1-9]\\d*)$",
14+              "type": [
15+                "integer",
16+                "string"
17+              ],
18+              "format": "int32"
19+            }
20+          }
21+        ],
22        "responses": {
23          "200": {
24            "description": "OK",
25            "content": {
26              "text/plain": {
27                "schema": {
28                  "type": "string"
29                }
30              }
31            }
32          }
33        }
34      }
35    }
36  },

Now we see a new section with "parameters" key. This section describes all parameters the endpoint expects from the client in order to successfully process the request.

In our example, the endpoint

PUT /posts/{id}

contains a route parameter named {id}. ASP.NET Core automatically detects this parameter from the route template and includes it in the generated OpenAPI document.

The "name" field represents the name of the parameter:

1"name": "id"

The "in" field specifies where the parameter comes from. In our case, the parameter is part of the URL path, therefore its location is "path":

1"in": "path"

The "required" field indicates whether the parameter is mandatory. Path parameters are always required because the route cannot be matched without them:

1"required": true

The "schema" section describes the expected data type and validation rules for the parameter.

1"schema": {
2  "pattern": "^-?(?:0|[1-9]\\d*)$",
3  "type": [
4    "integer",
5    "string"
6  ],
7  "format": "int32"
8}

In this example:


Customize the OpenAPI document

Now let’s see how we can customize the OpenAPI document, I’ll start from basic customization and then we’ll get into the transformers.

Use different OpenAPI document format

If you want to use the YAML format instead of JSON you’ll need to specify it in the Program.cs like this:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi();
 4
 5var app = builder.Build();
 6
 7if (app.Environment.IsDevelopment())
 8{
 9+   app.MapOpenApi("/openapi/{documentName}.yaml");
10//               Specify the URL with .yaml  ๐Ÿ‘†๏ธ
11}
12
13app.UseHttpsRedirection();
14
15app.MapGet("/posts", () => "get posts");
16
17app.Run();

Now the URL to access the document will have .yaml in the end of the URL:

https://localhost:5001/openapi/v1.yaml

As expected, the output will be in YAML format:

 1openapi: '3.1.1'
 2info:
 3  title: demo | v1
 4  version: 1.0.0
 5servers:
 6  - url: https://localhost:5001/
 7paths:
 8  /posts:
 9    get:
10      tags:
11        - demo
12      responses:
13        '200':
14          description: OK
15          content:
16            text/plain:
17              schema:
18                type: string
19tags:
20  - name: demo

Customize the OpenAPI document name

Each OpenAPI document in your application has it’s unique name

1builder.Services.AddOpenApi(); // ๐Ÿ‘ˆ๏ธ Default name is v1

The document name can be modified by passing the name as a parameter to the AddOpenApi call:

1builder.Services.AddOpenApi("custom");

Now the URL to access the resulting OpenAPI document will also change:

https://localhost:5001/openapi/custom.json

The resulting document will have those changeis in the "info" section:

1{
2  "openapi": "3.1.1",
3  "info": {
4    "title": "demo | custom", ๐Ÿ‘ˆ๏ธ Here is our value
5    "version": "1.0.0"
6  },
7  ...
8}

It’s important to understand, that the “demo” in the “title” was generated based on the project name, if you want to change it to something different you’ll need to use transformers, we’ll get to them in a minute.

Customize the OpenAPI endpoint route

You can customize the documentation URL as shown above defining the new URL path in .MapOpenApi() call:

1app.MapOpenApi("/my/documentation/openapi.json");

And believe me or not, it will work and you’ll be able to access the document.

Generate multiple OpenAPI documents

In some cases you’ll need to generate multiple OpenAPI documentations, because OpenAPI documentation will be needed for:

To generate multiple OpenAPI documents, call the .AddOpenApi() method once for each document, specifying a different document name:

1builder.Services.AddOpenApi("v1");
2builder.Services.AddOpenApi("v2");

To the different documents will have different URL’s:

https://localhost:5001/openapi/v1.json

https://localhost:5001/openapi/v2.json

Each invocation of .AddOpenApi() can specify its own set of options, so you can choose to use the same or different customizations for each OpenAPI document.

The framework uses the ShouldInclude delegate method of OpenApiOptions to determine which endpoints to include in each document.

To visualize it more clearly, consider the following code in your Program.cs file:

 1var builder = WebApplication.CreateBuilder();
 2
 3// Include "backend" endpoints
 4builder.Services.AddOpenApi("backend", options =>
 5{
 6    options.ShouldInclude = desc => desc.GroupName == "backend";
 7});
 8
 9// Include "frontend" endpoints
10builder.Services.AddOpenApi("frontend", options =>
11{
12    options.ShouldInclude = desc => desc.GroupName == "frontend";
13});
14
15var app = builder.Build();
16
17if (app.Environment.IsDevelopment())
18{
19    app.MapOpenApi();
20}
21app.UseHttpsRedirection();
22
23// Add the endpoint to the "backend" group
24app.MapGet("/backend/posts", () => "backend posts")
25    .WithGroupName("backend");
26
27// Add the endpoint to the frontend group
28app.MapGet("/frontend/posts", () => "frontend posts")
29    .WithGroupName("frontend");
30
31app.Run();

Now each OpenApi document is accessible from it’s own URL:

https://localhost:5001/openapi/backend.json

https://localhost:5001/openapi/frontend.json

Generate OpenAPI documents at build time

By default, the OpenAPI document is generated at runtime, which means a new, up-to-date document is produced every time the application starts.

In some scenarios, it’s helpful to generate the OpenAPI document during the app’s build time, for example if OpenAPI documentation needs to be:

The Microsoft.Extensions.ApiDescription.Server package enables build-time document generation.

After your project was built:

dotnet build

The place where you can find your generated document is

obj/{ProjectName}.json

Customize the OpenAPI document using transformers

Transformers provide a useful API for modifying the OpenAPI document with your customizations.

The common scenarios to use transformers:

There are 3 categories of transformers:

Document Operation Schema
Access to the entire OpenAPI document. These can be used to make global modifications to the document. Apply to each individual operation. Each individual operation is a combination of path and HTTP method. These can be used to modify parameters or responses on endpoints. Apply to each schema in the document. These can be used to modify the schema of request or response bodies, or any nested schemas.

Let’s analyze each transformers category with examples.


Transformers structure

Before creating our own transformers let’s analyze their structure. We have 3 categories of transformers, hence there are 3 interfaces for each transformer:

1class MyDocumentTransformer : IOpenApiDocumentTransformer
2{
3    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context,
4        CancellationToken cancellationToken)
5    {
6        // ...
7    }
8}
1class MyOperationTransformer : IOpenApiOperationTransformer
2{
3    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
4    {
5        // ...
6    }
7}
1class MySchemaTransformer : IOpenApiSchemaTransformer
2{
3    public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
4    {
5        // ...
6    }
7}

Transformer parameters

Let’s analyze the parameters of those transformers.

Each transformer receives three parameters that represent the current state of the OpenAPI generation pipeline.

OpenApiDocument | OpenApiOperation | OpenApiSchema

The first parameter represents the part of the OpenAPI document being modified.

Depending on the transformer type:

This parameter is mutable, meaning transformers can modify its structure (add, remove, or update properties).

OpenApiDocumentTransformerContext | OpenApiOperationTransformerContext | OpenApiSchemaTransformerContext

Each transformer also receives a context object that provides additional metadata about the current item being processed.

For example, the context may include:

etc.

CancellationToken

The CancellationToken allows the transformation process to be cancelled if the request is aborted or the application is shutting down.

Basically, these three parameters allow transformers:


Ways to register the transformers

There are 3 ways to register transformers:

Here is the visual example with Document transformer:

 1using Microsoft.AspNetCore.OpenApi;
 2using Microsoft.OpenApi;
 3
 4var builder = WebApplication.CreateBuilder();
 5
 6builder.Services.AddOpenApi(options =>
 7{
 8    // Register using a delegate
 9    options.AddDocumentTransformer((document, context, cancellationToken) => Task.CompletedTask);
10    // Register using an instance of interface
11    options.AddDocumentTransformer(new MyDocumentTransformer());
12    // Register using a DI
13    options.AddDocumentTransformer<MyDocumentTransformer>();
14});
15
16var app = builder.Build();
17
18if (app.Environment.IsDevelopment())
19{
20    app.MapOpenApi();
21}
22
23app.UseHttpsRedirection();
24
25app.MapGet("/posts", () => "get posts");
26app.Run();
27
28// Dummy Document transformer
29class MyDocumentTransformer : IOpenApiDocumentTransformer
30{
31    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
32    {
33        // ...
34
35        return Task.CompletedTask;
36    }
37}

Execution order for transformers

Transformers are always executed in the following order:

1Document transformers
2      โ†‘
3Operation transformers
4      โ†‘
5Schema transformers

This execution order is fixed and cannot be changed.

However, the order in which you register transformers matters for transformers of the same type. Transformers of the same category are executed sequentially in the exact order they were added.

For example, if you register two document transformers:

1options.AddDocumentTransformer<MyDocumentTransformer1>();
2options.AddDocumentTransformer<MyDocumentTransformer2>();

Then MyDocumentTransformer1 executes first MyDocumentTransformer2 executes second and has access to all modifications made by the first transformer. The same rule applies to Operation and Schema transformers.

Now we’ll analyze each transformer type and see how they behave through practical examples.


Open API generation pipeline

The entire process of the OpenAPI generation can be introduced like this:

 1JSON serialization
 2    โ†‘
 3Transformers execution (Schema -> Operation -> Document)
 4    โ†‘
 5Document generation
 6    โ†‘
 7Operation generation
 8    โ†‘
 9Schema generation
10    โ†‘
11ApiDescription metadata graph
12    โ†‘
13Endpoints

As you can see instead of producing the final JSON/YAML document immediately, the framework gradually builds an internal object graph that represents the entire API structure. This design allows each stage of the pipeline to modify and reuse metadata generated by previous stages.


Use Document transformers

Document transformers allow you to modify the generated OpenAPI document globally.

Unlike Operation or Schema transformers, Document transformers work with the entire OpenApiDocument object, which means they can customize top-level metadata, security requirements, servers, tags, and any other part of the final document.

1class MyDocumentTransformer : IOpenApiDocumentTransformer
2{
3    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
4    {
5        // ...
6    }
7}

Document transformers receive a context object that provides additional information about the current generation process, including:

The following example demonstrates a simple Document transformer that modifies the top-level "info" section of the generated OpenAPI document:

 1var builder = WebApplication.CreateBuilder();
 2
 3builder.Services.AddOpenApi(options =>
 4{
 5    options.AddDocumentTransformer((document, context, cancellationToken) =>
 6    {
 7        document.Info = new()
 8        {
 9            Title = "Demo API",
10            Version = "v99",
11            Description = "API for testing purposes"
12        };
13        return Task.CompletedTask;
14    });
15});
16
17var app = builder.Build();
18
19if (app.Environment.IsDevelopment())
20{
21    app.MapOpenApi();
22}
23
24app.UseHttpsRedirection();
25
26app.MapGet("/posts", () => "get posts");
27app.Run();

Since document transformers execute after all Schemas and Operations are generated, they act as the final customization layer of the OpenAPI generation pipeline.

Document transformers can also use services from ASP.NET Core’s DI IoC container.

This is useful when the generated OpenAPI document depends on the current application configuration or runtime services.

The following example demonstrates a Document transformer that uses the IAuthenticationSchemeProvider service to check whether JWT Bearer authentication is registered in the application.

If a Bearer authentication scheme exists, the transformer adds a security scheme definition to the generated OpenAPI document:

 1using Microsoft.AspNetCore.Authentication;
 2using Microsoft.AspNetCore.OpenApi;
 3using Microsoft.OpenApi;
 4
 5var builder = WebApplication.CreateBuilder();
 6
 7builder.Services.AddAuthentication().AddJwtBearer();
 8
 9builder.Services.AddOpenApi(options =>
10{
11    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
12});
13
14var app = builder.Build();
15
16if (app.Environment.IsDevelopment())
17{
18    app.MapOpenApi();
19}
20
21app.UseHttpsRedirection();
22
23app.MapGet("/posts", () => "get posts");
24
25app.Run();
26
27internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
28{
29    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
30    {
31        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
32        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
33        {
34            var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
35            {
36                ["Bearer"] = new OpenApiSecurityScheme
37                {
38                    Type = SecuritySchemeType.Http,
39                    Scheme = "bearer",
40                    In = ParameterLocation.Header,
41                    BearerFormat = "Json Web Token"
42                }
43            };
44            document.Components ??= new OpenApiComponents();
45            document.Components.SecuritySchemes = securitySchemes;
46        }
47    }
48}

After re-running the application you can see the new section in the documentation:

1"components": {
2  "securitySchemes": {
3    "Bearer": {
4      "type": "http",
5      "scheme": "bearer",
6      "bearerFormat": "Json Web Token"
7    }
8  }
9}

Let’s analyze this new section. The "components" section contains reusable objects that can be referenced across the OpenAPI document. In this example, it includes the “securitySchemes” section, which defines authentication methods supported by the API.

The "securitySchemes" object is a registry of available authentication mechanisms. Each scheme can later be applied to individual endpoints or to the entire API.

"Bearer"

This is the name of the security scheme. It acts as an identifier that can be referenced elsewhere in the OpenAPI document.

"type": "http"

This indicates that the authentication mechanism is based on HTTP authentication. In this case we’re using the HTTP Bearer authentication.

"scheme": "bearer"

Specifies the authentication scheme used within HTTP authentication. Here it defines Bearer token authentication, where the token is sent in the HTTP Authorization header

"bearerFormat"

Provides a human-readable hint about the token format. In this case, it indicates that the bearer token is expected to be a JSON Web Token (JWT).

It’s important to understand that Document transformers are unique to the document instance they’re associated with. By specifying something like builder.Services.AddOpenApi("internal", options => {...});

The "components" section contains globally reusable OpenAPI objects that can be referenced throughout the document instead of being duplicated multiple times.


Use Operation transformers

Operation transformers allow you to modify individual OpenAPI operations. You can use Operation transformers when you need to:

1class MyOperationTransformer : IOpenApiOperationTransformer
2{
3    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
4    {
5        // ...
6    }
7}

Operation transformers receive a context object containing:

The following example adds a 500 Internal Server Error response to all operations in the document:

 1builder.Services.AddOpenApi(options =>
 2{
 3    options.AddOperationTransformer((operation, context, cancellationToken) =>
 4    {
 5        operation.Responses ??= new OpenApiResponses();
 6
 7        operation.Responses.Add("500",
 8            new OpenApiResponse
 9            {
10                Description = "Internal server error"
11            });
12
13        return Task.CompletedTask;
14    });
15});

This Operation transformer will add this response to all endpoints in the document, for the sake of simplicity I specified only 1 endpoint. The OpenAPI document will look like this:

 1  // ...
 2  "paths": {
 3    "/posts": {
 4      "get": {
 5        "tags": [
 6          "demo"
 7        ],
 8        "responses": {
 9          "200": {
10          ...
11          },
12+         "500": {
13+           "description": "Internal server error"
14+         }
15        }
16      }
17    }
18  },
19  // ...

Operation transformers can also be attached to a specific endpoint instead of the entire document. The following example marks a single endpoint as deprecated in the generated OpenAPI document:

1app.MapGet("/old/posts", () => "get old posts")
2    .AddOpenApiOperationTransformer((operation, context, cancellationToken) =>
3    {
4        operation.Deprecated = true;
5        return Task.CompletedTask;
6    });

The resulting document will have a marker that this endpoint is deprecated and therefore shouldn’t be used:

 1// ...
 2    "/old/posts": {
 3      "get": {
 4        "tags": [
 5          "demo"
 6        ],
 7        "responses": {
 8          "200": {
 9            "description": "OK",
10            "content": {
11              "text/plain": {
12                "schema": {
13                  "type": "string"
14                }
15              }
16            }
17          },
18        },
19+       "deprecated": true
20      }
21    }
22// ...

This marker will be shown in the tools like Scalar for generating UI. This allows endpoint-specific customization without affecting the rest of the API endpoints.


Use Schema transformers

Schemas are the data models that are used in request and response bodies in an OpenAPI document. Schema transformers are useful when a modification:

Schema transformers have access to a context object which contains:

The following Schema transformer sets the format of decimal types to decimal instead of double:

 1using Microsoft.AspNetCore.OpenApi;
 2
 3var builder = WebApplication.CreateBuilder();
 4
 5builder.Services.AddOpenApi(options => {
 6    // Here is out Schema transformer to set the format of decimal to 'decimal'
 7    options.AddSchemaTransformer((schema, context, cancellationToken) =>
 8    {
 9        if (context.JsonTypeInfo.Type == typeof(decimal))
10        {
11            schema.Format = "decimal";
12        }
13        return Task.CompletedTask;
14    });
15});
16
17var app = builder.Build();
18
19if (app.Environment.IsDevelopment())
20{
21    app.MapOpenApi();
22}
23
24app.UseHttpsRedirection();
25
26app.MapGet("/", () => new Body { Amount = 1.1m });
27
28app.Run();
29
30//           ๐Ÿ‘‡๏ธ Body class with decimal Amount property
31public class Body {
32    public decimal Amount { get; set; }
33}

Generate the UI using Scalar

Finally, let’s see how we can generate a UI using the OpenAPI document. For the example I’ll use Scalar, as it’s the most common tool for generating a UI.

First, install the Scalar.AspNetCore package.

Then, add this app.MapScalarApiReference() line to the Program.cs file:

 1
 2using Scalar.AspNetCore;
 3
 4var builder = WebApplication.CreateBuilder();
 5
 6builder.Services.AddAuthentication().AddJwtBearer();
 7
 8builder.Services.AddOpenApi();
 9
10var app = builder.Build();
11
12if (app.Environment.IsDevelopment())
13{
14    app.MapOpenApi();
15    app.MapScalarApiReference(); // ๐Ÿ‘ˆ๏ธ Enable Scalar
16}
17
18app.UseHttpsRedirection();
19
20app.MapGet("/posts", () => "get posts");
21
22app.Run();

Now, simply open the documentation with this URL:

https://localhost:5001/scalar/

(change the port if needed)

As a result we have a beautiful UI generated using our OpenAPI document. It’s important to know that Scalar acts like a real API consumer, so be careful when you’re making some HTTP requests accessible for Scalar.


Bring me back to the top!

Well, we dove really deep this time, this entire post was describing OpenAPI specification and how ASP.NET Core works with it.

That’s it for today, thanks for reading :)

#deep dive #asp.net core #openapi

Reply to this post by email โ†ช