Noundry.Connector

Strongly-Typed API Client Library built on Refit

A powerful, flexible API connector library that provides strongly-typed HTTP clients with automatic authentication, smart caching, rate limiting, request deduplication, automatic pagination, and OpenTelemetry integration. Supports Token, API Key, Basic, and full OAuth 2.0 authentication flows.

dotnet add package Noundry.Connector

Key Features

Multi-Auth Support

Token, API Key, Basic Auth, and full OAuth 2.0 (Client Credentials, Auth Code + PKCE, Device Code, Password).

Smart Caching

ETag and Last-Modified validation, configurable expiration, LRU eviction, and automatic invalidation on mutations.

Rate Limiting

Token bucket algorithm with sliding window, 429 retry handling, concurrent request limits, and per-endpoint configuration.

Automatic Pagination

Page-based, offset, cursor, and Link header pagination styles with IAsyncEnumerable streaming.

Request Deduplication

Identical in-flight requests share a single HTTP call. Reduces duplicate API calls automatically.

OpenTelemetry

Built-in metrics (request duration, response size, status codes) and distributed tracing with W3C propagation.

Quick Start

1. Define a Refit Interface

using Refit;

public interface IMyApi
{
 [Get("/users")]
 Task<List<User>> GetUsersAsync(CancellationToken ct = default);

 [Get("/users/{id}")]
 Task<User> GetUserAsync(int id, CancellationToken ct = default);

 [Post("/users")]
 Task<User> CreateUserAsync([Body] User user, CancellationToken ct = default);
}

2. Register Services

using Noundry.Connector.Extensions;

// Add authentication
services.AddTokenAuthentication("your-api-token");

// Register the API client
services.AddConnector<IMyApi>(options =>
{
 options.BaseUrl = "https://api.example.com";
 options.MaxRetries = 3;
 options.UseExponentialBackoff = true;
});

3. Inject and Use

public class UserService
{
 private readonly IMyApi _api;
 public UserService(IMyApi api) => _api = api;

 public async Task<List<User>> GetActiveUsersAsync()
 {
 var users = await _api.GetUsersAsync();
 return users.Where(u => u.IsActive).ToList();
 }
}

Authentication Providers

// Bearer Token
services.AddTokenAuthentication("your-bearer-token");

// API Key (header or query parameter)
services.AddApiKeyAuthentication("your-api-key");
services.AddApiKeyAuthentication("your-key", "api_key", ApiKeyLocation.QueryParameter);

// Basic Auth
services.AddBasicAuthentication("username", "password");

// OAuth 2.0 Client Credentials
services.AddOAuthAuthentication(oauth =>
{
 oauth.ClientId = "client-id";
 oauth.ClientSecret = "client-secret";
 oauth.TokenEndpoint = "https://auth.example.com/oauth/token";
 oauth.Scope = "api:read api:write";
 oauth.GrantType = "client_credentials";
});

OAuth 2.0 Authorization Code with PKCE

var config = OAuthConfiguration.ForAuthorizationCode(
 clientId: "my-client",
 authorizationEndpoint: "https://auth.example.com/authorize",
 tokenEndpoint: "https://auth.example.com/token",
 redirectUri: "https://myapp.com/callback",
 scope: "openid profile",
 usePkce: true);

var provider = new OAuthAuthenticationProvider(httpClient, config);

// Step 1: Redirect user
var authUrl = provider.GetAuthorizationUrl();

// Step 2: Exchange code for token
var token = await provider.ExchangeCodeForTokenAsync(code, state);

Configuration Options

services.AddConnector<IMyApi>(options =>
{
 options.BaseUrl = "https://api.example.com";
 options.Timeout = TimeSpan.FromSeconds(30);

 // Retry Policy (Polly)
 options.MaxRetries = 3; // 0 = disabled
 options.RetryDelay = TimeSpan.FromSeconds(1);
 options.UseExponentialBackoff = true; // 2^n + RetryDelay

 // Circuit Breaker
 options.EnableCircuitBreaker = true;
 options.CircuitBreakerThreshold = 5; // Failures before open
 options.CircuitBreakerDuration = TimeSpan.FromSeconds(30);

 // Custom headers
 options.DefaultHeaders["X-Custom"] = "value";
});

HTTP Feature Handlers

services.AddHttpClient<IMyApi>()
 .AddConnectorFeatures(options =>
 {
 options.EnableCaching = true;
 options.EnableRateLimiting = true;
 options.EnableDeduplication = true;
 options.EnableTelemetry = true;

 options.ConfigureCaching = cache =>
 {
 cache.UseETag = true;
 cache.DefaultExpiration = TimeSpan.FromMinutes(5);
 cache.MaxCacheSize = 500;
 };

 options.ConfigureRateLimiting = rate =>
 {
 rate.MaxRequestsPerSecond = 10;
 rate.MaxConcurrentRequests = 5;
 rate.RespectRetryAfterHeader = true;
 };
 });

Automatic Pagination

Transparently fetch all pages using IAsyncEnumerable. Supports Page-based, Offset-based, Cursor-based, and Link Header styles.

using Noundry.Connector.Pagination;

var client = new PaginatedClient(httpClient);

// GitHub-style Link header pagination
var allRepos = await client
 .GetAllWithLinkHeaderAsync<Repository>("/users/octocat/repos")
 .ToListAsync();

// Cursor-based pagination (Slack, Twitter style)
var messages = await client
 .GetAllWithCursorAsync<Message>(
 "/conversations.history",
 cursorParam: "cursor",
 nextCursorPath: "$.response_metadata.next_cursor")
 .TakeAsync(100)
 .ToListAsync();

// Batch processing
await foreach (var batch in client.GetAllAsync<Order>("/orders").BatchAsync(100))
{
 await ProcessBatchAsync(batch);
}

Request Deduplication

Identical in-flight GET requests automatically share a single HTTP call:

var task1 = api.GetUserAsync(1); // Makes HTTP request
var task2 = api.GetUserAsync(1); // Joins task1
var task3 = api.GetUserAsync(1); // Joins task1
await Task.WhenAll(task1, task2, task3);
// Only ONE HTTP request was made, all three get the same result

Exception Hierarchy

ConnectorException

Base exception for all Connector errors.

AuthenticationException

Authentication failures (invalid token, expired credentials).

HttpException

HTTP errors with StatusCode and ResponseContent properties.

ValidationException

Validation errors with field-level error details.

ConfigurationException

Configuration errors (missing base URL, invalid options).

RateLimitExceededException

Queue full or timeout waiting for a rate limit slot.

CLI Code Generator

Automatically generate strongly-typed API clients from any REST API:

# Install the generator
dotnet tool install -g Noundry.Connector.Cli

# Generate a complete API client (interactive wizard)
connector-gen generate

# The wizard guides you through:
# 1. API base URL and connectivity check
# 2. Authentication method selection
# 3. Endpoint discovery (OpenAPI)
# 4. Model generation from JSON responses
# 5. Output: Models, Refit interfaces, DI extensions

Real-World Example: GitHub API Client

[Headers("User-Agent: MyApp/1.0", "Accept: application/vnd.github.v3+json")]
public interface IGitHubApi
{
 [Get("/users/{username}")]
 Task<GitHubUser> GetUserAsync(string username, CancellationToken ct = default);

 [Get("/users/{username}/repos")]
 Task<List<GitHubRepository>> GetUserReposAsync(
 string username,
 [Query] string? sort = null,
 [Query("per_page")] int pageSize = 30,
 CancellationToken ct = default);
}

// Registration with all features
services.AddTokenAuthentication(Environment.GetEnvironmentVariable("GITHUB_TOKEN")!);

services.AddConnector<IGitHubApi>(options =>
{
 options.BaseUrl = "https://api.github.com";
 options.MaxRetries = 3;
 options.EnableCircuitBreaker = true;
});