Last modified: Aug 4, 2022

Swagger

Guidelines for setting up swagger

We use Swagger to document our APIs.

Swashbuckle

We use Swashbuckle to generate the documentation. Further documentation on Swashbuckle.AspNetCore is available here

The configurations described below are all located in Program.cs in the AddSwaggerGen method.

Configure authorization

Most of our APIs requires authorization. To enable the end user to test these endpoints through Swagger support for authentication must be configured.

Authorization box
Authorization box in Swagger

The authorization box above is created with the configuration below. It enables the user provide a JWT token to the requests that are sent through Swagger as a bearer token.

c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
    In = ParameterLocation.Header,
    Description = "Please enter JWT token",
    Name = "Authorization",
    Type = SecuritySchemeType.Http,
    BearerFormat = "JWT",
    Scheme = "bearer"
});

c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
    {
        new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference
            {
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
            }
        },
        Array.Empty<string>()
    }
});

Further documentation on setting up security definitions and requirements is available here.

Set up schema filter

The swagger documentation contains examples of each schema that is used in the request and/or response of the APIs. By default, the example contains the default value of each value type. Our experience is that this might not be very explanatory to the end user.

To overwrite these examples with custom ones a SchemaFilter can be set up. First a schema filter class that implements the interface ISchemaFilter must be created. Secondly, the filter class must be registered in the AddSwaggerGen method in Program.cs.

The result of running a schema through a filter when generating the OpenAPI specification is shown below.

[
  {
    "id": "8fb5a622-87e7-4f5e-b25e-215c93c3b86b",
    "source": "https://ttd.apps.altinn.no/ttd/apps-test/instances/50015641/a72223a3-926b-4095-a2a6-bacc10815f2d",
    "specversion": "1.0",
    "type": "app.instance.created",
    "subject": "/party/50015641",
    "alternativesubject": "/person/27124902369",
    "time": "2020-10-29T07:22:19.438039Z"
  }
]

Implementation example

This example implements a schema filter for the CloudEvent model. The full implementation is shown below and then broken down to explain each part.

public class SchemaExampleFilter : ISchemaFilter
{
    /// <inheritdoc/>
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if(context.Type.Name ==  nameof(CloudEvent)){
            schema.Example = new OpenApiObject
                {
                    ["id"] = new OpenApiString(Guid.NewGuid().ToString()),
                    ["source"] = new OpenApiString("https://ttd.apps.altinn.no/ttd/apps-test/instances/50015641/a72223a3-926b-4095-a2a6-bacc10815f2d"),
                    ["specversion"] = new OpenApiString("1.0"),
                    ["type"] = new OpenApiString("app.instance.created"),
                    ["subject"] = new OpenApiString("/party/50015641"),
                    ["alternativesubject"] = new OpenApiString("/person/27124902369"),
                    ["time"] = new OpenApiString("2020-10-29T07:22:19.438039Z")
                };
        }

        return;
    }
}

The class SchemaExampleFilter is created and it implements the interface ISchemaFilter. The interface contains a single public method

public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
}

The context object will have details on the current type an example may be provided for. We choose to match the types using context.Type.Name and nameof().

The example is an OpenApiObject and is bound to the Example property of the input parameter schema.

schema.Example = new OpenApiObject
{
    ["id"] = new OpenApiString(Guid.NewGuid().ToString()),
    ["source"] = new OpenApiString("https://ttd.apps.altinn.no/ttd/apps-test/instances/50015641/a72223a3-926b-4095-a2a6-bacc10815f2d"),
    ["specversion"] = new OpenApiString("1.0"),
    ["type"] = new OpenApiString("app.instance.created"),
    ["subject"] = new OpenApiString("/party/50015641"),
    ["alternativesubject"] = new OpenApiString("/person/27124902369"),
    ["time"] = new OpenApiString("2020-10-29T07:22:19.438039Z")
};

Note that we use OpenApiString for many of the types, this is to simply the setup on our part, and will not be communicated to the end user as the type to use.

Finally the class must be registered as a schema filter in Program.cs.

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Altinn Platform Events", Version = "v1" });
        IncludeXmlComments(c);
        c.EnableAnnotations();

        c.SchemaFilter<SchemaExampleFilter>();
    });

The result of the schema filter as seen in Swagger
The result of the schema filter as seen in Swagger

Set up request body filter

When using schema filters, it is only possible to provide a single example per schema. In the case where the schema in the request body can have many variants where all could have the benefit of being exemplified in Swagger, request body filters should be used.

To overwrite the schema example with a request body example a RequestBodyFilter can be set up. First, a request body filter class that implements the interface IRequestBodyFilter must be created. Secondly, the filter class must be registered in the AddSwaggerGen method in Program.cs.

The result of running a request body through a filter when generating the OpenAPI specification is shown below for the SubscriptionRequestModel.

{
  "source": "https://ttd.apps.altinn.no/ttd/apps-test/instances/50015641/a72223a3-926b-4095-a2a6-bacc10815f2d",
  "specversion": "1.0",
  "type": "app.instance.created",
  "subject": "/party/50015677"
}

Implementation example

This example implements a request body filter for the SubscriptionRequestModel. The full implementation is shown below.

public class RequestBodyExampleFilter : IRequestBodyFilter
{
    public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
    {
        if(context.BodyParameterDescription.Type.Name == nameof(CloudEventRequestModel)){
            OpenApiMediaType appJson = requestBody.Content["application/json"];

            List<(string Name, OpenApiObject Value)> examples = new()
            {
                ("Instance created event with alternative subject",
                CreateOpenApiObject(new List<(string Name, string Value)>()
                {
                    ("source", "https://ttd.apps.altinn.no/ttd/apps-test/instances/50015641/a72223a3-926b-4095-a2a6-bacc10815f2d"),
                    ("specversion",  "1.0"),
                    ("type",  "app.instance.created"),
                    ("subject",  "/party/50015641"),
                    ("alternativesubject", "/person/01017512345")
                })),
                ("Instance created event without alternative subject",
                CreateOpenApiObject(new List<(string Name, string Value)>()
                {
                    ("source", "https://ttd.apps.altinn.no/ttd/apps-test/instances/50067592/f3c92d96-0eb3-4532-a16f-bcafd94bde3a"),
                    ("specversion",  "1.0"),
                    ("type",  "app.instance.created"),
                    ("subject",  "/party/50067592")
                }))
            };

            examples.ForEach(entry => appJson.Examples.Add(entry.Name, new OpenApiExample { Value = entry.Value }));

            requestBody.Content["application/json"] = appJson;
        }

        return;
    }
}

Finally the class must be registered as a schema filter in Program.cs.

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Altinn Platform Events", Version = "v1" });
        IncludeXmlComments(c);
        c.EnableAnnotations();

        c.RequestBodyFilter<RequestBodyExampleFilter>();
    });