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