Search
K
  1. Swagger Support

Swagger Support

Swagger support is provided via the excellent NSwag library. Simply install the FastEndpoints.Swagger package and add 3 lines to your app startup:

Installation:

terminal
dotnet add package FastEndpoints.Swagger

Usage:

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

var bld = WebApplication.CreateBuilder();
bld.Services
   .AddFastEndpoints()
   .SwaggerDocument(); //define a swagger document

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

You can then visit /swagger or /swagger/v1/swagger.json to see the generated Swagger document.

Configuration

Swagger generation/document settings can be configured by providing an action to SwaggerDocument():

Program.cs
bld.Services.SwaggerDocument(o =>
{
    o.DocumentSettings = s =>
    {
        s.Title = "My API";
        s.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.

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.

Default Produces Metadata:

  • 200 - Success "produces metadata" is added to all endpoints.
  • 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.
  • 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/json+problem") //if using RFC errors 
            .ProducesProblemFE<InternalErrorResponse>(500)); //if using FE exception handler
    }
}

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.

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

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 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 an instruction for Swagger to 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; }
}

Excluding Properties From Schema

There may be special circumstances where you'd need certain DTO properties to not show up in the swagger schema. Decorating the DTO properties to be ignored with either of the following two attributes will get the job done:

  • [JsonIgnore] //from System.Text.Json.Serialization
  • [HideFromDocs] //from FastEndpoints

Disable JWT Auth Scheme

Support for JWT Bearer Auth is automatically added. It can be disabled like so:

Program.cs
bld.Services.SwaggerDocument(o => o.EnableJWTBearerAuth = false);

Multiple Authentication Schemes

Multiple global auth scheme support can be enabled by using AddAuth() as shown below.

Program.cs
bld.Services.SwaggerDocument(o =>
{
    o.EnableJWTBearerAuth = false;
    o.DocumentSettings = s =>
    {
        s.DocumentName = "Initial-Release";
        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",
        });
    };
});
TIP

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 swagger doc. You can instruct nswag to only include fast-endpoints in the document like so:

bld.Services.SwaggerDocument(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:

//swagger doc
bld.Services.SwaggerDocument(o =>
{
    o.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 AutoTagPathSegmentIndex property to 0 or you can change the segment number which is used for auto-tagging like so:

bld.Services.SwaggerDocument(o => o.AutoTagPathSegmentIndex = 2);

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

bld.Services.SwaggerDocument(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 swagger tags has to be added at a global level which can be achieved as follows:

bld.Services.SwaggerDocument(o =>
{
    o.TagDescriptions = t =>
    {
        t["Admin"] = "This is a tag description";
        t["Users"] = "Another tag description";
    };
});

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).

bld.Services.SwaggerDocument(o =>
{
    o.SerializerSettings = s =>
    {
        s.PropertyNamingPolicy = null;
        s.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    };
});

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

Do note that if you don't specify any settings for the serializer, the same set of settings you've configured for FastEndpoints will be used.

Custom Converters

Due to a known limitation in NSwag, if your application uses custom Json converters for STJ, they won't be picked up by NSwag automatically. You will have to register a Newtonsoft version of the converter like below:

bld.Services.SwaggerDocument(o =>
{
    o.NewtonsoftSettings = s =>
    {
        s.Converters.Add(new MyJsonConverter());
    };
});

Do note however, Newtonsoft converters registered like above will only be used by the FastEndpoints NSwag processor for serializing example requests/responses when generating the swagger spec. They will not be used by NSwag itself for serializing schema. The recommended approach for customizing schema generation for a given type is to register a TypeMapper like below:

.SwaggerDocument(
    o => o.DocumentSettings =
        s => s.SchemaSettings.TypeMappers.Add(
            new PrimitiveTypeMapper(
                typeof(Guid), //the type you'd like to override the schema for
                schema =>
                {
                    schema.Type = JsonObjectType.String;
                    schema.Format = "uuid";
                })));

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:

bld.Services.SwaggerDocument(o => o.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:

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

This is a globally applicable setting and it's not possible to specify it per swagger 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 the following.

Custom Endpoint Names

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

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

When you manually specify a name/operation ID 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", ...);

Removing Empty Schema

The generated swagger document may contain empty request schemas due to all properties being removed by the built-in operation processor. If you'd like to remove the empty schema from the swagger doc, instruct the operation processor to do so by using the following property:

bld.Services.SwaggerDocument(o => o.RemoveEmptyRequestSchema = 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 API Clients, those client files may be slightly bigger depending on your use of DTO inheritance.

Non-Nullable Properties As Required

If you have nullable reference types enabled in your project, the following extension method can be used to make swagger generate request/response schema with required properties for non-nullable types. More info here.

bld.Services.SwaggerDocument(o => o.DocumentSettings = d => d.MarkNonNullablePropsAsRequired());

Display Operation IDs

Operation IDs can be displayed in the Swagger UI by calling the following extension method:

app.UseSwaggerGen(uiConfig: u => u.ShowOperationIDs());

API Client Generation

Client generation is facilitated by the excellent 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:

  1. Enable an endpoint which provides a downloadable zip file of the generated client package.
  2. Save client files to disk when running your app with the commandline argument --generateclients true.

Install Package

terminal
dotnet add package FastEndpoints.ClientGen.Kiota

Client Download Endpoint

Give your swagger document a name via the s.DocumentName property and pass in the same names 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.

Program.cs
bld.Services.SwaggerDocument(o =>
{
    o.DocumentSettings = s =>
    {
        s.DocumentName = "v1"; //must match what's being passed in to the map method below
    };
});

app.MapApiClientEndpoint("/cs-client", c =>
{
    c.SwaggerDocumentName = "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(); //hides this endpoint from swagger 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.

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 bld = WebApplication.CreateBuilder(args); //must pass in the args
bld.Services
   .AddFastEndpoints()
   .SwaggerDocument(o =>
   {
      o.DocumentSettings = s => s.DocumentName = "v1"; //must match doc name below
   }); 

var app = bld.Build();
app.UseFastEndpoints();

await app.GenerateApiClientsAndExitAsync(
    c =>
    {
        c.SwaggerDocumentName = "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.SwaggerDocumentName = "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.

MyProject.csproj

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

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 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 GenerateApiClientsAndExitAsync() 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 GenerateApiClientsAndExitAsync() call.

Exporting swagger.json files

It is also possible to export swagger.json files to disk using the same strategy as above by using the method call app.ExportSwaggerJsonAndExitAsync(...) and the cli command dotnet run --exportswaggerjson true.

Extensions for conditional middleware config

The following handy 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.IsSwaggerJsonExportMode(); //returns true if running in swagger export mode

WebApplication Extensions

app.IsNotGenerationMode(); //returns true if running normally
app.IsApiClientGenerationMode(); //returns true if running in client gen mode
app.IsSwaggerJsonExportMode(); //returns true if running in swagger export mode

© FastEndpoints 2024