使用 .NET 的 IHttpClientFactory
本文介绍如何使用 IHttpClientFactory
接口创建具有各种 .NET 基本功能(例如依赖关系注入 (DI)、日志记录和配置)的 HttpClient
类型。 HttpClient 类型是在 2012 年发布的 .NET Framework 4.5 中引入的。 换句话说,它已经存在一段时间了。 HttpClient
用于从由 Uri 标识的网络资源发出 HTTP 请求和处理 HTTP 响应。 HTTP 协议占所有 Internet 流量的绝大部分。
根据推动最佳做法的新式应用程序开发原则,IHttpClientFactory 充当工厂抽象,可以使用自定义配置创建 HttpClient
实例。 .NET Core 2.1 中引入了 IHttpClientFactory。 常见的基于 HTTP 的 .NET 工作负载可以轻松利用可复原和瞬态故障处理第三方中间件。
注意
如果你的应用需要 Cookie,最好不要在应用中使用 IHttpClientFactory。 有关管理客户端的替代方法,请参阅使用 HTTP 客户端的指南。
重要
IHttpClientFactory
创建的 HttpClient
实例的生存期管理与手动创建的实例完全不同。 策略是使用由 IHttpClientFactory
创建的短期客户端,或设置了 PooledConnectionLifetime
的长期客户端。 有关详细信息,请参阅 HttpClient 生存期管理部分和使用 HTTP 客户端的指南。
IHttpClientFactory
类型
本文中提供的所有示例源代码都需要安装 Microsoft.Extensions.Http
NuGet 包。 此外,代码示例演示如何使用 HTTP GET
请求从免费的 {JSON} 占位符 API 检索用户 Todo
对象。
调用任何 AddHttpClient 扩展方法时,将 IHttpClientFactory
和相关服务添加到 IServiceCollection。 IHttpClientFactory
类型具有以下优点:
- 将
HttpClient
类公开为 DI 就绪类型。 - 提供一个中心位置,用于命名和配置逻辑
HttpClient
实例。 - 通过
HttpClient
中的委托处理程序来编码出站中间件的概念。 - 提供基于 Polly 的中间件的扩展方法,以利用
HttpClient
中的委托处理程序。 - 管理基础 HttpClientHandler 实例的缓存和生存期。 自动管理可避免手动管理
HttpClient
生存期时出现的常见域名系统 (DNS) 问题。 - (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。
消耗模式
在应用中可以通过以下多种方式使用 IHttpClientFactory
:
最佳方法取决于应用要求。
基本用法
若要注册 IHttpClientFactory
,请调用 AddHttpClient
:
using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();
using IHost host = builder.Build();
使用服务可能需要 IHttpClientFactory
作为带有 DI 的构造函数参数。 以下代码使用 IHttpClientFactory
来创建 HttpClient
实例:
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;
namespace BasicHttp.Example;
public sealed class TodoService(
IHttpClientFactory httpClientFactory,
ILogger<TodoService> logger)
{
public async Task<Todo[]> GetUserTodosAsync(int userId)
{
// Create the client
using HttpClient client = httpClientFactory.CreateClient();
try
{
// Make HTTP GET request
// Parse JSON response deserialize into Todo types
Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
$"https://jsonplaceholder.typicode.com/todos?userId={userId}",
new JsonSerializerOptions(JsonSerializerDefaults.Web));
return todos ?? [];
}
catch (Exception ex)
{
logger.LogError("Error getting something fun to say: {Error}", ex);
}
return [];
}
}
像前面的示例一样,使用 IHttpClientFactory
是重构现有应用的好方法。 这不会影响 HttpClient
的使用方式。 在现有应用中创建 HttpClient
实例的位置,使用对 CreateClient 的调用替换这些匹配项。
命名客户端
在以下情况下,命名客户端是一个不错的选择:
- 应用需要
HttpClient
的许多不同用法。 - 许多
HttpClient
实例具有不同的配置。
可以在 IServiceCollection
上注册时指定命名 HttpClient
的配置:
using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);
builder.Services.AddHttpClient(
httpClientName,
client =>
{
// Set the base address of the named client.
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
在上述代码中,客户端配置如下:
- 从
"TodoHttpClientName"
下的配置中提取的名称。 - 基址为
https://jsonplaceholder.typicode.com/
。 - 一个
"User-Agent"
标头。
可以使用配置来指定 HTTP 客户端名称,这有助于避免在添加和创建时误命名客户端。 在本例中,appsettings.json 文件用于配置 HTTP 客户端名称:
{
"TodoHttpClientName": "JsonPlaceholderApi"
}
可以轻松扩展此配置,并存储有关你希望 HTTP 客户端如何工作的更多详细信息。 有关详细信息,请参阅 .NET 中的配置。
创建客户端
每次调用 CreateClient 时:
- 创建
HttpClient
的新实例。 - 调用配置操作。
要创建命名客户端,请将其名称传递到 CreateClient
中:
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;
namespace NamedHttp.Example;
public sealed class TodoService
{
private readonly IHttpClientFactory _httpClientFactory = null!;
private readonly IConfiguration _configuration = null!;
private readonly ILogger<TodoService> _logger = null!;
public TodoService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<TodoService> logger) =>
(_httpClientFactory, _configuration, _logger) =
(httpClientFactory, configuration, logger);
public async Task<Todo[]> GetUserTodosAsync(int userId)
{
// Create the client
string? httpClientName = _configuration["TodoHttpClientName"];
using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");
try
{
// Make HTTP GET request
// Parse JSON response deserialize into Todo type
Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
$"todos?userId={userId}",
new JsonSerializerOptions(JsonSerializerDefaults.Web));
return todos ?? [];
}
catch (Exception ex)
{
_logger.LogError("Error getting something fun to say: {Error}", ex);
}
return [];
}
}
在上述代码中,HTTP 请求不需要指定主机名。 代码可以仅传递路径,因为采用了为客户端配置的基址。
类型化客户端
类型化客户端:
- 提供与命名客户端一样的功能,不需要将字符串用作密钥。
- 在使用客户端时提供 IntelliSense 和编译器帮助。
- 提供单个位置来配置特定
HttpClient
并与其进行交互。 例如,可以使用单个类型化客户端:- 对于单个后端终结点。
- 封装处理终结点的所有逻辑。
- 使用 DI 且可以被注入到应用中需要的位置。
类型化客户端在构造函数中接受 HttpClient
参数:
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;
namespace TypedHttp.Example;
public sealed class TodoService(
HttpClient httpClient,
ILogger<TodoService> logger) : IDisposable
{
public async Task<Todo[]> GetUserTodosAsync(int userId)
{
try
{
// Make HTTP GET request
// Parse JSON response deserialize into Todo type
Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
$"todos?userId={userId}",
new JsonSerializerOptions(JsonSerializerDefaults.Web));
return todos ?? [];
}
catch (Exception ex)
{
logger.LogError("Error getting something fun to say: {Error}", ex);
}
return [];
}
public void Dispose() => httpClient?.Dispose();
}
在上述代码中:
- 该配置是在将类型化的客户端添加到服务集合时设置的。
HttpClient
被分配为类范围变量(字段),并与公开的 API 一起使用。
可以创建特定于 API 的方法来公开 HttpClient
功能。 例如,GetUserTodosAsync
方法可封装代码以检索特定于用户的 Todo
对象。
以下代码会调用 AddHttpClient 来注册类型化客户端类:
using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHttpClient<TodoService>(
client =>
{
// Set the base address of the typed client.
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
使用 DI 将类型客户端注册为暂时客户端。 在上述代码中,AddHttpClient
将 TodoService
注册为暂时性服务。 此注册使用工厂方法执行以下操作:
- 创建
HttpClient
的实例。 - 创建
TodoService
的实例,将HttpClient
的实例传入其构造函数。
重要
在单一实例服务中使用类型化客户端可能很危险。 有关详细信息,请参阅避免在单一实例服务中使用类型化客户端部分。
注意
在使用方法注册类型AddHttpClient<TClient>
化客户端时TClient
,该类型必须具有一个HttpClient
接受作为参数的构造函数。 此外,该类TClient
型不应单独与依赖注入容器注册,因为这将导致后续注册覆盖先前的注册。
生成的客户端
IHttpClientFactory
可结合第三方库(例如 Refit)使用。 Refit 是.NET 的 REST 库。 它允许声明性 REST API 定义,将接口方法映射到终结点。 RestService
动态生成该接口的实现,使用 HttpClient
进行外部 HTTP 调用。
请考虑以下 record
类型:
namespace Shared;
public record class Todo(
int UserId,
int Id,
string Title,
bool Completed);
以下示例依赖于 Refit.HttpClientFactory
NuGet 包,并且是一个简单的接口:
using Refit;
using Shared;
namespace GeneratedHttp.Example;
public interface ITodoService
{
[Get("/todos?userId={userId}")]
Task<Todo[]> GetUserTodosAsync(int userId);
}
前面的 C# 接口:
- 定义一个名为
GetUserTodosAsync
的方法,该方法返回一个Task<Todo[]>
实例。 - 使用外部 API 的路径和查询字符串声明
Refit.GetAttribute
属性。
可以添加类型化客户端,使用 Refit 生成实现:
using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddRefitClient<ITodoService>()
.ConfigureHttpClient(client =>
{
// Set the base address of the named client.
client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
可以在必要时使用定义的接口,以及由 DI 和 Refit 提供的实现。
发出 POST、PUT 和 DELETE 请求
在前面的示例中,所有 HTTP 请求均使用 GET
HTTP 谓词。 HttpClient
还支持其他 HTTP 谓词,其中包括:
POST
PUT
DELETE
PATCH
有关受支持的 HTTP 谓词的完整列表,请参阅 HttpMethod。 有关发出 HTTP 请求的详细信息,请参阅使用 HttpClient 发送请求。
下面的示例演示如何发出 HTTP POST
请求:
public async Task CreateItemAsync(Item item)
{
using StringContent json = new(
JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
Encoding.UTF8,
MediaTypeNames.Application.Json);
using HttpResponseMessage httpResponse =
await httpClient.PostAsync("/api/items", json);
httpResponse.EnsureSuccessStatusCode();
}
在前面的代码中,CreateItemAsync
方法:
- 使用
System.Text.Json
将Item
参数序列化为 JSON。 这将使用 JsonSerializerOptions 的实例来配置序列化过程。 - 创建 StringContent 的实例,以打包序列化的 JSON 以便在 HTTP 请求的正文中发送。
- 调用 PostAsync 将 JSON 内容发送到指定的 URL。 这是添加到 HttpClient.BaseAddress 的相对 URL。
- 如果响应状态代码不指示成功,则调用 EnsureSuccessStatusCode 引发异常。
HttpClient
还支持其他类型的内容。 例如,MultipartContent 和 StreamContent。 有关受支持的内容的完整列表,请参阅 HttpContent。
下面的示例演示了一个 HTTP PUT
请求:
public async Task UpdateItemAsync(Item item)
{
using StringContent json = new(
JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
Encoding.UTF8,
MediaTypeNames.Application.Json);
using HttpResponseMessage httpResponse =
await httpClient.PutAsync($"/api/items/{item.Id}", json);
httpResponse.EnsureSuccessStatusCode();
}
前面的代码与 POST
示例非常相似。 UpdateItemAsync
方法调用 PutAsync 而不是 PostAsync
。
下面的示例演示了一个 HTTP DELETE
请求:
public async Task DeleteItemAsync(Guid id)
{
using HttpResponseMessage httpResponse =
await httpClient.DeleteAsync($"/api/items/{id}");
httpResponse.EnsureSuccessStatusCode();
}
在前面的代码中,DeleteItemAsync
方法调用 DeleteAsync。 由于 HTTP DELETE 请求通常不包含正文,因此 DeleteAsync
方法不提供接受 HttpContent
实例的重载。
要详细了解如何将不同的 HTTP 谓词用于 HttpClient
,请参阅 HttpClient。
HttpClient
生存期管理
每次对 IHttpClientFactory
调用 CreateClient
都会返回一个新 HttpClient
实例。 每个客户端名称创建一个 HttpClientHandler 实例。 工厂管理 HttpClientHandler
实例的生存期。
IHttpClientFactory
将缓存工厂创建的 HttpClientHandler
实例,以减少资源消耗。 创建新的 HttpClient
实例时,可能会重用缓存中的 HttpClientHandler
实例(如果生存期尚未到期的话)。
由于每个处理程序通常管理自己的基础 HTTP 连接池,因此需要缓存处理程序。 创建超出必要数量的处理程序可能会导致套接字耗尽和连接延迟。 部分处理程序还保持连接无期限地打开,这样可以防止处理程序对 DNS 更改作出反应。
处理程序的默认生存期为两分钟。 要替代默认值,请在 IServiceCollection
上为每个客户端调用 SetHandlerLifetime:
services.AddHttpClient("Named.Client")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
重要
IHttpClientFactory
创建的 HttpClient
实例是短期的。
回收和重新创建
HttpMessageHandler
在其生存期到期时对于IHttpClientFactory
至关重要,可确保处理程序对 DNS 更改做出反应。HttpClient
在创建特定处理程序实例时与之绑定,因此应及时请求新的HttpClient
实例,以确保客户端将获取更新后的处理程序。释放工厂创建的此类
HttpClient
实例不会导致套接字耗尽,因为它的处置不会触发HttpMessageHandler
的处置。IHttpClientFactory
跟踪和释放用于创建HttpClient
实例的资源,特别是HttpMessageHandler
实例,只要其生存期到期,并且HttpClient
不再使用这些实例。
长时间使单个 HttpClient
实例保持活动状态是一种常见模式,可用作 IHttpClientFactory
的替代项,但是,此模式需要其他设置,例如 PooledConnectionLifetime
。 可以使用具有 PooledConnectionLifetime
的长期客户端,也可以使用由 IHttpClientFactory
创建的短期客户端。 有关应用中使用的策略的信息,请参阅使用 HTTP 客户端的指南。
配置 HttpMessageHandler
控制客户端使用的内部 HttpMessageHandler 的配置是有必要的。
在添加命名客户端或类型化客户端时,会返回 IHttpClientBuilder。 ConfigurePrimaryHttpMessageHandler 扩展方法可以用于在 IServiceCollection
上定义委托。 委托用于创建和配置客户端使用的主要 HttpMessageHandler
:
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
通过配置 HttClientHandler
在处理,可以在处理程序的各种其他属性中为 HttpClient
实例指定代理。 有关详细信息,请参阅每个客户端的代理。
其他配置
有几个额外的配置选项可用于控制 IHttpClientHandler
:
方法 | 说明 |
---|---|
AddHttpMessageHandler | 为已命名的 HttpClient 添加附加消息处理程序。 |
AddTypedClient | 配置 TClient 与已命名的 HttpClient (与 IHttpClientBuilder 关联)之间的绑定。 |
ConfigureHttpClient | 添加用于配置已命名的 HttpClient 的委托。 |
ConfigurePrimaryHttpMessageHandler | 从已命名的 HttpClient 的依赖关系注入容器中配置主要 HttpMessageHandler 。 |
RedactLoggedHeaders | 设置其值应在记录之前进行修正的 HTTP 标头名称的集合。 |
SetHandlerLifetime | 设置可重复使用 HttpMessageHandler 实例的时长。 每个已命名的客户端都可自行配置处理程序生存期值。 |
UseSocketsHttpHandler | 从依赖注入容器配置一个SocketsHttpHandler 新的或先前添加的实例,以用作命名的主处理程序HttpClient 。 (仅适用于 .NET 5+) |
将 IHttpClientFactory 和 SocketsHttpHandler 一起使用
在 .NET Core 2.1 中添加了 HttpMessageHandler
的 SocketsHttpHandler
实现,该实现允许配置 PooledConnectionLifetime
。 此设置用于确保处理程序对 DNS 更改做出反应,因此,使用 SocketsHttpHandler
被视为使用 IHttpClientFactory
替代项。 有关详细信息,请参阅使用 HTTP 客户端的指南。
但是,SocketsHttpHandler
和 IHttpClientFactory
可以结合使用来提高可配置性。 通过使用这两个 API,可以同时受益于低级别(例如,使用 LocalCertificateSelectionCallback
进行动态证书选择)和高级别(例如,利用 DI 集成和多个客户端配置)上的可配置性。
若要同时使用这两个 API,请执行以下操作:
- 通过指
SocketsHttpHandler
定作PrimaryHandler
为、ConfigurePrimaryHttpMessageHandler或UseSocketsHttpHandler(仅适用于 .NET 5+)。 - 根据您SocketsHttpHandler.PooledConnectionLifetime预期 DNS 将被更新的间隔来设置; 例如,设置为先前存在的值
HandlerLifetime
。 - (可选)由于
SocketsHttpHandler
将处理连接池和回收,因此在该级别IHttpClientFactory
上不再需要处理程序回收。 可以通过将HandlerLifetime
设置为Timeout.InfiniteTimeSpan
来禁用它。
services.AddHttpClient(name)
.UseSocketsHttpHandler((handler, _) =>
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime
在上面的示例中,2分钟是出于说明目的任意选择的,与默认值保持HandlerLifetime
一致。 您应该根据预期的 DNS 或其他网络更改的频率选择该值。 有关更多信息, 请参阅指南中的 DNS行为部分, HttpClient
以及 API 文档PooledConnectionLifetime中的备注部分。
避免在单一实例服务中使用类型化客户端
使用命名客户端方法时,IHttpClientFactory
会注入到服务中,并且每次需要 HttpClient
实例时,都会通过调用 CreateClient 创建 HttpClient
实例。
但是,使用类型化客户端方法,类型化客户端通常是注入到服务中的暂时性对象。 这可能会导致问题,因为可以将类型化客户端注入到单一实例服务中。
重要
类型化客户端应该是短期客户端,与 IHttpClientFactory
创建的 HttpClient
实例的意义相同(有关详细信息,请参阅HttpClient
生存期管理)。 创建类型化客户端实例后,IHttpClientFactory
就无法控制它。 如果在单一实例中捕获了类型化客户端实例,它可能会阻止它对 DNS 更改做出反应,从而破坏 IHttpClientFactory
的其中一个目的。
如果需要在单一实例服务中使用 HttpClient
实例,请考虑以下选项:
- 请改用命名客户端方法,根据需要在单一实例服务中注入
IHttpClientFactory
并重新创建HttpClient
实例。 - 如果需要类型化客户端方法,请使用配置了
PooledConnectionLifetime
的SocketsHttpHandler
作为主处理程序。 有关结合使用SocketsHttpHandler
和IHttpClientFactory
的详细信息,请参阅结合使用 IHttpClientFactory 和 SocketsHttpHandler 部分。
IHttpClientFactory 中的消息处理程序范围
IHttpClientFactory
为每个 HttpMessageHandler
实例创建单独的 DI 范围。 这些 DI 范围独立于应用程序 DI 范围(例如,ASP.NET 传入请求范围或用户创建的手动 DI 范围),因此它们不会共享区分范围的服务实例。 消息处理程序范围与处理程序生存期相关联,并且可能会超过应用程序范围,例如,这可能导致重用相同的 HttpMessageHandler
实例,在多个传入请求之间具有相同的注入区分范围的依赖关系。
强烈建议用户不要在 HttpMessageHandler
实例中缓存与范围相关的信息(例如来自 HttpContext
的数据),并谨慎使用区分范围的依赖关系以避免泄漏敏感信息。
如果需要从消息处理程序访问应用 DI 范围(例如进行身份验证),请将范围感知逻辑封装在单独的暂时性 DelegatingHandler
中,并将其包装在来自 IHttpClientFactory
缓存的 HttpMessageHandler
实例周围。 要访问处理程序,可为任何已注册的命名客户端调用 IHttpMessageHandlerFactory.CreateHandler。 在这种情况下,你将使用构造的处理程序自行创建 HttpClient
实例。
以下示例演示了如何创建具有范围感知 DelegatingHandler
的 HttpClient
:
if (scopeAwareHandlerType != null)
{
if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
{
throw new ArgumentException($"""
Scope aware HttpHandler {scopeAwareHandlerType.Name} should
be assignable to DelegatingHandler
""");
}
// Create top-most delegating handler with scoped dependencies
scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
if (scopeAwareHandler.InnerHandler != null)
{
throw new ArgumentException($"""
Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
Scope aware HttpHandler should be registered as Transient.
""");
}
}
// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);
if (scopeAwareHandler != null)
{
scopeAwareHandler.InnerHandler = handler;
handler = scopeAwareHandler;
}
HttpClient client = new(handler);
进一步的解决方法可以遵循一个扩展方法,以通过有权访问当前应用范围的暂时性服务注册范围感知 DelegatingHandler
和替代默认的 IHttpClientFactory
注册:
public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
builder.Services.TryAddTransient<THandler>();
if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
{
// Override default IHttpClientFactory registration
builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
}
builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
builder.Name, options => options.HttpHandlerType = typeof(THandler));
return builder;
}
有关详细信息,请参阅完整示例。