Generate Code with NSwag
First, let me state this more precisely: this is a post about generating c# code for ASP.Net Core from an Open API definition at build time using NSwag. If you’re looking for steps to generate code by using the NSwag toolchain manually, you won’t find that here. If you’re looking for a way to generate an Open API definition from an existing ASP.Net Core app using the NSwag toolchain, you won’t find that here either. In that latter case though you’ll get a statement from me telling you that for a professional service you probably shouldn’t do that: you wouldn’t define your interfaces after making the implementation either, right?
The Goal
So what’s the goal then? Let’s start here: assume you already have an Open API definition and you want to provide an implementation using c# and ASP.Net Core. You’re probably already found out about NSwag and the various tools it offers, including things like NSwagStudio. That’s great if you’re doing this as a one off and if that’s all you’re looking for, then you’re probably already done.
The scenario I’m talking about is when you have one or multiple Open API definitions that might still be changing ever so much and you really don’t want to go to NSwagStudio to generate new controllers or client stubs every time the Open API changes. A bit more advanced users would create a script to run NSwag commands, and run the script when the Open API changes. Yet more advanced users would run the script as part of the build.
And here, I’m going to show you how to integrate the NSwag toolchain in your .csproj project files in a way that allows for a very scalable approach to generating code if and only if that is actually needed.
The Sample
I’ve decided to use the Weather Forecast API that comes with the default ASP.Net Core template for Web APIs. Let’s assume that we have two versions of that API:
- A v1 version that, much like the one from the template, provides temperatures in Celsius and Fahrenheit.
- A v2 version that provides temperatures in Kelvin only. This obviously constitutes a breaking change compared to the v1 version, so I’m using this to illustrate the case when one needs to maintain multiple versions of the same API for a while.
Let’s further assume that there’s a single implementation providing forecast data, so that data needs to be massaged / adapted according to the version of the API that’s invoked. We wouldn’t want to implement different backends for different versions of the API, but instead have the web API be the abstraction of the backend.
You will find the sources of the sample on GitHub. Feel free to copy, borrow, or ignore anything from that example.
The Most Important Parts in the Sample
So what really matters in the sample? Let’s start with the Open API definitions. These are the two files v1.yaml and v2.yaml each representing a separate version. The reason why I’m not putting multiple versions in a single file should be kind of obvious: at some point I probably want to retire older versions, and it’s a lot easier to do so when the definitions and implementations of different versions are clearly separated. The Open API definitions are pretty straightforward, so I’m not going into the details of those.
Next are references to all Open API definitions in the Web API project that is supposed to implement those APIs. You can find these in the SampleApi.csproj. They contain references to the actual files, plus some metadata describing some aspects of the code we want generated. There’s also a reference to the NSwag.MSBuild NuGet Package which is of importance too. This package has the toolchain that is actually needed to generate the code.
The sample CLI console application has a reference only to the v2 version of the API in its SampleCli.csproj. Other than that, it also needs the reference to the NSwag.MSBuild package the Web API uses too.
Last but not least is the Directory.Build.targets at the root of the repository. The build targets defined in that file are made available to all project files. So when you need to generate controllers (or client stubs) for multiple projects, having them centralized comes in quite handy, so you don’t need to duplicate this into every .csproj.
The Magic Targets
Actually, there’s nothing magic about the targets in Directory.Build.targets
.
It’s all pretty much straightforward. So let’s look at the targets one by one.
Target PrepareOpenApiItems
The purpose of this target, like the name suggests, is to prepare the Open API
items from the project for code generation. First, it defines some static
properties holding static parameters for the NSwag toolchain for code generation.
Strictly speaking, since these properties are static, they could also be defined
in <PropertyGroup />
elements outside of the target, but I like to keep
related things together. If you’re unhappy with the code that is produced (e.g.
nullable vs not), this is probably where you would change the behavior. For
details on which commands and parameters ara available, check the
documentation.
It then takes the OpenApiController
and OpenApiClient
items, generates some
more metadata for them, and makes the updated items available under the new name
_PreparedOpenApiItem
.
The metadata includes the full namespace definition, which is based off of the
project’s RootNamespace
(defined in the .csproj) as well as the
OpenApiController
’s or OpenApiClient
’s RelativeNamespace
metadata property.
The metadata for OutDir
and OutFile
are used to define which directory the
generated code for the item should be written to, and what that file’s (relative)
path should be.
Finally, it combines the intermediary _PreparedOpenApiController
and
_PreparedOpenApiClient
items the same collection of _PreparedOpenApiItem
items, such that a single command can be used to run code generation. The Args
metadata is used to feed the arguments to that command. These arguments are
different per target type (controller vs client), but by putting them into item
metadata, we won’t need to worry about that later anymore.
<Project>
<Target Name="PrepareOpenApiItems">
<PropertyGroup>
<NSwagControllerArgs>openapi2cscontroller</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /ControllerStyle:abstract</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /UseActionResultType:true</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /GenerateModelValidationAttributes:true</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /GenerateJsonMethods:false</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /RequiredPropertiesMustBeDefined:true</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /GenerateOptionalPropertiesAsNullable:true</NSwagControllerArgs>
<NSwagControllerArgs>$(NSwagControllerArgs) /GenerateNullableReferenceTypes:true</NSwagControllerArgs>
<NSwagClientArgs>openapi2csclient</NSwagClientArgs>
<NSwagClientArgs>$(NSwagClientArgs) /GenerateClientInterfaces:true</NSwagClientArgs>
<NSwagClientArgs>$(NSwagClientArgs) /ClientClassAccessModifier:internal</NSwagClientArgs>
<NSwagClientArgs>$(NSwagClientArgs) /GenerateJsonMethods:false</NSwagClientArgs>
</PropertyGroup>
<ItemGroup>
<_PreparedOpenApiController Include="@(OpenApiController)">
<Namespace>$(RootNamespace).Controllers.%(RelativeNamespace)</Namespace>
<OutDir>Controllers/$([System.String]::Copy('%(RelativeNamespace)').Replace('.','/'))</OutDir>
<OutFile>Controllers/$([System.String]::Copy('%(RelativeNamespace)').Replace('.','/'))/%(BaseClassPrefix)ControllerBase.g.cs</OutFile>
</_PreparedOpenApiController>
<_PreparedOpenApiClient Include="@(OpenApiClient)">
<Namespace>$(RootNamespace).%(RelativeNamespace)</Namespace>
<OutDir>$([System.String]::Copy('%(RelativeNamespace)').Replace('.','/'))</OutDir>
<OutFile>$([System.String]::Copy('%(RelativeNamespace)').Replace('.','/'))/%(ClassName).g.cs</OutFile>
</_PreparedOpenApiClient>
</ItemGroup>
<ItemGroup>
<_PreparedOpenApiItem Include="@(_PreparedOpenApiController)">
<Args>$(NSwagControllerArgs) /Namespace:%(Namespace) /Input:%(Identity) /Output:%(OutFile) /ClassName:%(BaseClassPrefix)</Args>
</_PreparedOpenApiItem>
<_PreparedOpenApiItem Include="@(_PreparedOpenApiClient)">
<Args>$(NSwagClientArgs) /Namespace:%(Namespace) /Input:%(Identity) /Output:%(OutFile) /ClassName:%(_PreparedOpenApiClient.ClassName)</Args>
</_PreparedOpenApiItem>
</ItemGroup>
</Target>
<!-- ... -->
</Project>
It may be useful to quickly walk through an example input OpenApiController
item and the _PreparedOpenApiController
and _PreparedOpenApiItem
items that
result from this. Assume the following input, and the RootNamespace
property
defined as Sample.Api
:
<ItemGroup>
<OpenApiController Include="../v1.yaml">
<BaseClassPrefix>Forecast</BaseClassPrefix>
<RelativeNamespace>V1</RelativeNamespace>
</OpenApiController>
</ItemGroup>
The target would first generate the following _PreparedOpenApiController
output. Note though that this output is not written to a file, it’s kept only
in-memory during build-time. The XML representation is only to help with
visualization.
<ItemGroup>
<_PreparedOpenApiController Include="../v1.yaml">
<BaseClassPrefix>Forecast</BaseClassPrefix>
<RelativeNamespace>V1</RelativeNamespace>
<Namespace>Sample.Api.Controllers.V1</Namespace>
<OutDir>Controllers/V1</OutDir>
<OutFile>Controllers/V1/ForecastControllerBase.g.cs</OutFile>
</_PreparedOpenApiController>
</ItemGroup>
From this, the following _PreparedOpenApiItem
would be produced (again, for
visualization only):
<ItemGroup>
<_PreparedOpenApiItem Include="../v1.yaml">
<BaseClassPrefix>Forecast</BaseClassPrefix>
<RelativeNamespace>V1</RelativeNamespace>
<Namespace>Sample.Api.Controllers.V1</Namespace>
<OutDir>Controllers/V1</OutDir>
<OutFile>Controllers/V1/ForecastControllerBase.g.cs</OutFile>
<Args>openapi2cscontroller /ControllerStyle:abstract /UseActionResultType:true /GenerateModelValidationAttributes:true /GenerateJsonMethods:false /RequiredPropertiesMustBeDefined:true /GenerateOptionalPropertiesAsNullable:true /GenerateNullableReferenceTypes:true /Namespace:Sample.Api.Controllers.V1 /Input:../v1.yaml /Output:Controllers/V1/ForecastControllerBase.g.cs /ClassName:Forecast</Args>
</_PreparedOpenApiItem>
</ItemGroup>
Simple enough, eh?
Target GenerateOpenApiCode
This target is used to actually generate code. It may appear a bit more complicated. That complexity comes mostly from the desire to support incremental builds / re-generate the code only when the input files have changed.
<Project>
<!-- ... -->
<Target Name="GenerateOpenApiCode" BeforeTargets="BeforeBuild"
DependsOnTargets="PrepareOpenApiItems"
Inputs="$(NSwagDir_Net60)/dotnet-nswag.dll;@(_PreparedOpenApiItem)"
Outputs="%(_PreparedOpenApiItem.OutFile)">
<Message Importance="Normal"
Text="Generate code for OpenAPI(s) @(_PreparedOpenApiItem) ..." />
<MakeDir Directories="%(_PreparedOpenApiItem.OutDir)" />
<Exec WorkingDirectory="$(ProjectDir)"
Command="$(NSwagExe_Net60) %(_PreparedOpenApiItem.Args)" />
<ItemGroup>
<Compile Include="%(_PreparedOpenApiItem.OutFile)" KeepDuplicates="false" />
</ItemGroup>
</Target>
<!-- ... -->
</Project>
But let’s start at the top.
- The
BeforeTargets
attribute defines that this target should automatically run right before theBeforeBuild
target is run. This let’s us hook into the predefined targets of the build and execute our target to generate code before the code is actually compiled. (Read more about it here if interested). - The
DependsOnTargets
attribute tells the build system that we first want thePrepareOpenApiItems
target to be run. We do this so we get the additional metadata for the Open API definitions we should generate code for. - The
Inputs
attribute is used by the build system to determine whether the target actually needs to run. It produces a semi-colon separated list of file paths (absolute and relative mixed). Each file’s last-modified timestamp is evaluated against the output files (see next attribute), and the target is run only for those items for which theInputs
timestamp(s) are more recent than theOutputs
timestamps. - The
Outputs
attribute defines what files the target produces. - The
<Message />
task is used only to produce a somewhat friendly message to people who sometimes read build logs. This message is shown during the build conditionally though, depending on the chosen verbosity of the build command. - The
<MakeDir />
task is needed to make sure that the directories to which the generated code files should be written really exist. - The
<Exec />
task actually executes the command using the NSwag toolchain. You can see that the actual command is constructed from theNSwagExe_Net60
property which is initialized by the NSwag.MSBuild toolchain and points at the .Net 6.0 toolset, as well as the arguments constructed for each item in thePrepareOpenApiItems
target. If you need to generate code for a different version of .Net, be sure to check the documentation for the correct property first. - The last
<ItemGroup />
finally is used to produce additional<Compile />
items pointing to the newly generated.g.cs
files in case these files are not picked up automatically, or in case you chose an exotic location out of the project’s tree for the files.
With this, when you run dotnet build
, the target will generate the code if the
input (the .yaml file or the toolchain’s main assembly) have changed, and it
will skip in every other case, not wasting cycles on anything that isn’t needed.
Maybe important to note, this also works for Open API definitions in JSON.
Target CleanupGeneratedOpenApiCode
Ever get the feeling that the generated code is outdated, but running
dotnet build
doesn’t regenerate it? Do not despair. Just run dotnet clean
,
followed by dotnet build
again. What that does? It runs the following target:
<Project>
<!-- ... -->
<Target Name="CleanupGeneratedOpenApiCode" BeforeTargets="Clean"
DependsOnTargets="PrepareOpenApiItems">
<Delete Files="%(_PreparedOpenApiItem.OutFile)" />
</Target>
</Project>
This target, as you can see, hooks into the Clean
target and demands to be
run just before it. It too depends on the PrepareOpenApiItems
target which is
needed to determine which files would be generated, then deletes them. If this
doesn’t help, for instance because you actually moved the .yaml or .json files
around, you might however need to delete generated files manually. The build
tools do not track your history for you.
Do NOT Commit Generated Code
This should go without saying, but sometimes people forget, don’t care, or just
don’t know. When you commit your generated code, it often pollutes history and
pull requests, but most of all, it’s simply not necessary. Skip committing it
in git
by adding the following to your .gitignore
file:
*.g.cs
More Ideas?
Right now the code generation targets are designed for a setup that I would typically start to work with. Parameters may need tweaking, or you may want or need to generate contracts separately from client stubs. All of that is possible as long as the NSwag toolchain supports it. If you have other interesting ideas on what could be shown, feel free to drop a note in the repo on GitHub.
Summary
Generate your code when its input has changed, and don’t commit it. 😃 Find the sources on GitHub.