Opx.Api.Client 1.0.14

dotnet add package Opx.Api.Client --version 1.0.14
                    
NuGet\Install-Package Opx.Api.Client -Version 1.0.14
                    
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="Opx.Api.Client" Version="1.0.14" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Opx.Api.Client" Version="1.0.14" />
                    
Directory.Packages.props
<PackageReference Include="Opx.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 Opx.Api.Client --version 1.0.14
                    
#r "nuget: Opx.Api.Client, 1.0.14"
                    
#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 Opx.Api.Client@1.0.14
                    
#: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=Opx.Api.Client&version=1.0.14
                    
Install as a Cake Addin
#tool nuget:?package=Opx.Api.Client&version=1.0.14
                    
Install as a Cake Tool

Opx.Api.Client

Typed client wrapper for the Opx.Api.Web response contract.

Features

  • net10.0 only.
  • Async and sync API calls.
  • HttpClientFactory registration.
  • Named clients for multiple API sources.
  • OPX response wrapper: OpxApiResult<T>.
  • Result helpers: EnsureSuccess, GetDataOrThrow, GetDataOrDefault.
  • Route, query, body, custom header, and bearer token support.
  • Lazy bearer token auto-check and refresh.
  • Retry once on OPX 401.
  • Automatic per-operation X-Request-ID, preserved across an unauthorized retry.
  • CancellationToken overloads.
  • Fast JSON data parsing to <T>.
  • Configurable OPX body parsing for HTTP 4xx and 5xx responses.
  • Opt-in structured logging for the executed endpoint and result status code.
  • Streaming download for large files without buffering full content in memory.
  • Streaming multipart upload with a binary response, including retry-safe stream factories.
  • Native WebSocket client with typed messages, topics, ACK, heartbeat, automatic reconnect, and resubscribe.

Register with HttpClientFactory

builder.Services.AddOpxApiClient("http://localhost:5141");

Request ID behavior can be configured per client:

builder.Services.AddOpxApiClient(options =>
{
    options.BaseAddress = "https://api.server.com";
    options.GenerateRequestId = true;
    options.RequestIdHeaderName = "X-Request-ID";
});

To read OPX error content from actual HTTP 401, 403, 404, or 500 responses:

builder.Services.AddOpxApiClient(options =>
{
	options.BaseAddress = "https://api.server.com";
	options.ParseErrorResponseBody = true;
});

ParseErrorResponseBody defaults to true. Set it to false to use only the HTTP status and reason phrase for non-success responses.

Executed endpoint logging is disabled by default. Enable it per client when the HTTP method, endpoint path, and resulting OPX/HTTP status code are needed:

builder.Services.AddOpxApiClient(options =>
{
	options.BaseAddress = "https://api.server.com";
	options.EnableExecutedEndpointLogging = true;
});

The structured Information log includes HttpMethod, ExecutedEndpoint, and ResultStatusCode. The endpoint excludes its query string, and request headers and bodies are never logged by this feature.

Usage:

public sealed class ArtistService
{
	private readonly IOpxApiClient _client;

	public ArtistService(IOpxApiClient client)
	{
		_client = client;
	}

	public Task<OpxApiResult<List<ArtistDto>>> GetArtists()
	{
		return _client.GetAsync<List<ArtistDto>>("/api/artists");
	}

	public OpxApiResult<ArtistDto> GetArtist(int id)
	{
		return _client.Get<ArtistDto>("/api/artists/{id}", new OpxApiRequest
		{
			FromRoute = new { id }
		});
	}
}

Async and Sync

var asyncResult = await client.GetAsync<List<ArtistDto>>("/api/artists");
var syncResult = client.Get<List<ArtistDto>>("/api/artists");

var createdAsync = await client.PostAsync<ArtistDto>("/api/artists", new OpxApiRequest
{
	FromBody = new { name = "DeadSquad" }
});

var createdSync = client.Post<ArtistDto>("/api/artists", new OpxApiRequest
{
	FromBody = new { name = "DeadSquad" }
});

Named Clients

Register multiple API sources:

builder.Services.AddOpxApiClient("identity", "https://identity.server.com");
builder.Services.AddOpxApiClient("chinook", "https://chinook.server.com");

Use the factory:

public sealed class GatewayService
{
	private readonly IOpxApiClientFactory _factory;

	public GatewayService(IOpxApiClientFactory factory)
	{
		_factory = factory;
	}

	public async Task<UserDto> GetUserAsync(CancellationToken cancellationToken)
	{
		var identity = _factory.CreateClient("identity");
		var result = await identity.GetAsync<UserDto>("/api/users/me", null, cancellationToken);
		return result.GetDataOrThrow();
	}
}

Result Helpers

var result = await client.GetAsync<ArtistDto>("/api/artists/1");

result.EnsureSuccess();
var artist = result.GetDataOrThrow();
var fallback = result.GetDataOrDefault(new ArtistDto(0, "Unknown"));

CancellationToken

var result = await client.GetAsync<ArtistDto>(
	"/api/artists/{id}",
	new OpxApiRequest
	{
		FromRoute = new { id = 1 }
	},
	HttpContext.RequestAborted);

Bearer Token Auto Refresh

Register token provider once:

builder.Services.AddOpxInMemoryTokenProvider();
builder.Services.AddOpxApiClient("https://api.server.com");

After login, store the token:

await tokenProvider.SetTokenAsync(
	loginResult.AccessToken,
	loginResult.RefreshToken,
	loginResult.ExpiresAt);

Send device metadata in the login request so the authentication server can validate it and include the accepted values in the signed shared JWT:

var device = OpxClientDeviceMetadata.CreateDesktop(
    installationId,
    "Trust Desktop",
    "5.0.0");

var loginResult = await client.PostAsync<LoginToken>("/auth/login", new OpxApiRequest
{
    FromBody = new
    {
        userName,
        password,
        device
    }
});

var login = loginResult.GetDataOrThrow();
await tokenProvider.SetTokenAsync(
    login.AccessToken,
    login.RefreshToken,
    login.ExpiresAt);

Use CreateMobile for a mobile installation and CreateWeb for a browser session. Persist installationId securely; do not generate a new ID for every request or use a MAC address/hardware serial. Each device session should receive a different token, while all tokens share the same issuer and claim schema.

Requests will automatically attach the bearer token:

var result = await client.GetAsync<UserDto>("/api/users/me");

If the token is close to expiry, the provider refreshes before sending the request. If the API returns OPX status code 401 in either an HTTP 200 wrapper or an actual HTTP 401 response, the client refreshes once and retries once.

Manual bearer token still works and takes priority:

var result = await client.GetAsync<UserDto>("/api/users/me", new OpxApiRequest
{
	BearerToken = token
});

Override the generated request ID when an upstream operation already has one:

var result = await client.GetAsync<UserDto>("/api/users/me", new OpxApiRequest
{
    RequestId = upstreamRequestId
});

Streaming Download

Use DownloadAsync for large files such as updater ZIP packages:

await using var file = File.Create("update.zip");
var progress = new Progress<OpxDownloadProgress>(value =>
{
	if (value.Percent.HasValue)
	{
		Console.WriteLine($"{value.Percent.Value:N2}%");
	}
});

var result = await client.DownloadAsync(
	"/api/updates/{version}/package",
	file,
	new OpxApiRequest
	{
		FromRoute = new { version = "1.0.7" },
		Headers = new Dictionary<string, string?>
		{
			["X-Updater"] = "opx"
		}
	},
	progress,
	cancellationToken);

DownloadAsync uses HttpCompletionOption.ResponseHeadersRead, writes directly to the destination stream, supports custom headers and bearer token auto-refresh, and retries once on HTTP 401.

Multipart Upload With Streaming Response

Use PostDownloadAsync when a file upload returns a binary artifact. Input and output remain stream-based, so large documents do not need Base64 or full-memory buffering. OpenReadStream is a factory because an authentication retry must open a fresh source stream.

await using var destination = File.Create("report.pdf");
var result = await client.PostDownloadAsync(
	"api/v1/document-conversions",
	destination,
	new OpxApiRequest
	{
		BearerToken = token,
		FromMultipart = new OpxMultipartFormData
		{
			Fields = new Dictionary<string, string?>
			{
				["targetFormat"] = "Pdf"
			},
			Files =
			[
				new OpxMultipartFile
				{
					Name = "file",
					FileName = "report.docx",
					ContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
					OpenReadStream = () => File.OpenRead("report.docx")
				}
			]
		}
	},
	cancellationToken: cancellationToken);

WebSocket Client

Register a default or named realtime source:

builder.Services.AddOpxInMemoryTokenProvider();
builder.Services.AddOpxApiWebSocketClient("chinook-realtime", "https://api.server.com", options =>
{
	options.AutoReconnect = true;
	options.HeartbeatInterval = TimeSpan.FromSeconds(30);
	options.AckTimeout = TimeSpan.FromSeconds(10);
});

Create one stateful client for the consumer lifetime, connect, and subscribe:

await using var realtime = webSocketFactory.CreateClient("chinook-realtime");
await realtime.ConnectAsync("/ws/chinook", cancellationToken);
await realtime.SubscribeAsync("artists", cancellationToken);

var messageId = await realtime.SendAsync(
	"artist.watch",
	new { artistId = 1 },
	requireAck: true,
	cancellationToken: cancellationToken);

await foreach (var message in realtime.ReadAllAsync(cancellationToken))
{
	if (message.Type == "artist.updated")
	{
		var artist = message.GetData<ArtistDto>();
	}
}

The client uses the registered IOpxTokenProvider before the initial connection and each reconnect. It reconnects with exponential backoff and jitter, then restores all topic subscriptions. Application messages are not replayed automatically, preventing duplicate writes; use requireAck and retain the returned message ID when business-level retry is needed.

Connection health is available without network I/O:

var health = realtime.GetHealth();
Console.WriteLine($"connected={health.Connected}, reconnects={health.Reconnects}, pong={health.LastPongAt}");

Chinook Sample

Run the Chinook API first, then:

$env:CHINOOK_API_BASE_URL="http://localhost:5141"
dotnet run --project .\sample\ChinookClientSample\ChinookClientSample.csproj

Example output:

GET /api/artists => result=True, statusCode=200, count=275
GET /api/artists/1 => result=True, name=AC/DC
SYNC GET /api/artists/2 => result=True, name=Accept
GET /api/artists/with-albums => result=True, count=275
WS artist.watch => ack=<message-id>, artistId=1, reconnects=0

Test

dotnet test .\test\Opx.Api.Client.Tests.csproj

Run stress test with a live Chinook API:

$env:OPX_STRESS_BASE_URL="http://127.0.0.1:5141"
dotnet test .\test\Opx.Api.Client.Tests.csproj --filter "FullyQualifiedName~OpxApiClientStressTests" --logger "console;verbosity=detailed"

Recent smoke result on this PC:

500 concurrent requests without token provider: 667 ms, 1.33 ms/request
500 concurrent requests with token provider: 232 ms, 0.46 ms/request

Run parse performance test:

dotnet test .\test\Opx.Api.Client.Tests.csproj --filter "FullyQualifiedName~OpxApiClientParsePerformanceTests" --logger "console;verbosity=detailed"

Recent parse result:

Payload artists: 1000
Iterations: 200
Legacy JsonElement->T: 1307 ms
Fast data->T: 569 ms
Delta: 738 ms
Opx WebSocket client 500 acknowledged messages: 253 ms

Release Pack

.\pack-release.ps1

Set version manually:

.\pack-release.ps1 -VersionPrefix "1.0.1"
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 (1)

Showing the top 1 NuGet packages that depend on Opx.Api.Client:

Package Downloads
DbCloudConfig.Client

Secure client wrapper for the DB Cloud Config API using Opx.Api.Client.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.14 131 8/23/2026
1.0.13 183 8/17/2026
1.0.12 411 7/27/2026
1.0.11 195 7/18/2026
1.0.10 111 7/16/2026
1.0.9 130 7/15/2026
1.0.8 130 7/14/2026
1.0.7 113 7/13/2026