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.

Comments

Comment by aregaz

I was trying to use NSwag generated client once. There is NSwag Studio desktop app (Windows only) that allows you to setup all available options, by the way. And according to some answers in the internet, it might help with CI somehow.
But I ended up with my self-written API client since NSwag just didn't have enough flexibility for a couple pretty tricky cases.

aregaz
Comment by Mark Heath

yes, I still use self-written API clients in most cases, but its interesting to see the improvements that are being made for auto-generated clients

Mark Heath
Comment by David

Now how do you do this in Rider with the OpenAPI specification plugin?

David
Comment by Mark Heath

afraid I don't use Rider, but don't see why the NSwag technique I showed wouldn't work for it

Mark Heath
Comment by Frank

In the latest versions adding Name to HttpGet with a custom Route throws an exception. I avoided this by adding Name to Route. Adding Name to generate the operationid is a really great tip. Without it the client method names are awful.
Also if you go for Name="[Controller][Action]" do so without underscore between them otherwise the name is truncated again

Frank
Comment by Beantown Ace

Hey Mark. Do you have any examples of how you are using this startup setup within your service or controller layer for .Net Core? The AddHttpClient works when I am just setting up a external httpclient but I am using OpenApi 3.0 that has endpoint methods etc. available that were generated.
I tried this then added the interface in the constructor of a service but the base address is not sticking its just showing localhost when I new it up. I am using OpenAPI 3.0 spec that has endpoint methods etc. available for the client that are built from the generation of the service stubs so I have a ICustomerApi and CustomerApi that if I hit it direct in my code it works with "var client = new CustomerApi([base address]).

Beantown Ace
Comment by Stef Heyenrath

If you want to use a different Client Generator, you can take a look at https://github.com/StefH/Re...
With this Visual Studio Extension you can generate a RestEase compatible client (Interface & Models) based on a Swagger / OpenApi or RAML specification.

Stef Heyenrath