1. Swagger Support

Swagger Support

Swagger support is provided via the excellent NSwag library. your mileage may vary since NSwag is presently tied closely to the MVC framework and support for .NET Minimal APIs is lacking in some areas.

If you find some rough edges with the Swagger support in FastEndpoints, please get in touch by creating a github issue or submit a pull request if you have experience dealing with Swagger.

Enable Swagger

First install the FastEndpoints.Swagger package and add 4 lines to your app startup:

Installation:

terminal
dotnet add package FastEndpoints.Swagger

Usage:

Program.cs
global using FastEndpoints;
using FastEndpoints.Swagger; //add this

var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
builder.Services.AddSwaggerDoc(); //add this

var app = builder.Build();
app.UseAuthorization();
app.UseFastEndpoints();
app.UseSwaggerGen(); //add this
app.Run();

You can then visit /swagger or /swagger/v1/swagger.json to see Swagger output.

Configuration

Swagger options can be configured as you'd typically do via the AddSwaggerDoc() method:

Program.cs
builder.Services.AddSwaggerDoc(settings =>
{
    settings.Title = "My API";
    settings.Version = "v1";
});

Describe Endpoints

By default, both Accepts and Produces metadata are inferred from the request/response DTO types of your endpoints and added to the Swagger document automatically.

So, you only need to specify the additional accepts/produces metadata using the Description() method like so:

public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
    public override void Configure()
    {
        Post("/item/create");
        Description(b => b
          .Produces<ErrorResponse>(400, "application/json+problem")
          .ProducesProblemFE<InternalErrorResponse>(500)); //if using FE exception handler
    }
}

If the default Accepts & Produces is not to your liking, you can clear the defaults and do it all 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), //this is the same as .Produces<ErrorResponse>(400) above
        .ProducesProblemFE<InternalErrorResponse>(500),
    clearDefaults: true);
}

Swagger 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 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";
    });
}

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 swagger documentation from your endpoint classes and have the summary completely separated, you can implement the Summary<TEndpoint> abstract class like shown below:

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 and Request 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:

Request.cs
/// <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; }
}
Endpoint.cs
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, "overriden username description");
    });
}

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 s.RequestParam() method to specify descriptions for properties of the request dto in a strongly-typed manner.

RequestParam() is also available when you use the Summary<TEndpoint,TRequest> generic overload.

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:

Project.csproj
<PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>CS1591</NoWarn>
</PropertyGroup>

Adding Query Params To Swagger

In order to let Swagger know that a particular request DTO property is being bound from a query string parameter, 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 Swagger document for that property.

CreateEmployeeRequest.cs
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 a way to make Swagger add a query param for the operation.

Specifying Default Values

Keeping in line with the NSwag convention, the default values for swagger is provided by decorating the request DTOs with the [DefaultValue(...)] attribute like so:

Request.cs
public class Request
{
    [DefaultValue("Admin")]
    public string UserName { get; set; }

    [DefaultValue("Qwerty321")]
    public string Password { get; set; }
}

Disable JWT Auth Scheme

Support for JWT Bearer Auth is automatically added. If you need to disable it, simply pass a false value to the following parameter:

Program.cs
builder.Services.AddSwaggerDoc(addJWTBearerAuth: false);

Multiple Authentication Schemes

Multiple global auth scheme support can be enabled by using the AddAuth() method like below.

Program.cs
builder.Services.AddSwaggerDoc(s =>
{
    s.DocumentName = "Release 1.0";
    s.Title = "Web API";
    s.Version = "v1.0";
    s.AddAuth("ApiKey", new()
    {
        Name = "api_key",
        In = OpenApiSecurityApiKeyLocation.Header,
        Type = OpenApiSecuritySchemeType.ApiKey,
    });
    s.AddAuth("Bearer", new()
    {
        Type = OpenApiSecuritySchemeType.Http,
        Scheme = JwtBearerDefaults.AuthenticationScheme,
        BearerFormat = "JWT",
    });
}, addJWTBearerAuth: false);

Excluding Non-FastEndpoints

By default, all discovered endpoints will be included in the swagger doc. You can instruct nswag to only include fast-endpoints in the document like so:

builder.Services.AddSwaggerDoc(excludeNonFastEndpoints: true);

Filtering Endpoints

If you'd like to include only a subset of discovered endpoints, you can use an endpoint filter like below:

//swagger doc
builder.Services.AddSwaggerDoc(s =>
{
    s.EndpointFilter(ep => ep.EndpointTags?.Contains("include me") is true);
});

//endpoint
public override void Configure()
{
    Get("test");
    Tags("include me");
}

Swagger Operation Tags

By default, all endpoints/swagger operations are tagged/grouped using the first segment of the route. you can either disable the auto-tagging by setting the tagIndex parameter to 0 or you can change the segment number which is used for auto-tagging like so:

builder.Services.AddSwaggerDoc(tagIndex: 2);

If auto-tagging is not desirable, you can disable it and specify tags for each endpoint like so:

builder.Services.AddSwaggerDoc(tagIndex: 0);

public override void Configure()
{
    Post("api/users/update");
    Options(x => x.WithTags("Users"));
}

Swagger Serializer Options

Even though NSwag uses a separate serializer (Newtonsoft) internally, we specify serialization settings for NSwag using System.Text.Json.JsonSerializerOptions just so we don't have to deal with anything related to Newtonsoft (until NSwag fully switches over to System.Text.Json).

builder.Services.AddSwaggerDoc(serializerSettings: x =>
{
    x.PropertyNamingPolicy = null;
    x.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    ...
});

With the above approach, System.Text.Json annotations such as [JsonIgnore] and [JsonPropertyName] on your DTOs work out of the box.

Short Schema Names

The full name (including namespace) of the DTO classes are used to generate the swagger schema names by default. You can change it to use just the class names by doing the following at startup:

builder.Services.AddSwaggerDoc(shortSchemaNames: true);

Short Endpoint Names

The full name (including namespace) of the endpoint classes are used to generate the operation ids by default. You can change it to use just the class names by doing the following at startup:

Program.cs
app.UseFastEndpoints(c =>
{
    c.Endpoints.ShortNames = true;
});

Custom Endpoint Names

If the auto-generated operation ids are not to your liking, you can specify a name for an endpoint using the WithName() method.

Endpoint.cs
public override void Configure()
{
    Get("/sales/invoice/{InvoiceID}");
    Description(x => x.WithName("GetInvoice"));
}
INFO

When you manually specify a name for an endpoint like above and you want to point to that endpoint when using SendCreatedAtAsync() 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 SendCreatedAtAsync<GetInvoiceEndpoint>(...);

Instead you must do this:

await SendCreatedAtAsync("GetInvoice", ...);

API Client Generation

There are 3 methods of generating C# and TypeScript API clients.

  1. Enable an endpoint which provides a download of the generated client file.
  2. Cause the clients files to be saved to disk every time the swagger middleware builds the swagger document.
  3. Save client files to disk when running your app with the commandline argument --generateclients true.

Install Package

terminal
dotnet add package FastEndpoints.ClientGen

Client Download Endpoints

Give your swagger documents a name via the s.DocumentName property and pass in the same names to the MapCSharpClientEndpoint() and/or the MapTypeScriptClientEndpoint() methods as shown below. Doing so will register hidden endpoints at the specified routes with which the API client files can be downloaded from.

Program.cs
builder.Services.AddSwaggerDoc(s =>
{
    s.DocumentName = "version 1"; // must match what's being passed in to the map methods below
});

app.MapCSharpClientEndpoint("/cs-client", "version 1", s =>
{
    s.ClassName = "ApiClient";
    s.CSharpGeneratorSettings.Namespace = "My Namespace";
});

app.MapTypeScriptClientEndpoint("/ts-client", "version 1", s =>
{
    s.ClassName = "ApiClient";
    s.TypeScriptGeneratorSettings.Namespace = "My Namespace";
});

Save To Disk With Middleware

Doing the following will cause the API client to be generated and saved to disk at the specified location whenever the swagger middleware is triggered. I.e. when either the swagger UI is loaded or swagger.json file is accessed.

Program.cs
builder.Services.AddSwaggerDoc(s =>
{
    s.GenerateCSharpClient(
        settings: s => s.ClassName = "ApiClient",
        destination: "FILE PATH TO OUTPUT THE CLIENT");

    s.GenerateTypeScriptClient(
        settings: s => s.ClassName = "ApiClient",
        destination: "FILE PATH TO OUTPUT THE CLIENT");
});

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.

terminal
cd MyApp
dotnet run --generateclients true

In order for the above commandline argument to take effect, you must configure your app startup like so:

Program.cs
var builder = WebApplication.CreateBuilder(args); //must pass in the args
builder.Services.AddFastEndpoints();
builder.Services.AddSwaggerDoc(s => s.DocumentName = "v1"); //must match doc name below

var app = builder.Build();
app.UseAuthorization();
app.UseFastEndpoints();

await app.GenerateClientsAndExitAsync(
    documentName: "v1", //must match doc name above
    destinationPath: builder.Environment.WebRootPath,
    csSettings: c => c.ClassName = "ApiClient",
    tsSettings: null);

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.

MyProject.csproj
<Target Name="ClientGen" AfterTargets="Build" Condition="'$(Configuration)'=='Release'">
    <Exec WorkingDirectory="$(RunWorkingDirectory)" 
          Command="$(RunCommand) --generateclients true" />
</Target>

How It Works

The GenerateClientsAndExitAsync() 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 the 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 GenerateClientsAndExitAsync() call right after app.UseFastEndpoints() call 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 needs to communicate with external services such as database migrations, third-party api calls, etc. must come after the GenerateClientsAndExitAsync() call.

Removing Empty Schema

The generated swagger document may contain empty schemas due to all properties being removed by the operation processor. If you need them removed from the swagger doc, you can instruct the operation processor to do so by using the following parameter:

builder.Services.AddSwaggerDoc(removeEmptySchemas: true);

Note: Enabling empty schema removal also enables flattening of the inheritance hierarchy of schema. Base classes will no longer be referenced. The only downside to this is if you generate C# or TypeScript clients, those client files may be slightly bigger in size depending on your use of DTO inheritance.


© FastEndpoints 2022