Latest update Android YouTube

Chapter 5: Calling External APIs Asynchronously in .NET

Introduction

Modern .NET APIs rarely work in isolation. They commonly integrate with:

  • Payment gateways
  • SMS/Email services
  • Authentication providers
  • Third-party data APIs
  • Internal microservices

All such integrations are:

  • Network I/O bound
  • Unreliable by nature
  • Latency sensitive

Therefore, they must be implemented asynchronously and resiliently.

5.1 Why External API Calls MUST Be Async

External calls involve:

  • DNS lookup
  • TCP connection
  • SSL handshake
  • Network latency
  • Remote server processing

If done synchronously:

  • Your request thread is blocked
  • Under load, your entire API collapses

❌ Synchronous External Call (Bad)

var response = httpClient.GetAsync(url).Result; // Dangerous

✅ Asynchronous External Call (Correct)

var response = await httpClient.GetAsync(url);

✅ The thread is released while waiting on the network.

5.2 The HttpClient Problem (Very Important Concept)

❌ Wrong Pattern (Socket Exhaustion)

public async Task CallApiAsync() 
{ 
    var client = new HttpClient(); // NEW instance each time ❌ 
    var response = await client.GetAsync("https://api.example.com"); 
}

This causes:

  • TCP socket leaks
  • Port exhaustion
  • Random production outages under load

5.3 Correct Pattern: IHttpClientFactory

IHttpClientFactory:

  • Manages connection pooling
  • Reuses sockets safely
  • Avoids DNS issues
  • Is thread-safe and production-safe

Register in Program.cs

builder.Services.AddHttpClient();

Inject into Service

public class PaymentGatewayService 
{ 
    private readonly HttpClient _httpClient; 
    
    public PaymentGatewayService(HttpClient httpClient) 
    { 
        _httpClient = httpClient; 
    } 
}

Or with factory:

public class PaymentGatewayService 
{ 
    private readonly IHttpClientFactory _factory; 
    
    public PaymentGatewayService(IHttpClientFactory factory) 
    { 
        _factory = factory; 
    } 
    
    public HttpClient CreateClient() => _factory.CreateClient(); 
}

5.4 Typed HttpClient (Best Practice for Large Systems)

builder.Services.AddHttpClient<PaymentGatewayClient>(client => 
{ 
    client.BaseAddress = new Uri("https://payment.api.com/"); 
    client.Timeout = TimeSpan.FromSeconds(30); 
    client.DefaultRequestHeaders.Add("Accept", "application/json"); 
});
public class PaymentGatewayClient 
{ 
    private readonly HttpClient _client; 
    
    public PaymentGatewayClient(HttpClient client) 
    { 
        _client = client; 
    } 
    
    public async Task<PaymentResponse> ProcessAsync(PaymentRequest req) 
    { 
        var response = await _client.PostAsJsonAsync("pay", req); 
        response.EnsureSuccessStatusCode(); 
        return await response.Content.ReadFromJsonAsync<PaymentResponse>(); 
    } 
}

✅ Clean, testable, DI-friendly, production-safe

5.5 GET, POST, PUT, DELETE – Async Usage

GET

var data = await _client.GetFromJsonAsync<UserDto>("users/10");

POST

var response = await _client.PostAsJsonAsync("orders", order);

PUT

await _client.PutAsJsonAsync("orders/10", order);

DELETE

await _client.DeleteAsync("orders/10");

5.6 Handling HTTP Status Codes Properly

⚠️ Never assume success.

var response = await _client.GetAsync("payments/123"); 
if (!response.IsSuccessStatusCode) 
{ 
    throw new ExternalServiceException( 
        $"Payment API failed: {response.StatusCode}"); 
}

5.7 Timeouts in External API Calls

Default Timeout (100 seconds) is dangerous

client.Timeout = TimeSpan.FromSeconds(5);

✅ Always define:

  • Upper latency bounds
  • SLA-based timeout

5.8 CancellationToken + Client Disconnect

public async Task<PaymentStatus> CheckAsync( 
    string txnId, CancellationToken token) 
{ 
    var response = await _client 
        .GetAsync($"txn/{txnId}", token); 
    response.EnsureSuccessStatusCode(); 
    return await response.Content 
        .ReadFromJsonAsync<PaymentStatus>(token); 
}

✅ If:

  • Client disconnects
  • Request times out

→ External call is cancelled immediately.

5.9 The Reality of External APIs: Failures Are Normal

Failures include:

  • DNS failure
  • Timeout
  • 5xx errors
  • Partial outages
  • Network jitter

Hence we must use:

  • Retries
  • Circuit breakers
  • Bulkheads
  • Fallbacks

This is where Polly comes in.

5.10 Polly Integration (Retries + Circuit Breaker)

Install Polly

dotnet add package Microsoft.Extensions.Http.Polly

Register with Retry + Circuit Breaker

builder.Services 
    .AddHttpClient<PaymentGatewayClient>() 
    .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync( 
        3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry)))) 
    .AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync( 
        handledEventsAllowedBeforeBreaking: 5, 
        durationOfBreak: TimeSpan.FromSeconds(30)));

✅ This automatically:

  • Retries on 5xx + timeouts
  • Breaks circuit after repeated failures
  • Prevents cascading failures

5.11 Real-Life Example: Async Payment Gateway Integration

Request / Response DTO

public class PaymentRequest 
{ 
    public string OrderId { get; set; } = default!; 
    public decimal Amount { get; set; } 
}
public class PaymentResponse 
{ 
    public bool Success { get; set; } 
    public string TransactionId { get; set; } = default!; 
}

Gateway Client (Async + Resilient)

public class PaymentGatewayClient 
{ 
    private readonly HttpClient _client; 
    
    public PaymentGatewayClient(HttpClient client) 
    { 
        _client = client; 
    } 
    
    public async Task<PaymentResponse> PayAsync( 
        PaymentRequest request, CancellationToken token) 
    { 
        var response = await _client 
            .PostAsJsonAsync("payments", request, token); 
        response.EnsureSuccessStatusCode(); 
        return await response 
            .Content 
            .ReadFromJsonAsync<PaymentResponse>(token); 
    } 
}

Service Layer

public class OrderPaymentService 
{ 
    private readonly PaymentGatewayClient _gateway; 
    
    public OrderPaymentService(PaymentGatewayClient gateway) 
    { 
        _gateway = gateway; 
    } 
    
    public async Task<string> PayOrderAsync( 
        string orderId, decimal amount, CancellationToken token) 
    { 
        var request = new PaymentRequest 
        { 
            OrderId = orderId, 
            Amount = amount 
        }; 
        var response = await _gateway.PayAsync(request, token); 
        if (!response.Success) 
            throw new Exception("Payment failed"); 
        return response.TransactionId; 
    } 
}

Controller

[HttpPost("{orderId}/pay")] 
public async Task<IActionResult> PayOrder( 
    string orderId, [FromBody] decimal amount, CancellationToken token) 
{ 
    var txnId = await _paymentService 
        .PayOrderAsync(orderId, amount, token); 
    return Ok(new { transactionId = txnId }); 
}

✅ End-to-end:

  • Fully Async
  • Cancellation-aware
  • Retry + circuit breaker protected
  • Production ready

5.12 Parallel External API Calls (When Multiple APIs Are Required)

Example: Fetch order + user + payment from 3 systems.

var orderTask = _orderApi.GetAsync(orderId); 
var userTask = _userApi.GetAsync(userId); 
var payTask = _payApi.GetAsync(txnId); 

await Task.WhenAll(orderTask, userTask, payTask); 

return new DashboardDto 
{ 
    Order = await orderTask, 
    User = await userTask, 
    Payment = await payTask 
};

✅ Runs all external calls in parallel

✅ Massive latency reduction

5.13 Bulkhead Isolation (Advanced Microservice Pattern)

Protects your system from being overloaded by one failing dependency.

.AddPolicyHandler( 
    Policy.BulkheadAsync<HttpResponseMessage>( 
        maxParallelization: 10, 
        maxQueuingActions: 20));

✅ Only a limited number of requests reach that external API at once.

5.14 Common Async External Call Mistakes (Very Dangerous)

❌ Mistake 1: New HttpClient Per Call

Causes socket exhaustion

❌ Mistake 2: No Timeout

API can hang forever

❌ Mistake 3: No Retry Policy

Temporary failures break your system

❌ Mistake 4: No Circuit Breaker

Cascading failures across microservices

❌ Mistake 5: No CancellationToken

Wastes network + CPU after client disconnect

5.15 Observability for External API Calls

✅ Log:

  • Request duration
  • Status code
  • Retry count
  • Circuit state

✅ Track:

  • External API latency
  • Failure rates
  • Timeout frequency

✅ Use:

  • Structured logging
  • Distributed tracing

5.16 HttpClient Lifecycle and Disposal Patterns

The Problem: Improper HttpClient Disposal

// WRONG: Disposing HttpClient improperly
public async Task CallApiAsync()
{
    using var client = new HttpClient(); // ❌ Disposed too early
    var response = await client.GetAsync("https://api.example.com");
    // HttpClient disposed but underlying sockets may still be in use
    return await response.Content.ReadAsStringAsync();
}

Issue: HttpClient uses connection pooling. Early disposal can:

  • Cause socket leaks
  • Trigger TCP TIME_WAIT state accumulation
  • Reduce connection reuse benefits

Correct Patterns with IHttpClientFactory

// Option 1: Named client (reusable, managed)
services.AddHttpClient("PaymentGateway", client =>
{
    client.BaseAddress = new Uri("https://payments.example.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
});

// Usage
public class PaymentService
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public PaymentService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task ProcessPaymentAsync()
    {
        // Factory manages lifecycle - no manual disposal needed
        var client = _httpClientFactory.CreateClient("PaymentGateway");
        var response = await client.GetAsync("/payments");
        // HttpClient is returned to factory pool automatically
    }
}

Key Points:

  • IHttpClientFactory handles HttpClient disposal automatically
  • HttpClient instances are pooled and reused
  • Default HttpClientMessageHandler lifetime: 2 minutes
  • Factory ensures proper DNS refresh (prevents DNS caching issues)

5.17 Streaming Large Payloads Asynchronously

The Problem: Memory Pressure with Large Responses

// WRONG: Loading entire response into memory
public async Task<string> DownloadLargeFileAsync(string url)
{
    var response = await _httpClient.GetAsync(url);
    var content = await response.Content.ReadAsStringAsync(); // ❌ 1GB in memory!
    return content; // Memory spike, potential OutOfMemoryException
}

Solution: Stream Response Content

// CORRECT: Stream processing
public async Task ProcessLargeFileAsync(string url, string outputPath)
{
    using var response = await _httpClient.GetAsync(
        url, 
        HttpCompletionOption.ResponseHeadersRead); // Don't buffer body
    
    await using var contentStream = await response.Content.ReadAsStreamAsync();
    await using var fileStream = File.Create(outputPath);
    
    // Stream chunk by chunk (64KB buffer)
    await contentStream.CopyToAsync(fileStream, bufferSize: 65536);
    
    // Memory usage: ~64KB instead of potentially gigabytes
}

Streaming Uploads

public async Task UploadLargeFileAsync(string filePath, string url)
{
    await using var fileStream = File.OpenRead(filePath);
    using var streamContent = new StreamContent(fileStream);
    
    // Chunked transfer encoding
    var response = await _httpClient.PostAsync(url, streamContent);
    response.EnsureSuccessStatusCode();
}

Benefits:

  • Constant memory usage regardless of file size
  • Supports files larger than available RAM
  • Better performance with chunked transfer
  • Progress reporting capability

5.18 Custom Message Handlers for Cross-Cutting Concerns

Creating Custom DelegatingHandler

public class LoggingHandler : DelegatingHandler
{
    private readonly ILogger<LoggingHandler> _logger;
    
    public LoggingHandler(ILogger<LoggingHandler> logger)
    {
        _logger = logger;
    }
    
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        
        _logger.LogInformation("Outgoing HTTP Request: {Method} {Url}", 
            request.Method, request.RequestUri);
        
        try
        {
            var response = await base.SendAsync(request, cancellationToken);
            
            _logger.LogInformation(
                "HTTP Response: {StatusCode} in {ElapsedMilliseconds}ms",
                response.StatusCode, stopwatch.ElapsedMilliseconds);
            
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "HTTP Request failed after {ElapsedMilliseconds}ms",
                stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}

Registering Custom Handlers

// Register with dependency injection
services.AddTransient<LoggingHandler>();
services.AddTransient<AuthHeaderHandler>();

// Chain multiple handlers
services.AddHttpClient<ExternalApiClient>()
    .AddHttpMessageHandler<LoggingHandler>()
    .AddHttpMessageHandler<AuthHeaderHandler>()
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

Common Handler Use Cases

// 1. Authentication Handler
public class AuthHeaderHandler : DelegatingHandler
{
    private readonly ITokenService _tokenService;
    
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await _tokenService.GetAccessTokenAsync();
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

// 2. Correlation ID Handler
public class CorrelationIdHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-Correlation-ID", 
            Guid.NewGuid().ToString());
        return base.SendAsync(request, cancellationToken);
    }
}

5.19 DNS Resolution in Long-Running Services

The Problem: DNS Caching

// Without IHttpClientFactory
var client = new HttpClient 
{ 
    BaseAddress = new Uri("https://api.example.com") 
};

// DNS resolved once at HttpClient creation
// If IP changes (failover, scaling), connections fail

Real-world impact: Service outages during:

  • Cloud provider failover
  • Auto-scaling events
  • Blue-green deployments
  • DNS-based load balancing

IHttpClientFactory Solution

// With factory, DNS is refreshed periodically
services.AddHttpClient<ExternalApiClient>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    // Control DNS refresh behavior
    PooledConnectionLifetime = TimeSpan.FromMinutes(2), // Default
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
    
    // DNS resolution settings
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
    UseCookies = false,
    AllowAutoRedirect = false
});

Manual DNS Refresh Pattern

public class DnsAwareHttpClientFactory
{
    private readonly IHttpClientFactory _httpClientFactory;
    private DateTime _lastDnsRefresh = DateTime.MinValue;
    private readonly TimeSpan _dnsRefreshInterval = TimeSpan.FromMinutes(5);
    
    public async Task<HttpResponseMessage> SendWithDnsRefreshAsync(
        HttpRequestMessage request)
    {
        if (DateTime.UtcNow - _lastDnsRefresh > _dnsRefreshInterval)
        {
            // Force new HttpClient creation (with fresh DNS)
            var client = _httpClientFactory.CreateClient("FreshClient");
            _lastDnsRefresh = DateTime.UtcNow;
            return await client.SendAsync(request);
        }
        else
        {
            var client = _httpClientFactory.CreateClient();
            return await client.SendAsync(request);
        }
    }
}

5.20 HttpClient Configuration Best Practices

Connection Pool Tuning

services.AddHttpClient<HighVolumeApiClient>()
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        // Connection pooling settings
        MaxConnectionsPerServer = 100,           // Default: int.MaxValue
        PooledConnectionLifetime = TimeSpan.FromMinutes(2), // DNS refresh
        PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
        EnableMultipleHttp2Connections = true,   // HTTP/2 optimization
        
        // Timeout settings
        ConnectTimeout = TimeSpan.FromSeconds(5),
        ResponseDrainTimeout = TimeSpan.FromSeconds(2),
        
        // Performance
        UseCookies = false,                      // Disable if not needed
        AutomaticDecompression = DecompressionMethods.GZip,
        UseProxy = false                         // Disable if not using proxy
    });

HttpClient Defaults by Use Case

High-Volume Internal Service

// Many small requests between microservices
MaxConnectionsPerServer: 200
PooledConnectionLifetime: 5 minutes
Timeout: 2 seconds
Keep-Alive: enabled
HTTP/2: enabled

External API Integration

// Fewer, larger requests to external providers
MaxConnectionsPerServer: 50
PooledConnectionLifetime: 10 minutes
Timeout: 30 seconds
Retry: enabled with exponential backoff
Circuit Breaker: enabled

File Upload/Download Service

// Large payloads, streaming
MaxConnectionsPerServer: 20
Timeout: 5 minutes
BufferSize: 65536 (64KB)
ChunkedTransfer: enabled
ResponseHeadersRead: enabled

5.21 Testing Async External API Calls

Unit Testing with MockHttpMessageHandler

[Fact]
public async Task GetUserAsync_ReturnsUser_WhenApiSucceeds()
{
    // Arrange
    var handler = new MockHttpMessageHandler();
    handler.When("https://api.example.com/users/123")
           .Respond("application/json", 
                    JsonSerializer.Serialize(new { Id = 123, Name = "John" }));
    
    var httpClient = new HttpClient(handler);
    var service = new UserService(httpClient);
    
    // Act
    var user = await service.GetUserAsync(123);
    
    // Assert
    Assert.NotNull(user);
    Assert.Equal("John", user.Name);
}

[Fact]
public async Task GetUserAsync_Throws_WhenApiFails()
{
    // Arrange
    var handler = new MockHttpMessageHandler();
    handler.When("*").Respond(HttpStatusCode.InternalServerError);
    
    var httpClient = new HttpClient(handler);
    var service = new UserService(httpClient);
    
    // Act & Assert
    await Assert.ThrowsAsync<ExternalApiException>(
        () => service.GetUserAsync(123));
}

Integration Testing with TestServer

[Fact]
public async Task PaymentIntegration_WorksEndToEnd()
{
    // Arrange - Create test server
    var factory = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                // Mock external dependency
                services.AddHttpClient<PaymentGatewayClient>()
                    .ConfigurePrimaryHttpMessageHandler(() => 
                        new MockHttpMessageHandler());
            });
        });
    
    var client = factory.CreateClient();
    
    // Act
    var response = await client.PostAsJsonAsync("/api/payments", 
        new { Amount = 100 });
    
    // Assert
    response.EnsureSuccessStatusCode();
    var payment = await response.Content.ReadFromJsonAsync<PaymentResponse>();
    Assert.True(payment.Success);
}

5.22 Advanced Resilience Patterns

Fallback Strategies

// Polly Fallback Policy
services.AddHttpClient<WeatherService>()
    .AddPolicyHandler(Policy<HttpResponseMessage>
        .HandleResult(r => !r.IsSuccessStatusCode)
        .FallbackAsync(
            fallbackAction: async (ct) => 
            {
                // Return cached/stub data
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent(
                        JsonSerializer.Serialize(GetCachedWeatherData()))
                };
            },
            onFallbackAsync: (result, context) => 
            {
                _logger.LogWarning("Weather API failed, using fallback");
                return Task.CompletedTask;
            }));

Timeout Per Request (Not Per Client)

public async Task<HttpResponseMessage> CallWithTimeoutAsync(
    string url, 
    TimeSpan timeout, 
    CancellationToken cancellationToken)
{
    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(
        cancellationToken);
    timeoutCts.CancelAfter(timeout);
    
    try
    {
        return await _httpClient.GetAsync(
            url, 
            HttpCompletionOption.ResponseHeadersRead,
            timeoutCts.Token);
    }
    catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
    {
        throw new TimeoutException($"Request to {url} timed out after {timeout}");
    }
}

Chapter 5 Summary

  • External calls are I/O bound → must be async
  • Never create HttpClient manually per request
  • Always use IHttpClientFactory or Typed Clients
  • Always configure:
    • Timeout
    • Retry
    • Circuit Breaker
  • Always pass CancellationToken
  • Parallelize independent calls with Task.WhenAll
  • Use Polly for real production resiliency

✅ Chapter 5 (Deep Dive) is now complete.

Next extremely important chapter:

👉 Chapter 6: Cancellation, Timeouts & Cooperative Cancellation in Async APIs

  • How cancellation flows from client → controller → DB → external APIs
  • How to properly cancel long-running jobs
  • Avoiding orphaned background tasks
  • Real-life example: cancelable report generation API

Would you like me to proceed with Chapter 6 now?

إرسال تعليق

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.