-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Problem Statement
The current design with IHttpResponseHandler<T> creates "Generic Hell" when working with REST APIs that return many different entity types (CRM systems, E-commerce APIs, etc.):
// Current problem - need to register each type separately
services.AddSingleton<IHttpResponseHandler<Lead>, JsonResponseHandler<Lead>>();
services.AddSingleton<IHttpResponseHandler<Contact>, JsonResponseHandler<Contact>>();
services.AddSingleton<IHttpResponseHandler<Company>, JsonResponseHandler<Company>>();
services.AddSingleton<IHttpResponseHandler<Order>, JsonResponseHandler<Order>>();
services.AddSingleton<IHttpResponseHandler<Product>, JsonResponseHandler<Product>>();
// ... 20+ registrations for a typical REST API clientProposed Solution
1. Add Non-Generic Interface to Reliable.HttpClient
namespace Reliable.HttpClient;
/// <summary>
/// Universal HTTP response handler without type constraints
/// </summary>
public interface IHttpResponseHandler
{
/// <summary>
/// Handles HTTP response and returns typed result
/// </summary>
/// <typeparam name="TResponse">Response type after deserialization</typeparam>
/// <param name="response">HTTP response to handle</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Processed typed response</returns>
Task<TResponse> HandleAsync<TResponse>(HttpResponseMessage response, CancellationToken cancellationToken = default);
}
/// <summary>
/// Default implementation of universal response handler
/// </summary>
public class DefaultHttpResponseHandler : IHttpResponseHandler
{
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<DefaultHttpResponseHandler> _logger;
public DefaultHttpResponseHandler(
IOptions<JsonSerializerOptions> jsonOptions = null,
ILogger<DefaultHttpResponseHandler> logger = null)
{
_jsonOptions = jsonOptions?.Value ?? new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
_logger = logger;
}
public virtual async Task<TResponse> HandleAsync<TResponse>(
HttpResponseMessage response,
CancellationToken cancellationToken = default)
{
try
{
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrEmpty(content))
{
throw new HttpRequestException($"Empty response received");
}
var result = JsonSerializer.Deserialize<TResponse>(content, _jsonOptions);
if (result == null)
{
throw new HttpRequestException($"Failed to deserialize response to {typeof(TResponse).Name}");
}
return result;
}
catch (JsonException ex)
{
_logger?.LogError(ex, "JSON deserialization error for type {Type}", typeof(TResponse).Name);
throw new HttpRequestException($"Invalid JSON response", ex);
}
}
}2. Extend HttpClient Extensions
namespace Reliable.HttpClient;
public static class HttpClientExtensions
{
/// <summary>
/// Performs GET request with universal response handler
/// </summary>
public static async Task<TResponse> GetAsync<TResponse>(
this HttpClient httpClient,
string requestUri,
IHttpResponseHandler responseHandler,
CancellationToken cancellationToken = default)
{
var response = await httpClient.GetAsync(requestUri, cancellationToken);
return await responseHandler.HandleAsync<TResponse>(response, cancellationToken);
}
/// <summary>
/// Performs POST request with universal response handler
/// </summary>
public static async Task<TResponse> PostAsync<TRequest, TResponse>(
this HttpClient httpClient,
string requestUri,
TRequest content,
IHttpResponseHandler responseHandler,
CancellationToken cancellationToken = default)
{
var response = await httpClient.PostAsJsonAsync(requestUri, content, cancellationToken);
return await responseHandler.HandleAsync<TResponse>(response, cancellationToken);
}
}3. For Reliable.HttpClient.Caching
namespace Reliable.HttpClient.Caching;
/// <summary>
/// Universal HTTP client with caching, not tied to specific types
/// </summary>
public interface IHttpClientWithCache
{
Task<TResponse> GetAsync<TResponse>(
string requestUri,
TimeSpan? cacheDuration = null,
CancellationToken cancellationToken = default) where TResponse : class;
Task<TResponse> PostAsync<TRequest, TResponse>(
string requestUri,
TRequest content,
CancellationToken cancellationToken = default) where TResponse : class;
Task InvalidateCacheAsync(string pattern);
}
public class HttpClientWithCache : IHttpClientWithCache
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly IHttpResponseHandler _responseHandler;
public HttpClientWithCache(
HttpClient httpClient,
IMemoryCache cache,
IHttpResponseHandler responseHandler)
{
_httpClient = httpClient;
_cache = cache;
_responseHandler = responseHandler;
}
public async Task<TResponse> GetAsync<TResponse>(
string requestUri,
TimeSpan? cacheDuration = null,
CancellationToken cancellationToken = default) where TResponse : class
{
var cacheKey = $"http_cache:{typeof(TResponse).Name}:{requestUri}";
if (_cache.TryGetValue(cacheKey, out TResponse cachedResult))
{
return cachedResult;
}
var response = await _httpClient.GetAsync(requestUri, cancellationToken);
var result = await _responseHandler.HandleAsync<TResponse>(response, cancellationToken);
var duration = cacheDuration ?? TimeSpan.FromMinutes(5);
_cache.Set(cacheKey, result, duration);
return result;
}
public async Task<TResponse> PostAsync<TRequest, TResponse>(
string requestUri,
TRequest content,
CancellationToken cancellationToken = default) where TResponse : class
{
var response = await _httpClient.PostAsJsonAsync(requestUri, content, cancellationToken);
return await _responseHandler.HandleAsync<TResponse>(response, cancellationToken);
}
public Task InvalidateCacheAsync(string pattern)
{
// Implementation to clear cache entries matching pattern
return Task.CompletedTask;
}
}Real-World Benefits
Use Case: CRM System Client
Consider a typical CRM system HTTP client that needs to handle multiple entity types:
Current Architecture (Generic Hell)
// 15+ registrations for different entity types
services.AddSingleton<IHttpResponseHandler<Lead>, JsonResponseHandler<Lead>>();
services.AddSingleton<IHttpResponseHandler<Contact>, JsonResponseHandler<Contact>>();
services.AddSingleton<IHttpResponseHandler<Company>, JsonResponseHandler<Company>>();
services.AddSingleton<IHttpResponseHandler<Order>, JsonResponseHandler<Order>>();
services.AddSingleton<IHttpResponseHandler<Product>, JsonResponseHandler<Product>>();
services.AddSingleton<IHttpResponseHandler<User>, JsonResponseHandler<User>>();
services.AddSingleton<IHttpResponseHandler<AuthToken>, JsonResponseHandler<AuthToken>>();
// ... more registrations
// Overloaded constructor with many dependencies
internal class CrmApiClient(
HttpClient httpClient,
IHttpResponseHandler<Lead> leadHandler,
IHttpResponseHandler<Contact> contactHandler,
IHttpResponseHandler<Company> companyHandler,
IHttpResponseHandler<Order> orderHandler,
IHttpResponseHandler<Product> productHandler,
IHttpResponseHandler<User> userHandler,
IHttpResponseHandler<AuthToken> authHandler,
// ... more handlers
ILogger<CrmApiClient> logger)With Enhanced Library (Clean Architecture)
// Single universal registration
services.AddSingleton<IHttpResponseHandler, DefaultHttpResponseHandler>();
services.AddSingleton<IHttpClientWithCache, HttpClientWithCache>();
// Clean constructor
internal class CrmApiClient(
IHttpClientWithCache httpClient,
IUriBuilderFactory uriFactory,
IOptions<CrmClientOptions> options,
ILogger<CrmApiClient> logger)
// Elegant methods
public async Task<Lead> GetLeadAsync(int id) =>
await _httpClient.GetAsync<Lead>($"/api/leads/{id}", TimeSpan.FromMinutes(5));
public async Task<AuthToken> AuthorizeAsync(AuthRequest request) =>
await _httpClient.PostAsync<AuthRequest, AuthToken>("/api/auth", request);Impact Analysis
| Metric | Before | After | Improvement |
|---|---|---|---|
| Lines of Code | 1000+ | ~300 | -70% |
| DI Registrations | 15+ handlers | 1 handler | -93% |
| Constructor Parameters | 7+ dependencies | 4 dependencies | -43% |
| Unit Testing Complexity | High (15+ mocks) | Low (1-2 mocks) | -80% |
| Maintenance Effort | High | Low | -75% |
Additional Benefits
- Scalability - Pattern works for any REST API (Shopify, Stripe, GitHub, etc.)
- Built-in Caching - Automatic caching for GET requests with configurable TTL
- Easy Testing - Single mock instead of 15+ generic mocks
- Backward Compatibility - Existing
IHttpResponseHandler<T>continues to work - Performance - Reduced memory allocation from fewer generic instantiations
Compatibility
- Existing
IHttpResponseHandler<T>interfaces remain functional - New non-generic interfaces are additive
- Migration is optional and gradual
- No breaking changes to current API
Conclusion
These enhancements would dramatically simplify REST API client development with Reliable.HttpClient, reducing boilerplate code by 70% while maintaining all existing functionality. The pattern scales to any REST API and provides a much cleaner architecture for enterprise applications.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request