The HTTP Adapter provides a robust interface for calling external REST APIs with built-in resilience patterns including retry logic, circuit breakers, and comprehensive error handling.
Core Interface
IExternalApiClient
The main interface for making HTTP requests to external services.
Performs an HTTP GET request and returns the response as a string. Parameters:
url: The endpoint URL
authentication: Optional tuple of (scheme, credentials)
headers: Optional dictionary of custom headers
Returns: Response body as string
PostAsync<T>
Task<HttpResponseMessage>
Performs an HTTP POST request with a JSON payload. Parameters:
uri: The endpoint URL
item: The object to serialize and send
authentication: Optional tuple of (scheme, credentials)
headers: Optional dictionary of custom headers
Returns: The HTTP response message
PutAsync<T>
Task<HttpResponseMessage>
Performs an HTTP PUT request with a JSON payload. Parameters:
url: The endpoint URL
item: The object to serialize and send
authentication: Optional tuple of (scheme, credentials)
headers: Optional dictionary of custom headers
Returns: The HTTP response message
DeleteAsync
Task<HttpResponseMessage>
Performs an HTTP DELETE request. Parameters:
url: The endpoint URL
authentication: Optional tuple of (scheme, credentials)
headers: Optional dictionary of custom headers
Returns: The HTTP response message
Core.Application.Adapters.Http/IExternalApiClient.cs
namespace Core . Application
{
public interface IExternalApiClient
{
Task < string > GetAsync (
string url ,
Tuple < string , string >? authentication = null ,
IDictionary < string , string >? headers = null );
Task < HttpResponseMessage > PostAsync < T >(
string uri ,
T item ,
Tuple < string , string >? authentication = null ,
IDictionary < string , string >? headers = null );
Task < HttpResponseMessage > PutAsync < T >(
string url ,
T item ,
Tuple < string , string >? authentication = null ,
IDictionary < string , string >? headers = null );
Task < HttpResponseMessage > DeleteAsync (
string url ,
Tuple < string , string >? authentication = null ,
IDictionary < string , string >? headers = null );
}
}
Implementation
ExternalApiHttpAdapter
The default implementation includes Polly-based resilience patterns.
Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs
using Core . Application ;
using Microsoft . Extensions . Logging ;
using Polly ;
using Polly . CircuitBreaker ;
using Polly . Retry ;
using Polly . Wrap ;
using System . Net ;
using System . Text ;
namespace Core . Infraestructure
{
public class ExternalApiHttpAdapter : IExternalApiClient
{
private readonly HttpClient _client ;
private readonly AsyncPolicyWrap < HttpResponseMessage > _policy ;
private readonly ILogger < ExternalApiHttpAdapter > _logger ;
public ExternalApiHttpAdapter ( ILogger < ExternalApiHttpAdapter > logger )
{
_logger = logger ?? throw new ArgumentNullException ( nameof ( logger ));
_client = new HttpClient ();
// Configure retry policy with exponential backoff
AsyncRetryPolicy < HttpResponseMessage > retryPolicy = Policy
. HandleResult < HttpResponseMessage >( r => ! r . IsSuccessStatusCode )
. Or < HttpRequestException >()
. WaitAndRetryAsync (
retryCount : 3 ,
sleepDurationProvider : attempt => TimeSpan . FromSeconds ( Math . Pow ( 2 , attempt )),
onRetry : ( outcome , timespan , attempt , context ) =>
{
_logger . LogWarning (
$"[Retry] Intento { attempt } después de { timespan . TotalSeconds } s. Resultado: { outcome . Result ? . StatusCode } " );
});
// Configure circuit breaker
AsyncCircuitBreakerPolicy < HttpResponseMessage > circuitBreakerPolicy = Policy
. HandleResult < HttpResponseMessage >( r => ! r . IsSuccessStatusCode )
. Or < HttpRequestException >()
. CircuitBreakerAsync (
handledEventsAllowedBeforeBreaking : 5 ,
durationOfBreak : TimeSpan . FromSeconds ( 30 ),
onBreak : ( outcome , timespan ) =>
{
_logger . LogError (
$"[CircuitBreaker] Circuito ABIERTO durante { timespan . TotalSeconds } s" );
},
onReset : () => _logger . LogInformation ( "[CircuitBreaker] Circuito RESTABLECIDO." ),
onHalfOpen : () => _logger . LogInformation ( "[CircuitBreaker] Circuito HALF-OPEN, probando..." ));
// Combine retry and circuit breaker policies
_policy = Policy . WrapAsync ( retryPolicy , circuitBreakerPolicy );
}
public async Task < string > GetAsync (
string url ,
Tuple < string , string >? authentication = null ,
IDictionary < string , string >? headers = null )
{
HttpRequestMessage requestMessage = new HttpRequestMessage ( HttpMethod . Get , url );
ServicePointManager . SecurityProtocol = SecurityProtocolType . Tls12 ;
AddAuthentication ( requestMessage , authentication );
AddHeaders ( requestMessage , headers );
HttpResponseMessage response = await _policy . ExecuteAsync (
() => _client . SendAsync ( requestMessage ));
return await response . Content . ReadAsStringAsync ();
}
}
}
Resilience Patterns
Retry Policy
The adapter implements exponential backoff retry:
Maximum number of retry attempts before giving up.
Wait time doubles with each retry: 2s, 4s, 8s.
Non-success HTTP status codes (4xx, 5xx)
HttpRequestException thrown
Circuit Breaker
Prevents cascading failures to unhealthy services:
Circuit opens after 5 consecutive failures.
Time circuit remains open before attempting recovery.
Closed : Normal operation, requests flow through
Open : Requests fail fast without calling the service
Half-Open : Testing if service has recovered
Retry Logic Handles transient failures with exponential backoff.
Circuit Breaker Prevents overwhelming failing services.
Logging Comprehensive logging of all retry and circuit breaker events.
TLS 1.2 Enforces secure communication protocols.
Registration
Register the HTTP adapter as a singleton:
Infrastructure/Registrations/InfraestructureServicesRegistration.cs
public static IServiceCollection AddInfrastructureServices (
this IServiceCollection services ,
IConfiguration configuration )
{
// Register HTTP adapter
services . AddSingleton < IExternalApiClient , ExternalApiHttpAdapter >();
return services ;
}
Registered as singleton to reuse HttpClient instances and avoid socket exhaustion.
Authentication
The adapter supports two authentication schemes:
Basic Authentication
var credentials = Tuple . Create ( "basic" , "username:password" );
var response = await _httpAdapter . GetAsync (
"https://api.example.com/data" ,
authentication : credentials );
Bearer Token Authentication
var token = Tuple . Create ( "bearer" , "your-jwt-token-here" );
var response = await _httpAdapter . GetAsync (
"https://api.example.com/data" ,
authentication : token );
Implementation Details
Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs:136
private void AddAuthentication ( HttpRequestMessage requestMessage , Tuple < string , string >? authentication )
{
if ( authentication == null ) return ;
if ( authentication . Item1 . Equals ( "basic" , StringComparison . OrdinalIgnoreCase ))
{
var base64Auth = Convert . ToBase64String ( Encoding . ASCII . GetBytes ( authentication . Item2 ));
requestMessage . Headers . Authorization =
new System . Net . Http . Headers . AuthenticationHeaderValue ( "Basic" , base64Auth );
}
else if ( authentication . Item1 . Equals ( "bearer" , StringComparison . OrdinalIgnoreCase ))
{
requestMessage . Headers . Authorization =
new System . Net . Http . Headers . AuthenticationHeaderValue ( "Bearer" , authentication . Item2 );
}
}
Add custom HTTP headers to requests:
var headers = new Dictionary < string , string >
{
{ "X-Custom-Header" , "value" },
{ "X-Correlation-Id" , Guid . NewGuid (). ToString () },
{ "Accept-Language" , "en-US" }
};
var response = await _httpAdapter . GetAsync (
"https://api.example.com/data" ,
headers : headers );
Usage Examples
GET Request
Application/ApplicationServices/ExternalDataService.cs
public class ExternalDataService
{
private readonly IExternalApiClient _httpAdapter ;
private readonly ILogger < ExternalDataService > _logger ;
public ExternalDataService (
IExternalApiClient httpAdapter ,
ILogger < ExternalDataService > logger )
{
_httpAdapter = httpAdapter ?? throw new ArgumentNullException ( nameof ( httpAdapter ));
_logger = logger ?? throw new ArgumentNullException ( nameof ( logger ));
}
public async Task < ExternalData > GetDataAsync ( string id )
{
try
{
var token = Tuple . Create ( "bearer" , await GetAccessTokenAsync ());
string jsonResponse = await _httpAdapter . GetAsync (
$"https://api.example.com/data/ { id } " ,
authentication : token );
return JsonConvert . DeserializeObject < ExternalData >( jsonResponse );
}
catch ( HttpRequestException ex )
{
_logger . LogError ( ex , "Failed to fetch data for ID: {Id}" , id );
throw ;
}
}
}
POST Request
public async Task < CreatedResponse > CreateResourceAsync ( CreateResourceRequest request )
{
var token = Tuple . Create ( "bearer" , await GetAccessTokenAsync ());
HttpResponseMessage response = await _httpAdapter . PostAsync (
"https://api.example.com/resources" ,
request ,
authentication : token );
if ( response . IsSuccessStatusCode )
{
string content = await response . Content . ReadAsStringAsync ();
return JsonConvert . DeserializeObject < CreatedResponse >( content );
}
throw new HttpRequestException (
$"Failed to create resource. Status: { response . StatusCode } " );
}
PUT Request
public async Task UpdateResourceAsync ( string id , UpdateResourceRequest request )
{
var token = Tuple . Create ( "bearer" , await GetAccessTokenAsync ());
HttpResponseMessage response = await _httpAdapter . PutAsync (
$"https://api.example.com/resources/ { id } " ,
request ,
authentication : token );
if ( ! response . IsSuccessStatusCode )
{
string errorContent = await response . Content . ReadAsStringAsync ();
_logger . LogError (
"Failed to update resource {Id}. Status: {Status}, Error: {Error}" ,
id ,
response . StatusCode ,
errorContent );
throw new HttpRequestException ( $"Update failed: { errorContent } " );
}
}
DELETE Request
public async Task DeleteResourceAsync ( string id )
{
var token = Tuple . Create ( "bearer" , await GetAccessTokenAsync ());
HttpResponseMessage response = await _httpAdapter . DeleteAsync (
$"https://api.example.com/resources/ { id } " ,
authentication : token );
response . EnsureSuccessStatusCode ();
}
Error Handling
The adapter automatically handles common HTTP errors:
Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs:115
private async Task < HttpResponseMessage > CheckForErrors ( HttpResponseMessage response )
{
string res = await response . Content . ReadAsStringAsync ();
switch ( response . StatusCode )
{
case HttpStatusCode . InternalServerError :
case HttpStatusCode . BadRequest :
case HttpStatusCode . Unauthorized :
throw new HttpRequestException ( res , null , response . StatusCode );
default :
return response ;
}
}
Recommended Error Handling Pattern
try
{
var result = await _httpAdapter . GetAsync ( url , auth );
return ProcessResult ( result );
}
catch ( HttpRequestException ex ) when ( ex . StatusCode == HttpStatusCode . Unauthorized )
{
_logger . LogWarning ( "Authentication failed, refreshing token..." );
await RefreshAuthenticationAsync ();
// Retry with new token
}
catch ( HttpRequestException ex ) when ( ex . StatusCode == HttpStatusCode . NotFound )
{
_logger . LogWarning ( "Resource not found: {Url}" , url );
return null ;
}
catch ( BrokenCircuitException )
{
_logger . LogError ( "Circuit breaker is open, service unavailable" );
throw new ServiceUnavailableException ( "External service is temporarily unavailable" );
}
Best Practices
Always use the singleton pattern when registering IExternalApiClient to properly reuse HttpClient instances.
Don’t catch and suppress exceptions from the adapter. Let them propagate or handle them specifically to maintain proper circuit breaker behavior.
Monitor circuit breaker state changes through logs to identify problematic external services.
Advanced Configuration
For custom resilience policies, you can create your own adapter implementation:
public class CustomHttpAdapter : IExternalApiClient
{
private readonly AsyncPolicyWrap < HttpResponseMessage > _policy ;
public CustomHttpAdapter ( ILogger < CustomHttpAdapter > logger )
{
// Custom timeout policy
var timeoutPolicy = Policy . TimeoutAsync < HttpResponseMessage >( 10 );
// Custom retry with jitter
var retryPolicy = Policy
. HandleResult < HttpResponseMessage >( r => ! r . IsSuccessStatusCode )
. WaitAndRetryAsync (
5 ,
attempt => TimeSpan . FromSeconds ( Math . Pow ( 2 , attempt )) +
TimeSpan . FromMilliseconds ( Random . Shared . Next ( 0 , 1000 )));
// Custom circuit breaker
var circuitBreaker = Policy
. HandleResult < HttpResponseMessage >( r => ! r . IsSuccessStatusCode )
. AdvancedCircuitBreakerAsync (
failureThreshold : 0.5 ,
samplingDuration : TimeSpan . FromSeconds ( 10 ),
minimumThroughput : 20 ,
durationOfBreak : TimeSpan . FromSeconds ( 60 ));
_policy = Policy . WrapAsync ( timeoutPolicy , retryPolicy , circuitBreaker );
}
}