0 Comments Posted in:

Many of you will know that with ASP.NET Core, it's really easy to generate OpenAPI (Swagger) documentation. In fact, it's now a part of the default template for a web API. If you type dotnet new webapi you'll get a project that already references the Swashbuckle.AspNetCore NuGet package which will give you a nice webpage showing all the endpoints in your API and letting you test them easily

Swagger UI

There's also a link on this page to a swagger.json document which describes your API in such a way that a client could be automatically generated from it.

Not this again!

Now, auto-generated clients is something that I've been burned by in the past. The tooling to generate them was often complex, painful to automate and resulted in ugly generated code that was never flexible enough to do what I wanted. And so I've avoided using it for a long time.

However, with the rise of gRPC, it seems auto-generated clients are making something of a comeback, and the latest Visual Studio can generate clients for both gRPC and OpenAPI sevices now.

Add Service Reference

I recently needed to make a quick demo app where one ASP.NET Core web application needed to call into an ASP.NET Core web API, and it seemed an ideal opportunity for me to give auto-generated clients another chance. It mostly was a smooth experience, but I did run into a couple of minor issues, so I thought I'd document my findings here.

Adding a Service Reference in Visual Studio

Adding a service reference in Visual Studio is very easy. Select the project in Solution Explorer and choose Project | Add Service Reference. From here you can choose whether to add a reference for an OpenAPI or gRPC service.

If you choose OpenAPI you have the option of either pointing directly to a Swagger document, or accessing one via a URL. I chose a local Swagger JSON file, which I'd saved to disk by visiting the Swagger page for my web API and downloading the JSON.

We'll discuss the configuration options shortly, but if just you accept the defaults you end up with something like the following in your csproj file:

  <ItemGroup>
    <OpenApiReference Include="..\api\swagger-v1.json">
      <CodeGenerator>NSwagCSharp</CodeGenerator>
      <Link>OpenAPIs\swagger-v1.json</Link>
    </OpenApiReference>
  </ItemGroup>

Customising the generated code

How do we use the generated client? Well, first we need to know what it's called. We can actually see the code if we navigate into our obj folder. In my example, the file was called swagger-v1Client.cs, and the name of the generated class was swagger_v1Client which was not what I wanted.

What's more, the names of the methods on the client were also not what I would consider to be intuitive names. So let's see a few ways to improve the generated code.

My first tip, is that your Swagger file should include an operationId for each method. This is achieved by providing a Name property in the method attributes in the web API. Here's a simple example:

[HttpGet(Name=nameof(Get))]
public IEnumerable<WeatherForecast> Get()

Second, in the csproj file we can specify a number of options. For example, we can control the class name, namespace, and output path of the generated file, by adding extra properties to the OpenApiReference node:

<Namespace>Weather</Namespace>
<ClassName>WeatherServiceClient</ClassName>
<OutputPath>WeatherServiceClient.cs</OutputPath>

Finally, there are two more things I want to change about the generated client. First, I want an interface, allowing unit tests to mock the client if necessary. And second, by default the constructor takes two parameters - a base URL and a HttpClient. The base URL gets in the way of us easily registering this client in our DI container, so I want to turn that off and configure the base URL a different way.

Both of these changes can be achieved by customising the NSwag options. (NSwag is the tool that is generating the client). The options I changed were setting /UseBaseUrl to false, and /GenerateClientInterfaces to true. And we can set these with an options property:

<Options>/UseBaseUrl:false /GenerateClientInterfaces:true</Options>

Here's what the full configuration in the csproj file looks like after these customizations:

  <ItemGroup>
    <OpenApiReference Include="..\api\swagger-v1.json">
      <CodeGenerator>NSwagCSharp</CodeGenerator>
      <Link>OpenAPIs\swagger-v1.json</Link>
      <Namespace>Weather</Namespace>
      <ClassName>WeatherServiceClient</ClassName>
      <OutputPath>WeatherServiceClient.cs</OutputPath>
      <Options>/UseBaseUrl:false /GenerateClientInterfaces:true</Options>
    </OpenApiReference>
  </ItemGroup>

Note that Visual Studio sometimes seemed reluctant to re-generate the client even though I had changed the options. Deleting the file from the obj folder seemed to get it working again.

Registering the client

The final step is that I want to use the AddHttpClient method in my Startup.ConfigureServices method. (Check out Steve Gordon's series on HttpClientFactory for more information on why this is a good idea).

This allows us to set the base address for the client after it has been created, fetching the value from configuration:

services.AddHttpClient<IWeatherServiceClient, WeatherServiceClient>(
    (provider, client) => {
    client.BaseAddress = new Uri(Configuration.GetValue(
        "WeatherServiceBaseAddress", "https://localhost:44369/"));
});

With this step completed, any controller or Razor page that needs to access the web API can just take a dependency on IWeatherServiceClient.

Vote on HN