Clywell.Core.ApiClient
1.0.1
dotnet add package Clywell.Core.ApiClient --version 1.0.1
NuGet\Install-Package Clywell.Core.ApiClient -Version 1.0.1
<PackageReference Include="Clywell.Core.ApiClient" Version="1.0.1" />
<PackageVersion Include="Clywell.Core.ApiClient" Version="1.0.1" />
<PackageReference Include="Clywell.Core.ApiClient" />
paket add Clywell.Core.ApiClient --version 1.0.1
#r "nuget: Clywell.Core.ApiClient, 1.0.1"
#:package Clywell.Core.ApiClient@1.0.1
#addin nuget:?package=Clywell.Core.ApiClient&version=1.0.1
#tool nuget:?package=Clywell.Core.ApiClient&version=1.0.1
Clywell.Core.ApiClient
Shared API client utilities for Clywell .NET services — Refit-based HTTP client helpers, API key delegation handler, and result-mapping extensions. Provides the common infrastructure for inter-service communication without leaking third-party dependencies into application layers.
Overview
This package encapsulates HTTP client patterns used across Clywell microservices. It provides:
- ApiKeyDelegatingHandler — adds
x-api-keyheader to outgoing HTTP requests with fail-fast configuration validation - ApiResponseExtensions — maps Refit
ApiResponse<T>toResult<T>fromClywell.Primitiveswith RFC 7807 Problem Details support
The design emphasizes fast-fail on misconfiguration (API key missing at startup) and seamless integration with Clywell.Primitives error handling.
Installation
dotnet add package Clywell.Core.ApiClient
Quick Start
Register ApiKeyDelegatingHandler
Register the handler in your DI container, providing a factory that resolves the API key from configuration, a secret store, or environment variables:
services.AddTransient<ApiKeyDelegatingHandler>(sp =>
new ApiKeyDelegatingHandler(() => sp.GetRequiredService<IConfiguration>()["Notifications:ApiKey"]!));
The factory is called once at construction time. If it returns null or empty, an exception is thrown immediately — misconfiguration is never silent.
Add to an HttpClient
Use the handler with HttpClientFactory:
services.AddHttpClient<INotificationApiClient, NotificationApiClient>()
.AddHttpMessageHandler<ApiKeyDelegatingHandler>();
All requests through this client will include the x-api-key header automatically.
Define a Refit Client Interface
[Headers("User-Agent: Clywell")]
public interface INotificationApiClient
{
[Post("/notifications/send")]
Task<ApiResponse<SendNotificationResponse>> SendAsync(
SendNotificationRequest request,
CancellationToken ct = default);
[Get("/notifications/{id}")]
Task<ApiResponse<NotificationDto>> GetAsync(
string id,
CancellationToken ct = default);
}
Map ApiResponse to Result
Use ApiResponseExtensions.ToResult() to convert Refit responses to Result<T>:
public class SendNotificationHandler
{
private readonly INotificationApiClient _client;
private readonly ILogger<SendNotificationHandler> _logger;
public SendNotificationHandler(
INotificationApiClient client,
ILogger<SendNotificationHandler> logger)
{
_client = client;
_logger = logger;
}
public async Task<Result<SendNotificationResponse>> HandleAsync(
SendNotificationRequest request,
CancellationToken ct)
{
var response = await _client.SendAsync(request, ct);
// Map to Result<T> with optional logging
return response.ToResult(_logger, operationName: "SendNotification");
}
}
Error Mapping
HTTP Status Codes
Responses are mapped to appropriate ErrorCode values:
| Status Code | ErrorCode |
|---|---|
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | NotFound |
| 409 | Conflict |
| 400 | Validation (with field errors in metadata) |
| 503, 502, 504 | Unavailable |
| Network/timeout errors | Unavailable |
| Default | Failure |
RFC 7807 Problem Details Support
If the response body contains RFC 7807 Problem Details (JSON), it is automatically parsed:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Failed",
"detail": "One or more validation errors occurred.",
"status": 400,
"errors": {
"email": ["Email is required", "Email must be valid"],
"name": ["Name cannot be empty"]
}
}
Validation errors (HTTP 400 with errors field) are returned with ErrorCode.Validation and field-level errors attached as metadata:
var result = response.ToResult(_logger, "CreateUser");
if (!result.IsSuccess)
{
// result.Error.Code == ErrorCode.Validation
var validationErrors = result.Error.GetMetadata<IReadOnlyDictionary<string, string[]>>("ValidationErrors");
foreach (var (field, messages) in validationErrors)
{
Console.WriteLine($"{field}: {string.Join(", ", messages)}");
}
}
Handler (No Logging)
Map responses without logging:
var result = response.ToResult(); // logger=null, operationName=null
Both Typed and Untyped Responses
The extension supports both ApiResponse<T> (typed) and IApiResponse (untyped):
// Typed response — returns Result<T>
var result = typedResponse.ToResult<OrderDto>(_logger);
// Untyped response — returns Result (no value)
var result = untypedResponse.ToResult(_logger);
Exception Handling
If the HTTP request fails at the network level (timeout, DNS resolution, connection refused), the response.Error?.InnerException contains the underlying exception. The extension maps this to ErrorCode.Unavailable with the exception message as the description.
var result = response.ToResult(_logger, "FetchOrder");
// If network error: result.Error.Code == ErrorCode.Unavailable
// result.Error.Description == exception message
Use Cases
Notification Service Client
[Headers("Authorization: Bearer")]
public interface INotificationApiClient
{
[Post("/send")]
Task<ApiResponse<NotificationResponse>> SendEmailAsync(
EmailNotificationRequest request,
CancellationToken ct = default);
}
// In DI:
services
.AddHttpClient<INotificationApiClient, NotificationApiClient>()
.AddHttpMessageHandler<ApiKeyDelegatingHandler>();
// Usage in handler:
var response = await _client.SendEmailAsync(request, ct);
return response.ToResult(_logger, "SendEmail");
Resilient Retry Pattern
Combine with Polly for resilient calls:
services
.AddHttpClient<INotificationApiClient, NotificationApiClient>()
.AddHttpMessageHandler<ApiKeyDelegatingHandler>()
.AddTransientHttpErrorPolicy()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30))
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
Key Concepts
- Fast-Fail Misconfiguration — API key factory exceptions occur at container build time, not at first request
- Zero Third-Party Leakage — Consumer applications depend only on the abstractions from
Clywell.Primitives, not on Refit - RFC 7807 Compliance — Automatic parsing of standard Problem Details format
- Scoped Logger — Logger parameter in
ToResult()is optional; passnullto skip logging - Network-Aware — Distinguishes between HTTP errors and network-level failures
Dependencies
- Refit — HTTP client abstraction built on HttpClientFactory
- Refit.HttpClientFactory — DI integration for Refit
- Clywell.Primitives — Result and Error types
- AspNetCore.App — Implicit reference for
IConfiguration,ILogger, etc.
License
MIT — see LICENSE.
| Product | Versions 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. |
-
net10.0
- Clywell.Primitives (>= 1.2.0)
- Refit (>= 10.1.6)
- Refit.HttpClientFactory (>= 10.1.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.