Copyright Johan Kronberg 2009-2024 Latest posts RSS feed X: johankronberg LinkedIn Profile GitHub: krompaco
Site generated by: Record Collector

Please do this with your Swagger-enabled Web APIs

Quite often I encounter APIs that have a Swagger UI but lacking much of the rest (hehe) of the Swagger tooling support.

This is a shame when you know what a quality OpenAPI specification with attention to detail can offer. My endorsed checklist contains the following steps.

Generate code in languages your consumers are using

Download and install NSwagStudio and try generating API client code. If this works and you get good affordance you can take comfort in knowing you are somewhat following API conventions and good practices. Here it is also easy to spot junk data in your specification and route mistakes resulting in silly names after generating.

When you have the settings you prefer you can use the included CLI and push a PowerShell script similar to this for re-generating client code (in this case C#).

add-type @"
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy :ICertificatePolicy {
        public bool CheckValidationResult(
            ServicePoint srvPoint, X509Certificate certificate,
            WebRequest request, int certificateProblem) {
            return true;
        }
    }
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
$url = "https://localhost:5001/swagger/v1/swagger.json"
$output = "$PSScriptRoot\swagger.json"
$start_time = Get-Date

Invoke-WebRequest -Uri $url -OutFile $output
Write-Output "Time taken:$((Get-Date).Subtract($start_time).Seconds) second(s)"

nswag swagger2csclient /input:"swagger.json" /generateUpdateJsonSerializerSettingsMethod:true /generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:"true" /disposeHttpClient:"false" /generateExceptionClasses:true /exceptionClass:"ApiException" /wrapDtoExceptions:true /useHttpClientCreationMethod:false /httpClientType:"System.Net.Http.HttpClient" /useHttpRequestMessageCreationMethod:false /useBaseUrl:false /generateBaseUrlProperty:false /generateSyncMethods:false /exposeJsonSerializerSettings:true /clientClassAccessModifier:"public" /typeAccessModifier:"public" /generateContractsOutput:false /parameterDateTimeFormat:"s" /generateUpdateJsonSerializerSettingsMethod:false /serializeTypeInformation:false /queryNullValue:"" /className:"{controller}Client" /operationGenerationMode:"MultipleClientsFromPathSegments" /generateOptionalParameters:false /generateJsonMethods:false /enforceFlagEnums:false /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:false /generateResponseClasses:true /responseClass:"SwaggerResponse" /namespace:"WebApp.Services.Generated" /requiredPropertiesMustBeDefined:true /dateType:"System.DateTimeOffset" /anyType:"object" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.Generic.ICollection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /handleReferences:false /generateImmutableArrayProperties:false /generateImmutableDictionaryProperties:false /inlineNamedArrays:false /inlineNamedDictionaries:false /inlineNamedTuples:true /inlineNamedAny:false /generateDtoTypes:true /generateOptionalPropertiesAsNullable:false /output:"src\WebApp\Services\Generated\ApiClient.cs"

First part is to accept any dodgy self-signed certificate you might run on your local development site. Then you can see that it makes an HTTP request and writes the swagger.json to disk to finally run the the NSwag console application with all the options specified explicitly.

For the recommended setup part below two key switches are /generateClientInterfaces:true and /injectHttpClient:true. Of course provide this script as part of your documentation.

Use the generated code in your integration tests

Lookup on how to use the WebApplicationFactory from the Microsoft.AspNetCore.Mvc.Testing package and use an HttpClient from there with the generated ApiClient.cs in a (for example XUnit) Test Project. This means you will have all your (hopefully well documented) API models and async methods for each endpoint available to use when writing tests, and you will use the API in the same manor as the smart/experienced ones of your consumers. The test runner will start the web app internally and run impressively fast.

Validate and recommend this setup with ASP.NET Core/5.0

The generated code has put everything in place for you to do something like the following in your consuming client app's Startup.ConfigureServices().

services.AddHttpClient<IApiClient, ApiClient>(c =>
    {
        c.BaseAddress = new Uri(apiBaseUrlFromSomeSettingEtc);
        c.DefaultRequestHeaders.Accept.Clear();
        c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    })
    .ConfigurePrimaryHttpMessageHandler(messageHandler =>
    {
        var handler = new HttpClientHandler();

        if (handler.SupportsAutomaticDecompression)
        {
            handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
        }

        return handler;
    });

To be extra cool I'm then using Blazor to validate and in a Razor Component I can now just do something like this.

@inject IApiClient ApiClient
..
@code {
    private async Task LoadCompanies(int segmentId)
    {
        this.Companies = await this.ApiClient.CompaniesAsync(segmentId);
    }
}

Hopefully Content and Commerce Cloud sites will be able to run on 5.0 in not too long making this the favored setup for calling APIs from those as well. Meanwhile Daniel Ovaska's HttpClient blog post should be followed for the HttpClient handling but in all other ways this blog post's example ApiClient.cs is fully compatible with Newtonsoft.Json on old framework 4.8 for example.

I'm not on top of the whole background but there are things missing for NSwag to be able to generate clients that use System.Text.Json for deserialization.

Provide documentation for authorization header values handling

This part is only if your API is protected by some OIDC flow.

Test and describe how to acquire tokens and a recommended cache approach with retry mechanism for when tokens have expired or don't work for other reasons. Add this to your Integration Test project and make available as a type of documentation.

Make sure that you are there re-using the HttpClient for the OIDC token issuer with a similar services.AddHttpClient() approach as the API client use above.

Setup your Swagger UI so that you can use the Authorize button and at least paste an access token there when wanting to execute actions in the UI.

Thanks!

Published and tagged with these categories: Episerver, ASP.NET, MVC, Development