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 ofHttpClient
s and their handlers, which historically has been problematic. - Typed Clients - This is a feature of
HttpClientFactory
which lets you register a class which simply wrapsHttpClient
(i.e. takes one in its constructor), thus giving you a typed client to your API, while not owning the lifetime of anyHttpClient
s, lettingHttpClientFactory
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 theHttpClient
level (shout out to httpclient-interception). -
InjectHttpClient
- This creates a constructor to the client that takes in aHttpClient
, which is what we need to use it as a typed client withHttpClientFactory
. -
disposeHttpClient
- It mostly doesn't matter if you callDispose
onHttpClient
when used withHttpClientFactory
, 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 aCancellationToken
, 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 theBaseAddress
ofHttpClient
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 tofalse
. We can configure theBaseAddress
at the time we register the typed client. -
exceptionClass
- The type of exception that is thrown on error, by default it would beSwaggerException
, which feels like an implementation leak. -
classStyle
- By default NSwag uses the styleInpc
which is short forINotifyPropertyChange
, 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 selectingPoco
, 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 ofobj
, this is exactly howAssemblyInfo.cs
is generated, and is likely already part of your.gitignore
. - We have included the required
Newtonsoft.Json
andSystem.ComponentModel.Annotations
packages. - Variables are passed into the
nswag.json
using thevariables
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.
Top comments (0)