我编写了一个针对
.net6,.net7,.net8
的 C# 包装器库。我想添加 DI 支持,以便用户可以通过调用服务扩展方法来配置它AddMyApp
。
namespace Project1
{
public class FooHttpClientOptions : IBasicAuthHttpClientOptions
{
public Uri Url { get; set; }
public BasicAuthCredential Credentials { get; set; }
}
public interface IBasicAuthHttpClientOptions
{
Uri Url { get; set; }
BasicAuthCredential Credentials { get; set; }
}
public class BasicAuthCredential
{
public const string Scheme = "x-api-key";
public string ApiKey { get; set; } = string.Empty;
}
public static class MyAppExtensions
{
public static IServiceCollection AddMyApp<THttpClient, THttpClientOptions>(
this IServiceCollection services,
Action<THttpClientOptions> configure) where THttpClient : class where THttpClientOptions : class, IBasicAuthHttpClientOptions
{
services
.AddOptions<THttpClientOptions>()
.Configure(configure)
.Validate(options => string.IsNullOrEmpty(options.Credentials.ApiKey)
|| string.IsNullOrWhiteSpace(options.Credentials.ApiKey), MyAppExceptionMessages.InvalidApiKeyMessage)
.ValidateOnStart();
services.AddTransient<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddHttpClient<THttpClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<THttpClientOptions>>().Value;
client.BaseAddress = options.Url;
})
.AddHttpMessageHandler<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddTransient<ICustomHttpClient, CustomHttpClient>();
services.AddTransient<ICarsService, CarsService>();
return services;
}
}
public class BasicAuthenticationHandler<TOptions> : DelegatingHandler
where TOptions : class, IBasicAuthHttpClientOptions
{
private readonly TOptions _options;
public BasicAuthenticationHandler(IOptions<TOptions> options)
{
_options = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue(BasicAuthCredential.Scheme, _options.Credentials.ApiKey);
return await base.SendAsync(request, cancellationToken);
}
}
public class MyAppExceptionMessages
{
public const string InvalidApiKeyMessage = "Your API key is invalid.";
}
}
这是
CustomHttpClient
课程。我已在其构造函数中应用了断点,但 httpClient
对象不包含 BaseUrl
并且请求标头也为空。
public class CustomHttpClient : ICustomHttpClient
{
private readonly HttpClient _httpClient;
public CustomHttpClient (HttpClient httpClient)
{
_httpClient = httpClient;
}
}
这是我调用扩展方法的方式:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IBasicAuthHttpClientOptions, FooHttpClientOptions>();
builder.Services.AddMyApp<CustomHttpClient, FooHttpClientOptions>(option =>
{
option.Url = new Uri("xxxxxxxxxxxxxx");
option.Credentials = new BasicAuthCredential
{
ApiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
});
var app = builder.Build();
为什么提供的
Url
和 ApiKey
值没有到达 httpClient
内的 CustomHttpClient
对象?
您已经非常接近正确,但有两件事阻碍了您:
HttpClient
服务当执行此行时:
services.AddHttpClient<THttpClient>()
类型化客户端以所提供类型的名称在服务集合中注册。
此外,当您将鼠标悬停在
AddHttpClient()
上时,其中一个句子会显示:
(...) 通过提供
作为服务类型,可以从THttpClient
(及相关方法)检索使用适当HttpClient
构造的IServiceProvider.GetService(Type)
实例。THttpClient
因此,我们可以得出的结论是,您的自定义
HttpClient
将与实现类型绑定,因为这是您在调用时传递的类型:
services.AddMyApp<CustomHttpClient, FooHttpClientOptions>(x => { });
但是,您的
CustomHttpClient
实现了 ICustomHttpClient
接口,这就是您在解析它时使用的类型(我假设)。但 HttpClient
不与 ICustomHttpClient
绑定,仅与 CustomHttpClient
绑定。您可以通过为接口添加第三个类型参数并将其传递给 AddHttpClient()
扩展方法来解决此问题:
public static IServiceCollection AddMyApp<TService, THttpClient, THttpClientOptions>(
this IServiceCollection services,
Action<THttpClientOptions> configure)
where TService : class
where THttpClient : class, TService
where THttpClientOptions : class, IBasicAuthHttpClientOptions
{
services.AddHttpClient<TService, THttpClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<THttpClientOptions>>().Value;
client.BaseAddress = options.Url;
});
return services;
}
CustomHttpClient
现在,这可能会起作用,但您很可能会陷入第二个陷阱。在关于IHttpClientFactory
的
官方文档中有一个注册示例:
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();
重要提示:
类型化客户端在 DI 容器中注册为瞬态。在上面的代码中,AddHttpClient() 将 CatalogService、BasketService、OrderingService 注册为临时服务,因此可以直接注入和使用它们,无需额外注册。
这意味着通过调用
services.AddHttpClient<TService, THttpClient>()
,扩展方法已经为您注册了 TService
,并且实现为 THttpClient
。因此,如果您调用自己的自定义扩展方法:
services.AddMyApp<CustomHttpClient, FooHttpClientOptions>(x => { });
您最终将获得两次
CustomHttpClient
注册(如果您使用上面的修改版本,则为 ICustomHttpClient
)。下面是从调试器收集服务的视图:
允许注册相同的服务,但解析单个服务时,将使用最后一次注册的内容,且不与
HttpClient
绑定。要修复它,请删除此行:
services.AddTransient<ICustomHttpClient, CustomHttpClient>();
我之前说过这可能会起作用,因为如果您使用不同的客户端,则不会发生这种情况,因为传递的类型将与
CustomHttpClient
不同,但我怀疑您正在使用CustomHttpClient
测试该扩展,如下所示:
services.AddMyApp<CustomHttpClient, FooHttpClientOptions>(x => { });
这是扩展方法的最终代码:
public static IServiceCollection AddMyApp<TService, THttpClient, THttpClientOptions>(
this IServiceCollection services,
Action<THttpClientOptions> configure)
where TService : class
where THttpClient : class, TService
where THttpClientOptions : class, IBasicAuthHttpClientOptions
{
services
.AddOptions<THttpClientOptions>()
.Configure(configure)
.Validate(options => !string.IsNullOrEmpty(options.Credentials.ApiKey)
&& !string.IsNullOrWhiteSpace(options.Credentials.ApiKey), MyAppExceptionMessages.InvalidApiKeyMessage)
.ValidateOnStart();
services.AddTransient<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddHttpClient<TService, THttpClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<THttpClientOptions>>().Value;
client.BaseAddress = options.Url;
})
.AddHttpMessageHandler<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddTransient<ICarsService, CarsService>();
return services;
}
PS 我在选项中颠倒了 API 密钥验证逻辑,除非您希望它在 API 密钥为 null 或为空时抛出异常。