Written by 09:53 Computer Environment, Languages & Coding

Swashbuckle (Swagger) Customization for WebAPI

Who has ever tested their WebAPI knows such tools as Postman or Advanced REST (extensions for Chrome). These tools are convenient in every way, except that they are not able to recognize which models the API accepts, which ones it returns and do not provide information about all possible endpoints. The Swashbuckle package solves this disadvantage. It builds Swagger specification generation and UI in the project. In this article, I will briefly describe how to bind it to the project and provide some details about authorization and work with “overloaded” endpoints.

Binding to Project

Swashbuckle is the NuGet package that integrates the auto-generation of information about nodes in WebAPI according to the OpenAPI specification. This specification is de facto the standard, as WSDL once was. To install it, you need to perform four simple steps:

  1. Install it from NuGet using the Install-Package Swashbuckle command
  2. Enable XML documentation in the project settings
  3. Uncomment the IncludeXmlComments(GetXmlCommentsPath()); line in the SwaggerConfig.cs file, which is created when installing the package
  4. Write return string.Format(@”{0}\bin\BookStoreApiService.XML”, AppDomain.CurrentDomain.BaseDirectory); in the implementation of the GetXmlCommentsPath() method

That is all. Then, it is necessary to describe the API methods, response codes, and customize them.

Nuances when deploying WebAPI

Deploying WebAPI into production may cause the issue when the XML file is missing. The release build does not include them by default. However, we can work around it by editing the csproj file. It is necessary to add <ExcludeXmlAssemblyFiles>false</ExcludeXmlAssemblyFiles> in the project PropertyGroup and the file will be left in bin/.

Another issue relates to those who hide their API behind a proxy. The solution is not universal, however, it works in my case. The proxy adds headers to the request, by which we know what URL of endpoints should be for the client.

Example of URL recognition behind proxy

// in the SwaggerConfig.cs file
c.RootUrl(req => ComputeClientHost(req));

// below you may find the implementation of the method
public static string ComputeClientHost(HttpRequestMessage req)
{
    var authority = req.RequestUri.Authority;
    var scheme = req.RequestUri.Scheme;
    // we receive the host, which the client can see
    if (req.Headers.Contains("X-Forwarded-Host"))
    {
        // if there is a proxy chain, it is necessary to take the first one
        var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
        var firstForwardedHost = xForwardedHost.Split(',')[0];
        authority = firstForwardedHost;
    }
    // we get a protocol, which is used by the client 
    if (req.Headers.Contains("X-Forwarded-Proto"))
    {
        var xForwardedProto = req.Headers.GetValues("X-Forwarded-Proto").First();
        xForwardedProto = xForwardedProto.Split(',')[0];
        scheme = xForwardedProto;
    }	
    return scheme + "://" + authority;
}

Adding Response Codes

The return HTTP Status Codes can be added using XML comments and attributes.

Examples of adding status codes

/// <response code="404">Not Found</response>

[SwaggerResponse(HttpStatusCode.NotFound, Type = typeof(Model), Description = "Not Found: no such endpoint")]

In addition, it is necessary to remember that XML comments override attributes. The latter ones will be ignored if both ways are used simultaneously for the same method. Also, if XML comments are used, it is necessary to specify all the codes, including 200 (OK), while the return model cannot be specified. Therefore, using SwaggerResponse is more preferable, as it does not have these drawbacks. When the endpoint returns another code, for example, 201 (Created), instead of the default 200, the first one must be removed with the [SwaggerResponseRemoveDefaults] attribute.

There is an option to add common codes, like 400 (BadRequest) or 401 (Unauthorized) to all methods at once. To do this, it is necessary to implement the IOperationFilter interface and register such a class using c.OperationFilter<T>();.

An example of using the Apply method to add a certain code list:

HttpStatusCode[] _codes; // codes for adding 
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
    // this property is now always initialized 
    if (operation.responses == null)
        operation.responses = new Dictionary<string, Response>();
    foreach (var code in _codes) {
        var codeNum = ((int)code).ToString();
        var codeName = code.ToString();
        // add description 
        if (!operation.responses.ContainsKey(codeNum))
            operation.responses.Add(codeNum, new Response { description = codeName });
    }
}

Authorization of WebAPI and Swashbuckle

The text below provides several options for implementing the Basic authorization. However, the package supports others.

If AuthorizeAttribute is used, then Swashbuckle will build UI, but queries will fail. There are several ways to provide this information:

  1. Using authorization built into a browser
  2. Using a built-in authorization form in the package
  3. Using operation parameters
  4. Using JavaScript

Authorization built into browser

Authorization built into a browser will be available “out of the box” if you use the attribute and filter:

// Basic Authorization attributes
config.Filters.Add(new AuthorizeAttribute());
config.Filters.Add(new BasicAuthenticationFilter()); // IauthenticationFilter implementation

After adding them in the WebAPI configuration, the browser prompts you to enter data for authentication when executing the query. The difficulty is that dropping this data is not as easy and fast as adding them.

Built-in authorization form in Swashbuckle

The other way is more comfortable as it provides a special form. To include the built-in authorization form in the package, do the following steps:

  1. Enable an attribute and filter for authentication
  2. Uncomment the c.BasicAuth(“basic”).Description(“Basic HTTP Authentication”);  line in the Swagger settings
  3. Add IOperationFilter which adds the information in the c.OperationFilter<MarkSecuredMethodsOperationFilter>(); nodes

Implementation of the Apply method of this filter

public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
    var filterPipeline = apiDescription.ActionDescriptor.GetFilterPipeline();
    // check if authorization is required
    var isAuthorized = filterPipeline
        .Select(filterInfo => filterInfo.Instance)
        .Any(filter => filter is IAuthorizationFilter);
    // check if anonymous access is allowed
    var allowAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
    if (isAuthorized && !allowAnonymous)
    {
        if (operation.security == null)
            operation.security = new List<IDictionary<string, IEnumerable<string>>>();
        var auth = new Dictionary<string, IEnumerable<string>>
        {
            {"basic", Enumerable.Empty<string>()}
        };
        operation.security.Add(auth);
    }
}

After that, you will be able to use this authorization form, and the data you entered will be used for all queries.

Authorization using a parameter and JS code

The following two methods should be considered as examples of working with IOperationFilter and injecting your JavaScript.

Parameters can send data not only to body and query but also to the header. In this case, it is necessary to add a hash.

Adding such a parameter

operation.parameters.Add(new Parameter
{
    name = "Authorization",
    @in = "header", // specify that the value will be sent in the header
    description = "Basic U3dhZ2dlcjpUZXN0", // Basic Swagger:Test
    required = true, // parameter required
    type = "string"
});

By injecting your JavaScript, you can also send data in the header of queries. To do this, go through the following steps:

  1. Add a JS file as an embedded resource
  2. Uncomment the line in the Swagger configuration and specify your file as a resource name: InjectJavaScript(thisAssembly, “assembly.namesapce.swagger-basic-auth.js”);
  3. In the file, write the following: swaggerUi.api.clientAuthorizations.add(“basic”, new SwaggerClient.ApiKeyAuthorization(“Authorization”, “Basic U3dhZ2dlcjpUZXN0”, “header”));

Now, this data will be added as a header to each query. Actually, with this JS code, it is possible to send any headers, as I understand. The key parameter, which equals «basic» in the example, should be unique so that you do not receive the JS error when sending a query.

For example, JS sending headers to Swagger

swaggerUi.api.clientAuthorizations.add("custom1", new SwaggerClient.ApiKeyAuthorization("X-Header-1", "value1", "header"));
swaggerUi.api.clientAuthorizations.add("custom2", new SwaggerClient.ApiKeyAuthorization("X-Header-2", "value2", "header"));
swaggerUi.api.clientAuthorizations.add("custom3", new SwaggerClient.ApiKeyAuthorization("X-Header-3", "value3", "header"));

Working with mandatory headers

In some cases, unauthorized headers may be mandatory. For example, headers with information about a client. Usually, a message handler is built into the WebAPI pipeline, the DelegatingHandler is implemented and registered in the WebAPI config.MessageHandlers.Add (new MandatoryHeadersHandler ()); configuration.

In this case, Swagger will stop showing any information, as the handler will not allow passing queries. It is not solved out of the box, that’s why it is necessary to make provision for this case in your handler, i.e. in the case of a query to the swagger URL, the handler should pass it. And then the addition of headers with JS will help, as described above.

Endpoints with overloaded methods

WebAPI allows creating several action methods for one endpoint, the call of which depends on the query parameters.

[ResponseType(typeof (IList<Model>))]
public IHttpActionResult Get() {...}

[ResponseType(typeof (IList<Model>))]
public IHttpActionResult Get(int count, bool descending) {...}

Swagger does not support these methods by default. UI will throw the error message 500: Not supported by Swagger 2.0: Multiple operations with path ‘api/<URL>’ and method ‘<METHOD>’. See the config setting — \«ResolveConflictingActions\» for a potential workaround.

As the message says, you should solve the situation by yourself. There are several options to do this:

  1. select only one method
  2. create one method with all parameters
  3. change the document generation

The first and the second ways are implemented using the c.ResolveConflictingActions(Func<IEnumerable<ApiDescription>, ApiDescription> conflictingActionsResolver) settings. The principle of the method is to take several conflicted methods and return only one.

An example of combining all the parameters

return apiDescriptions =>
{
    var descriptions = apiDescriptions as ApiDescription[] ?? apiDescriptions.ToArray();
    var first = descriptions.First(); // we build relative to the first method 
    var parameters = descriptions.SelectMany(d => d.ParameterDescriptions).ToList();

    first.ParameterDescriptions.Clear();
    // we add all the parameters and make them optional
    foreach (var parameter in parameters)
        if (first.ParameterDescriptions.All(x => x.Name != parameter.Name))
        {
            first.ParameterDescriptions.Add(new ApiParameterDescription
            {
                Documentation = parameter.Documentation,
                Name = parameter.Name,
                ParameterDescriptor = new OptionalHttpParameterDescriptor((ReflectedHttpParameterDescriptor) parameter.ParameterDescriptor),
                Source = parameter.Source
            });
        }
    return first;
};

// this inheritance is required, as IsOptional has only getter
public class OptionalHttpParameterDescriptor : ReflectedHttpParameterDescriptor
{
    public OptionalHttpParameterDescriptor(ReflectedHttpParameterDescriptor parameterDescriptor)
        : base(parameterDescriptor.ActionDescriptor, parameterDescriptor.ParameterInfo)
    {
    }
    public override bool IsOptional => true;
}

Drastic method

The third way is more drastic and originates from the OpenAPI specification. We can output all the endpoints with parameters:

To do this, it is necessary to modify the way the Swagger documentation is generated using IDocumentFilter and generate the description by yourself.

In the real world, this method is rarely needed, so we dig even deeper.

Another way I would recommend only to those who are interested in Swashbuckle internals is to replace SwaggerGenerator. This is done in the c.CustomProvider (defaultProvider => new NewSwaggerProvider (defaultProvider)); line. To do this, follow these steps:

  1. create your MySwaggerGenerator: ISwaggerProvider class
  2. find SwaggerGenerator.cs in the Swashbuckle repository on GitHub (here)
  3. copy the GetSwagger method and other related methods into yours
  4. duplicate internal variables and initialize them in the constructor of your class
  5. register in the Swagger configuration

Initializing internal variables

Then, you need to find ar paths = GetApiDescriptionsFor(apiVersion)….., a place where paths are created. For example, to get what is in the example, it is necessary to replace GroupBy() with .GroupBy(apiDesc => apiDesc.RelativePath).

References

  1. Swagger example
  2. RESTful Web API specification formats
  3. Customize Swashbuckle-generated API definitions
  4. Swagger object schema
  5. Authentication Filters in ASP.NET Web API 2
  6. A WebAPI Basic Authentication Authorization Filter
  7. Customize Authentication Header in SwaggerUI using Swashbuckle
  8. HTTP Message Handlers in ASP.NET Web API
  9. Managing Action Conflicts in ASP.Net 5 with Swashbuckle
  10. Tutorial Swagger project at GitHub
Tags: Last modified: September 23, 2021
Close