IHttpClientFactory
でのキー付き DI のサポート
この記事では、キー付きサービスと IHttpClientFactory
を統合する方法について説明します。
Keyed Services (Keyed DIとも呼ばれます) は、1 つのサービスの複数の実装で便利に操作できる依存関係挿入 (DI) 機能です。 登録時に、さまざまな サービス キー を特定の実装に関連付けることができます。 実行時に、このキーはサービスの種類と組み合わせて参照で使用されます。つまり、一致するキーを渡すことで特定の実装を取得できます。 キー付きサービスと DI 全般の詳細については、「.NET 依存関係の挿入 を参照してください。
.NET アプリケーションで IHttpClientFactory
を使用する方法の概要については、「.NET での IHttpClientFactory の」を参照してください。
バックグラウンド
IHttpClientFactory
と名前付き HttpClient
インスタンスは、当然ながら、キー付きサービスのアイデアとよく一致します。 歴史的に、他のことと共に、特に IHttpClientFactory
は、長い間欠けていたDI機能を克服する方法でした。 ただし、プレーンな名前付きクライアントでは、構成された HttpClient
を挿入するのではなく、IHttpClientFactory
インスタンスを取得、格納、クエリする必要があります。これは不便な場合があります。 Typed クライアントはその部分を簡素化しようとしますが、これには注意点があります。Typed クライアントは、誤設定や誤用が生じやすく、サポート インフラストラクチャもまた、特定のシナリオ (モバイル プラットフォームなど) で明確な負荷になる可能性があります。
.NET 9 (Microsoft.Extensions.Http
および Microsoft.Extensions.DependencyInjection
パッケージ バージョン 9.0.0+
) 以降では、IHttpClientFactory
はキー付き DI を直接利用でき、("名前付き" および "型指定された" アプローチではなく) 新しい "キー付き DI アプローチ" を導入できます。 "キーによる DI アプローチは、非常に柔軟に構成可能な便利な HttpClient
登録と、具体的に構成された HttpClient
インスタンスの直接的な注入を組み合わせています。"
基本的な使用方法
.NET 9 以降、この機能を使用するには拡張メソッドを呼び出してオプト インするAddAsKeyed必要があります。 オプトインすると、構成を適用する名前付きクライアントは、クライアントの名前をサービス キーとして使用して、キー付き HttpClient
サービスとして DI コンテナーに追加されるため、標準のキー付きサービス API (FromKeyedServicesAttributeなど) を使用して、目的の名前付き HttpClient
インスタンス (IHttpClientFactory
によって作成および構成) を取得できます。 既定では、クライアントは Scoped 有効期間で登録されます。
次のコードは、IHttpClientFactory
、キー付き DI、ASP.NET Core 9.0 最小 API の統合を示しています。
var builder = WebApplication.CreateBuilder(args);
// --- (1) Registration ---
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
})
.AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"
var app = builder.Build();
// --- (2) Obtaining HttpClient instance ---
// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
// --- (3) Using HttpClient instance ---
httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));
app.Run();
record Repo(string Name, string Url);
エンドポイントの応答:
> ~ curl http://localhost:5000/
{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"}
この例では、構成された HttpClient
は、標準のキー付き DI インフラストラクチャを介して要求ハンドラーに挿入されます。これは、ASP.NET Core パラメーター バインドに統合されています。 ASP.NET Core のキー付きサービスの詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。
キー付き、名前付き、および型指定されたアプローチの比較
の基本的な使用法 例の IHttpClientFactory
関連コードのみを検討してください。
services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1)
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2)
//httpClient.Get.... // (3)
このコード スニペットは(1)
、設定済みHttpClient
インスタンス(2)
を取得し、必要に応じてクライアント インスタンスを(3)
使用する登録が、Keyed DI アプローチを使用した場合にどのように表示されるかを示しています。
同じ手順を 2 つの "古い" アプローチで実現する方法を比較します。
1 番目は、Named アプローチを使用した場合:
services.AddHttpClient("github", /* ... */); // (1)
app.MapGet("/github", (IHttpClientFactory httpClientFactory) =>
{
HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2)
//return httpClient.Get.... // (3)
});
2 番目は、Typed アプローチを使用した場合:
services.AddHttpClient<GitHubClient>(/* ... */); // (1)
app.MapGet("/github", (GitHubClient gitHubClient) =>
gitHubClient.GetRepoAsync());
public class GitHubClient(HttpClient httpClient) // (2)
{
private readonly HttpClient _httpClient = httpClient;
public Task<Repo> GetRepoAsync() =>
//_httpClient.Get.... // (3)
}
3 つのうち、キー付き DI アプローチは、同じ動作を実現するための最も簡潔な方法を提供します。
組み込みの DI コンテナーの検証
特定の名前付きクライアントに対してキー付き登録を有効にした場合は、既存のキー付き DI API を使用してアクセスできます。 ただし、まだ有効になっていない名前を誤って使用しようとすると、標準のキー付き DI 例外が発生します。
services.AddHttpClient("keyed").AddAsKeyed();
services.AddHttpClient("not-keyed");
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
// Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("not-keyed");
さらに、クライアントのスコープ付き有効期間は、キャプティブ依存関係のケースをキャッチするのに役立ちます。
services.AddHttpClient("scoped").AddAsKeyed();
services.AddSingleton<CapturingSingleton>();
// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.
rootProvider.GetRequiredKeyedService<HttpClient>("scoped");
using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("scoped"); // OK
// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.
public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient)
//{ ...
サービスの有効期間の選択
既定では、AddAsKeyed()
はキー付き スコープ付き サービスとして HttpClient
を登録します。 ServiceLifetime
パラメーターを AddAsKeyed()
メソッドに渡すことで、有効期間を明示的に指定することもできます。
services.AddHttpClient("explicit-scoped")
.AddAsKeyed(ServiceLifetime.Scoped);
services.AddHttpClient("singleton")
.AddAsKeyed(ServiceLifetime.Singleton);
型指定されたクライアント登録内で AddAsKeyed()
を呼び出すと、基になる名前付きクライアントのみがキー付きとして登録されます。 型付けされたクライアント自体は、シンプルな一時サービスとして引き続き登録されます。
一時的な HttpClient メモリ リークを回避する
重要
HttpClient
は IDisposable
であるため、Keyed HttpClient
インスタンスに対して Transient 有効期間を避けることを強く推奨します。
クライアントを Keyed Transient サービスとして登録すると、HttpClient
インスタンスと HttpMessageHandler
インスタンスの両方が IDisposable
を実装するため、これらのインスタンスが DI コンテナーによってキャプチャされます。 これにより、クライアントがシングルトン サービス内で複数回解決された場合、 メモリ リークが発生する可能性があります。
キャプティブ依存関係を回避する
重要
HttpClient
が登録されている場合:
- Keyed Singleton として、または
- Keyed Scoped または Transient として、長期間 (
HandlerLifetime
より長期) 実行されるアプリケーション Scope 内に挿入されるか、または - Keyed Transient として、Singleton サービスにされる
—HttpClient
は、キャプティブになり、想定される HandlerLifetime
を超えて存続する可能性があります。 キャプティブ クライアントを制御できない IHttpClientFactory
は、ハンドラーのローテーションに参加できず、DNS の変更が失われる可能性があります。 同様の問題が、Transient サービスとして登録された Typed クライアントにも既に存在します。
クライアントの長寿命が避けられない場合、または意図的に望まれる場合 (例えば、Keyed Singleton の場合)、PooledConnectionLifetime
を適切な値に設定して、SocketsHttpHandler
を活用することが推奨されます。
services.AddHttpClient("shared")
.AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton
.UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2))
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation
services.AddSingleton<MySingleton>();
public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ...
スコープの不一致に注意してください
Scoped 有効期間は、Named HttpClient
に対しては (Singleton や Transient の問題点と比較して) 問題が少ないものの、独自の注意点があります。
重要
特定の HttpClient
インスタンスのキー付きスコープ有効期間は、想定どおりに、それが解決された "通常の" アプリケーション スコープ (受信要求スコープなど) にバインドされます。 ただし、基になるメッセージ ハンドラー チェーンには適用されません。これは、ファクトリから直接作成された名前付きクライアントの場合と同じ方法で、IHttpClientFactory
によって引き続き管理されます。 HttpClient
が、同じ名前を持ち、異なるスコープ (例えば、同じエンドポイントへの 2 つの同時リクエスト) 内で (同じHandlerLifetime
期間に) 解決される場合、同じHttpMessageHandler
インスタンスが再利用される可能性があります。 そのインスタンスには、メッセージ ハンドラーのスコープに示すように、独自の個別のスコープがあります。
手記
スコープの不一致 問題は厄介で、既存の問題であり、.NET 9 の時点ではまだ未解決の 残っています。 通常の DI インフラストラクチャを介して挿入されたサービスからは、すべての依存関係が同じスコープから満たされると予想されますが、キー付きスコープ HttpClient
インスタンスの場合は、残念ながらそうではありません。
キー付きメッセージ処理チェーン
一部の高度なシナリオでは、HttpClient
オブジェクトではなく、HttpMessageHandler
チェーンに直接アクセスできます。 IHttpClientFactory
は、ハンドラーを作成するための IHttpMessageHandlerFactory
インターフェイスを提供します。キー付き DI を有効にすると、HttpClient
だけでなく、それぞれの HttpMessageHandler
チェーンもキー付きサービスとして登録されます。
services.AddHttpClient("keyed-handler").AddAsKeyed();
var handler = provider.GetRequiredKeyedService<HttpMessageHandler>("keyed-handler");
var invoker = new HttpMessageInvoker(handler, disposeHandler: false);
方法: 型指定されたアプローチからキー付き DI に切り替える
手記
現在、型指定されたクライアントではなく、キー付き DI アプローチを使用することをお勧めします。
既存の型指定されたクライアントからキー付き依存関係への最小限の変更の切り替えは、次のようになります。
- services.AddHttpClient<Service>( // (1) Typed client
+ services.AddHttpClient(nameof(Service), // (1) Named client
c => { /* ... */ } // HttpClient configuration
//).Configure....
- );
+ ).AddAsKeyed(); // (1) + Keyed DI opt-in
+ services.AddTransient<Service>(); // (1) Plain Transient service
public class Service(
- // (2) "Hidden" Named dependency
+ [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency
HttpClient httpClient) // { ...
この例では、次のようになります。
- Typed クライアント
Service
は以下に分割されています。- 同じ
HttpClient
構成を持つ名前付きクライアントnameof(Service)
の登録と、キー付き DI へのオプトイン。そして - 基本的な Transient サービス
Service
。
- 同じ
Service
のHttpClient
依存関係は、キーnameof(Service)
を持つキー付きサービスに明示的にバインドされます。
名前を nameof(Service)
する必要はありませんが、動作の変更を最小限に抑えることを目的とした例です。 内部的には、型指定されたクライアントは名前付きクライアントを使用します。既定では、このような「非表示」の名前付きクライアントは、リンクされた型指定されたクライアントの型名で呼ばれます。 この場合、「隠し名」とされていたものが nameof(Service)
であったため、この例でもそれが保持されています。
技術的には、この例では、型指定されたクライアントを "ラップ解除" して、以前の "非表示" の名前付きクライアントが "公開" になり、型指定されたクライアント インフラストラクチャではなくキー付き DI インフラストラクチャを介して依存関係が満たされるようにします。
方法: 既定でキー付き DI にオプトインする
単一のクライアントごとに AddAsKeyed を呼び出す必要はありません。ConfigureHttpClientDefaultsを使用して、(任意のクライアント名に対して) "グローバル" を簡単にオプトインできます。 Keyed Services の視点からは、結果として KeyedService.AnyKey の登録が行われます。
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("first", /* ... */);
services.AddHttpClient("second", /* ... */);
services.AddHttpClient("third", /* ... */);
public class MyController(
[FromKeyedServices("first")] HttpClient first,
[FromKeyedServices("second")] HttpClient second,
[FromKeyedServices("third")] HttpClient third)
//{ ...
"不明" クライアントに注意する
手記
KeyedService.AnyKey
登録は、任意のキー値から特定のサービス インスタンスへのマッピングを定義します。 しかし、その結果として、Container 検証は適用されず、誤ったキー値によってエラーが発生することなく、、誤ったインスタンスが挿入されることになります。
重要
キー付き HttpClient
の場合、クライアント名の間違いにより、"不明な" クライアント (つまり、名前が登録されなかったクライアント) が誤って挿入される可能性があります。
プレーンな名前付きクライアントでも同じことが当てはまります。IHttpClientFactory
は、クライアント名を明示的に登録する必要はありません (オプション パターン 動作する方法に合わせて調整します)。 不明な名前に対しては、未設定の (より正確には既定の) HttpClient
がファクトリから提供されます。
手記
したがって、"既定で Keyed を使用" というアプローチは、すべての登録済みHttpClient
クライアントだけでなく、IHttpClientFactory
が作成可能なすべてのクライアントを対象としていることに注意が必要です。
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("known", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("known"); // OK
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
"オプトイン" 戦略に関する考慮事項
"グローバル" オプトインは 1 行で済むとはいえ、この機能が「設定なしに即座に」動作するわけではなく、オプトインが必要です。このような判断に関する詳細な背景と理由については、dotnet/runtime#89755 および dotnet/runtime#104943 を参照してください。 つまり、「デフォルトでオン」に対する主な障害は、ServiceLifetime
"論争" です。現在の DI および IHttpClientFactory
の実装の状態(9.0.0
)では、あらゆる状況で全ての HttpClient
に対して合理的に安全な単一の ServiceLifetime
はありません。 ただし、今後のリリースの注意事項に対処し、戦略を "オプトイン" から "オプトアウト" に切り替える意図があります。
方法: キー付き登録からオプトアウトする
HttpClient
のキー付き DI から明示的にオプトアウトするには、クライアント名ごとに RemoveAsKeyed 拡張メソッドを呼び出します。
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default
services.AddHttpClient("keyed", /* ... */);
services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
または、ConfigureHttpClientDefaultsを使用して "グローバル" に設定します。
services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default
services.AddHttpClient("keyed", /* ... */).AddAsKeyed(); // opt IN per name
services.AddHttpClient("not-keyed", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
優先順位
一緒に呼び出された場合、またはそのいずれかが複数回呼び出された場合、AddAsKeyed()
と RemoveAsKeyed()
は、通常、IHttpClientFactory
構成と DI 登録の規則に従います。
- 同じ名前で呼び出された場合、最後の設定が優先されます。最後の
AddAsKeyed()
の有効期間は、キー付き登録の作成に使用されます (RemoveAsKeyed()
が最後に呼び出された場合を除き、その場合は名前が除外されます)。 ConfigureHttpClientDefaults
内でのみ使用すると、最後の設定が優先されます。ConfigureHttpClientDefaults
と特定のクライアント名の両方を使用した場合、すべての既定値は、すべての名前ごとの設定よりも先に実行されると見なされます。 したがって、既定値は無視でき、名前ごとの設定の最後が優先されます。
関連項目
.NET