Fix Autorest Problems when Generating Client with Swashbuckle swagger.json

The generation of clients for your REST APIs is a huge time saver, but it is not as straightforward as expected. Even though asp.netcore 5 projects support OpenAPI Specification out of the box, the swagger.json created with the Swashbuckle default configuration will throw exceptions when trying to generate an autorest client!

This post shows:

  1. How to fix the Swashbuckle configuration so the autorest client can be generated.
  2. How to improve the swagger.json so that the generated client is easier to consume.

Note: This post aims to list the required changes to generate clients with autorest – for other client generators this may differ.

A complete little sample project with all code samples can be found here: https://github.com/AngelaE/openapi-sample/tree/v1.0

Update 15 March 2022: The new CSharp code generator creates a completely different client and needs fewer fixes. Check my post Fix Autorest Problems Generating a C# Client with .Net 6

Pre-Requisite: Add Swashbuckle to Project

ASP.NET Core 5.0 now adds OpenApi Specification to new services by default. Alternatively it can be added manually. Check this Microsoft tutorial for details.

Fix the OpenApi Specification

The API generates a nice looking swagger.json with the default configuration, but when trying to generate the client with autorest we would get exceptions.

Problem 1 – FATAL: Error parsing swagger file. Error converting value False to type ‘AutoRest.Modeler.Model.Schema’

This image has an empty alt attribute; its file name is image.png

Reason: Autorest cannot handle the ‘additionalProperties’ set to false in the schemas, see https://github.com/Brixel/SpaceAPI/pull/19 for details.

Fix
Creating a DocumentFilter and setting this property to ‘true’ will remove it from the schema and allow autorest to work.

public class AdditionalPropertiesDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument openApiDoc, DocumentFilterContext context)
    {
        foreach (var schema in context.SchemaRepository.Schemas
          .Where(schema => schema.Value.AdditionalProperties == null))
        {
            schema.Value.AdditionalPropertiesAllowed = true;
        }
    }
}

In the startup.cs add the filter to the swagger configuration:

services.AddSwaggerGen(c =>
{
...
    c.DocumentFilter<AdditionalPropertiesDocumentFilter>();

Problem 2: FATAL: OperationId is required for all operations. Please add it for ‘get’ operation of ‘/Authors’ path.

Operation IDs are used to generate the client method names, and to group operations together. An easy convention is to name operations as <controller>_<methodname>. This will group the methods by controller.

A call to get all authors could then look like this:
var authors = await client.Authors.GetAllAuthorsAsync();

By default the operation IDs are not generated by Swashbuckle since it is a non trivial task. The sample below can be used to generate Operation IDs. This implementation allows customizing the ‘method’ part of the operation ID by adding an attribute to the method. The code needs to be added into the services.AddSwaggerGen block in the ConfigureServices method.

c.CustomOperationIds(apiDesc =>
{
    // use ControllerName_Method as operation id. That will group the methods in the generated client
    if(apiDesc.ActionDescriptor is ControllerActionDescriptor desc)
    {
        var operationAttribute = (desc.EndpointMetadata
        .FirstOrDefault(a => a is SwaggerOperationAttribute) as SwaggerOperationAttribute);
        return $"{desc.ControllerName}_{operationAttribute?.OperationId ?? desc.ActionName}";
    }
    // otherwise get the method name from the methodInfo
    var controller = apiDesc.ActionDescriptor.RouteValues["controller"];
    apiDesc.TryGetMethodInfo(out MethodInfo methodInfo);
    string methodName = methodInfo?.Name ?? null;
    return $"{controller}_{methodName}";
});

The operation ID for an endpoint can be customized to <controller>_<OperationId> with the attribute below.

[SwaggerOperation(OperationId = "GetAll")] 
public IEnumerable<Author> GetAllAuthors() 

Improve the Generated Specification

Improvement 1: Serialize Enumerations as String

Enumerations are serialized as integers by default. To use descriptive strings, add the json options to the ConfigureServices method.

Default

generate autorest client

Enum as String

generate autorest client
services.AddControllers()
  .AddJsonOptions(o => { 
    o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 
  });

This approach will not work for enumerations with custom values. See the this post for a potential solution. Another option would be to not expose the custom enumeration and instead use a DTO with a default enum (no assigned values) which is mapped to the custom enumeration.

Improvement 2: Declare [only] the Expected Happy Path Response Code

This is topic is very subjective, lots of sources mention declaring both success and failure responses. That is not working well with Autorest generated clients in my experience.

By only declaring the ‘happy path’ response code and type we achieve the following:

  1. The generated method in the client has a strongly typed response type. Since error and success responses usually have a different structure, autorest creates a response type of ‘object’ if response types with different return values are defined.
  2. Autorest generated clients throw an exception when an undefined response code is returned. This bubbles up by default and can be caught where appropriate without adding conditional statements.

Swashbuckle creates a 200 response by default, so the value only needs to be changed for different response codes.
Note: Generated clients may not work if the response codes are not defined. The typescript client does not map the results properly if they are missing.

Ignore Warnings for missing XML Comments

If using XML comments, in most cases it will make sense to disable warnings for missing XML documentation. Add the following section to the .csproj file:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Generate the Client

Autorest can be used to generate a client from an API specification and is configured either via command line arguments or literate configuration via a readme.md file. The latter option has the advantage that the version of the used extension can be specified. That means that different team members can re-generate the exactly same client at any time.

Steps

  1. Install autorest (npm i autorest)
  2. Create the readme.md file below in the directory where you want to generate the client and point it to the generated swagger.json (either the saved file or the service)
  3. type ‘autorest‘ to generate the client
  4. Create a library project with the generated files and add the Microsoft.Rest.ClientRuntime package to the project

Readme.md file:

# Generate BookApi Client 
The Book API client is generated with autorest, see https://aka.ms/autorest for details.
## AutoRest Client Generator Configuration
``` yaml
use-extension:
  "@microsoft.azure/autorest.modeler": "2.3.55"
  "@microsoft.azure/autorest.csharp": "2.3.82"
version: 3.0.6247 #autorest version
input-file: http://localhost:5000/swagger/v1/swagger.json
output-folder: .
csharp: 
  namespace: Clients 
  override-client-name: WhateverNameApiClient
  client-side-validation: false # disable client side validation of constraints
```

As of March 2021 enumerations are generated as strings in C#. Check my post How to Fix Enum Types for C# Autorest Clients to see how to generate enumerations as types.

It is also possible to create typescript clients by defining ‘typescript’ options in the readme.md and the corresponding extension version, see this sample readme.md.

Use the Client

The generated client can now be used to call the API like this:

C#

// https://github.com/AngelaE/openapi-sample/tree/v1.0/BookApiConsumer
// Read up on how to use the http client to avoid socket exhaustion.
var client = new BookApiClient(httpClient, false)
{
  BaseUri = new Uri("http://localhost:5000")
};
var authors = await client.Authors.GetAllAuthorsAsync();

Typescript:

// https://github.com/AngelaE/openapi-sample/tree/v1.0/BookApiConsumer.TypeScript
const client = new BookApiClient({ baseUri: 'http://localhost:5000' });
const authors = await client.authors.getAll();

Sample Code

Check https://github.com/AngelaE/openapi-sample for a working sample app.

Further Reading

How to add authentication to your autorest client

How to add Swashbuckle to your project: https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle

https://github.com/domaindrivendev/Swashbuckle.AspNetCore

Angela Evans

Senior Software Engineer at Diligent in Christchurch, New Zealand

This Post Has One Comment

Comments are closed.