![]() |
VOOZH | about |
dotnet add package Davasorus.Utility.DotNet.Api --version 2026.2.3.3
NuGet\Install-Package Davasorus.Utility.DotNet.Api -Version 2026.2.3.3
<PackageReference Include="Davasorus.Utility.DotNet.Api" Version="2026.2.3.3" />
<PackageVersion Include="Davasorus.Utility.DotNet.Api" Version="2026.2.3.3" />Directory.Packages.props
<PackageReference Include="Davasorus.Utility.DotNet.Api" />Project file
paket add Davasorus.Utility.DotNet.Api --version 2026.2.3.3
#r "nuget: Davasorus.Utility.DotNet.Api, 2026.2.3.3"
#:package Davasorus.Utility.DotNet.Api@2026.2.3.3
#addin nuget:?package=Davasorus.Utility.DotNet.Api&version=2026.2.3.3Install as a Cake Addin
#tool nuget:?package=Davasorus.Utility.DotNet.Api&version=2026.2.3.3Install as a Cake Tool
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.
Davasorus.Utility.DotNet.Api provides six protocol types, each with a single unified interface where authentication is optional:
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:
Architecture Note: Only interact with Service interfaces in your application code. Services handle all Client interactions, error logging, and telemetry internally.
Install-Package Davasorus.Utility.DotNet.Api
dotnet add package Davasorus.Utility.DotNet.Api
<PackageReference Include="Davasorus.Utility.DotNet.Api" Version="*" />
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:
IApiServiceIGraphQLServiceIGrpcServiceIWebSocketServiceISignalRServiceISseService| 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 |
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)
);
{
"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
}
}
All REST operations use a single IApiService interface. Authentication is controlled by optional parameters.
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"
);
}
}
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
);
}
}
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;
}
}
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;
}
}
}
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();
}
}
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();
}
}
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}");
}
}
}
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:
Authorization: Bearer {token}Authorization: Basic {base64(credentials)}X-API-Key: {key} with optional X-Usage headerEvery 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:
AuthenticationProviderFactory.GetProvider inline in a public method body.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".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.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()
);
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 |
- |
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:
EnableErrorReporting is true)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)RetryOnServerErrors is true)Not retried:
CancellationTokenSSE, WebSocket, and SignalR have their own built-in reconnection logic configured via their respective auto-reconnect options.
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
If you are upgrading from a version that used separate ISecure* and IUnsecure* interfaces, this section covers the breaking changes and how to migrate.
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 |
| 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 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.
// 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);
}
// 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");
// 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")) { }
// 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);
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.
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.
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
);
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
);
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 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.
| 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 |
| 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) |
| 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 |
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. |
Showing the top 1 NuGet packages that depend on Davasorus.Utility.DotNet.Api:
| Package | Downloads |
|---|---|
|
SA.OpenSearchTool.Business
Package Description |
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 |
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