Skip to content

Pragmatic.Client

Compile-time typed API clients generated from Pragmatic manifests. Zero reflection, zero hand-written HTTP code, full Result<T, IError> integration.

⚠️ Known limitations (tracked for resolution before the beta)

  • manifest.assembly requires a dot: the boundary name is derived by splitting assembly on . and taking the last segment. "Showcase.Booking"Booking, but "SampleBookingApi"SampleBookingApi (whole string). Always author the manifest with a dotted assembly path.
  • Generated code is not #nullable enable-clean: consumer projects with <TreatWarningsAsErrors>true</TreatWarningsAsErrors> need to NoWarn on CS8618 / CS8669 / CS8604 / CS8601 until the SG emits explicit nullable directives.
  • List<T> response types surface as object: the response type resolver doesn’t walk collection generics through the manifest’s type table yet. Workaround: model collection responses as a DTO wrapper ({ Items: GuestDto[] }) until resolved.

A runnable end-to-end demo lives in samples/Pragmatic.Client.Samples.

Consuming Pragmatic APIs from .NET clients (Blazor, MAUI, console apps, other services) requires:

  • Writing boilerplate HttpClient calls for every endpoint
  • Manually deserializing responses and mapping errors
  • Keeping client code in sync when the server API changes
  • Handling ProblemDetails (RFC 7807) error responses consistently

This leads to fragile, duplicated code that drifts from the actual API contract over time.

Pragmatic.Client uses a source generator that reads PragmaticManifest.json at compile time and generates:

Generated artifactPurpose
I{Boundary}ClientTyped interface with Result<T, IError> return types
{Boundary}HttpClientFull HttpClient implementation with error mapping
Request DTOssealed record per endpoint with request body
Response DTOssealed record per entity/DTO in the manifest
EnumsEnum types from the manifest
Error typesIError-implementing records with typed error codes
DI registrationAdd{Boundary}Client(baseUrl) extension method

Two discovery modes ensure the manifest reaches the generator:

  • Mode A (AdditionalFiles): drop a manifest.json or PragmaticManifest.json in the project
  • Mode B (Assembly refs): reference the domain assembly with Private="false" and the SG reads [PragmaticMetadata] attributes automatically

Mode A takes priority. Mode B enables zero-file-sync: just reference the server project and the client is always up to date.

<PackageReference Include="Pragmatic.Client" />
<PackageReference Include="Pragmatic.Client.SourceGenerator"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

Place a PragmaticManifest.json in your client project and register it as an additional file:

<ItemGroup>
<AdditionalFiles Include="PragmaticManifest.json" />
</ItemGroup>

Or use Mode B by referencing the domain assembly directly:

<ProjectReference Include="..\Showcase.Booking\Showcase.Booking.csproj"
Private="false" ReferenceOutputAssembly="true" />
Program.cs
services.AddBookingClient("https://api.example.com");
// Inject and call
public class GuestPage(IBookingClient client)
{
public async Task LoadGuest(Guid id)
{
var result = await client.GetGuest(id);
if (result.IsSuccess)
Console.WriteLine($"Guest: {result.Value.FirstName}");
else
Console.WriteLine($"Error: {result.Error.Title}");
}
}
  • Compile-time generation — no runtime reflection, no dynamic proxies
  • Result-based error handling — every method returns Result<T, IError> or VoidResult<IError>, never throws
  • ProblemDetails error mapping — server errors are deserialized and matched to typed IError records via error code switch
  • Typed error records with extensions — error-specific context (e.g., ConflictError.ConflictingId) flows through ProblemDetails extensions
  • Named HttpClient — uses IHttpClientFactory pattern via AddHttpClient<TInterface, TImpl>
  • Boundary filtering — generate clients for specific boundaries via PragmaticClientBoundaries MSBuild property
  • Dual discovery — manifest from file (Mode A) or assembly attribute (Mode B)
  • DTO generation — entities, DTOs, and enums from the manifest become local types in the client namespace

Generate clients only for specific boundaries:

<PropertyGroup>
<PragmaticClientBoundaries>Booking;Billing</PragmaticClientBoundaries>
</PropertyGroup>

This filters endpoints by operationId prefix, generating only matching clients.

services.AddBookingClient("https://api.example.com", client =>
{
client.DefaultRequestHeaders.Add("X-Api-Key", "my-key");
client.Timeout = TimeSpan.FromSeconds(30);
});
PackageTargetDescription
Pragmatic.Clientnet10.0Runtime: ApiError, PragmaticClientException
Pragmatic.Client.SourceGeneratornetstandard2.0Source generator: reads manifests, generates typed clients
PackageDepends on
Pragmatic.ClientPragmatic.Abstractions, Pragmatic.Result
Pragmatic.Client.SourceGeneratorSystem.Text.Json (compile-time only)
  • .NET 10 (runtime)
  • C# 14 / Roslyn 4.12+ (source generator)
  • A PragmaticManifest.json (Mode A) or domain assembly reference with [PragmaticMetadata] (Mode B)
Pragmatic.Client/
├── src/
│ ├── Pragmatic.Client/ # Runtime helpers
│ │ ├── ApiError.cs # Generic ProblemDetails → IError record
│ │ └── PragmaticClientException.cs # Exception for unmappable responses
│ └── Pragmatic.Client.SourceGenerator/ # Compile-time generator
│ └── PragmaticClientGenerator.cs # Manifest → typed client code
└── tests/
└── Pragmatic.Client.Tests/
└── Generator/ # Snapshot tests for generated output

Part of the Pragmatic.Design ecosystem — see Licensing. Pragmatic.Client is licensed under the PolyForm Small Business 1.0.0 license (free for small businesses; commercial license above the threshold).