Generate Refit interfaces from OpenAPI specifications on build time with MSBuild
Around 2 years ago, I created a tool called Refitter that can generate Refit interfaces and contracts from OpenAPI specifications. I have used Refitter in multiple projects and found it to be a valuable tool when working with REST APIs and primarilly use it as part of the build process.
I have tried multiple approaches to using Refitter to generate Refit interfaces and contracts from OpenAPI specifications at build time. My initial approach was to use Rosyln Source Generators to update the generated code. However, I found that using MSBuild was a more straightforward approach. This was because Source Generators run independently of each other, which means that the Refit Source Generators will not pickup the code generated by Refitter at compile time, resulting in an experience that requires to build the project twice.
To use Refitter from MSBuild you can do something like this in your .csproj
file
<Target Name="Refitter" AfterTargets="PreBuildEvent">
<Exec WorkingDirectory="$(ProjectDir)"
Command="refitter --settings-file .refitter --skip-validation" />
</Target>
What happens in the code above is that the Refitter will run before the build process starts, generating the Refit interfaces and contracts from the OpenAPI specification specified in the .refitter
file.
The code above also assumes that you already have Refitter installed as a global .NET Tool and that the project folder contains a file called .refitter
. This might not be the case if you’re running on a build agent from a CI/CD environment. In this case you might want to install Refitter as a local tool using a manifest file, as described in this tutorial
Installing Refitter as a local .NET tool would produce a dotnet-tools.json
file in the .config
folder, which you can use to restore the tool before running Refitter. This can be done by adding a dotnet tool restore
command before running Refitter.
An example of a .NET tool manifest file would be something like this:
{
"version": 1,
"isRoot": true,
"tools": {
"refitter": {
"version": "1.4.0",
"commands": [
"refitter"
]
}
}
}
To use the dotnet tool restore
command you can modify the MSBuild target to look like this:
<Target Name="Refitter" AfterTargets="PreBuildEvent">
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)"
Command="refitter --settings-file .refitter --skip-validation" />
</Target>
The dotnet build
process does will probably not have access to the package repository in which to download Refitter from, this is at least the case with Azure Pipelines and Azure Artifacts. To workaround this, you can provide a separate nuget.config
that only uses nuget.org
as a <packageSource>
.
Something like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
You might want to place the nuget.config
file in another folder to avoid using it to build the .NET project, then you can specify this when executing dotnet tool restore
<Target Name="Refitter" AfterTargets="PreBuildEvent">
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet tool restore --configfile refitter/nuget.config" />
<Exec WorkingDirectory="$(ProjectDir)"
Command="refitter --settings-file .refitter --skip-validation" />
</Target>
In the example above, the nuget.config
file is placed under the refitter
folder.
Files that are generated during the PreBuildEvent
are not automatically included in the project. To include the generated files, you can add them to the project file using the
ItemGroup` element. You can either specify the files directly or use a wild card to include all files with a specific extension, or files in a specific folder, or you can explicitly specify the files that should be included.
<Target Name="Refitter" AfterTargets="PreBuildEvent">
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet tool restore --configfile refitter/nuget.config" />
<Exec WorkingDirectory="$(ProjectDir)"
Command="refitter --settings-file .refitter --skip-validation" />
<ItemGroup>
<Compile Include="**\*.Generated.cs" />
</ItemGroup>
</Target>
In the example above, I know that Refitter will output files ending in .Generated.cs
, so I’m including all files with that extension. An issue with including generated files like this is that the compiler will complain if the file already exists, a way around this would be to use the Condition
parameter and only include the file if it doesn’t already exist. Something like this:
<Target Name="Refitter" AfterTargets="PreBuildEvent">
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet tool restore --configfile refitter/nuget.config" />
<Exec WorkingDirectory="$(ProjectDir)"
Command="refitter --settings-file .refitter --skip-validation" />
<ItemGroup>
<Compile Include="Petstore.cs"
Condition="!Exists('Petstore.cs')" />
</ItemGroup>
</Target>
When running into situations where the build fails due to Refitter failing to generate the files and no error is shown when running dotnet build
then you can use the --verbosity detailed
argument, dotnet build --verbosity detailed
or dotnet build -v d
for short. If for example, .refitter
contained an incorrect URL to the OpenAPI specifications document and the operation fails, the build output will look something like this:
$ dotnet build -v d
Restore complete (0.1s)
Determining projects to restore...
All projects are up-to-date for restore.
refitter-from-msbuild succeeded (2.1s) → bin/Debug/net9.0/refitter-from-msbuild.dll
Refitter v1.4.1.0
Support key: 7o5ljuh
Error: Response status code does not indicate success: 404 (Not Found).
Exception: System.Net.Http.HttpRequestException
Stack Trace:
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request,
CancellationToken cancellationToken)
at Refitter.Core.OpenApiDocumentFactory.GetHttpContent(String openApiPath) in
/_/src/Refitter.Core/OpenApiDocumentFactory.cs:line 100
at Refitter.Core.OpenApiDocumentFactory.CreateUsingNSwagAsync(String
openApiPath) in /_/src/Refitter.Core/OpenApiDocumentFactory.cs:line 49
at Refitter.Core.OpenApiDocumentFactory.CreateAsync(String openApiPath) in
/_/src/Refitter.Core/OpenApiDocumentFactory.cs:line 41
at Refitter.Core.RefitGenerator.GetOpenApiDocument(RefitGeneratorSettings
settings) in /_/src/Refitter.Core/RefitGenerator.cs:line 35
at Refitter.Core.RefitGenerator.CreateAsync(RefitGeneratorSettings settings)
in /_/src/Refitter.Core/RefitGenerator.cs:line 19
at Refitter.GenerateCommand.ExecuteAsync(CommandContext context, Settings
settings) in /_/src/Refitter/GenerateCommand.cs:line 48
####################################################################
# Consider reporting the problem if you are unable to resolve it #
# https://github.com/christianhelle/refitter/issues #
####################################################################
Build succeeded in 2.4s
That gives you a chance to figure out what went wrong, and perhaps report an issue to the Refitter repository. Once you resolve the problem, you can run dotnet build -v d
again and see that the build now succeeds. An output of something like this is what you want:
$ dotnet build -v d
Restore complete (0.1s)
Determining projects to restore...
All projects are up-to-date for restore.
refitter-from-msbuild succeeded (3.0s) → bin/Debug/net9.0/refitter-from-msbuild.dll
Refitter v1.4.1.0
Support key: 7o5ljuh
Output: /Users/christianhelle/projects/refitter-from-msbuild/Petstore.cs
Length: 27405 bytes
Duration: 00:00:02.1367434
###################################################################
# Do you find this tool useful and feel a bit generous? #
# https://github.com/sponsors/christianhelle #
# https://www.buymeacoffee.com/christianhelle #
# #
# Does this tool not work or does it lack something you need? #
# https://github.com/christianhelle/refitter/issues #
###################################################################
Build succeeded in 3.3s