Location>code7788 >text

How to build a flexible HTTP request mechanism in .NET Core?

Popularity:958 ℃/2025-01-21 17:49:18

1. Understand the elastic HTTP request mechanism

What is elasticity?

Resilience refers to the ability of a system to maintain or quickly recover to a normal state when faced with faults or abnormal conditions. In the context of HTTP requests, resiliency means that when a request fails, the system is able to automatically take a series of actions (such as retrying, downgrading, circuit breaking, etc.) to ensure that the request ultimately succeeds or handles the failure gracefully.

Why do you need an elastic HTTP request mechanism?

In a distributed system, the dependencies between services are complex, and the failure of any service may cause the entire system to become unavailable. The elastic HTTP request mechanism can help us:

  • Improve system availability: Reduce system unavailability caused by transient faults through retry, circuit breaking and other strategies.
  • Enhance user experience: Reduce user-perceived downtime through fast recovery and graceful degradation.
  • Reduce operation and maintenance costs: Reduce the need for manual intervention by automating fault handling.

Core Principles of Resilience Mechanisms

  • Retry: When the request fails, automatically retry a certain number of times.
  • Circuit Breaker: When the failure rate reaches a certain threshold, the request is temporarily stopped to avoid the avalanche effect.
  • Timeout:Set the request timeout to avoid long waiting.
  • Fallback: Provides an alternative response or behavior when a request fails.
  • Load Balancing: Distribute requests to multiple service instances to avoid single points of failure.

2. Basics of HTTP requests in .NET Core

Usage of HttpClient

In .NET Core,HttpClientIs the main class used to send HTTP requests and receive HTTP responses. The following is a simpleHttpClientUsage example:

using System;
 using ;
 using ;

 public class HttpClientApplication
 {
     public static async Task Main(string[] args)
     {
         using (HttpClient client = new HttpClient())
         {
             //Send GET request
             HttpResponseMessage response = await ("https://******");
             if()
             {
                 //Read response content
                 string content = await ();
                 (content);
             }
             else
             {
                 // Output error status code
                 ($"Error: {}");
             }
         }
     }
 }

Introduction of HttpClientFactory

HttpClientThere are some issues with direct use, such as DNS update issues and socket exhaustion issues. To solve these problems, .NET Core introducesHttpClientFactory, which provides betterHttpClientLifecycle management and configuration options.

existMedium configurationHttpClientFactory

public class Startup
 {
     public void ConfigureServices(IServiceCollection services)
     {
         // Register HttpClientFactory and add a named HttpClient
         ("ResilientClient", client =>
         {
              = new Uri("https://******"); // Set the base address
             ("Accept", "application/json"); // Set default request headers
         });
     }

     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
     {
         //Other middleware configuration
     }
 }

Used in a controller or serviceHttpClientFactory

using ;
 using ;
 using ;

 [ApiController]
 [Route("[controller]")]
 public class ResilientController : ControllerBase
 {
     private readonly IHttpClientFactory _httpClientFactory;

     public ResilientController(IHttpClientFactory httpClientFactory)
     {
         _httpClientFactory = httpClientFactory;
     }

     [HttpGet]
     public async Task<IActionResult> Get()
     {
         // Get the HttpClient instance by name
         var client = _httpClientFactory.CreateClient("ResilientClient");

         //Send GET request
         var response = await ("posts/list");
         if()
         {
             var content = await ();
             return Ok(content); // Return successful response
         }

         return StatusCode((int)); // Return error status code
     }
 }

advantage:

  • life cycle managementHttpClientFactoryAutomatic managementHttpClientlife cycle to avoid socket exhaustion problems.
  • Flexible configuration: Different APIs can be configured for differentHttpClientExample.
  • DNS update supportHttpClientFactoryThe DNS cache is refreshed periodically.

3. Implement a basic retry mechanism

Simple retry logic

Without using any library, we can implement the retry logic with a simple loop:

public async Task<string> GetDataWithRetryAsync(int maxRetries = 3)
 {
     int retryCount = 0;
     while(true)
     {
         try
         {
             //Send GET request
             HttpResponseMessage response = await _httpClient.GetAsync("data");
             (); // Ensure the request is successful
             return await (); // Return response content
         }
         catch(HttpRequestException)
         {
             retryCount++;
             if (retryCount >= maxRetries)
             {
                 throw; // throws an exception after exceeding the number of retries
             }
         }
     }
 }

Using Polly to implement a retry strategy

Polly is a popular .NET elastic library that provides rich strategies to implement retry, circuit breaking, timeout and other functions. Here is an example of using Polly to implement a retry strategy:

using Polly;
 using ;

 public class RetryService
 {
     private readonly HttpClient _httpClient;
     private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;

     public RetryService(HttpClient httpClient)
     {
         _httpClient = httpClient;
         //Configure retry strategy: retry up to 3 times, wait 2 seconds each time
         _retryPolicy = Policy
             .HandleResult<HttpResponseMessage>(r => !) // Handle failure response
             .Or<HttpRequestException>() // Handle request exception
             .WaitAndRetryAsync(3, retryAttempt => ((2, retryAttempt))); // Exponential backoff
     }

     public async Task<string> GetDataWithRetryAsync()
     {
         //Execute retry strategy
         HttpResponseMessage response = await _retryPolicy.ExecuteAsync(() => _httpClient.GetAsync("data"));
         (); // Ensure the request is successful
         return await (); // Return response content
     }
 }

Configuration of retry strategy

Polly allows us to flexibly configure retry strategies, including the number of retries, retry intervals, etc. The following is an example of configuring an exponential backoff retry policy:

_retryPolicy = Policy
    .HandleResult<HttpResponseMessage>(r => !)
    .Or<HttpRequestException>()
    .WaitAndRetryAsync(5, retryAttempt => ((2, retryAttempt)));

4. Handle transient faults

What is a transient fault?

Transient faults are those that are temporary and usually recover automatically. For example, network jitters, service temporarily unavailable, etc. The characteristic of transient faults is that they are usually short-lived and may succeed after retries.

Common types of transient faults

  • Network jitter: The request failed due to unstable network connection.
  • Service is temporarily unavailable: The target service is temporarily unavailable due to high load or maintenance.
  • Resource limits: The target service is temporarily unable to process the request due to resource constraints (such as CPU, memory).

Handle transient faults with Polly

Polly provides a variety of strategies to handle transient failures, including retries, circuit breaks, timeouts, etc. Here is an example that combines retry and circuit breaking strategies:

// Define a retry strategy to retry when the HTTP request fails
   var retryPolicy = Policy
       .HandleResult<HttpResponseMessage>(r => !)
       .Or<HttpRequestException>()
       //Set the number of retries to 3, and the interval between each retry increases exponentially (2^retryAttempt seconds)
       .WaitAndRetryAsync(3, retryAttempt => ((2, retryAttempt)));

   // Define the circuit breaker strategy. When the number of consecutive failures reaches the threshold, the circuit breaker will be circuit breaker for a period of time.
   var circuitBreakerPolicy = Policy
       .HandleResult<HttpResponseMessage>(r => !)
       .Or<HttpRequestException>()
       .CircuitBreakerAsync(5, (30)); // Set the circuit breaker condition: after 5 consecutive failures, the circuit breaker will be circuit breaker for 30 seconds.

   // Combine the retry strategy and the circuit breaker strategy into a comprehensive strategy
   var combinedPolicy = (retryPolicy, circuitBreakerPolicy);

   HttpResponseMessage response = await (() => _httpClient.GetAsync("data"));

5. Implement circuit breaker mode

The concept of circuit breaker pattern

The circuit breaker pattern is a design pattern used to prevent a system from crashing due to the failure of a dependent service. When the failure rate of a dependent service reaches a certain threshold, the circuit breaker will open and stop all requests until the dependent service is restored.

Use Polly to implement circuit breaker strategy

Polly providedCircuitBreakerstrategy to implement the circuit breaker strategy. The following is an example of using Polly to implement a circuit breaker strategy:

var circuitBreakerPolicy = Policy
     .HandleResult<HttpResponseMessage>(r => !)
     .Or<HttpRequestException>()
     .CircuitBreakerAsync(5, (30)); // After 5 consecutive failures, the circuit breaker opens for 30 seconds

 HttpResponseMessage response = await (() => _httpClient.GetAsync("data"));

Configure circuit breaker policy parameters

Polly allows us to configure the parameters of the circuit breaker strategy, including failure threshold, circuit break time, etc. Here is an example of configuring a circuit breaker:

var circuitBreakerPolicy = Policy
     .HandleResult<HttpResponseMessage>(r => !)
     .Or<HttpRequestException>()
     .CircuitBreakerAsync(
         exceptionsAllowedBeforeBreaking: 5, // Allowed number of failures
         durationOfBreak: (30) // Break time
     );

6. Timeouts and timeout strategies

Set request timeout

existHttpClient, we can passTimeoutProperty sets the timeout for the request:

_httpClient.Timeout = (10); //Set the timeout to 10 seconds

Implementing timeout strategies using Polly

Polly providedTimeoutStrategies to implement timeout control. Here is an example of using Polly to implement a timeout policy:

var timeoutPolicy = <HttpResponseMessage>((10)); //Set the timeout to 10 seconds

 HttpResponseMessage response = await (() => _httpClient.GetAsync("data"));

Combining timeouts and retries

We can use a timeout strategy in conjunction with a retry strategy to deal with request failures due to timeouts:

var retryPolicy = Policy
     .HandleResult<HttpResponseMessage>(r => !)
     .Or<HttpRequestException>()
     .WaitAndRetryAsync(3, retryAttempt => ((2, retryAttempt))); // Retry strategy

 var timeoutPolicy = <HttpResponseMessage>((10)); // Timeout policy

 var combinedPolicy = (retryPolicy, timeoutPolicy); // Combined policy

 HttpResponseMessage response = await (() => _httpClient.GetAsync("data"));

7. Load balancing and request offloading

Basic concepts of load balancing

Load balancing refers to spreading requests to multiple service instances to avoid single points of failure and improve system scalability. Common load balancing strategies include polling, random, weighted polling, etc.

Load balancing in .NET Core

In .NET Core, we can configure multipleHttpClientinstances to achieve load balancing. Here is a simple load balancing example:

public class LoadBalancer
 {
     private readonly List<HttpClient> _httpClients;
     private readonly Random _random = new Random();

     public LoadBalancer(IHttpClientFactory httpClientFactory)
     {
         _httpClients = new List<HttpClient>
         {
             ("ServiceInstance1"), // instance 1
             ("ServiceInstance2"), // instance 2
             ("ServiceInstance3") //Instance 3
         };
     }

     public async Task<string> GetDataAsync()
     {
         // Randomly select an HttpClient instance
         HttpClient client = _httpClients[_random.Next(_httpClients.Count)];
         HttpResponseMessage response = await ("data");
         ();
         return await ();
     }
 }

Request diversion strategy

Request offloading refers to distributing requests to different service instances based on certain conditions (such as request content, user identity, etc.). The following is a simple request offloading example:

public async Task<string> GetDataAsync(string userId)
 {
     //Select different HttpClient instances based on user ID
     HttpClient client = ("A") ? _httpClients[0] : _httpClients[1];
     HttpResponseMessage response = await ("data");
     ();
     return await ();
 }

8. Monitoring and logging

The Importance of Monitoring HTTP Requests

Monitoring HTTP requests can help us discover and solve problems in time to ensure the stability and reliability of the system. Common monitoring indicators include request success rate, response time, error rate, etc.

Monitor with Application Insights

Application Insights is an application performance management service provided by Azure that can help us monitor and analyze HTTP requests. Here is an example of using Application Insights to monitor HTTP requests:

public class HttpRemoteService
 {
     private readonly HttpClient _httpClient;
     private readonly TelemetryClient _telemetryClient;

     public HttpRemoteService(HttpClient httpClient, TelemetryClient telemetryClient)
     {
         _httpClient = httpClient;
         _telemetryClient = telemetryClient;
     }

     public async Task<string> GetDataAsync()
     {
         var startTime = ;
         var timer = ();

         try
         {
             HttpResponseMessage response = await _httpClient.GetAsync("data");
             ();
             return await ();
         }
         catch (Exception ex)
         {
             _telemetryClient.TrackException(ex); // Record exception
             throw;
         }
         finally
         {
             ();
             _telemetryClient.TrackDependency("HTTP", "GET", "data", startTime, , true); // Record dependency calls
         }
     }
 }

Best practices for logging

Logging is an important tool for monitoring and debugging. Here are some logging best practices:

  • Record key information: Such as request URL, response status code, response time, etc.
  • Use structured logging: Convenient for log query and analysis.
  • Avoid logging sensitive information: Such as password, token, etc.
public async Task<string> GetDataAsync()
 {
     _logger.LogInformation("Sending HTTP GET request to {Url}", "https://api.*****.com/data");

     try
     {
         HttpResponseMessage response = await _httpClient.GetAsync("data");
         ();
         string content = await ();
         _logger.LogInformation("Request successful, response status code: {StatusCode}", );
         return content;
     }
     catch (Exception ex)
     {
         _logger.LogError(ex, "Request failed: {Message}", );
         throw;
     }
 }

References

  • Polly official documentation
  • .NET Core official documentation
  • Application Insights official documentation

Conclusion

Building a resilient HTTP request mechanism in .NET Core is a complex but worthwhile task. I hope this article can help you build a robust HTTP request mechanism in .NET Core.