Quản lý HTTP Client trong ASP.NET Core với IHttpClientFactory và Các Mô Hình Tích Hợp

ASP.NET Core cung cấp IHttpClientFactory như một cơ chế trung tâm để tạo, quản lý và mở rộng các phiên bản HttpClient, giúp tránh các vấn đề phổ biến như rò rỉ socket, cấu hình trùng lặp hoặc thiếu kiểm soát vòng đời.

1. Cấu hình chính sách thời gian chờ với Polly

Để xử lý linh hoạt các yêu cầu có độ trễ khác nhau (ví dụ: GET nhanh vs POST cần xử lý lâu), bạn có thể áp dụng các chính sách timeout riêng biệt dựa trên phương thức HTTP:

// Định nghĩa hai chính sách timeout
var quickTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(8));
var extendedTimeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(25));

// Đăng ký client kiểu mạnh với chính sách động
builder.Services.AddHttpClient<ApiGatewayClient>()
    .AddHttpMessageHandler<CustomHeaderHandler>()
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Post ? extendedTimeout : quickTimeout);

2. Client được đặt tên (Named Client)

Dùng khi cần nhiều cấu hình khác nhau cho cùng một loại yêu cầu — ví dụ: một client dành riêng cho hệ thống thanh toán, một client khác cho dịch vụ xác thực:

builder.Services.AddHttpClient("payment-service")
    .SetBaseAddress(new Uri("https://api.payments.example/"))
    .AddHttpMessageHandler<AuthHeaderHandler>()
    .AddTransientHttpErrorPolicy(policy => policy
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100)
        ));

Sử dụng trong controller:

[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
    private readonly HttpClient _paymentClient;

    public PaymentController(IHttpClientFactory factory)
    {
        _paymentClient = factory.CreateClient("payment-service");
    }

    [HttpPost("charge")]
    public async Task<IActionResult> Charge([FromBody] PaymentRequest req)
    {
        var response = await _paymentClient.PostAsJsonAsync("v1/charges", req);
        return Ok(await response.Content.ReadFromJsonAsync<ChargeResult>());
    }
}

3. Client kiểu mạnh (Typed Client)

Là cách khuyến nghị nhất để đóng gói logic gọi HTTP vào một lớp rõ ràng, dễ test và tái sử dụng:

public class ApiGatewayClient
{
    private readonly HttpClient _client;

    public ApiGatewayClient(HttpClient client)
    {
        _client = client;
        _client.BaseAddress = new Uri("https://api.example.com/");
        _client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }

    public async Task<UserDto> FetchUserAsync(int id)
    {
        var response = await _client.GetAsync($"/users/{id}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<UserDto>();
    }

    public async Task<bool> SubmitOrderAsync(OrderDto order)
    {
        var response = await _client.PostAsJsonAsync("/orders", order);
        return response.IsSuccessStatusCode;
    }
}

Đăng ký trong Program.cs:

builder.Services.AddHttpClient<ApiGatewayClient>()
    .AddHttpMessageHandler<LoggingHandler>()
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
        .WaitAndRetryAsync(2, _ => TimeSpan.FromMilliseconds(300)));

4. Xử lý yêu cầu ra (Outgoing Handler)

Các lớp kế thừa từ DelegatingHandler cho phép chèn logic trước/sau mỗi yêu cầu HTTP. Ví dụ: thêm header tự định nghĩa, ghi log, hoặc kiểm tra token:

public class CustomHeaderHandler : DelegatingHandler
{
    private readonly ILogger<CustomHeaderHandler> _logger;

    public CustomHeaderHandler(ILogger<CustomHeaderHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Gửi yêu cầu tới {Uri}", request.RequestUri);

        request.Headers.TryAddWithoutValidation("X-Client-ID", "web-app-v2");
        request.Headers.TryAddWithoutValidation("X-Request-Time", DateTime.UtcNow.ToString("o"));

        return await base.SendAsync(request, cancellationToken);
    }
}

Đảm bảo đăng ký handler như một service riêng:

builder.Services.AddScoped<CustomHeaderHandler>();

5. Tích hợp Refit cho API Client tự sinh

Với Refit, bạn mô tả API dưới dạng interface và để thư viện tự sinh implementation runtime:

// Định nghĩa interface
public interface IUserApi
{
    [Get("/users/{id}")]
    Task<UserDto> GetByIdAsync(int id);

    [Post("/users")]
    Task<ResponseResult> CreateAsync([Body] CreateUserDto user);

    [Patch("/users/{id}")]
    Task<UserDto> UpdatePartialAsync(int id, [Body] JsonPatchDocument<UserDto> patch);
}

// Đăng ký trong DI container
builder.Services.AddRefitClient<IUserApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com/"))
    .AddHttpMessageHandler<AuthenticationHandler>();

6. Gọi trực tiếp qua IHttpClientFactory (Unconfigured Client)

Khi cần linh hoạt cao (ví dụ: URL động hoặc cấu hình runtime), có thể tạo client không tên và thiết lập thủ công:

[ApiController]
[Route("api/[controller]")]
public class DynamicClientController : ControllerBase
{
    private readonly IHttpClientFactory _factory;

    public DynamicClientController(IHttpClientFactory factory) => _factory = factory;

    [HttpGet("fetch")]
    public async Task<IActionResult> FetchData([FromQuery] string baseUrl, [FromQuery] string path)
    {
        var client = _factory.CreateClient();
        client.BaseAddress = new Uri(baseUrl);
        
        var response = await client.GetAsync(path);
        var content = await response.Content.ReadAsStringAsync();
        
        return Ok(new { StatusCode = response.StatusCode, Body = content });
    }
}

7. Gửi dữ liệu đa dạng

Hỗ trợ đầy đủ các kiểu gửi: JSON, form-urlencoded, multipart/form-data (file upload), và JSON Patch:

  • JSON POST: Dùng PostAsJsonAsync
  • Form POST: Dùng FormUrlEncodedContent
  • File upload: Dùng MultipartFormDataContent kết hợp ByteArrayContent
  • Patch: Đặt Content-Type: application/json-patch+json

Thẻ: IHttpClientFactory polly Refit DelegatingHandler HttpClient

Đăng vào ngày 11 tháng 6 lúc 18:50