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:

  1. A v1 version that, much like the one from the template, provides temperatures in Celsius and Fahrenheit.
  2. 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.

  1. The BeforeTargets attribute defines that this target should automatically run right before the BeforeBuild 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).
  2. The DependsOnTargets attribute tells the build system that we first want the PrepareOpenApiItems target to be run. We do this so we get the additional metadata for the Open API definitions we should generate code for.
  3. 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 the Inputs timestamp(s) are more recent than the Outputs timestamps.
  4. The Outputs attribute defines what files the target produces.
  5. 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.
  6. The <MakeDir /> task is needed to make sure that the directories to which the generated code files should be written really exist.
  7. The <Exec /> task actually executes the command using the NSwag toolchain. You can see that the actual command is constructed from the NSwagExe_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 the PrepareOpenApiItems target. If you need to generate code for a different version of .Net, be sure to check the documentation for the correct property first.
  8. 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.