Skip to content

Generated clients ​

The hardened-web template writes src/Todos.Client, a project with no hand-written code. Its build generates a C# client from src/Todos/openapi/Todos.json, the document the library's build writes, and compiles it.

bash
dotnet build
dotnet pack src/Todos.Client -p:PackageVersion=1.2.0

The examples come from an application scaffolded with dotnet new hardened-web -n Todos. The name given with -n sets Todos in the paths, the namespaces and the client's name.

dotnet build in the solution writes the document, generates the client and compiles it. The default generator is Kiota, which writes the class TodosClient. With --client refit, Refitter writes the interface ITodosClient. Project templates covers the option.

dotnet pack on the client project writes a package. The Kiota client's only dependency is Microsoft.Kiota.Bundle 2.0.0. The Refit client's only dependency is Refit 16.3.0.

The OpenAPI document covers the file: what reaches it and how the build writes it.

The Kiota project ​

The project targets net8.0, whatever the library targets. It references the library with ReferenceOutputAssembly="false" and SkipGetTargetFrameworkProperties="true". The library builds first. The client compiles against none of it.

Kiota writes into obj/kiota/. The client class is in the namespace Todos.Client, and the models are in Todos.Client.Models.

.config/dotnet-tools.json pins the tool as microsoft.openapi.kiota 1.34.1. Directory.Packages.props pins the runtime as KiotaBundleVersion 2.0.0.

src/Todos.Client/Todos.Client.csproj declares five targets:

TargetRunsDoes
RestoreKiotaBefore CheckKiotaPinsRuns dotnet tool restore. A failure is HTPL002
CheckKiotaPinsBefore GenerateClientAsks the tool, with kiota info, which bundle version it expects. A mismatch with KiotaBundleVersion is HTPL003
GenerateClientBefore CoreCompileRuns kiota generate over the document into obj/kiota/
IncludeClientAfter GenerateClientAdds obj/kiota/**/*.cs to the compilation
CleanClientAfter CoreCleanDeletes obj/kiota/

The first build on a machine needs network, to restore the tool.

GenerateClient uses kiota-lock.json in obj/kiota/ as its up-to-date stamp. When the document, the tool manifest and Directory.Packages.props have not changed, the build skips the target:

xml
<Target Name="GenerateClient"
        DependsOnTargets="RestoreKiota;CheckKiotaPins"
        BeforeTargets="CoreCompile"
        Condition="'$(DesignTimeBuild)' != 'true'"
        Inputs="$(KiotaSpec);$(MSBuildProjectDirectory)/../../.config/dotnet-tools.json;$(MSBuildProjectDirectory)/../../Directory.Packages.props"
        Outputs="$(KiotaOutput)kiota-lock.json">
  <Exec Command="dotnet kiota generate --language CSharp --openapi &quot;$(KiotaSpec)&quot; --class-name TodosClient --namespace-name $(RootNamespace) --output &quot;$(KiotaOutput)&quot; --clean-output --exclude-backward-compatible --log-level warning"
        WorkingDirectory="$(MSBuildProjectDirectory)"
        IgnoreStandardErrorWarningFormat="true"
        EnvironmentVariables="KIOTA_OFFLINE_ENABLED=true;KIOTA_TUTORIAL_ENABLED=false;DOTNET_NOLOGO=1" />
</Target>

The project file sets KiotaSpec to ../Todos/openapi/Todos.json and KiotaOutput to $(BaseIntermediateOutputPath)kiota/.

The generated constructor sets the request adapter's BaseUrl to the document's first servers entry when the adapter has none. In the template that entry is http://localhost:5080, from [Server] on the library module.

Kiota warns when the document has no servers entry. It also warns when the document has more than one. It then uses the first. The build prints the warning and continues.

A consumer of the package sets BaseUrl to the service's address. Kiota's documentation covers the request adapter, authentication and registering the client in a container.

The Refit project ​

With --client refit, src/Todos.Client holds .refitter beside the project file:

json
{
  "openApiPath": "../Todos/openapi/Todos.json",
  "namespace": "Todos.Client",
  "contractsNamespace": "Todos.Client.Models",
  "naming": {
    "useOpenApiTitle": false,
    "interfaceName": "TodosClient"
  },
  "returnIApiResponse": true,
  "useCancellationTokens": true,
  "optionalParameters": true,
  "generateXmlDocCodeComments": true,
  "generateStatusCodeComments": true,
  "addAutoGeneratedHeader": true
}

Refitter reads .refitter and writes the interface and its models into obj/refitter/TodosClient.cs. Refit's source generator implements the interface in the same project. The models are in Todos.Client.Models.

returnIApiResponse makes each operation return Task<IApiResponse<T>>, or Task<IApiResponse> where the success has no body. useCancellationTokens adds the CancellationToken parameter. optionalParameters makes each optional parameter an optional C# argument. Without it, an optional header such as the If-None-Match that [ConditionalGet] adds became a required argument, and every existing call stopped compiling.

The Refit project also targets net8.0. It references the library the same way as the Kiota project.

.config/dotnet-tools.json pins the tool as refitter 2.2.1. Directory.Packages.props pins Refit 16.3.0. Nothing checks that the two agree.

The targets are RestoreRefitter, GenerateClient, IncludeClient and CleanClient. RestoreRefitter reports HTPL004 when the tool does not restore. GenerateClient runs again when the document, .refitter, a Liquid template or the tool manifest changes.

The generated interface carries no address. A consumer gives Refit the service's address when it creates the client. Refit's documentation covers creating the client.

Refitter's documentation and its repository cover the settings file.

Build errors ​

The client project reports these errors:

CodeReported whenMessage
HTPL002The Kiota tool does not restoreThe Kiota tool could not be restored, so the client cannot be generated. The pin is microsoft.openapi.kiota in .config/dotnet-tools.json; a fresh machine needs network for the first restore, and two builds restoring at once on one machine can fail it, each holding a file the other needs. dotnet tool restore in the solution folder shows what NuGet said.
HTPL003The tool and KiotaBundleVersion disagreeThe Kiota tool and the Kiota bundle disagree: microsoft.openapi.kiota in .config/dotnet-tools.json expects Microsoft.Kiota.Bundle 2.0.0, and KiotaBundleVersion in Directory.Packages.props is 2.1.2. The two move together; set both to one Kiota release.
HTPL004The Refitter tool does not restoreThe Refitter tool could not be restored, so the client cannot be generated. The pin is refitter in .config/dotnet-tools.json; a fresh machine needs network for the first restore, and two builds restoring at once on one machine can fail it, each holding a file the other needs. dotnet tool restore in the solution folder shows what NuGet said.

The HTPL003 message names the two versions that CheckKiotaPins found. The table shows the message for a KiotaBundleVersion of 2.1.2.

Keeping the document current ​

The template commits src/Todos/openapi/Todos.json and not the generated client. The build rewrites the file when the document changes. A change to a route, a model or a doc comment shows as a change to the file.

In CI, build, then fail when the file differs from the committed one:

bash
dotnet build
git diff --exit-code src/Todos/openapi

What each generator writes ​

Kiota and Refitter write different code for these parts of the document:

In the documentKiota 1.34.1Refitter 2.2.1
A success with a bodyThe method returns the model: Task<Todo?>Task<IApiResponse<Todo>>
An error status with a body, such as the 404 with NotFoundA model deriving from ApiException, thrown for that statusReturned rather than thrown, on the IApiResponse
A stream: an array under schema and the item under itemSchemaTask<Stream>, the raw bodyIAsyncEnumerable<Todo>, read one item at a time as the lines arrive
A Smithy @streaming union, as a oneOf of its membersA composed type, after a warning that the oneOf has no discriminator. The member's name is the SSE event: field, which is not in the data, so the document has no property to nameIAsyncEnumerable<PetAdopted>, the first member's type. Refit reads each event's data: as that type, so another member's own fields are lost without an error
Two cases at one status, as oneOfA composed type with one member per case, after a warning that the oneOf has no discriminatorThe first case's type
The operation's media typesAn Accept header listing them, each after the first with q=0.9: application/json, text/plain;q=0.9[Headers("Accept: application/json, text/plain")], in the document's order

The template's tests assert the 404 as Returns<NotFound<ClientModels.NotFound>>() with either generator. Typed clients covers the client in a test.

Any other generator reads the same file. A generator that refuses OpenAPI 3.2 reads a file written with <HardenedOpenApiOutputVersion>. The OpenAPI document covers that setting.

MessagePack clients ​

With --serializer message-pack-named or message-pack-keyed and --client refit, the client project also holds two Liquid templates in templates/, MessagePackContentSerializer.cs, and a reference to the MessagePack package, 3.1.9. .refitter then names the folder in customTemplateDirectory. It also sets generateDefaultAdditionalProperties to false.

The templates put these attributes on the generated models:

SerializerModelEach property
message-pack-keyed[MessagePack.MessagePackObject][MessagePack.Key(n)], where n is its x-message-pack-index
message-pack-keyed, for a schema without indexes, such as ErrorModel[MessagePack.MessagePackObject(true)]Keyed by name
message-pack-named[MessagePack.MessagePackObject(true)][MessagePack.Key("name")]

A Kiota project gets no templates. MessagePack covers the serializer and the wire.

Where the client and the service disagree ​

A client generated from the file inherits each place where the document and the service disagree. The OpenAPI document lists them.

A member named with a leading acronym is the one the Kiota client gets wrong with no error. The document publishes ID as iD. From the body {"id":7,"urlPath":"a"}, the client reads ID and URLPath as null.

A route registered at startup has no call in the client, because the file does not hold it. Registered routes covers those routes.

Next ​

PageCovers
The OpenAPI documentThe file, and what reaches it
Typed clientsThe generated client in a test
Project templatesThe --client option
MessagePackMessagePack on the wire

Released under the MIT License.