PAYATHON 2026

.NET 中创建具体 HttpClient 实例是否违背 Dependency Injection?

技术老齐

结论

这种写法符合 Dependency Injection(DI)的用法。Auth 通过构造函数接收 IHttpClientFactory,再通过工厂获取 HttpClient。客户端配置和底层处理程序的管理由外部注册决定。

直接依赖 HttpClient 具体类型,也不一定会造成不合理的紧耦合。DI 不要求每个依赖都有接口,.NET 的 typed client 就支持通过构造函数直接注入 HttpClient。Microsoft Learn:使用 IHttpClientFactory

示例中仍有几个问题需要修正,首先是客户端名称不一致。

成员字段中调用工厂是否合适?

在字段初始化时,可以通过工厂获取客户端:

private readonly HttpClient _client =
    httpClientFactory.CreateClient("AuthService");

这里由 Auth 选择需要的客户端,再由工厂按配置创建,属于正常的工厂注入用法。

支持 C# 12 主构造函数的项目允许在实例字段初始化器中使用主构造函数参数,因此这段写法没有问题,字段也可以设为 readonly。

需要注意的是客户端的生命周期。字段中的 HttpClient 会随 Auth 一起存活:如果 Auth 只在短期内使用,这种方式通常合适;如果它是单例或会被长期持有,就应调整获取客户端的位置。

先修正名称和请求地址

注册时使用的名称是:

builder.Services.AddHttpClient("AuthService", ...);

创建时却使用了:

httpClientFactory.CreateClient("Auth");

这两个名称必须一致。默认的 IHttpClientFactory 实现通常也会为未配置的名称创建客户端,但这个客户端不会获得 "AuthService" 对应的配置,包括这里的 BaseAddress 和处理程序设置。

请求地址也有问题:

await client.GetAsync("www.example.com");

"www.example.com" 会被当作相对 URI,不能作为指定网站的绝对地址使用:

  • 请求指定网站时,使用 "https://www.example.com/"。
  • 请求已配置服务中的资源时,使用 "api/stuff" 这样的相对路径。

如果请求 URI 是相对地址,又没有设置 BaseAddress,GetAsync 会抛出 InvalidOperationException。原代码只捕获 HttpRequestException,无法处理这个配置错误。Microsoft Learn:HttpClient.GetAsync

保留工厂注入的修改示例

可以继续注入 IHttpClientFactory,在执行操作时获取命名客户端。这种方式也适用于需要长期存活的服务。

下面的示例只展示客户端配置和请求流程,返回值是状态码,转换为 Result<Uri> 的部分仍需由业务代码处理。示例假设接口不依赖自动保存的 Cookie。

Program.cs:

builder.Services
    .AddHttpClient("AuthService", client =>
    {
        var address = builder.Configuration.GetValue<string>("Address")
            ?? throw new InvalidOperationException("缺少 Address 配置。");

        client.BaseAddress = new Uri(address, UriKind.Absolute);
    })
    .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseCookies = false
    });

builder.Services.AddTransient<Auth>();

Auth.cs:

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class Auth(IHttpClientFactory httpClientFactory)
{
    public async Task<HttpStatusCode> GetStatusAsync(
        CancellationToken cancellationToken = default)
    {
        using var client =
            httpClientFactory.CreateClient("AuthService");

        using var response =
            await client.GetAsync("api/stuff", cancellationToken);

        return response.StatusCode;
    }
}

使用时,将 "api/stuff" 替换为实际接口路径。Address 应设为包含协议的绝对地址,例如 https://service.example.com/。如果地址包含路径前缀,还需要留意结尾的 /,它会影响相对 URI 的合并结果。

每次调用 CreateClient 都会创建新的 HttpClient,底层的 HttpMessageHandler 则由工厂池化并复用。释放工厂创建的客户端不会立即释放池中的处理程序,因此这里可以使用 using。Microsoft Learn:IHttpClientFactory 生命周期与常见问题

也可以直接注入 HttpClient

如果 Auth 始终只访问一个固定服务,可以改用 typed client,让构造函数直接接收配置好的 HttpClient:

builder.Services.AddHttpClient<Auth>(client =>
{
    var address = builder.Configuration.GetValue<string>("Address")
        ?? throw new InvalidOperationException("缺少 Address 配置。");

    client.BaseAddress = new Uri(address, UriKind.Absolute);
});

对应的构造函数:

public class Auth
{
    private readonly HttpClient _client;

    public Auth(HttpClient client)
    {
        _client = client;
    }

    // 请求方法通过 _client 访问服务。
}

采用这种替代注册方式时,无需再单独调用 AddTransient<Auth>()。再次注册可能覆盖 typed client 的注册,导致注入的客户端失去预期配置。Microsoft Learn:typed client 注册问题

两种方式都合理。需要按名称选择客户端时,可以保留工厂;一个类封装一个服务时,直接注入 HttpClient 往往更简洁。

何时需要再增加接口?

Auth 使用 HttpClient,就需要了解 HTTP 请求,这属于 HTTP 接入代码的正常职责。

如果上层业务只关心“获取认证结果”,可以让它依赖 IAuthService,由实现类封装 HTTP 请求和响应解析。上层业务便可以替换认证实现,无需了解状态码、请求头或接口地址。

如果只是为了测试,不必立即包装整个 HttpClient API。可以通过 new HttpClient(fakeHandler) 传入自定义 HttpMessageHandler 来模拟响应;采用工厂注入时,让测试工厂返回这个客户端即可。

原代码还需要注意什么?

原来的证书回调:

ServerCertificateCustomValidationCallback = (_, _, _, _) => true

会无条件接受服务器证书。正常部署时应移除该回调,并正确配置证书信任。使用 DI 或工厂不会消除这项风险。

UseCookies = true 也需要单独评估。工厂复用处理程序时,会一起复用 CookieContainer,可能导致不同调用共享 Cookie;处理程序轮换后,其中保存的 Cookie 又会丢失。如果认证依赖 Cookie,需要明确会话隔离和保存策略。Microsoft Learn:IHttpClientFactory 的 Cookie 注意事项

原来的 GetStuff 还缺少非 200 OK 情况下的返回值。如果省略的代码中没有其他处理,这个方法无法编译。GetAsync 不会仅因收到 401、404 或 500 就抛出异常,需要将这些响应转换为 Result<Uri>.Failure(...),或按业务规则处理。对于取消和超时,也不能只依赖 catch (HttpRequestException),因为现代 .NET 中还可能出现 OperationCanceledException。Microsoft Learn:GetAsync 异常说明

还应避免在单例中长期保存工厂创建的客户端或 typed client。工厂轮换处理程序时,现有 HttpClient 不会自动切换到新的处理程序,长期持有可能影响它响应 DNS 变化的能力。Microsoft Learn:客户端未响应 DNS 变化

备注:内容仅供参考。