VOOZH about

URL: https://www.nuget.org/packages/Davasorus.Utility.DotNet.Api/

⇱ NuGet Gallery | Davasorus.Utility.DotNet.Api 2026.2.3.3




Davasorus.Utility.DotNet.Api 2026.2.3.3

dotnet add package Davasorus.Utility.DotNet.Api --version 2026.2.3.3
 
 
NuGet\Install-Package Davasorus.Utility.DotNet.Api -Version 2026.2.3.3
 
 
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="Davasorus.Utility.DotNet.Api" Version="2026.2.3.3" />
 
 
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Davasorus.Utility.DotNet.Api" Version="2026.2.3.3" />
 
Directory.Packages.props
<PackageReference Include="Davasorus.Utility.DotNet.Api" />
 
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 Davasorus.Utility.DotNet.Api --version 2026.2.3.3
 
 
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Davasorus.Utility.DotNet.Api, 2026.2.3.3"
 
 
#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 Davasorus.Utility.DotNet.Api@2026.2.3.3
 
 
#: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=Davasorus.Utility.DotNet.Api&version=2026.2.3.3
 
Install as a Cake Addin
#tool nuget:?package=Davasorus.Utility.DotNet.Api&version=2026.2.3.3
 
Install as a Cake Tool
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

Davasorus.Utility.DotNet.Api

A comprehensive .NET 8+ API client library providing strongly-typed clients for REST, GraphQL, gRPC, WebSocket, SignalR, and SSE protocols. Features built-in logging, error reporting, and OpenTelemetry integration. Designed for Tyler Technologies' Enterprise Public Safety suite with production-grade features including connection pooling, compression, auto-reconnect, and automatic error handling.

Overview

Davasorus.Utility.DotNet.Api provides six protocol types, each with a single unified interface where authentication is optional:

  • REST - HTTP REST APIs with optional authentication
  • GraphQL - GraphQL query and mutation support
  • gRPC - High-performance RPC with channel pooling
  • WebSocket - Full-duplex communication with auto-reconnect
  • SignalR - Real-time hub connections with auto-reconnect
  • SSE - Server-Sent Events streaming

Each protocol has one client interface and one service interface. Pass token/authType/usage to authenticate, or omit them for unauthenticated requests.

All services include:

  • Automatic error handling with configurable SQS error reporting
  • Structured logging with correlation IDs
  • OpenTelemetry tracing with Activity propagation
  • Connection pooling and HTTP compression
  • Generic and string-based response methods
  • Consolidated authentication (Bearer, Basic, API Key, Custom)

Architecture Note: Only interact with Service interfaces in your application code. Services handle all Client interactions, error logging, and telemetry internally.

Installation

NuGet Package Manager

Install-Package Davasorus.Utility.DotNet.Api

.NET CLI

dotnet add package Davasorus.Utility.DotNet.Api

PackageReference

<PackageReference Include="Davasorus.Utility.DotNet.Api" Version="*" /> 

Quick Start

Basic Setup with Dependency Injection

using Tyler.Utility.Api.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Register all API client services with default configuration
builder.Services.AddApiClient();

var app = builder.Build();

This registers one service per protocol:

  • REST: IApiService
  • GraphQL: IGraphQLService
  • gRPC: IGrpcService
  • WebSocket: IWebSocketService
  • SignalR: ISignalRService
  • SSE: ISseService

Configuration

ApiClientOptions Properties

Property Type Default Description
HttpClientTimeoutSeconds int 100 HTTP client timeout duration
MaxConnectionsPerServer int 10 Maximum concurrent connections
PooledConnectionLifetimeMinutes int 5 HTTP handler pooled connection lifetime for DNS rotation
EnableResponseCompression bool true Enable GZip/Deflate compression
EnableErrorReporting bool true Enable/disable SQS error reporting
GrpcDeadlineSeconds int 30 gRPC call deadline
GrpcMaxReceiveMessageSize int 4MB Max gRPC receive message size
GrpcMaxSendMessageSize int 4MB Max gRPC send message size
WebSocketConnectTimeoutSeconds int 30 WebSocket connection timeout
WebSocketKeepAliveIntervalSeconds int 30 WebSocket keep-alive interval
WebSocketReceiveBufferSize int 4096 WebSocket receive buffer size in bytes
WebSocketAutoReconnect bool true Enable WebSocket auto-reconnect
WebSocketMaxReconnectAttempts int 5 Max WebSocket reconnect attempts
WebSocketReconnectDelayMs int 1000 Delay (ms) between WebSocket reconnect attempts
SignalRServerTimeoutSeconds int 30 SignalR server timeout
SignalRKeepAliveIntervalSeconds int 15 SignalR keep-alive interval
SignalRAutoReconnect bool true Enable SignalR auto-reconnect
SseConnectTimeoutSeconds int 30 SSE connection timeout
SseReadTimeoutSeconds int 300 SSE read timeout for long-poll streams
SseAutoReconnect bool true Enable SSE auto-reconnect
SseMaxReconnectAttempts int 10 Max SSE reconnect attempts
EnableRetry bool true Enable automatic retry with exponential backoff
MaxRetryAttempts int 3 Maximum retry attempts for transient failures
RetryBaseDelayMs int 500 Base delay (ms) for exponential backoff
RetryMaxDelayMs int 10000 Maximum delay (ms) between retries
RetryOnServerErrors bool true Retry on HTTP 5xx server errors

Fluent Configuration

builder.Services.AddApiClient(api => api
 .WithTimeout(TimeSpan.FromSeconds(120))
 .WithMaxConnectionsPerServer(20)
 .WithCompression()
 .WithErrorReporting()
 // gRPC options
 .WithGrpcDeadline(TimeSpan.FromSeconds(60))
 .WithGrpcMaxReceiveMessageSize(8 * 1024 * 1024)
 .WithGrpcMaxSendMessageSize(8 * 1024 * 1024)
 // WebSocket options
 .WithWebSocketConnectTimeout(TimeSpan.FromSeconds(30))
 .WithWebSocketKeepAliveInterval(TimeSpan.FromSeconds(30))
 .WithWebSocketReceiveBufferSize(8192)
 .WithWebSocketAutoReconnect(enabled: true, maxAttempts: 10, delayMs: 1000)
 // SignalR options
 .WithSignalRServerTimeout(TimeSpan.FromSeconds(30))
 .WithSignalRKeepAliveInterval(TimeSpan.FromSeconds(15))
 .WithSignalRAutoReconnect(true)
 // SSE options
 .WithSseConnectTimeout(TimeSpan.FromSeconds(30))
 .WithSseReadTimeout(TimeSpan.FromMinutes(5))
 .WithSseAutoReconnect(enabled: true, maxAttempts: 10)
 // Retry/resilience options
 .WithRetry(enabled: true, maxAttempts: 3, baseDelayMs: 500)
 .WithRetryMaxDelay(10000)
 .WithRetryOnServerErrors(true)
);

appsettings.json Configuration

{
 "ApiClientOptions": {
 "HttpClientTimeoutSeconds": 120,
 "MaxConnectionsPerServer": 15,
 "PooledConnectionLifetimeMinutes": 5,
 "EnableResponseCompression": true,
 "EnableErrorReporting": true,
 "GrpcDeadlineSeconds": 60,
 "GrpcMaxReceiveMessageSize": 4194304,
 "GrpcMaxSendMessageSize": 4194304,
 "WebSocketConnectTimeoutSeconds": 30,
 "WebSocketKeepAliveIntervalSeconds": 30,
 "WebSocketReceiveBufferSize": 4096,
 "WebSocketAutoReconnect": true,
 "WebSocketMaxReconnectAttempts": 5,
 "WebSocketReconnectDelayMs": 1000,
 "SignalRServerTimeoutSeconds": 30,
 "SignalRKeepAliveIntervalSeconds": 15,
 "SignalRAutoReconnect": true,
 "SseConnectTimeoutSeconds": 30,
 "SseReadTimeoutSeconds": 300,
 "SseAutoReconnect": true,
 "SseMaxReconnectAttempts": 10,
 "EnableRetry": true,
 "MaxRetryAttempts": 3,
 "RetryBaseDelayMs": 500,
 "RetryMaxDelayMs": 10000,
 "RetryOnServerErrors": true
 }
}

REST API Usage

All REST operations use a single IApiService interface. Authentication is controlled by optional parameters.

Unauthenticated Request

public class WeatherService
{
 private readonly IApiService _apiService;

 public WeatherService(IApiService apiService)
 {
 _apiService = apiService;
 }

 public async Task<WeatherForecast?> GetForecastAsync()
 {
 // No token = unauthenticated request
 return await _apiService.GetAsync<WeatherForecast>(
 "https://api.weather.com",
 "forecast/daily"
 );
 }
}

Authenticated Requests

public class UserService
{
 private readonly IApiService _apiService;

 public UserService(IApiService apiService)
 {
 _apiService = apiService;
 }

 // Bearer token (default when authType is omitted)
 public async Task<User?> GetUserAsync(int id, string bearerToken)
 {
 return await _apiService.GetAsync<User>(
 "https://api.example.com",
 $"users/{id}",
 bearerToken // Authorization: Bearer {token}
 );
 }

 // Basic authentication
 public async Task<User?> GetWithBasicAuthAsync(string username, string password)
 {
 return await _apiService.GetAsync<User>(
 "https://api.example.com",
 "users/me",
 $"{username}:{password}",
 "Basic" // Authorization: Basic {base64}
 );
 }

 // API Key authentication
 public async Task<User?> GetWithApiKeyAsync(string apiKey)
 {
 return await _apiService.GetAsync<User>(
 "https://api.example.com",
 "users/me",
 apiKey,
 "X-API-Key", // Header: X-API-Key: {apiKey}
 "MyApp" // Header: X-Usage: MyApp
 );
 }

 // POST with typed request and response
 public async Task<UserResponse?> CreateUserAsync(CreateUserRequest request, string token)
 {
 return await _apiService.PostAsync<CreateUserRequest, UserResponse>(
 "https://api.example.com",
 "users",
 request,
 token
 );
 }

 // With cancellation support
 public async Task<User?> GetUserWithTimeoutAsync(int id, string bearerToken)
 {
 using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
 return await _apiService.GetAsync<User>(
 "https://api.example.com",
 $"users/{id}",
 bearerToken,
 cancellationToken: cts.Token
 );
 }
}

GraphQL API Usage

public class UserGraphQLService
{
 private readonly IGraphQLService _graphqlService;

 public UserGraphQLService(IGraphQLService graphqlService)
 {
 _graphqlService = graphqlService;
 }

 // Authenticated query
 public async Task<UserQueryResponse?> GetUsersAsync(string token)
 {
 var query = @"
 query GetUsers($limit: Int!) {
 users(limit: $limit) {
 id
 name
 email
 }
 }";

 var response = await _graphqlService.ExecuteQueryAsync<UserQueryResponse>(
 "https://api.example.com/graphql",
 query,
 token,
 variables: new { limit = 10 }
 );

 return response?.Data;
 }

 // Unauthenticated query
 public async Task<StatusResponse?> GetStatusAsync()
 {
 var response = await _graphqlService.ExecuteQueryAsync<StatusResponse>(
 "https://api.example.com/graphql",
 "{ status { healthy version } }"
 );

 return response?.Data;
 }

 // Mutation
 public async Task<CreateUserResponse?> CreateUserAsync(string name, string email, string token)
 {
 var request = new GraphQLRequest
 {
 Query = @"
 mutation CreateUser($input: CreateUserInput!) {
 createUser(input: $input) {
 id
 name
 email
 }
 }",
 Variables = new { input = new { name, email } }
 };

 var response = await _graphqlService.ExecuteMutationAsync<CreateUserResponse>(
 "https://api.example.com/graphql",
 request,
 token
 );

 return response?.Data;
 }
}

gRPC Usage

public class GrpcUserService
{
 private readonly IGrpcService _grpcService;

 public GrpcUserService(IGrpcService grpcService)
 {
 _grpcService = grpcService;
 }

 public async Task<UserResponse?> GetUserAsync(int userId, string token)
 {
 var request = new GetUserRequest { UserId = userId };

 var requestMarshaller = Marshallers.Create(
 r => JsonSerializer.SerializeToUtf8Bytes(r),
 b => JsonSerializer.Deserialize<GetUserRequest>(b)!
 );
 var responseMarshaller = Marshallers.Create(
 r => JsonSerializer.SerializeToUtf8Bytes(r),
 b => JsonSerializer.Deserialize<UserResponse>(b)!
 );

 // Pass token for authenticated call, omit for unauthenticated
 return await _grpcService.UnaryCallAsync(
 "https://grpc.example.com",
 "UserService",
 "GetUser",
 request,
 requestMarshaller,
 responseMarshaller,
 token
 );
 }

 // Server streaming
 public async IAsyncEnumerable<UserEvent> StreamUserEventsAsync(string token)
 {
 var request = new StreamRequest();

 var requestMarshaller = Marshallers.Create(
 r => JsonSerializer.SerializeToUtf8Bytes(r),
 b => JsonSerializer.Deserialize<StreamRequest>(b)!
 );
 var responseMarshaller = Marshallers.Create(
 r => JsonSerializer.SerializeToUtf8Bytes(r),
 b => JsonSerializer.Deserialize<UserEvent>(b)!
 );

 await foreach (var evt in _grpcService.ServerStreamingCallAsync(
 "https://grpc.example.com",
 "UserService",
 "StreamEvents",
 request,
 requestMarshaller,
 responseMarshaller,
 token))
 {
 yield return evt;
 }
 }
}

WebSocket Usage

public class ChatWebSocketService
{
 private readonly IWebSocketService _wsService;

 public ChatWebSocketService(IWebSocketService wsService)
 {
 _wsService = wsService;
 }

 public async Task ConnectAndCommunicateAsync(string token)
 {
 // Connect with authentication (omit token for public WebSocket)
 await _wsService.ConnectAsync("wss://ws.example.com/socket", token, "Bearer");

 // Subscribe to events
 _wsService.MessageReceived += (sender, message) =>
 {
 Console.WriteLine($"Received: {message}");
 };

 // Send messages
 await _wsService.SendAsync("Hello, server!");

 // Receive messages via IAsyncEnumerable
 await foreach (var message in _wsService.ReceiveMessagesAsync())
 {
 Console.WriteLine($"Message: {message}");
 }

 // Disconnect
 await _wsService.DisconnectAsync();
 }
}

SignalR Usage

public class ChatSignalRService
{
 private readonly ISignalRService _signalRService;

 public ChatSignalRService(ISignalRService signalRService)
 {
 _signalRService = signalRService;
 }

 public async Task ConnectToHubAsync(string token)
 {
 // Connect to hub with authentication (omit token for public hub)
 await _signalRService.ConnectAsync("https://example.com/chathub", token, "Bearer");

 // Subscribe to hub method
 _signalRService.On<ChatMessage>("ReceiveMessage", message =>
 {
 Console.WriteLine($"{message.User}: {message.Text}");
 });

 // Invoke hub method (fire-and-forget)
 await _signalRService.SendAsync("SendMessage", new object?[] { "Hello, hub!" });

 // Invoke hub method with result
 var users = await _signalRService.InvokeAsync<List<string>>(
 "GetOnlineUsers",
 Array.Empty<object?>()
 );

 // Stream from hub
 await foreach (var item in _signalRService.StreamAsync<StockPrice>(
 "StreamStockPrices",
 new object?[] { "AAPL", "MSFT" }))
 {
 Console.WriteLine($"Stock: {item.Symbol} = {item.Price}");
 }

 // Disconnect
 await _signalRService.DisconnectAsync();
 }
}

SSE (Server-Sent Events) Usage

public class EventStreamService
{
 private readonly ISseService _sseService;

 public EventStreamService(ISseService sseService)
 {
 _sseService = sseService;
 }

 // Authenticated stream
 public async Task StreamEventsAsync(string token)
 {
 await foreach (var evt in _sseService.StreamEventsAsync(
 "https://api.example.com/events",
 token,
 "Bearer"))
 {
 Console.WriteLine($"Event: {evt.Event}, Data: {evt.Data}");
 }
 }

 // Unauthenticated stream
 public async Task StreamPublicEventsAsync()
 {
 await foreach (var evt in _sseService.StreamEventsAsync(
 "https://api.example.com/public/events"))
 {
 Console.WriteLine($"Event: {evt.Data}");
 }
 }

 // Filter by event type
 public async Task StreamSpecificEventTypeAsync(string token)
 {
 await foreach (var evt in _sseService.SubscribeToEventTypeAsync(
 "https://api.example.com/events",
 "user-update",
 token,
 "Bearer"))
 {
 Console.WriteLine($"User update: {evt.Data}");
 }
 }

 // Resume from last event ID
 public async Task ResumeStreamAsync(string token, string lastEventId)
 {
 await foreach (var evt in _sseService.StreamEventsAsync(
 "https://api.example.com/events",
 lastEventId: lastEventId,
 token: token,
 authType: "Bearer"))
 {
 Console.WriteLine($"Resumed event: {evt.Data}");
 }
 }
}

Authentication Providers

The package includes a consolidated authentication system that works across all protocols:

using Tyler.Utility.Api.Common.Authentication;

// Get provider for auth type
var provider = AuthenticationProviderFactory.GetProvider("Bearer");

// Apply to different transports
provider.ApplyToHeaders(headers, token); // WebSocket, SSE
provider.ApplyToHttpClient(httpClient, token); // GraphQL
provider.ApplyToRestRequest(restRequest, token); // REST
provider.CreateGrpcMetadata(token); // gRPC

Supported auth types:

  • Bearer - Authorization: Bearer {token}
  • Basic - Authorization: Basic {base64(credentials)}
  • X-API-Key - X-API-Key: {key} with optional X-Usage header
  • Custom - Any custom header scheme

Transport Auth Contract

Every public transport method that accepts a token must apply authentication through a single named helper per transport. This keeps auth a one-line call site, makes the "did this method actually authenticate?" question answerable by grep, and lets one regression test per transport prove the guarantee. PSDO-2279 established this contract after health-check and fire-and-forget POST endpoints were found bypassing auth entirely.

Transport Single auth helper Location
REST ExecuteAsync / ExecuteDownloadAsync REST/Client/ApiClient.cs
gRPC CreateMetadata gRPC/Client/GrpcClient.cs
GraphQL Execute{Query,Mutation,Subscription}InternalAsync GraphQL/Client/GraphQLClient.cs
WebSocket ApplyAuth(ClientWebSocket, ...) WebSocket/Client/WebSocketClient.cs
SignalR ApplyAuth(HttpConnectionOptions, ...) SignalR/Client/SignalRClient.cs
SSE ConfigureRequest callback SSE/Client/SseClient.cs

Rules for contributors:

  1. New public transport methods MUST route through the transport's helper; never call AuthenticationProviderFactory.GetProvider inline in a public method body.
  2. New transports MUST provide a single named helper that is the only call site for the factory within that transport's client.
  3. Every public method that may be authenticated must accept the same triplet: string? token = null, string? authType = null, string? usage = null. A null token means no auth is applied; a non-null token with a null authType defaults to "Bearer".
  4. The regression test suite (Davasorus.Utility.DotNet.Api.Tests/Common/Authentication/TransportAuthContractTests.cs) injects a mock provider via AuthenticationProviderFactory.RegisterProvider and asserts ApplyToRestRequest (or the transport's equivalent) runs exactly once per public call with a token, and zero times without. Extend that file when you add a new public method.

OpenTelemetry Integration

All services emit OpenTelemetry Activities. Activity names reflect whether the request was authenticated:

  • Api.REST.Unsecure.{Method} / Api.REST.Secure.{Method}
  • Api.GraphQL.Unsecure.{Query|Mutation} / Api.GraphQL.Secure.{Query|Mutation}
  • Api.gRPC.Unsecure.{Method} / Api.gRPC.Secure.{Method}
  • Api.WebSocket.Unsecure.{Action} / Api.WebSocket.Secure.{Action}
  • Api.SignalR.Unsecure.{Action} / Api.SignalR.Secure.{Action}
  • Api.SSE.Unsecure.{Action} / Api.SSE.Secure.{Action}
builder.Services.AddOpenTelemetry()
 .WithTracing(tracing => tracing
 .AddSource("Tyler.Utility.Api.REST")
 .AddSource("Tyler.Utility.Api.GraphQL")
 .AddSource("Tyler.Utility.Api.gRPC")
 .AddSource("Tyler.Utility.Api.WebSocket")
 .AddSource("Tyler.Utility.Api.SignalR")
 .AddSource("Tyler.Utility.Api.SSE")
 .AddJaegerExporter()
 );

Metrics

All protocols emit OpenTelemetry Meters with request counters and duration histograms:

Meter Name Counter Histogram
Davasorus.Utility.Api.REST.* rest.requests.total rest.request.duration (ms)
Davasorus.Utility.Api.GraphQL.* graphql.requests.total graphql.request.duration (ms)
Davasorus.Utility.Api.gRPC.* grpc.calls.total grpc.call.duration (ms)
Davasorus.Utility.Api.WebSocket.* websocket.messages.total, websocket.connections.total -
Davasorus.Utility.Api.SignalR.* signalr.invocations.total, signalr.connections.total -
Davasorus.Utility.Api.SSE.* sse.events.total, sse.connections.total -

Error Handling

Request-response services (REST, GraphQL, gRPC unary) return null on error and automatically log errors:

var user = await _apiService.GetAsync<User>("https://api.example.com", "users/123", token);
if (user is not null)
{
 Console.WriteLine($"User: {user.Name}");
}

Connection-oriented services (WebSocket, SignalR) throw exceptions on connection/send failures, allowing callers to handle reconnection:

try
{
 await _wsService.ConnectAsync("wss://ws.example.com/socket", token, "Bearer");
}
catch (WebSocketException ex)
{
 // Handle connection failure
}

All errors are automatically logged with:

  • Correlation IDs for request tracing
  • OpenTelemetry Activity spans
  • SQS error reporting for authenticated services (if EnableErrorReporting is true)

Retry and Resilience

REST and GraphQL calls automatically retry on transient failures using Polly with exponential backoff and jitter:

// Default: 3 retries with 500ms base delay, exponential backoff up to 10s
builder.Services.AddApiClient(api => api
 .WithRetry(enabled: true, maxAttempts: 3, baseDelayMs: 500)
 .WithRetryOnServerErrors(true)
);

// Disable retry for low-latency scenarios
builder.Services.AddApiClient(api => api.DisableRetry());

Retried transient errors:

  • HttpRequestException (network failures, DNS resolution errors)
  • TaskCanceledException with inner TimeoutException (request timeouts)
  • IOException (connection resets, stream errors)
  • HTTP 5xx server errors (when RetryOnServerErrors is true)
  • HTTP 408 Request Timeout and 429 Too Many Requests

Not retried:

  • HTTP 4xx client errors (400, 401, 403, 404, etc.)
  • Explicit cancellation via CancellationToken
  • GraphQL-level errors (valid HTTP response with GraphQL errors)

SSE, WebSocket, and SignalR have their own built-in reconnection logic configured via their respective auto-reconnect options.

Selective Protocol Registration

Register only the protocols you need to reduce startup overhead:

builder.Services.AddApiClient(api => api.WithTimeout(TimeSpan.FromSeconds(60)));

// Instead of AddApiClient() which registers all 6 protocols:
builder.Services.AddApiClientRest(); // REST only
builder.Services.AddApiClientGraphQL(); // GraphQL only
builder.Services.AddApiClientGrpc(); // gRPC only
builder.Services.AddApiClientWebSocket(); // WebSocket only
builder.Services.AddApiClientSignalR(); // SignalR only
builder.Services.AddApiClientSse(); // SSE only

Migration from Secure/Unsecure Interfaces

If you are upgrading from a version that used separate ISecure* and IUnsecure* interfaces, this section covers the breaking changes and how to migrate.

What Changed

The previous design had two interfaces per protocol per layer (e.g., ISecureApiService and IUnsecureApiService). These have been consolidated into a single interface per protocol where authentication is controlled by optional parameters.

Before After
ISecureApiService / IUnsecureApiService IApiService
ISecureApiClient / IUnsecureApiClient IApiClient
ISecureGraphQLService / IUnsecureGraphQLService IGraphQLService
ISecureGraphQLClient / IUnsecureGraphQLClient IGraphQLClient
ISecureGrpcService / IUnsecureGrpcService IGrpcService
ISecureGrpcClient / IUnsecureGrpcClient IGrpcClient
ISecureWebSocketService / IUnsecureWebSocketService IWebSocketService
ISecureWebSocketClient / IUnsecureWebSocketClient IWebSocketClient
ISecureSignalRService / IUnsecureSignalRService ISignalRService
ISecureSignalRClient / IUnsecureSignalRClient ISignalRClient
ISecureSseService / IUnsecureSseService ISseService
ISecureSseClient / IUnsecureSseClient ISseClient

Namespace Changes

Before After
Tyler.Utility.Api.REST.Secure.Service Tyler.Utility.Api.REST.Service
Tyler.Utility.Api.REST.Unsecure.Service Tyler.Utility.Api.REST.Service
Tyler.Utility.Api.REST.Secure.Client Tyler.Utility.Api.REST.Client
Tyler.Utility.Api.REST.Unsecure.Client Tyler.Utility.Api.REST.Client

The same pattern applies to all protocols: remove the .Secure or .Unsecure segment from the namespace.

DI Registration Changes

DI now registers one interface per protocol per layer:

// Before: 12 registrations per protocol (secure + unsecure x client + service)
// After: 2 registrations per protocol (client + service)

// Before
services.AddTransient<ISecureApiClient, SecureApiClient>();
services.AddTransient<IUnsecureApiClient, UnsecureApiClient>();
services.AddScoped<ISecureApiService, SecureApiService>();
services.AddScoped<IUnsecureApiService, UnsecureApiService>();

// After
services.AddTransient<IApiClient, ApiClient>();
services.AddScoped<IApiService, ApiService>();

No changes are needed to your Startup.cs / Program.cs if you use AddApiClient() -- the DI extension method handles registration automatically.

Code Migration Examples

REST
// Before - Unsecure
public class MyService
{
 private readonly IUnsecureApiService _api;
 public MyService(IUnsecureApiService api) => _api = api;

 public Task<string?> GetAsync()
 => _api.GetAsync("https://api.example.com", "resource");
}

// After - Same call, just change the interface
public class MyService
{
 private readonly IApiService _api;
 public MyService(IApiService api) => _api = api;

 public Task<string?> GetAsync()
 => _api.GetAsync("https://api.example.com", "resource");
}
// Before - Secure
public class MySecureService
{
 private readonly ISecureApiService _api;
 public MySecureService(ISecureApiService api) => _api = api;

 public Task<string?> GetAsync(string token)
 => _api.GetAsync("https://api.example.com", "resource", token);
}

// After - Same interface, pass token as optional param
public class MySecureService
{
 private readonly IApiService _api;
 public MySecureService(IApiService api) => _api = api;

 public Task<string?> GetAsync(string token)
 => _api.GetAsync("https://api.example.com", "resource", token);
}
WebSocket / SignalR
// Before
private readonly ISecureWebSocketService _ws;
await _ws.ConnectAsync("wss://example.com/ws", token);

// After
private readonly IWebSocketService _ws;
await _ws.ConnectAsync("wss://example.com/ws", token, "Bearer");
SSE
// Before
private readonly ISecureSseService _sse;
await foreach (var evt in _sse.StreamEventsAsync(url, token)) { }

// After - use named params to disambiguate overloads
private readonly ISseService _sse;
await foreach (var evt in _sse.StreamEventsAsync(url, token: token, authType: "Bearer")) { }
GraphQL
// Before
private readonly ISecureGraphQLService _gql;
var result = await _gql.ExecuteQueryAsync<T>(url, query, token);

// After
private readonly IGraphQLService _gql;
var result = await _gql.ExecuteQueryAsync<T>(url, query, token);

Find-and-Replace Cheat Sheet

These replacements cover the majority of migration work:

Find Replace
ISecureApiService IApiService
IUnsecureApiService IApiService
ISecureApiClient IApiClient
IUnsecureApiClient IApiClient
ISecureGraphQLService IGraphQLService
IUnsecureGraphQLService IGraphQLService
ISecureGrpcService IGrpcService
IUnsecureGrpcService IGrpcService
ISecureWebSocketService IWebSocketService
IUnsecureWebSocketService IWebSocketService
ISecureSignalRService ISignalRService
IUnsecureSignalRService ISignalRService
ISecureSseService ISseService
IUnsecureSseService ISseService
using Tyler.Utility.Api.{Protocol}.Secure.Service; using Tyler.Utility.Api.{Protocol}.Service;
using Tyler.Utility.Api.{Protocol}.Unsecure.Service; using Tyler.Utility.Api.{Protocol}.Service;

After find-and-replace, remove duplicate using statements and build to catch any remaining signature mismatches.

Troubleshooting

Socket Exhaustion

Symptom: SocketException: An attempt was made to access a socket in a way forbidden by its access permissions after sustained load.

Cause: Creating new HttpClient instances for each request exhausts the available socket pool.

Fix: This package manages HttpClient lifetimes via IHttpClientFactory and singleton RestClient. Ensure you're using DI registration (AddApiClient()) rather than creating clients manually. The PooledConnectionLifetimeMinutes option (default: 5) controls DNS rotation for long-lived connections.

Request Timeouts

Symptom: TaskCanceledException on requests that should succeed.

Fix: Increase timeout via configuration:

builder.Services.AddApiClient(api => api
 .WithTimeout(TimeSpan.FromSeconds(300)) // HTTP timeout
 .WithGrpcDeadline(TimeSpan.FromSeconds(60)) // gRPC deadline
);

Retry Storm

Symptom: Downstream service overloaded after an outage, retries making it worse.

Fix: Reduce retry attempts or disable retry:

builder.Services.AddApiClient(api => api
 .WithRetry(enabled: true, maxAttempts: 1, baseDelayMs: 1000)
 .WithRetryMaxDelay(30000) // Cap retry delay at 30s
);

SSE Stream Drops

Symptom: SSE events stop arriving without error.

Fix: Ensure SseAutoReconnect is enabled (default: true). The client will automatically reconnect with exponential backoff. Use lastEventId parameter to resume from the last received event:

string? lastId = null;
await foreach (var evt in sseService.StreamEventsAsync(url, lastEventId: lastId, token: token, authType: "Bearer"))
{
 lastId = evt.Id;
 // Process event
}

gRPC Channel Reuse

gRPC channels are pooled via a singleton GrpcChannelPool. Channels are shared across calls to the same address, eliminating per-call connection overhead. Channel configuration (max message sizes, deadlines) is applied from ApiClientOptions.

Performance Tuning

Connection Pooling

Setting Default Tuning Guidance
MaxConnectionsPerServer 10 Increase to 20-50 for high-throughput REST/GraphQL workloads
PooledConnectionLifetimeMinutes 5 Increase for stable endpoints; decrease for DNS-based load balancing
EnableResponseCompression true Disable if endpoints don't support it, to avoid decompression overhead

Retry Tuning

Setting Default Tuning Guidance
MaxRetryAttempts 3 Reduce to 1 for latency-sensitive paths; increase to 5 for batch jobs
RetryBaseDelayMs 500 Increase for rate-limited APIs; decrease for fast-recovering backends
RetryMaxDelayMs 10000 Controls the ceiling on exponential backoff
RetryOnServerErrors true Disable if 5xx responses are not transient (e.g., validation errors returned as 500)

Real-Time Protocol Tuning

Setting Default Tuning Guidance
WebSocketReceiveBufferSize 4096 Increase to 8192+ for large message payloads
SignalRServerTimeoutSeconds 30 Increase for slow servers; decrease for faster failure detection
SseReadTimeoutSeconds 300 Increase for infrequent event streams

Requirements

  • .NET 8.0 or later
  • RestSharp 114.0.0 - HTTP client library
  • GraphQL.Client 6.1.0 - GraphQL client library
  • Grpc.Net.Client 2.76.0 - gRPC client library
  • Google.Protobuf 3.34.1 - Protocol Buffers serialization for gRPC
  • Microsoft.AspNetCore.SignalR.Client 8.0.25 - SignalR client library
  • Polly.Core 8.6.6 - Resilience and transient fault handling
  • Davasorus.Utility.DotNet.Telemetry - OpenTelemetry integration
  • Davasorus.Utility.DotNet.SQS - SQS error reporting
  • Davasorus.Utility.DotNet.Contracts.Types - Shared contract types
  • Davasorus.Utility.DotNet.Encryption - Encryption utilities

License

Proprietary - Tyler Technologies, Inc.

Copyright (c) 2026 Tyler Technologies, Inc. All rights reserved.

Product Versions Compatible and additional computed target framework versions.
.NET net8.0 net8.0 is compatible.  net8.0-android net8.0-android was computed.  net8.0-browser net8.0-browser was computed.  net8.0-ios net8.0-ios was computed.  net8.0-maccatalyst net8.0-maccatalyst was computed.  net8.0-macos net8.0-macos was computed.  net8.0-tvos net8.0-tvos was computed.  net8.0-windows net8.0-windows was computed.  net9.0 net9.0 was computed.  net9.0-android net9.0-android was computed.  net9.0-browser net9.0-browser was computed.  net9.0-ios net9.0-ios was computed.  net9.0-maccatalyst net9.0-maccatalyst was computed.  net9.0-macos net9.0-macos was computed.  net9.0-tvos net9.0-tvos was computed.  net9.0-windows net9.0-windows was computed.  net10.0 net10.0 is compatible.  net10.0-android net10.0-android was computed.  net10.0-browser net10.0-browser was computed.  net10.0-ios net10.0-ios was computed.  net10.0-maccatalyst net10.0-maccatalyst was computed.  net10.0-macos net10.0-macos was computed.  net10.0-tvos net10.0-tvos was computed.  net10.0-windows 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 Davasorus.Utility.DotNet.Api:

Package Downloads
SA.OpenSearchTool.Business

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.2.3.3 93 6/13/2026
2026.2.3.2 104 6/11/2026
2026.2.3.1 88 6/11/2026
2026.2.2.11 816 5/31/2026
2026.2.2.10 89 5/31/2026
2026.2.2.9 107 5/31/2026
2026.2.2.8 97 5/23/2026
2026.2.2.7 537 5/23/2026
2026.2.2.6 98 5/17/2026
2026.2.2.5 93 5/17/2026
2026.2.2.4 99 5/15/2026
2026.2.2.3 1,858 5/8/2026
2026.2.2.2 112 5/7/2026
2026.2.2.1 359 5/4/2026
2026.2.1.12 315 4/29/2026
2026.2.1.11 125 4/22/2026
2026.2.1.10 646 4/21/2026
2026.2.1.9 185 4/20/2026
2026.2.1.8 118 4/16/2026
2026.2.1.7 837 4/9/2026
Loading failed

v2026.2.02.04 (in flight):
     - Multi-targeted to net8.0 and net10.0
     - Migrated default namespace to Davasorus.Utility.DotNet.Api.*
     - OpenTelemetry v1.28 semantic conventions (HTTP, RPC, Messaging, Network)
     - http.client.request.duration metric (seconds) replaces rest.request.duration (ms)
     - Migrated SQS error reporting from ISqsService to ISqsPublisher
     - Optional ICacheOperations integration: conditional GET (ETag), gRPC channel cache, GraphQL schema cache, SSE/WS Last-Event-ID resume
     - Polly resilience pipeline lifted to singleton DI
     - SSE/WebSocket/SignalR clients moved from Transient to Scoped lifetime
     - GraphQL/SSE HttpClient handlers now share REST connection-pool config
     - Removed unused packages: GraphQL, GraphQL.MicrosoftDI, GraphQL.Primitives, GraphQL.SystemTextJson, Microsoft.Extensions.Options, Davasorus.Utility.DotNet.Encryption, Davasorus.Utility.DotNet.Auth
     - Auth is now token-source-agnostic: providers always invoked when authType is supplied; consumers wanting Entra/etc. write their own IAuthenticationProvider