如何优化C#库中自定义HttpClient的DI配置与替换逻辑?
自定义C#第三方API包装库的HttpClient配置优化方案
问题背景
我正在为自己编写的第三方API调用C#包装库寻找合适的自定义HttpClient配置方案,已在仓库的单独分支完成了配置库的扩展方法及相关DI实现,核心需求是:
- 允许消费者为不同服务指定不同的HttpClient配置(例如
TextService用默认配置,ChatService用带代理的自定义配置) - 简化自定义类型化HttpClient的配置流程
- 避免先调用默认注册后再自定义时,旧客户端实例残留在DI容器中的问题
现有实现
默认使用方式
builder.Services.Configure<GeminiHttpClientOptions>(builder.Configuration.GetSection("Gemini")); builder.Services.AddGemini();
核心扩展方法实现
public static class GeminiExtensions { public static IServiceCollection AddGemini(this IServiceCollection services) { services.AddTransient<GeminiAuthHandler<GeminiHttpClientOptions>>(); services.AddHttpClient<GeminiClient>((sp, client) => { var options = sp.GetRequiredService<IOptions<GeminiHttpClientOptions>>().Value; client.BaseAddress = options.Url; }) .AddHttpMessageHandler<GeminiAuthHandler<GeminiHttpClientOptions>>(); services.AddTransient<ITextService, TextService>(); services.AddTransient<IVisionService, VisionService>(); services.AddTransient<IChatService, ChatService>(); services.AddTransient<IEmbeddingService, EmbeddingService>(); services.AddTransient<IModelInfoService, ModelInfoService>(); return services; } }
类型化HttpClient实现
public class GeminiClient { private readonly HttpClient _httpClient; public GeminiClient(HttpClient httpClient) { _httpClient = httpClient; } public async Task<TResponse> GetAsync<TResponse>(string endpoint) { var response = await _httpClient.GetAsync(endpoint); return await HandleResponse<TResponse>(response) ?? throw new GeminiException("The API has returned a null response."); } public async Task<TResponse> PostAsync<TRequest, TResponse>(string endpoint, TRequest data) { var serializedContent = JsonSerializer.Serialize(data, options: new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); var jsonContent = new StringContent(serializedContent, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(endpoint, jsonContent); return await HandleResponse<TResponse>(response) ?? throw new GeminiException("The API has returned a null response."); } private static async Task<T> HandleResponse<T>(HttpResponseMessage response) { var content = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { return JsonSerializer.Deserialize<T>(content) ?? throw new GeminiException("The API has returned a null response."); } else { var geminiError = JsonSerializer.Deserialize<ApiErrorResponse>(content) ?? throw new GeminiException("The API has returned a null response."); throw new GeminiException(geminiError, geminiError.error.message); } } }
当前自定义痛点
现在要实现自定义HttpClient,需要以下繁琐步骤:
- 创建
GeminiClient的子类 - 重复编写HttpClient注册逻辑
- 手动注册对应服务
- 若先调用
AddGemini再自定义,旧的客户端实例会残留在DI容器中
优化方案
1. 扩展AddGemini方法,支持直接配置默认HttpClient
修改扩展方法,增加接受HttpClient配置委托的重载,让用户无需创建子类即可自定义默认客户端:
public static class GeminiExtensions { public static IServiceCollection AddGemini(this IServiceCollection services, Action<IHttpClientBuilder>? configureHttpClient = null) { services.AddTransient<GeminiAuthHandler<GeminiHttpClientOptions>>(); // 先移除已有的GeminiClient注册,避免残留 var existingClientReg = services.FirstOrDefault(sd => sd.ServiceType == typeof(GeminiClient)); if (existingClientReg != null) { services.Remove(existingClientReg); } // 注册并配置默认HttpClient var clientBuilder = services.AddHttpClient<GeminiClient>((sp, client) => { var options = sp.GetRequiredService<IOptions<GeminiHttpClientOptions>>().Value; client.BaseAddress = options.Url; }) .AddHttpMessageHandler<GeminiAuthHandler<GeminiHttpClientOptions>>(); // 执行用户自定义配置 configureHttpClient?.Invoke(clientBuilder); // 注册业务服务 RegisterServices(services); return services; } // 抽离服务注册逻辑,方便复用 private static void RegisterServices(IServiceCollection services) { services.AddTransient<ITextService, TextService>(); services.AddTransient<IVisionService, VisionService>(); services.AddTransient<IChatService, ChatService>(); services.AddTransient<IEmbeddingService, EmbeddingService>(); services.AddTransient<IModelInfoService, ModelInfoService>(); } }
用户使用示例:
builder.Services.Configure<GeminiHttpClientOptions>(builder.Configuration.GetSection("Gemini")); builder.Services.AddGemini(clientBuilder => { clientBuilder.ConfigurePrimaryHttpMessageHandler(() => { var proxy = new WebProxy { Address = new Uri("http://localhost:1080/") }; var handler = new HttpClientHandler { Proxy = proxy, UseProxy = true }; // 仅用于测试,生产环境请勿使用 handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; return handler; }); });
2. 支持多命名HttpClient,实现服务级别的配置隔离
扩展方法,允许注册命名HttpClient,并为指定服务绑定该客户端:
public static class GeminiExtensions { // 注册命名HttpClient public static IHttpClientBuilder AddGeminiNamedClient(this IServiceCollection services, string clientName) { services.AddTransient<GeminiAuthHandler<GeminiHttpClientOptions>>(); return services.AddHttpClient(clientName, (sp, client) => { var options = sp.GetRequiredService<IOptions<GeminiHttpClientOptions>>().Value; client.BaseAddress = options.Url; }) .AddHttpMessageHandler<GeminiAuthHandler<GeminiHttpClientOptions>>(); } // 为ChatService绑定指定命名客户端 public static IServiceCollection AddGeminiChatServiceWithNamedClient(this IServiceCollection services, string clientName) { services.AddTransient<IChatService>(sp => { var factory = sp.GetRequiredService<IHttpClientFactory>(); var httpClient = factory.CreateClient(clientName); return new ChatService(new GeminiClient(httpClient)); }); return services; } }
用户使用示例:
// 注册默认客户端及所有服务 builder.Services.AddGemini(); // 注册带代理的命名客户端 builder.Services.AddGeminiNamedClient("ProxyGeminiClient") .ConfigurePrimaryHttpMessageHandler(() => { var proxy = new WebProxy("http://localhost:1080/"); return new HttpClientHandler { Proxy = proxy, UseProxy = true }; }); // 让ChatService使用带代理的客户端 builder.Services.AddGeminiChatServiceWithNamedClient("ProxyGeminiClient");
3. 强制覆盖DI容器中的旧注册
在扩展方法中,使用Replace方法确保新注册覆盖旧实例:
// 替换已有的GeminiClient注册 services.Replace(ServiceDescriptor.Transient<GeminiClient>(sp => { var factory = sp.GetRequiredService<IHttpClientFactory>(); var httpClient = factory.CreateClient("CustomGeminiClient"); return new GeminiClient(httpClient); }));
内容的提问来源于stack exchange,提问作者Junaid
相关产品推荐
相关产品推荐

