You can generate up-to-date client code for your API, on solution build, leveraging the power of swagger and NSwag. This post is inspired by and builds on Generating HTTP API clients using Visual Studio Connected Services - .NET Blog (microsoft.com).
Prerequisites
- Visual Studio 2022
- Familiarity with writing a dotnet API
- Familiarity with Swashbuckle
Why
Writing client code involves a lot of boilerplate code and becomes tedious whenever you need to update your API and then mirror the required changes in your client code. By generating your client code through swagger and NSwag on build you no longer need to maintain the changes in the client code yourself and it encourages writing more descriptive API endpoints.
Demo
In this video, I give a quick demo; however, if you prefer written format, I cover the same material below in slightly greater depth.
Creating a Swagger Document
Creating any new API project in dotnet comes with Swagger turned on but it helps to also incorporate your comments on your endpoints. The better documented your endpoints are then the better the resulting generated code will be. Particularly helpful is providing attributes describing the resulting types that your endpoint could generate.
Once you have Swagger generating a descriptive Open API JSON document (which is what the Swagger UI pulls from) it is time to use the dotnet tool for swagger to generate this file for our solution files after building our API project. This can be managed by first adding the dotnet swagger tool to our project. If you don’t have a tool manifest in your project already, you can create one using
dotnet new tool-manifest
Then you install the swagger tool locally to this manifest with
dotnet tool install swashbuckle.aspnetcore.cli
Then add the following code to your csproj file to invoke the tool after building the project.
<Target Name="Generate OpenAPI Specification Document" AfterTargets="Build">
<Exec Command="dotnet tool run swagger tofile --output swagger.json $(OutputPath)$(AssemblyName).dll v1" ContinueOnError="true" />
</Target>
This will result in a swagger.json file being generated after build. This file can be included in your solution and importantly targeted by NSwag and the connected services part of Visual Studio.
Creating an HTTP Client
Now that you have your swagger.json it is time to generate your API client from it using NSwag. In Visual Studio, right click on connected services for your client project and select “Manage Connected Services”
Now under Service References, hit the plus button and then choose OpenAPI.
In this menu, we will be selecting our swagger.json file from our API project though it is worth noting that if the API you want to generate a client for is not one you managed and is a deployed Azure resource, you can just provide the URL to their hosted OpenAPI document and generate client code that way – though in my experience it does not automatically stay up to date like ours will.
Complete the rest of the dialogue, giving your generated code a namespace, class name, and choosing your preferred language. Once you finish you will have your class. At this point, updates to the swagger.json will get picked up when you rebuild your client project, however dotnet doesn’t know that your client project has this dependency on your API project when it builds. You can test this out by removing the swagger document and then building your solution. Both projects try to build but your client project fails because no swagger.json exists. To be clear about this dependency without including your API’s dlls in your client releases, we add the following project reference in your client csproj file.
<ProjectReference Include="..\TeamBuilder.GitHub\TeamBuilder.GitHub.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
Now, even if the swagger.json file exists, building the solution will now result in no errors as the API project will build first then the client project will build. This means that anytime you make a big change in your API project, building your solution can provide immediate insight on the downstream impact to your client code.
Extending Your Client
If the client code you’ve generated needs to serialize some tricky custom classes or perhaps do some custom FHIR serialization, don’t panic. You can add your own serializers and modify the serializer settings by leveraging the UpdateJsonSerializerSettings method provided in the generated code for just this purpose.
You can see in the generated code that this method is called during instantiation of the class but that the method itself is just a stub on the partial class.
namespace TeamBuilder.Serverless.Services
{
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.0.5.0 (NJsonSchema v10.0.22.0 (Newtonsoft.Json v11.0.0.0))")]
public partial class GitHubApiClient
{
private string _baseUrl = "";
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
public GitHubApiClient(string baseUrl, System.Net.Http.HttpClient httpClient)
{
BaseUrl = baseUrl;
_httpClient = httpClient;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}
public string BaseUrl
{
get { return _baseUrl; }
set { _baseUrl = value; }
}
protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
This means you can provide your own definition for the method in a partial class and overwrite the default JSON settings like so:
You can leverage similar extensibility options with the PrepareRequest and ProcessResponse methods or choose to overwrite methods entirely wherever you deem more manual control is required while still preserving a lot of work being done for you in creating a robust API client.
Conclusion
Now you can write microservices without a lot of the overhead of having to write and maintain client code for each new service all without losing control over the final client methods and regenerating the code is as easy hitting the hotkey for solution build.
Quick Plug
You can learn more about the project I do this in, Hackathon Team Builder, here.
Posted at https://sl.advdat.com/3KZ852Ehttps://sl.advdat.com/3KZ852E