Deep dive into OpenApi in ASP.NET Core
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
- What OpenAPI is?
- What does it brings to the table?
- How OpenAPI was integrated in ASP.NET Core?
- OpenAPI structure and naming conventions
- Customize the OpenAPI document
- Use Document transformers
- Use Operation transformers
- Use Schema transformers
- 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:
- Generating information about the endpoints in the app.
- Gathering the information into a format that matches the OpenAPI schema.
- Exposing the generated OpenAPI document through a visual UI or a serialized file.
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 httpsand 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/postsThe 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:
"200"- success"404"- resource was not found"500"- internal server error
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:
- media type (
text/plain,application/json, etc.) - schema describing the structure of the returned data
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:
application/jsontext/plainapplication/xml
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": trueThe "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:
-
"format": "int32"means the parameter should represent a 32-bit integer. -
"pattern"contains additional validation rules generated by ASP.NET Core.-
"^-?(?:0|[1-9]\\d*)$"this validation rule ensures that the parameter value represents a valid integer number.
-
-
"type"describes the underlyingJSONrepresentation of the value.
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.yamlAs 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: demoCustomize the OpenAPI document name
Each OpenAPI document in your application has it’s unique name
1builder.Services.AddOpenApi(); // ๐๏ธ Default name is v1The 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.jsonThe 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:
- Audiences, such as public and internal APIs.
- Versions of an API.
- Parts of an app, such as a frontend and backend API.
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.jsonEach 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.jsonGenerate 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:
- Committed into source control.
- Used for spec-based integration testing.
- Served statically from the web server.
The Microsoft.Extensions.ApiDescription.Server package enables build-time document generation.
After your project was built:
dotnet buildThe place where you can find your generated document is
obj/{ProjectName}.jsonCustomize the OpenAPI document using transformers
Transformers provide a useful API for modifying the OpenAPI document with your customizations.
The common scenarios to use transformers:
- Adding parameters to all operations in a document.
- Modifying descriptions for parameters or operations.
- Adding top-level information to the OpenAPI document.
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:
IOpenApiDocumentTransformer(Document transformer).
1class MyDocumentTransformer : IOpenApiDocumentTransformer
2{
3 public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context,
4 CancellationToken cancellationToken)
5 {
6 // ...
7 }
8}IOpenApiOperationTransformer(Operation transformer).
1class MyOperationTransformer : IOpenApiOperationTransformer
2{
3 public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
4 {
5 // ...
6 }
7}IOpenApiSchemaTransformer(Schema transformer).
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:
IOpenApiDocumentTransformer- works with the entireOpenApiDocumentIOpenApiOperationTransformer- works with a singleOpenApiOperationIOpenApiSchemaTransformer- works with a singleOpenApiSchema
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:
- information about the current endpoint (for operation transformers)
- information about the associated API description
- the document name being generated
- access to dependency injection services (
IServiceProvider)
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:
- inspect the current OpenAPI model
- access runtime metadata from ASP.NET Core
- safely modify the document during generation
Ways to register the transformers
There are 3 ways to register transformers:
- using a delegate
- using an instance of interface
- using a DI
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 transformersThis 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 โ
13EndpointsAs 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 name of the document being generated
- the
ApiDescriptionGroupsassociated with that document - the
IServiceProviderused during document generation
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:
- modify all endpoints in the application
- apply changes only to specific endpoints
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 OpenAPI document name
- the
ApiDescriptionassociated with the endpoint - the
IServiceProvider
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:
- should be made to each schema in the document
- conditionally applied to certain schemas
Schema transformers have access to a context object which contains:
- the name of the document the schema belongs to
- the
JSONtype information associated with the target schema IServiceProviderused in document generation
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.
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 :)