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
                    
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="Clywell.Core.ApiClient" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Clywell.Core.ApiClient" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Clywell.Core.ApiClient" />
                    
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 Clywell.Core.ApiClient --version 1.0.1
                    
#r "nuget: Clywell.Core.ApiClient, 1.0.1"
                    
#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 Clywell.Core.ApiClient@1.0.1
                    
#: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=Clywell.Core.ApiClient&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Clywell.Core.ApiClient&version=1.0.1
                    
Install as a Cake Tool

Clywell.Core.ApiClient

License: MIT NuGet: 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-key header to outgoing HTTP requests with fail-fast configuration validation
  • ApiResponseExtensions — maps Refit ApiResponse<T> to Result<T> from Clywell.Primitives with 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; pass null to 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 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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 135 6/2/2026
1.0.0 222 3/18/2026