Generating a Typed Client for use with HttpClientFactory using NSwag

stuartblang

Stuart Lang

Posted on March 18, 2019

Generating a Typed Client for use with HttpClientFactory using NSwag

If you're a .NET developer building web apps or microservices, odds are at some point you're going to want to call an HTTP API from an ASP.NET Core app. This post covers how to create a client using NSwag, with the appropriate settings for using it with HttpClientFactory.

Typed Clients and HttpClientFactory

For a proper introduction to these concepts I encourage you to read Steve Gordon's HttpClientFactory in ASP.NET Core 2.1 series, An Introduction to HttpClientFactory and Defining Named and Typed Clients, but the really short version is:

  • HttpClientFactory - This was introduced to solve the problems around managing the lifetime of HttpClients and their handlers, which historically has been problematic.
  • Typed Clients - This is a feature of HttpClientFactory which lets you register a class which simply wraps HttpClient (i.e. takes one in its constructor), thus giving you a typed client to your API, while not owning the lifetime of any HttpClients, letting HttpClientFactory do its thing.

NSwag

Where does NSwag fit into this?

NSwag is, on one hand, like Swashbuckle, in that with a couple of lines of code you can have your ASP.NET Core application serving up a swagger doc and swagger ui within your app at runtime. Then it also has the functionality of AutoRest, where it can take a swagger file and generate a C# typed client for you (and other languages such as TypeScript).

There are a couple of things that impress me about NSwag:

  • The number of integrations - it's a swiss army knife! Just take a minute to read the README, you quickly get the idea, it can generate swagger or OpenAPI file not just at runtime, but at build time from your web api assembly. Then you can generate clients in various languages with lots of settings to play around with.
  • Rico Suter, who is the primary maintainer, is continuously contributing to it and is so responsive to issues, and checking in fixes and features. It's like he has a team running his GitHub account!

Starting with swagger.json

We are going to be generating a client from swagger.json, if you need to produce this from your own ASP.NET Core application then you can generate a swagger.json using the NSwag CLI, see here.

Generating the Client in Build

For this, we will use the NSwag.ConsoleCore CLI tool package (we could also use the NSwag.MSBuild package, the process is largely the same). I'm using the Pet Store swagger, and start by dropping the swagger.json into my project folder.

There are 2 ways to pass config into the NSwag commands, one is via command line args, the other is via a JSON config file which is what I'll be using here.

Firs create an empty library (here we will target netstandard2.0) and add the following snippet to an ItemGroup in your .csproj file:

<DotNetCliToolReference Include="NSwag.ConsoleCore" Version="12.0.15" />

After a dotnet restore we can now run in the project folder dotnet nswag new, this will create us a default config file, we can strip it down so it looks like this. Here's a trimmed down version:

{
  "runtime": "NetCore22",
  "defaultVariables": null,
  "swaggerGenerator":{
    "fromSwagger":{
      "json":"$(InputSwagger)"
    }
  },
  "codeGenerators": {
    "swaggerToCSharpClient": {
      ... OMITTING LOTS OF DEFAULT CONFIG ...
      "generateClientInterfaces": true,
      "injectHttpClient": true,
      "disposeHttpClient": false,
      "generateExceptionClasses": true,
      "exceptionClass": "$(ClientName)Exception",
      "useBaseUrl": false,
      "className": "$(ClientName)Client",
      "generateOptionalParameters": true,
      "generateJsonMethods": false,
      "namespace": "$(ClientNamespace)",
      "classStyle": "Poco",
      "output": "$(GeneratedSwaggerClientFile)"
    }
  }
}

Here are settings that I changed from their defaults:

  • generateClientInterfaces - This gives us an interface for our typed client, abstracting the implementation away. This may seem like something you wouldn't think of doing without, but there is merit in using the implementation of the typed client under test and mocking at the HttpClient level (shout out to httpclient-interception).
  • InjectHttpClient - This creates a constructor to the client that takes in a HttpClient, which is what we need to use it as a typed client with HttpClientFactory.
  • disposeHttpClient - It mostly doesn't matter if you call Dispose on HttpClient when used with HttpClientFactory, it's mostly a no-op. However, for correctness sake, it would be weird for the typed client to dispose of something it doesn't own.
  • generateOptionalParameters - By default NSwag will create 2 methods per operation, one with and one without a CancellationToken, enabling this will combine them.
  • generateJsonMethods - By default NSwag will add instance methods that serialize and deserialize types, they just delegate to one-liner static methods from Json.NET. I like my POCOs nice and plain, so I disable this.
  • useBaseUrl - This is an important one, we want to use the BaseAddress of HttpClient only, we don't want to override that with a property on the typed client (which is what will happen by default), so we set this to false. We can configure the BaseAddress at the time we register the typed client.
  • exceptionClass - The type of exception that is thrown on error, by default it would be SwaggerException, which feels like an implementation leak.
  • classStyle - By default NSwag uses the style Inpc which is short for INotifyPropertyChange, which is a strange default, I can only think it would be useful if you were building an MVVM client app and wanted to reuse the classes to bind to directly (still feels wrong to me). By selecting Poco, we end up with just a typical class with getter and setter properties.

You can see I use variables in this file, such as "$(InputSwagger)", we can pass these in via the nswag command.

What we want to do is to just do a dotnet build and to have the project automatically create the generated .cs file, and include it in the files to build. So here's the .csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <DebugType>embedded</DebugType>
        <EmbedAllSources>true</EmbedAllSources>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
        <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
        <DotNetCliToolReference Include="NSwag.ConsoleCore" Version="12.0.15" />
    </ItemGroup>

    <Target Name="GenerateNSwagClient">
        <PropertyGroup>
            <InputSwagger>swagger.json</InputSwagger>
            <ClientName>PetStore</ClientName>
            <GeneratedSwaggerClientFile Condition="'$(GeneratedSwaggerClientFile)' ==''">$(IntermediateOutputPath)$(MSBuildProjectName).$(ClientName)Client.cs</GeneratedSwaggerClientFile>
        </PropertyGroup>
        <Exec Command="dotnet nswag run nswag.json /variables:InputSwagger=$(InputSwagger),ClientName=$(ClientName),ClientNamespace=$(RootNamespace),GeneratedSwaggerClientFile=$(GeneratedSwaggerClientFile)" />
    </Target>

    <Target Name="IncludeNSwagClient" BeforeTargets="CoreCompile" DependsOnTargets="GenerateNSwagClient">
        <ItemGroup Condition="Exists('$(GeneratedSwaggerClientFile)')">
            <Compile Include="$(GeneratedSwaggerClientFile)" />
            <FileWrites Include="$(GeneratedSwaggerClientFile)" />
        </ItemGroup>
    </Target>
</Project>

Things to note:

  • The only thing in this file that is specific to the Pet Store swagger, is the <ClientName>PetStore</ClientName>, feel free to use this as a recipe for your own clients.
  • We hook our targets before CoreCompile, giving us the opportunity to add files to compile.
  • The generated file goes into the IntermediateOutputPath, this is the configuration-specific folder inside of obj, this is exactly how AssemblyInfo.cs is generated, and is likely already part of your .gitignore.
  • We have included the required Newtonsoft.Json and System.ComponentModel.Annotations packages.
  • Variables are passed into the nswag.json using the variables argument.
  • <DebugType>embedded</DebugType> and <EmbedAllSources>true</EmbedAllSources> gives us one .dll which contains an embedded PDB file, which contains all of the source within it for debugging purposes. You could also use symbol packages, which in my option is a step backwards from this solution.

Using the Client with HttpClientFactory

Right, we have our package/project, let's use it!

Let's create a new ASP.NET Core Web API using the template, and add a reference to our client project and add the Microsoft.Extensions.Http package. See here for an example.

Now we can register our client in the ConfigureServices method, like this:

services.AddHttpClient<IPetStoreClient, PetStoreClient>(c => c.BaseAddress = new Uri("https://petstore.swagger.io/v2/"));

And that's it! Now we can start using our client, here's an example from our controller:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    readonly IPetStoreClient petStoreClient;

    public ValuesController(IPetStoreClient petStoreClient)
    {
        this.petStoreClient = petStoreClient;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<string>>> Get(CancellationToken ct)
    {
        var stu = await petStoreClient.GetUserByNameAsync("Stu", ct);
        return new[] { stu.Email };
    }
}

You can see all of the code here.

πŸ’– πŸ’ͺ πŸ™… 🚩
stuartblang
Stuart Lang

Posted on March 18, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related