Skip to content

在 ASP.NET Core 中通过 Delegating Handlers 扩展 HttpClient

Published: at 12:00 AM

摘要

Delegating handlers 很像 ASP.NET Core 的中间件。不同的是,它们与 HttpClient 一起工作。我将向你展示如何使用 delegating handlers

Delegating handlers 很像 ASP.NET Core 中间件。不同的是,它们与 HttpClient 一起工作。ASP.NET Core 请求管道允许你通过使用 中间件 引入自定义行为。你可以使用中间件解决许多横切关注点 — 日志记录、跟踪、验证、认证、授权等。

但是,一个重要的方面是,中间件与进入你的 API 的 HTTP 请求一起工作。Delegating handlers 与发出的请求一起工作。

HttpClient 是我首选的在 ASP.NET Core 中发送 HTTP 请求的方法。它使用起来非常简单,并解决了我的大多数用例。你可以使用 delegating handlers 在发送 HTTP 请求前或后扩展 HttpClient 的行为。

今天,我想向你展示如何使用 DelegatingHandler 引入:

配置 HttpClient

这是一个非常简单的应用,它:

我们将使用 delegating handlers 扩展 GitHubService 的行为。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
});

var app = builder.Build();

app.MapGet("api/users/{username}", async (
    string username,
    GitHubService gitHubService) =>
{
    var content = await gitHubService.GetByUsernameAsync(username);

    return Results.Ok(content);
});

app.Run();

GitHubService 类是一个 类型化客户端 的实现。类型化客户端允许你暴露一个强类型的 API 并隐藏 HttpClient。运行时通过依赖注入提供一个配置过的 HttpClient 实例。你也不必考虑释放 HttpClient。它从一个管理 HttpClient 生命周期的底层 IHttpClientFactory 解析出来。

public class GitHubService(HttpClient client)
{
    public async Task<GitHubUser?> GetByUsernameAsync(string username)
    {
        var url = $"users/{username}";

        return await client.GetFromJsonAsync<GitHubUser>(url);
    }
}

使用 Delegating Handlers 记录 HTTP 请求

让我们从一个简单的例子开始。我们将在发送 HTTP 请求之前和之后添加日志记录。为此,我们将创建一个自定义的 delegating handler - LoggingDelegatingHandler

自定义的 delegating handler 实现了 DelegatingHandler 基类。然后,你可以覆盖 SendAsync 方法以引入额外的行为。

public class LoggingDelegatingHandler(ILogger<LoggingDelegatingHandler> logger)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        try
        {
            logger.LogInformation("Before HTTP request");

            var result = await base.SendAsync(request, cancellationToken);

            result.EnsureSuccessStatusCode();

            logger.LogInformation("After HTTP request");

            return result;
        }
        catch (Exception e)
        {
            logger.LogError(e, "HTTP request failed");

            throw;
        }
    }
}

你还需要通过依赖注入注册 LoggingDelegatingHandler。Delegating handlers 必须以 短暂 的服务注册。

AddHttpMessageHandler 方法为 GitHubService 添加了 LoggingDelegatingHandler 作为一个 delegating handler。任何使用 GitHubService 发出的 HTTP 请求都将首先通过 LoggingDelegatingHandler

builder.Services.AddTransient<LoggingDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>();

让我们看看还可以做些什么。

通过 Delegating Handlers 增加弹性

构建弹性应用是云开发的重要需求。

RetryDelegatingHandler 类使用 Polly 创建一个 AsyncRetryPolicy。重试策略封装了 HTTP 请求,并在遇到瞬态故障时重试它。

public class RetryDelegatingHandler : DelegatingHandler
{
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy =
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .RetryAsync(2);

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var policyResult = await _retryPolicy.ExecuteAndCaptureAsync(
            () => base.SendAsync(request, cancellationToken));

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw new HttpRequestException(
                "Something went wrong",
                policyResult.FinalException);
        }

        return policyResult.Result;
    }
}

你还需要通过依赖注入注册 RetryDelegatingHandler。同时,记得将其配置为消息处理器。在这个例子中,我将两个 delegating handlers 链接在一起,它们将依次运行。

builder.Services.AddTransient<RetryDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>()
.AddHttpMessageHandler<RetryDelegatingHandler>();

通过 Delegating Handlers 解决认证问题

在任何微服务应用中,你都将不得不解决认证这一横切关注点。delegating handlers 的一个常见用途是在发送 HTTP 请求之前添加 Authorization 头。

例如,GitHub API 要求在认证传入请求时存在访问令牌。AuthenticationDelegatingHandler 类从 GitHubOptions 添加了 Authorization 头的值。另一个要求是指定 User-Agent 头,这是从应用配置中设置的。

public class AuthenticationDelegatingHandler(IOptions<GitHubOptions> options)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        request.Headers.Add("Authorization", options.Value.AccessToken);
        request.Headers.Add("User-Agent", options.Value.UserAgent);

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

不要忘记将 AuthenticationDelegatingHandlerGitHubService 配置在一起:

builder.Services.AddTransient<AuthenticationDelegatingHandler>();

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com");
})
.AddHttpMessageHandler<LoggingDelegatingHandler>()
.AddHttpMessageHandler<RetryDelegatingHandler>()
.AddHttpMessageHandler<AuthenticationDelegatingHandler>();

以下是使用 KeyCloakAuthorizationDelegatingHandler 的更复杂的认证示例。这是一个从 Keycloak 获取访问令牌的 delegating handler。Keycloak 是一个开源身份和访问管理服务。

我在我的实用洁净架构课程中使用了 Keycloak 作为身份提供者。

这个例子中的 delegating handler 使用了 OAuth 2.0 客户端凭据授权流程来获取访问令牌。当应用请求访问令牌以访问它们自己的资源,而不是代表一个用户时,就会使用这种授权。

public class KeyCloakAuthorizationDelegatingHandler(
    IOptions<KeycloakOptions> keycloakOptions)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var authToken = await GetAccessTokenAsync();

        request.Headers.Authorization = new AuthenticationHeaderValue(
            JwtBearerDefaults.AuthenticationScheme,
            authToken.AccessToken);

        var httpResponseMessage = await base.SendAsync(
            request,
            cancellationToken);

        httpResponseMessage.EnsureSuccessStatusCode();

        return httpResponseMessage;
    }

    private async Task<AuthToken> GetAccessTokenAsync()
    {
        var params = new KeyValuePair<string, string>[]
        {
            new("client_id", _keycloakOptions.Value.AdminClientId),
            new("client_secret", _keycloakOptions.Value.AdminClientSecret),
            new("scope", "openid email"),
            new("grant_type", "client_credentials")
        };

        var content = new FormUrlEncodedContent(params);

        var authRequest = new HttpRequestMessage(
            HttpMethod.Post,
            new Uri(_keycloakOptions.TokenUrl))
        {
            Content = content
        };

        var response = await base.SendAsync(authRequest, cancellationToken);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<AuthToken>() ??
               throw new ApplicationException();
    }
}

总结

Delegating handlers 为你提供了一个强大的机制,以扩展使用 HttpClient 发送请求时的行为。你可以使用 delegating handlers 解决横切关注点,就像你会使用中间件一样。

以下是一些你可能会使用 delegating handlers 的想法:

我相信你自己也能想出一些用例。

我制作了一个展示如何实现 delegating handlers的视频,你可以在这里观看。