无法使用.net中的扩展方法配置服务

问题描述 投票:0回答:1

我编写了一个针对

.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
对象?

c# .net dependency-injection extension-methods
1个回答
0
投票

您已经非常接近正确,但有两件事阻碍了您:

1.使用实现类型注册
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;
}

2.重复注册
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 或为空时抛出异常。

© www.soinside.com 2019 - 2024. All rights reserved.