Skip to main content
The ExternalApiHttpAdapter provides a robust HTTP client implementation with built-in resilience patterns using Polly for calling external APIs.

ExternalApiHttpAdapter

Class Definition

Core.Infraestructure.Adapters.Http/ExternalApiHttpAdapter.cs
namespace Core.Infraestructure
{
    public class ExternalApiHttpAdapter : IExternalApiClient
    {
        private readonly HttpClient _client;
        private readonly AsyncPolicyWrap<HttpResponseMessage> _policy;
        private readonly ILogger<ExternalApiHttpAdapter> _logger;
    }
}

Constructor

public ExternalApiHttpAdapter(ILogger<ExternalApiHttpAdapter> logger)
The constructor initializes:
  • An HttpClient instance
  • A combined retry and circuit breaker policy
  • A logger for diagnostics

Resilience Policies

The adapter combines two Polly policies for maximum resilience:

1. Retry Policy

Exponential backoff retry with 3 attempts:
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}");
        });
Retry Schedule:
  • Attempt 1: Immediate
  • Attempt 2: After 2 seconds
  • Attempt 3: After 4 seconds
  • Attempt 4: After 8 seconds (if needed)

2. Circuit Breaker Policy

Prevents cascading failures by opening the circuit after repeated failures:
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. " +
                $"Último error: {outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString()}");
        },
        onReset: () => _logger.LogInformation("[CircuitBreaker] Circuito RESTABLECIDO."),
        onHalfOpen: () => _logger.LogInformation("[CircuitBreaker] Circuito HALF-OPEN, probando..."));
Circuit Breaker States:
  • Closed: Normal operation, requests flow through
  • Open: After 5 failures, circuit opens for 30 seconds, rejecting all requests
  • Half-Open: After 30 seconds, allows one test request

Combined Policy

_policy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
The retry policy wraps the circuit breaker, ensuring retries happen before the circuit opens.

HTTP Methods

GET Request

public async Task<string> GetAsync(
    string url, 
    Tuple<string, string>? authentication = null, 
    IDictionary<string, string>? headers = null)
Example:
public class WeatherService
{
    private readonly IExternalApiClient _httpClient;

    public WeatherService(IExternalApiClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetWeatherAsync(string city)
    {
        var headers = new Dictionary<string, string>
        {
            ["Accept"] = "application/json"
        };

        var auth = Tuple.Create("bearer", "your-api-token");

        return await _httpClient.GetAsync(
            $"https://api.weather.com/v1/weather?city={city}",
            authentication: auth,
            headers: headers);
    }
}

POST Request

public async Task<HttpResponseMessage> PostAsync<T>(
    string uri, 
    T item, 
    Tuple<string, string>? authentication = null, 
    IDictionary<string, string>? headers = null)
Example:
public class OrderService
{
    private readonly IExternalApiClient _httpClient;

    public OrderService(IExternalApiClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<HttpResponseMessage> CreateOrderAsync(CreateOrderRequest order)
    {
        var auth = Tuple.Create("bearer", "your-api-token");

        return await _httpClient.PostAsync(
            "https://api.orders.com/v1/orders",
            order,
            authentication: auth);
    }
}

public class CreateOrderRequest
{
    public int CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal Total { get; set; }
}

PUT Request

public async Task<HttpResponseMessage> PutAsync<T>(
    string url, 
    T item, 
    Tuple<string, string>? authentication = null, 
    IDictionary<string, string>? headers = null)
Example:
public async Task<HttpResponseMessage> UpdateProductAsync(int productId, UpdateProductRequest product)
{
    return await _httpClient.PutAsync(
        $"https://api.products.com/v1/products/{productId}",
        product);
}

DELETE Request

public async Task<HttpResponseMessage> DeleteAsync(
    string url, 
    Tuple<string, string>? authentication = null, 
    IDictionary<string, string>? headers = null)
Example:
public async Task<HttpResponseMessage> DeleteCustomerAsync(int customerId)
{
    return await _httpClient.DeleteAsync(
        $"https://api.customers.com/v1/customers/{customerId}");
}

Authentication

The adapter supports two authentication schemes:

Bearer Token Authentication

var auth = Tuple.Create("bearer", "your-jwt-token");

var response = await _httpClient.GetAsync(
    "https://api.example.com/protected",
    authentication: auth);
This adds the header:
Authorization: Bearer your-jwt-token

Basic Authentication

var auth = Tuple.Create("basic", "username:password");

var response = await _httpClient.GetAsync(
    "https://api.example.com/protected",
    authentication: auth);
This adds the header:
Authorization: Basic base64(username:password)

Custom Headers

Add custom headers to any request:
var headers = new Dictionary<string, string>
{
    ["X-Custom-Header"] = "custom-value",
    ["X-Request-Id"] = Guid.NewGuid().ToString(),
    ["Accept-Language"] = "en-US"
};

var response = await _httpClient.GetAsync(
    "https://api.example.com/data",
    headers: headers);

Error Handling

The adapter automatically checks for common HTTP errors:
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;
    }
}
Handled Status Codes:
  • 500 Internal Server Error: Throws HttpRequestException
  • 400 Bad Request: Throws HttpRequestException
  • 401 Unauthorized: Throws HttpRequestException

Service Registration

Register the adapter as a singleton in your DI container:
Infrastructure/Registrations/InfraestructureServicesRegistration.cs
public static IServiceCollection AddInfrastructureServices(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    // Adapters
    services.AddSingleton<IExternalApiClient, ExternalApiHttpAdapter>();

    return services;
}

Usage in Startup/Program.cs

services.AddInfrastructureServices(configuration);

JSON Serialization

The adapter uses Newtonsoft.Json for request serialization:
Content = new StringContent(
    JsonConvert.SerializeObject(item), 
    Encoding.UTF8, 
    "application/json")
All POST and PUT requests are automatically serialized to JSON with UTF-8 encoding.

TLS Configuration

The adapter enforces TLS 1.2:
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

Complete Usage Example

1. Define Request/Response Models

public class CreateUserRequest
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class UserResponse
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

2. Create Service

public class UserManagementService
{
    private readonly IExternalApiClient _httpClient;
    private readonly ILogger<UserManagementService> _logger;
    private const string BaseUrl = "https://api.users.com/v1";

    public UserManagementService(
        IExternalApiClient httpClient,
        ILogger<UserManagementService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<UserResponse> CreateUserAsync(CreateUserRequest request)
    {
        try
        {
            var auth = Tuple.Create("bearer", GetApiToken());
            
            var response = await _httpClient.PostAsync(
                $"{BaseUrl}/users",
                request,
                authentication: auth);

            var content = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<UserResponse>(content);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to create user");
            throw;
        }
    }

    public async Task<List<UserResponse>> GetUsersAsync()
    {
        try
        {
            var auth = Tuple.Create("bearer", GetApiToken());
            var headers = new Dictionary<string, string>
            {
                ["Accept"] = "application/json"
            };

            var content = await _httpClient.GetAsync(
                $"{BaseUrl}/users",
                authentication: auth,
                headers: headers);

            return JsonConvert.DeserializeObject<List<UserResponse>>(content);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to retrieve users");
            throw;
        }
    }

    public async Task DeleteUserAsync(int userId)
    {
        try
        {
            var auth = Tuple.Create("bearer", GetApiToken());
            
            await _httpClient.DeleteAsync(
                $"{BaseUrl}/users/{userId}",
                authentication: auth);
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "Failed to delete user {UserId}", userId);
            throw;
        }
    }

    private string GetApiToken()
    {
        // Retrieve token from configuration or token service
        return "your-api-token";
    }
}

3. Register Service

services.AddScoped<UserManagementService>();

4. Use Service

public class UserController : ControllerBase
{
    private readonly UserManagementService _userService;

    public UserController(UserManagementService userService)
    {
        _userService = userService;
    }

    [HttpPost("users")]
    public async Task<ActionResult<UserResponse>> CreateUser([FromBody] CreateUserRequest request)
    {
        var user = await _userService.CreateUserAsync(request);
        return Created($"/users/{user.Id}", user);
    }

    [HttpGet("users")]
    public async Task<ActionResult<List<UserResponse>>> GetUsers()
    {
        var users = await _userService.GetUsersAsync();
        return Ok(users);
    }

    [HttpDelete("users/{id}")]
    public async Task<ActionResult> DeleteUser(int id)
    {
        await _userService.DeleteUserAsync(id);
        return NoContent();
    }
}

Key Features

  • Retry Logic: Exponential backoff with configurable retry count
  • Circuit Breaker: Prevents cascading failures
  • Authentication: Support for Bearer and Basic authentication
  • Custom Headers: Add custom headers to any request
  • Error Handling: Automatic error detection and exception throwing
  • TLS 1.2: Enforces secure connections
  • JSON Serialization: Automatic serialization using Newtonsoft.Json
  • Logging: Comprehensive logging for diagnostics
  • Resilience: Combined policies for maximum reliability

Build docs developers (and LLMs) love