OpenAPI Documents
OpenAPI support is provided via the Microsoft.AspNetCore.OpenApi package. Simply install the FastEndpoints.OpenApi package and add a couple of lines to your app startup:
Installation:
dotnet add package FastEndpoints.OpenApi
Usage:
using FastEndpoints;
using FastEndpoints.OpenApi; //add this
var bld = WebApplication.CreateBuilder(args);
bld.Services
.AddFastEndpoints()
.OpenApiDocument(); //define a open-api document
var app = bld.Build();
app.UseFastEndpoints()
.MapOpenApi(); //add this
app.Run();
Do not use the underlying services.AddOpenApi() registration directly for FastEndpoints documents, as it won't wire up the FastEndpoints transformers and metadata handling.
You can then visit /openapi/v1.json to see the generated OpenAPI document. If you define multiple documents, each one will be served from its own /openapi/*.json route using the configured document name.
This package generates the OpenAPI document. If you want an interactive API explorer, you can plug in something like Scalar or Swagger UI separately.
using Scalar.AspNetCore; //dotnet add package Scalar.AspNetCore
app.MapScalarApiReference(
o =>
{
o.AddDocuments("v1", "v2"); //inform scalar of your doc names
o.OperationTitleSource = OperationTitleSource.Path; //change title source
});
Configuration
OpenAPI generation/document settings can be configured by providing an action to OpenApiDocument():
bld.Services.OpenApiDocument(o =>
{
o.DocumentName = "v1";
o.Title = "My API";
o.Version = "v1";
});
To define multiple documents, simply call .OpenApiDocument() multiple times, each with a unique document name.
The most commonly used document options are the following:
If you need access to the service provider while the document is being generated, it becomes available via the DocumentOptions.Services property during generation-time hooks such as ConfigureOpenApi or custom transformers. Do note however, that DocumentOptions.Services is not populated during the initial OpenApiDocument() registration call.
Describe Endpoints
By default, both Accepts and Produces metadata are inferred from the request/response DTO types of your endpoints and added to the OpenAPI document automatically.
Default Accepts Metadata:
- GET/HEAD/DELETE endpoints will by default accept */* and application/json content types.
- POST/PUT/PATCH by default only accepts application/json content type.
- Any Endpoint with a request DTO where all of its properties are annotated with non-json binding source attributes such as [RouteParam], [QueryParam], [FormField], [FromHeader], [FromClaim], etc. will by default accept */*.
Default Produces Metadata:
- 200 - Success "produces metadata" is added if endpoint defines a response type.
- 204 - No Content is added if the endpoint doesn't define a response DTO type.
- 400 - Bad Request is added if there's a Validator associated with the endpoint.
- 401 - Unauthorized is added if the endpoint is not accessible anonymously.
- 402 - Payment Required is added for x402-enabled endpoints.
- 403 - Forbidden is added if any claims/roles/permissions/policies are required by the endpoint.
If the defaults are appropriate for your endpoint, you only need to specify any additional metadata using the Description() method like below:
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
public override void Configure()
{
Post("/item/create");
Description(b => b
.ProducesProblemDetails(400, "application/problem+json")
.ProducesProblemFE<InternalErrorResponse>(500));
}
}
Clearing Default Accepts/Produces Metadata
If the default Accepts & Produces metadata is not a good fit or seems to be producing 415 - Media Type Not Supported responses, you can clear the defaults and set them up yourself by setting the clearDefaults argument to true:
public override void Configure()
{
Post("/item/create");
Description(b => b
.Accepts<MyRequest>("application/json+custom")
.Produces<MyResponse>(200, "application/json+custom")
.ProducesProblemFE(400) //shortcut for .Produces<ErrorResponse>(400)
.ProducesProblemFE<InternalErrorResponse>(500),
clearDefaults: true);
}
Clearing Only Accepts Metadata
In order to override just the default accepts metadata for a request DTO so that the endpoint can accept any content-type, simply do the following:
Description(x => x.Accepts<MyRequest>());
If the endpoint should only be accepting a particular set of content-types, they can be specified like so:
Description(x => x.Accepts<Request>("text/plain", "text/csv"));
Clearing Only Produces Metadata
If it's only a specific "produces metadata" you need cleared, instead of everything as with clearDefaults: true, you can specify one or more status codes to be cleared like so:
Description(x => x.ClearDefaultProduces(200, 401, 403));
It is also possible to clear all the "produces metadata" by not specifying any status codes for the above extension method.
OpenAPI Documentation
Summary & description text of the different responses the endpoint returns, as well as an example request object and example response objects can be specified with the Summary() method:
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
public override void Configure()
{
Post("/item/create");
Description(b => b.Produces(403));
Summary(s =>
{
s.Summary = "short summary goes here";
s.Description = "long description goes here";
s.ExampleRequest = new MyRequest { ... };
s.ResponseExamples[200] = new MyResponse { ... };
s.Responses[200] = "ok response description goes here";
s.Responses[403] = "forbidden response description goes here";
});
}
}
Note that only one response example can be specified per status code. Multiple request examples however can be specified by either setting the ExampleRequest property multiple times or by adding to the RequestExamples collection like so:
Summary(s =>
{
s.ExampleRequest = new MyRequest { ... };
s.ExampleRequest = new MyRequest { ... };
s.RequestExamples.Add(new(new MyRequest { ... }));
s.RequestExamples.Add(new(new MyRequest { ... }, "Example Label"));
});
If you prefer to move the summary text out of the endpoint class, you can do so by subclassing the EndpointSummary type:
class AdminLoginSummary : EndpointSummary
{
public AdminLoginSummary()
{
Summary = "short summary goes here";
Description = "long description goes here";
ExampleRequest = new MyRequest { ... };
Responses[200] = "success response description goes here";
Responses[403] = "forbidden response description goes here";
}
}
public override void Configure()
{
Post("/admin/login");
AllowAnonymous();
Description(b => b.Produces(403));
Summary(new AdminLoginSummary());
}
Alternatively, if you'd like to get rid of all traces of documentation from your endpoint classes and have the summary completely separated, you can implement the Summary<TEndpoint> or Summary<TEndpoint, TRequest> abstract classes.
public class MySummary : Summary<MyEndpoint>
{
public MySummary()
{
Summary = "short summary goes here";
Description = "long description goes here";
ExampleRequest = new MyRequest { ... };
Response<MyResponse>(200, "ok response with body", example: new() { ... });
Response<ErrorResponse>(400, "validation failure");
Response(404, "account not found");
}
}
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
public override void Configure()
{
Post("/api/my-endpoint");
//no need to specify summary here
}
}
The Response() method above does the same job as the Produces() method mentioned earlier. Do note however, if you use the Response() method, the default 200 response is automatically removed, and you'd have to specify the 200 response yourself if it applies to your endpoint.
Describe Request Params
Route parameters, query parameters, request DTO property descriptions, and response DTO property descriptions can be specified either with xml comments or with the Summary() method or EndpointSummary or Summary<TEndpoint, TRequest> subclassing.
Take the following for example:
/// <summary>
/// the admin login request summary
/// </summary>
public class Request
{
/// <summary>
/// username field description
/// </summary>
public string UserName { get; set; }
/// <summary>
/// password field description
/// </summary>
public string Password { get; set; }
}
public override void Configure()
{
Post("admin/login/{ClientID?}");
AllowAnonymous();
Summary(s =>
{
s.Summary = "summary";
s.Description = "description";
s.Params["ClientID"] = "client id description";
s.RequestParam(r => r.UserName, "overridden username description");
s.ResponseParam(r => r.Token, "jwt access token");
});
}
Use the s.Params dictionary to specify descriptions for params that don't exist on the request dto or when there is no request DTO.
Use the RequestParam() and ResponseParam() methods to specify descriptions for request/response properties in a strongly-typed manner.
Whatever you specify within the Summary() method as above takes higher precedence over XML comments.
Enabling XML Documentation
A subset of XML comments are supported on request/response DTOs as well as endpoint classes which can be enabled by adding the following to the csproj file:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
</PropertyGroup>
Adding Query Params To OpenAPI
By default, GET request DTO properties are automatically converted to query parameters because most OpenAPI tooling expects GET input to come from the query string rather than the request body.
In order to let the document generator know that a particular request DTO property is being bound from a query string parameter on a non-GET endpoint, you need to decorate that property with the [QueryParam] attribute like below.
When you annotate a property with the [QueryParam] attribute, a query parameter will be added to the OpenAPI document for that property.
public class CreateEmployeeRequest
{
[QueryParam]
public string Name { get; set; } // bound from query string
[QueryParam, BindFrom("id")]
public string? ID { get; set; } // bound from query string
public Address Address { get; set; } // bound from body
}
The [QueryParam] attribute does not affect the model binding order in any way. It is simply an instruction for the OpenAPI generator to add a query parameter for the operation.
If for some reason you'd like GET requests to keep a body instead of being converted to query parameters, you can enable the following option:
bld.Services.OpenApiDocument(o => o.EnableGetRequestsWithBody = true);
Specifying Default Values
Default values for the document can be provided by decorating request DTO properties with the [DefaultValue(...)] attribute like so:
public class Request
{
[DefaultValue("Admin")]
public string UserName { get; set; }
[DefaultValue("Qwerty321")]
public string Password { get; set; }
}
Excluding Properties From Schema
There may be special circumstances where you'd need certain DTO properties to not show up in the generated schema. Decorating the DTO properties to be ignored with either of the following two attributes will get the job done:
- [JsonIgnore]
- [HideFromDocs]
Disable JWT Auth Scheme
Support for JWT Bearer Auth is automatically added. It can be disabled like so:
bld.Services.OpenApiDocument(o => o.EnableJWTBearerAuth = false);
Multiple Authentication Schemes
Multiple global auth scheme support can be enabled by using AddAuth() as shown below.
bld.Services.OpenApiDocument(o =>
{
o.EnableJWTBearerAuth = false;
o.DocumentName = "Initial-Release";
o.Title = "Web API";
o.Version = "v1.0";
o.AddAuth("ApiKey", new()
{
Name = "api_key",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
o.AddAuth("Bearer", new()
{
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT"
});
});
Here's an example of a full implementation of API Key authentication with FastEndpoints.
Excluding Non-FastEndpoints
By default, all discovered endpoints will be included in the document. You can instruct the generator to only include FastEndpoints in the document like so:
bld.Services.OpenApiDocument(o => o.ExcludeNonFastEndpoints = true);
Filtering Endpoints
If you'd like to include only a subset of discovered endpoints, you can use an endpoint filter like below:
// openapi doc
bld.Services.OpenApiDocument(o =>
{
o.EndpointFilter = ep => ep.EndpointTags?.Contains("include me") is true;
});
// endpoint
public override void Configure()
{
Get("test");
Tags("include me");
}
Operation Tags
By default, all endpoints/operations are tagged/grouped using the configured route segment. You can either disable the auto-tagging by setting the AutoTagPathSegmentIndex property to 0 or you can change the segment number which is used for auto-tagging like so:
bld.Services.OpenApiDocument(o => o.AutoTagPathSegmentIndex = 2);
If auto-tagging is not desirable, you can disable it and specify tags for each endpoint:
bld.Services.OpenApiDocument(o => o.AutoTagPathSegmentIndex = 0);
public override void Configure()
{
Post("api/users/update");
Description(x => x.WithTags("Users"));
}
Or keep auto-tagging enabled and override the auto value per endpoint:
Description(x => x.AutoTagOverride("Overriden Tag Name"));
Descriptions for tags has to be added at a global level which can be achieved as follows:
bld.Services.OpenApiDocument(o =>
{
o.TagDescriptions = t =>
{
t["Admin"] = "This is a tag description";
t["Users"] = "Another tag description";
};
});
You can further normalize auto-generated tags by using TagCase and TagStripSymbols.
Short Schema Names
The full name, including namespace, of DTO classes are used to generate schema names by default. You can change it to use just the class names by doing the following:
bld.Services.OpenApiDocument(o => o.ShortSchemaNames = true);
Short Endpoint Names
The full name, including namespace, of endpoint classes are used to generate operation IDs by default. You can change it to use just the class names by doing the following:
app.UseFastEndpoints(c =>
{
c.Endpoints.ShortNames = true;
});
This is a globally applicable setting, and it's not possible to specify it per document. Also note, if your endpoint class names are not unique, enabling this setting will not be possible unless you manually set a unique name per endpoint as follows.
Custom Endpoint Names
If the auto-generated operation IDs are not to your liking, you can specify a name for the endpoint using the WithName() method.
public override void Configure()
{
Get("/sales/invoice/{InvoiceID}");
Description(x => x.WithName("GetInvoice"));
}
When you manually specify a name/operation ID for an endpoint like above, and you want to point to that endpoint when using Send.CreatedAtAsync() method, you must use the overload that takes a string argument with which you can specify the name of the target endpoint. I.e. you lose the convenience/type-safety of being able to simply point to another endpoint using the class type like so:
await Send.CreatedAtAsync<GetInvoiceEndpoint>(...);
Instead, you must do this:
await Send.CreatedAtAsync("GetInvoice", ...);
Override Endpoint Name Generation
If you'd like to modify the default endpoint name generation logic, a function such as the one below can be specified, which simply returns a unique string per endpoint.
app.UseFastEndpoints(
c => c.Endpoints.NameGenerator =
ctx =>
{
return ctx.EndpointType.Name.TrimEnd("Endpoint");
});
This strategy is compatible with the Send.CreatedAtAsync() method and will not lose functionality as with the use of .WithName() method.
Retrieve Endpoint Name Using Endpoint Type
The auto generated endpoint name of any endpoint can be retrieved like so:
var endpointName = IEndpoint.GetName<MyEndpoint>();
This can be useful when you need to generate links using the LinkGenerator class.
Polymorphism Support
If you have base class request/response dto types and would like the document to include possible derived types within a oneOf field, enable polymorphism support like so:
bld.Services.OpenApiDocument(o => o.UseOneOfForPolymorphism = true);
Property Naming Policy
By default, the configured serializer naming policy is used for identifying and matching documented properties as well as path parameter names. If you'd prefer to keep clr property names in the generated document, you can disable that behavior like so:
bld.Services.OpenApiDocument(o => o.UsePropertyNamingPolicy = false);
Response Headers
Response headers are documented automatically for response DTO properties decorated with the [ToHeader] attribute. Additional response headers can also be supplied via endpoint summaries using the ResponseHeaders collection.
FluentValidation Integration
FluentValidation rules are automatically applied to documented schemas. This includes things such as required properties, minimum/maximum lengths, regex patterns, numeric ranges, and nested validators.
If there is a special circumstance where a validation rule should not affect the generated document, you can disable documentation integration for that rule like so:
using FastEndpoints.OpenApi;
public class MyValidator : Validator<Request>
{
public MyValidator()
{
RuleFor(x => x.Secret)
.NotEmpty()
.SwaggerIgnore();
}
}
Advanced Configuration
If you'd like to customize the underlying Microsoft OpenAPI pipeline directly, you can use the ConfigureOpenApi option to access OpenApiOptions and register additional transformers alongside the FastEndpoints transformers.
API Client Generation
Client generation is facilitated by the Kiota library by Microsoft. You can use our wrapper library to integrate client generation straight into your FastEndpoints app instead of using their CLI tools. Clients can be easily generated for supported languages in the following ways:
- Enable an endpoint which provides a downloadable zip file of the generated client package.
- Save client files to disk when running your app with the commandline argument --generateclients true.
Install Package
dotnet add package FastEndpoints.OpenApi.Kiota
Client Download Endpoint
Give your OpenAPI document a name via the o.DocumentName property and pass the same name to the MapApiClientEndpoint() method as shown below. Doing so will register an endpoint at the specified route with which the API client can be downloaded as a zip file.
using FastEndpoints;
using FastEndpoints.OpenApi;
using FastEndpoints.OpenApi.Kiota;
using Kiota.Builder;
using Kiota.Builder.Configuration;
var bld = WebApplication.CreateBuilder(args);
bld.Services
.AddFastEndpoints()
.OpenApiDocument(o =>
{
o.DocumentName = "v1"; //must match what's being passed below
});
var app = bld.Build();
app.UseFastEndpoints();
app.MapApiClientEndpoint("/cs-client", c =>
{
c.OpenApiDocumentName = "v1"; //must match document name set above
c.Language = GenerationLanguage.CSharp;
c.ClientNamespaceName = "MyCompanyName";
c.ClientClassName = "MyCsClient";
...
},
o => //endpoint customization settings
{
o.CacheOutput(p => p.Expire(TimeSpan.FromDays(365))); //cache the zip
o.ExcludeFromDescription(); //hide this endpoint from OpenAPI docs
});
NOTE: Don't forget to enable the output caching middleware in the ASP.NET pipeline when caching the generated files.
Save To Disk With App Run
This method can be used in any environment that can execute your application with a commandline argument. Most useful in CI/CD pipelines.
cd MyApp
dotnet run --generateclients true
In order for the above commandline argument to take effect, you must configure your app startup like so:
using FastEndpoints.OpenApi.Kiota;
using FastEndpoints.OpenApi;
using Kiota.Builder;
using Kiota.Builder.Configuration;
var bld = WebApplication.CreateBuilder(args); //must pass in the args
bld.Services
.AddFastEndpoints()
.OpenApiDocument(o =>
{
o.DocumentName = "v1"; //must match doc name below
});
var app = bld.Build();
app.UseFastEndpoints();
await app.GenerateApiClientsAndExitAsync(
c =>
{
c.OpenApiDocumentName = "v1"; //must match doc name above
c.Language = GenerationLanguage.CSharp;
c.OutputPath = Path.Combine(app.Environment.WebRootPath, "ApiClients", "CSharp");
c.ClientNamespaceName = "MyCompanyName";
c.ClientClassName = "MyCsClient";
c.CreateZipArchive = true; //if you'd like a zip file as well
},
c =>
{
c.OpenApiDocumentName = "v1";
c.Language = GenerationLanguage.TypeScript;
c.OutputPath = Path.Combine(app.Environment.WebRootPath, "ApiClients", "Typescript");
c.ClientNamespaceName = "MyCompanyName";
c.ClientClassName = "MyTsClient";
});
app.Run();
MSBuild Task
If you'd like to generate the client files on every release build, you can set up an MSBuild task by setting up your app like above and adding the following to your csproj file.
<Target Name="ClientExport" AfterTargets="Build" Condition="'$(Configuration)'=='Release'">
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet '$(TargetPath)' --generateclients true"/>
</Target>
If you have multiple API projects in a single solution, the task can fail due to port collisions. To avoid this, add --urls http://127.0.0.1:0 to the command or set ASPNETCORE_URLS to http://127.0.0.1:0 via the EnvironmentVariables property on the <Exec> element. Using port 0 automatically selects an inactive port for your app to start the client export.
How It Works
The GenerateApiClientsAndExitAsync() method first checks to see if the correct commandline argument was passed in to the application. If it was, the client files are generated and persisted to disk according to the settings passed in to it. If not, it does nothing so program execution can continue and stand up your application as usual.
In client generation mode, the application will be temporarily stood up with all the ASP.NET pipeline configuration steps that have been done up to that position of the code and shuts down and exits the program with a zero exit code once the client generation is complete.
The thing to note is that you must place the GenerateApiClientsAndExitAsync() call right after app.UseFastEndpoints() in order to prevent the app from starting in normal mode if the app was run using the commandline argument for client generation.
Any configuration steps that need to communicate with external services such as database migrations, third-party API calls, etc. must come after the GenerateApiClientsAndExitAsync() call.
Exporting OpenAPI JSON Files With Kiota
When using FastEndpoints.OpenApi.Kiota, you can also export openapi.json files to disk by using app.ExportOpenApiJsonAndExitAsync(...) and the CLI command dotnet run --exportopenapijson true.
Extensions For Conditional Middleware Config
When using FastEndpoints.OpenApi.Kiota, the following extension methods can be used for conditionally configuring your middleware pipeline depending on the mode the app is running in:
WebApplicationBuilder Extensions
bld.IsNotGenerationMode(); //returns true if running normally
bld.IsApiClientGenerationMode(); //returns true if running in client gen mode
bld.IsOpenApiJsonExportMode(); //returns true if running in openapi export mode
WebApplication Extensions
app.IsNotGenerationMode(); //returns true if running normally
app.IsApiClientGenerationMode(); //returns true if running in client gen mode
app.IsOpenApiJsonExportMode(); //returns true if running in openapi export mode
If you're only using FastEndpoints.OpenApi without Kiota, the core package exposes IsJsonExportMode() and IsNotJsonExportMode() helpers instead.
Export OpenAPI Documents
The FastEndpoints.OpenApi package can also export generated OpenAPI documents to disk.
using FastEndpoints;
using FastEndpoints.OpenApi;
var bld = WebApplication.CreateBuilder(args);
bld.Services
.AddFastEndpoints()
.OpenApiDocument(o => o.DocumentName = "v1"); // doc name must match below
var app = bld.Build();
app.UseFastEndpoints();
await app.ExportOpenApiDocsAndExitAsync("v1"); // doc name should match above
app.Run();
Run the app with the following command to export the configured documents:
dotnet run --export-openapi-docs true
Exported files are written to wwwroot/openapi by default. To change that path, set OpenApiExportPath in your csproj file:
<PropertyGroup>
<OpenApiExportPath>wwwroot/openapi</OpenApiExportPath>
</PropertyGroup>
See here for more information on how the above works.