Rystem.RepositoryFramework.Api.Client 10.0.7

dotnet add package Rystem.RepositoryFramework.Api.Client --version 10.0.7
                    
NuGet\Install-Package Rystem.RepositoryFramework.Api.Client -Version 10.0.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Rystem.RepositoryFramework.Api.Client" Version="10.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rystem.RepositoryFramework.Api.Client" Version="10.0.7" />
                    
Directory.Packages.props
<PackageReference Include="Rystem.RepositoryFramework.Api.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rystem.RepositoryFramework.Api.Client --version 10.0.7
                    
#r "nuget: Rystem.RepositoryFramework.Api.Client, 10.0.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Rystem.RepositoryFramework.Api.Client@10.0.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rystem.RepositoryFramework.Api.Client&version=10.0.7
                    
Install as a Cake Addin
#tool nuget:?package=Rystem.RepositoryFramework.Api.Client&version=10.0.7
                    
Install as a Cake Tool

What is Rystem?

Rystem.RepositoryFramework.Api.Client

NuGet NuGet Downloads

.NET client adapter for APIs generated by Rystem.RepositoryFramework.Api.Server.

It lets you inject IRepository<T, TKey>, IQuery<T, TKey>, or ICommand<T, TKey> locally while the actual work happens over HTTP against a remote repository API.

Resources


Installation

dotnet add package Rystem.RepositoryFramework.Api.Client

The current package metadata in src/Repository/RepositoryFramework.Api.Client/RepositoryFramework.Api.Client.csproj is:

  • package id: Rystem.RepositoryFramework.Api.Client
  • version: 10.0.6
  • target framework: net10.0
  • HTTP packages: Microsoft.Extensions.Http, Microsoft.Extensions.Http.Polly, Polly

Package architecture

Area Purpose
WithApiClient(...) Register repository/query/command consumers backed by HTTP
RepositoryClient<T, TKey> Concrete client implementation that translates repository calls to HTTP requests
IApiRepositoryBuilder<T, TKey> Configure base address, path, version, server name, and server factory name
HttpClientRepositoryBuilder<T, TKey> Access IHttpClientBuilder and the built-in policy helper
Request interceptors Enrich outgoing HttpClient instances
Response interceptors Inspect responses and optionally retry requests
Bearer authorization helpers Provide token-based request enrichment and optional 401 retry flow

Mental model

This package is not a general REST client generator. It is a client for the wire format produced by RepositoryFramework.Api.Server.

That means it expects the server to expose routes and payload shapes like:

  • Get, Exist, Delete endpoints following repository conventions
  • Query/Stream and Batch/Stream endpoints
  • SerializableFilter, BatchOperations<T, TKey>, State<T, TKey>, and Entity<T, TKey> payloads

If the server follows the matching Rystem API-server conventions, the client can make the remote repository feel local.


Quick start

builder.Services.AddRepository<Product, int>(repositoryBuilder =>
{
    repositoryBuilder.WithApiClient(apiBuilder =>
    {
        apiBuilder
            .WithHttpClient("https://api.example.com")
            .ApiBuilder
            .WithStartingPath("api")
            .WithVersion("v1");
    });
});

public sealed class ProductService
{
    private readonly IRepository<Product, int> _repository;

    public ProductService(IRepository<Product, int> repository)
        => _repository = repository;

    public Task<Product?> GetAsync(int id)
        => _repository.GetAsync(id);
}

Registration patterns

WithApiClient(...) is available on repository, query, and command builders.

Full repository

builder.Services.AddRepository<Product, int>(repositoryBuilder =>
{
    repositoryBuilder.WithApiClient(apiBuilder =>
    {
        apiBuilder.WithHttpClient("https://api.example.com");
    });
});

Query only

builder.Services.AddQuery<Product, int>(queryBuilder =>
{
    queryBuilder.WithApiClient(apiBuilder =>
    {
        apiBuilder.WithHttpClient("https://api.example.com");
    });
});

Command only

builder.Services.AddCommand<Product, int>(commandBuilder =>
{
    commandBuilder.WithApiClient(apiBuilder =>
    {
        apiBuilder.WithHttpClient("https://api.example.com");
    });
});

WithApiClient(...) itself defaults to ServiceLifetime.Scoped.


IApiRepositoryBuilder<T, TKey>

Method Purpose
WithHttpClient(string domain) Register a named HttpClient; adds https:// automatically when missing
WithHttpClient(Action<HttpClient>) Configure the HttpClient directly
WithStartingPath(string) Override the base path, default api
WithVersion(string) Add a version segment
WithName(string) Override the remote model segment
WithServerFactoryName(string) Target a named repository registration on the server

WithHttpClient(string domain)

apiBuilder.WithHttpClient("localhost:7058");

If the string does not start with http:// or https://, the builder prepends https://.

WithHttpClient(Action<HttpClient>)

Use this overload when you want full control.

apiBuilder.WithHttpClient(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
    client.Timeout = TimeSpan.FromSeconds(30);
});

Route building

ApiClientSettings<T, TKey> builds the final paths from:

  • StartingPath, default api
  • optional Version
  • remote Name, default typeof(T).Name
  • optional remote FactoryName

The effective base route is:

{startingPath}/{version?}/{name}/{serverFactoryName?}

Examples:

  • api/Product/Get?key=42
  • api/v2/Product/Get?key=42
  • SuperApi/v2/calamityuser/Get?key=abc@example.com
  • api/Product/premium/Get?key=42

Named segments and factory segments are different concepts

  • WithName(...) changes the remote model segment
  • WithServerFactoryName(...) adds the remote server factory segment
  • the name parameter on WithApiClient(..., name: "local") is only the local DI factory name for your client registration

So a local named client and a remote named server repository are separate configuration knobs.


Request shapes used by the client

The client mirrors the server conventions from RepositoryFramework.Api.Server.

Non-JSON keys

For keys that are not treated as JSONable by KeySettings<TKey>:

  • GetAsync, ExistAsync, DeleteAsync use GET ...?key=...
  • InsertAsync, UpdateAsync use POST ...?key=... with body T

JSONable keys

For keys treated as JSONable:

  • GetAsync, ExistAsync, DeleteAsync send POST with body TKey
  • InsertAsync, UpdateAsync send POST with body Entity<T, TKey>

Query, batch, and operation

  • QueryAsync(...) posts a serialized filter to .../Query/Stream
  • BatchAsync(...) posts BatchOperations<T, TKey> to .../Batch/Stream
  • OperationAsync(...) posts a serialized filter to .../Operation?op=...&returnType=...

The client intentionally uses the streaming endpoints for query and batch operations, even when the caller later materializes the results into a list.


What RepositoryClient<T, TKey> actually does

The concrete client implementation maps local repository calls to HTTP like this:

  • GetAsync → GET or POST depending on key serialization
  • ExistAsync → GET or POST depending on key serialization
  • InsertAsync → POST
  • UpdateAsync → POST
  • DeleteAsync → GET or POST depending on key serialization
  • QueryAsync → POST to Query/Stream and DeserializeAsyncEnumerable<Entity<T, TKey>>
  • BatchAsync → POST to Batch/Stream and DeserializeAsyncEnumerable<BatchResult<T, TKey>>
  • OperationAsync → POST to Operation

The client expects 200 OK for success. Any other status code becomes an HttpRequestException containing the raw response body.

That is one reason this package works best against the matching Rystem API server rather than arbitrary REST endpoints.


Built-in HTTP policy helper

WithHttpClient(...) returns HttpClientRepositoryBuilder<T, TKey>.

repositoryBuilder.WithApiClient(apiBuilder =>
{
    apiBuilder
        .WithHttpClient("https://api.example.com")
        .WithDefaultRetryPolicy();
});

Important note about WithDefaultRetryPolicy()

Despite the method name, the built-in policy is not a retry policy.

It registers an advanced circuit breaker with these settings:

  • failure threshold: 50%
  • sampling duration: 10 seconds
  • minimum throughput: 10
  • break duration: 15 seconds

If you want retries, attach your own policy through ClientBuilder.

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .RetryAsync(3);

repositoryBuilder.WithApiClient(apiBuilder =>
{
    apiBuilder
        .WithHttpClient("https://api.example.com")
        .ClientBuilder
        .AddPolicyHandler(retryPolicy);
});

Request interceptors

Request interceptors let you mutate the HttpClient before the repository client starts using it.

Registration options:

services.AddApiClientInterceptor<MyGlobalInterceptor>();
services.AddApiClientSpecificInterceptor<Product, MyProductInterceptor>();
services.AddApiClientSpecificInterceptor<Product, int, MyProductKeyInterceptor>();

Interfaces:

public interface IRepositoryClientInterceptor
{
    Task<HttpClient> EnrichAsync(HttpClient client, RepositoryMethods path);
}

public interface IRepositoryClientInterceptor<T> : IRepositoryClientInterceptor { }
public interface IRepositoryClientInterceptor<T, TKey> : IRepositoryClientInterceptor { }

Important lifecycle nuance

In the current implementation, request interceptors do not run on every repository call.

They run the first time a given RepositoryClient<T, TKey> instance enriches its HttpClient, then that client is reused for the rest of the repository-client lifetime.

So request interceptors are a good fit for:

  • default auth headers
  • sticky headers
  • long-lived client setup

They are not a good fit for truly per-request values such as a fresh correlation ID on every repository method call.

The execution order is:

  1. global interceptors
  2. model-specific interceptors
  3. model-plus-key-specific interceptors

Response interceptors

Response interceptors run after the HTTP call returns and before the client enforces the final 200 OK success check.

Registration options:

services.AddApiClientResponseInterceptor<MyGlobalResponseInterceptor>();
services.AddApiClientResponseInterceptor<Product, MyProductResponseInterceptor>();
services.AddApiClientResponseInterceptor<Product, int, MyProductKeyResponseInterceptor>();

Interfaces:

public interface IRepositoryResponseClientInterceptor
{
    Task<HttpResponseMessage> CheckResponseAsync(
        HttpClient client,
        HttpResponseMessage response,
        Func<HttpClient, Task<HttpResponseMessage>> request);
}

public interface IRepositoryClientResponseInterceptor<T> : IRepositoryResponseClientInterceptor { }
public interface IRepositoryResponseClientInterceptor<T, TKey> : IRepositoryResponseClientInterceptor
    where TKey : notnull { }

Typical use cases:

  • retry after refreshing a token
  • logging failed responses
  • translating selected server failures before the client throws

The same ordering applies here: global, then model-specific, then model-plus-key-specific.


Bearer token helpers

The package includes a default bearer authenticator based on ITokenManager.

Implement ITokenManager

public sealed class MyTokenManager : ITokenManager
{
    public Task<string?> GetTokenAsync()
    {
        throw new NotImplementedException();
    }

    public Task<string?> RefreshTokenAsync()
    {
        throw new NotImplementedException();
    }

    public async Task EnrichWithAuthorizationAsync(HttpClient client)
    {
        var token = await GetTokenAsync();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);
    }
}

Register globally

services.AddDefaultAuthorizationInterceptorForApiHttpClient<MyTokenManager>();

Register for one model

services.AddDefaultAuthorizationInterceptorForApiHttpClient<Product, MyTokenManager>();

Register for one model and key

services.AddDefaultAuthorizationInterceptorForApiHttpClient<Product, int, MyTokenManager>();

Authenticator settings

services.AddDefaultAuthorizationInterceptorForApiHttpClient<MyTokenManager>(settings =>
{
    settings.Scopes = ["api.read", "api.write"];
    settings.ExceptionHandler = async (exception, serviceProvider) =>
    {
        await Task.CompletedTask;
    };
});

Important behavior notes

  • global bearer registration adds both a request interceptor and a response interceptor
  • model-specific bearer registration also adds both request and response interceptors
  • model-plus-key-specific bearer registration currently adds only the request interceptor, not the response interceptor

So automatic 401 Unauthorized refresh-and-retry is available in the global and model-specific registrations, but not fully wired in the model-plus-key-specific registration path.

Also note that AuthenticatorSettings.Scopes exists in the core client package but is not consumed by the core bearer interceptor itself. It becomes meaningful in the companion authentication packages that provide concrete token managers.


Bootstrap note

RepositoryClient<T, TKey> exposes BootstrapAsync() because the repository abstraction includes it.

However, the current client implementation simply performs:

GET {basePath}/Bootstrap

without sending a key.

The stock API-server bootstrap endpoint is currently wired through the key-binding path, so bootstrap interoperability is less polished than the rest of the client surface. In practice, prefer server-side WarmUpAsync() for startup bootstrap flows.


Practical examples from the repo

Point the client at a custom server path and version

The API tests register clients like this:

settings.WithApiClient(apiBuilder =>
{
    apiBuilder
        .WithVersion("v2")
        .WithStartingPath("SuperApi");
});

Match a renamed server route segment

The tests also use the client-side name override to match a server-side WithName<T>(...) route customization:

settings.WithApiClient(apiBuilder =>
{
    apiBuilder
        .WithName("calamityuser")
        .WithVersion("v2")
        .WithStartingPath("SuperApi");
});

Attach a custom Polly policy

The web client sample configures a retry policy through ClientBuilder:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .Or<TimeoutRejectedException>()
    .RetryAsync(3);

settings.WithApiClient(apiBuilder =>
{
    apiBuilder
        .WithHttpClient("localhost:7058")
        .ClientBuilder
        .AddPolicyHandler(retryPolicy);
});

Global bearer integration

The web client sample also registers the built-in authorization interceptor globally:

builder.Services.AddDefaultAuthorizationInterceptorForApiHttpClient(settings =>
{
    settings.Scopes = builder.Configuration["AzureAd:Scopes"]!.Split(' ');
});

Package Purpose
Rystem.RepositoryFramework.Abstractions Core repository contracts and DI registration
Rystem.RepositoryFramework.Api.Server The server package this client is designed to call
Rystem.RepositoryFramework.Api.Client.Authentication.BlazorServer Token-manager helpers for Blazor Server
Rystem.RepositoryFramework.Api.Client.Authentication.BlazorWasm Token-manager helpers for Blazor WebAssembly

If you are continuing through the repository area, this is the package to read right after src/Repository/RepositoryFramework.Api.Server/README.md.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Rystem.RepositoryFramework.Api.Client:

Package Downloads
Rystem.RepositoryFramework.Api.Client.Authentication.BlazorServer

Rystem.RepositoryFramework allows you to use correctly concepts like repository pattern, CQRS and DDD. You have interfaces for your domains, auto-generated api, auto-generated HttpClient to simplify connection "api to front-end", a functionality for auto-population in memory of your models, a functionality to simulate exceptions and waiting time from external sources to improve your implementation/business test and load test.

Rystem.RepositoryFramework.Api.Client.Authentication.BlazorWasm

Rystem.RepositoryFramework allows you to use correctly concepts like repository pattern, CQRS and DDD. You have interfaces for your domains, auto-generated api, auto-generated HttpClient to simplify connection "api to front-end", a functionality for auto-population in memory of your models, a functionality to simulate exceptions and waiting time from external sources to improve your implementation/business test and load test.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.7 84 3/26/2026
10.0.6 167,553 3/3/2026
10.0.5 187 2/22/2026
10.0.4 205 2/9/2026
10.0.3 147,968 1/28/2026
10.0.1 209,150 11/12/2025
9.1.3 376 9/2/2025
9.1.2 764,638 5/29/2025
9.1.1 97,903 5/2/2025
9.0.32 186,694 4/15/2025
9.0.31 5,928 4/2/2025
9.0.30 88,899 3/26/2025
9.0.29 9,101 3/18/2025
9.0.28 289 3/17/2025
9.0.27 285 3/16/2025
9.0.26 321 3/13/2025
9.0.25 52,171 3/9/2025
9.0.21 435 3/6/2025
9.0.20 19,665 3/6/2025
9.0.19 354 3/6/2025
Loading failed