Skip to main content
Version: Preview (0.3.0-preview.283)

Call an API with Refit, Kiota or NSwag

A client in the same solution as the API can share its value objects. A client generated from the OpenAPI document gets what the document describes, the underlying types and their rules, and can map them back.

Refit​

Refit works as is. Path and query values are formatted as the underlying text, a [Query] object flattens each value object to one key, and bodies go through the generated JSON converter. A response holding a value the type rejects surfaces as an ApiException wrapping the JsonException.

Kiota​

Kiota 1.35.0 generates primitives with no constraint, string? From and int? Qty, a C# enum for a closed set of strings, and has no option to map a schema onto a type of your own. Run against a document of this version, it names the members of that enum after the known values, France rather than FR, with a declared description as the member's summary; a closed set of numbers stays a number. A closed set taken as a parameter becomes an enum named after the operation, such as GetCountryQueryParameterType, beside the one named after the component. Its models are partial, so typed accessors can be added beside them:

public partial class Transfer
{
public Iban FromIban => Iban.Parse(From!);
}

Kiota was run against an earlier form of the document, in which a TimeSpan value object carried format: duration, which Kiota writes as an ISO 8601 duration, PT1H30M, that the server refuses, and in which the items of a List<Iban>, undescribed, gave an UntypedNode. The document now describes a TimeSpan value object as a string in its constant form, without a format, and the items of a collection and each parameter with the value object's schema, so a generated client types them from it; that was not re-run.

NSwag​

From the OpenAPI 3.1 document, NSwag 14.7.1 generates primitives carrying the rules as DataAnnotations: [StringLength(34, MinimumLength = 15)], [RegularExpression], [Range(1, 100)], and an enum for a closed set, strings and numbers alike, whose members it names after the known values. A client in the same solution can map the components back to the shared value objects with a type resolver, which was run end to end:

sealed class ValueObjectTypeResolver(CSharpGeneratorSettings settings, OpenApiDocument document, Dictionary<string, Type> valueObjects)
: CSharpTypeResolver(settings)
{
public override string Resolve(JsonSchema schema, bool isNullable, string? typeNameHint)
{
var target = schema.ActualSchema;
var name = document.Components.Schemas.FirstOrDefault(p => ReferenceEquals(p.Value, target)).Key;
return name is not null && valueObjects.TryGetValue(name, out var type)
? $"global::{type.FullName}" + (isNullable ? "?" : "")
: base.Resolve(schema, isNullable, typeNameHint);
}
}

ValueObjectRegistry.EnsureAssemblyRegistered(typeof(Iban).Assembly);
var byName = ValueObjectRegistry.GetRegistered().ToDictionary(d => d.ValueObjectType.Name, d => d.ValueObjectType);
var resolver = new ValueObjectTypeResolver(settings.CSharpGeneratorSettings, document, byName);
resolver.RegisterSchemaDefinitions(document.Components.Schemas
.Where(p => !byName.ContainsKey(p.Key))
.ToDictionary(p => p.Key, p => p.Value));
var code = new CSharpClientGenerator(document, settings, resolver).GenerateFile();

The probe forced the module initializer with RuntimeHelpers.RunModuleConstructor; EnsureAssemblyRegistered is the public call that does it. Prefer the 3.1 document: from 3.0, NSwag turns anyOf [integer, string] into empty wrapper classes. The type resolver maps components, and the document describes most parameters in place rather than as a reference to a component, so such a parameter is generated as its underlying type, with its rules (read from the document, not re-run).