とりあえず、どんなサンプルになるか知りたい人は下記のサンプルをご覧ください。
- ASP.NET Core: IHttpClientFactoryの単純サンプル
- ASP.NET Core: IHttpClientFactoryのサンプル
- ASP.NET Core: IHttpClientFactoryの検証用サンプル
ASP.NET Coreや.NET CoreでWebAPIの実行等のHTTP通信を行う場合はHttpClientを使用できますが、HtppClientが想定する利用シナリオを理解して使用しないと、大きな問題が発生するため、IHttpClientFactoryからHttpClientを生成する方法が推奨されます。
ここでは、IHttpClientFactoryを使ったHttpClientの使用方法について説明します。記載のベースになっているのは、マイクロソフトのこちらの記事です。
なお、想定している.NET Coreのバージョンは3.1です。
HttpClientの単独使用の問題点
HttpClientを使うことで容易にHTTP通信の処理を実装できますが、前提となる利用方法を理解せずに使用すると利用シナリオによっては次の問題が発生します。
- 短期間で多数の接続を繰り返すとサーバに接続できなくなる場合がある(ソケット枯渇)
HttpClientはインスタンス単位でサーバとの接続を保持します。IDisposeを実装しており、using句を使ってインスタンスを使い終わったら破棄(Dispose)することもできます。限定的な用途であれば問題ありませんが、短期間で多数のインスタンスを作成・破棄すると、接続が何度も作成・破棄されることになります。
サーバとの接続はソケットプログラミングと呼ばれる、クライアントとサーバにソケットと呼ばれる通信のための出入り口(ソケット)を作成し、それぞれにIPアドレスやポート番号を紐づけて相互に接続することで実現されます。サーバへの接続・切断はソケットの作成・破棄の繰り返しになります。
ソケットは通信の誤動作を防止するためにすぐに破棄ができない仕組(TIME_WAITを参照)になっているため、いずれソケットが作成できなくなり、サーバとの接続ができなくなります。
このような状況を回避するために、HttpClientを使う場合はインスタンスを再利用する前提の使い方が期待されます。ASP.NET MonstersI’ve been using HttpClient wrong for years and it finally ca…
- ホスト名に対応するIPアドレスの変更に対応できない(DNS変更問題)
Webサーバのホスト名に対応するIPアドレスは、冗長化や負荷分散等の目的で変更される場合があります。
前述のようにHttpClientのインスタンスを再利用する前提です。インスタンス作成時にサーバとの接続が確立した後、そのインスタンスと接続は保持されます。その後にホスト名に対するIPアドレスが変更されて場合、新しいIPアドレスに再接続する仕組みがありません。GitHubAs described in the following post: http://byterot.blogspot.…
IHttpClientFactoryの利点
上記の問題に対処するために、.NET Core 2.1からIHttpClientFactoryが導入されており、このインターフェイスを介して作成したHttpClientの使用が推奨されます。
IHttpClientFactoryの利点は次の通りです。
- HttpClientの名前付けと構成を一元管理できる。
HttpClientの種類を識別するための名前を付けて、HttpClientを取得できます。HttpClientを使用する前に、名前毎に異なる既定値を設定することもできます。例えばWebAPのAとBに接続するためのHttpClientをそれぞれ”weapi-a”, “webapi-b”という名前でIHttpClientFactoryに登録し、”webapi-a”, “webapi-b”に対してURLや認証情報を事前に設定しておくことができます。 - デリゲートハンドラによる通信処理の拡張
HTTP通信時の入出力をカスタマイズするためのデリゲートハンドラ(DelegatingHandler)を、パイプラインとして登録できます。デリゲートハンドラを使用することで、HTTP送信前後で独自にHTTPヘッダやボディの編集・検証等を実装できます。 - 高度なエラー処理用のPollyライブラリの統合
HTTPをベースとしたクラウドベースのサービス利用では、アプリケーションの信頼性・安定性を向上するために、HTTP通信エラーに対する高度なハンドリングが必要となります。IHttpClientFactoryでは、このようなハンドリングが行えるオープンソースライブラリであるPollyを使用できるようになっています。 - HttpMessageHandlerの管理が可能
HttpClientは内部でHttpMessageHandlerを使用してHTTP通信を行っています。
前述のDNS変更の問題は、HttpMessageHandlerインスタンスの保持期間が無制限になっていることが問題だったのですが、このインスタンスのプーリングやライフサイクルの管理が可能になっています。
IHttpClientFactoryの使用方法
IHttpClientFactoryを介したHttpClientの使用方法は次の4種類がある。
ちょっとした検証で使うなら「基本的な方法」で十分かと思いますが、実業務で使うならHTTP通信+接続先固有の処理をカプセル化できDI可能な「型付きクライアント」がお薦めです。
使用方法 | 説明 |
---|---|
1. 基本的な方法 (検証等で単純に使用する場合) | HttpClientをシンプルに使用する方法。Startup.csで下記の宣言を行い、コントローラ等で使用する場合はコンストラクタでIHttpClientFactoryをDIしてからHttpClientを生成する。(宣言時): services.AddHttpClient(); (使用時): HttpClient client = _clientFactory.CreateClient(); |
2. 名前付きクライアント (接続先や構成パターンが多数ある場合や接続先が動的に変更される場合等) | クライアントの種類毎に名前(文字列)を付けて使用する方法。Startup.csで名前を指定してクライアントを登録・設定を宣言し、コントローラ等で使用する場合はコンストラクタでIHttpClientFactoryをDIしてから名前を指定してHttpClientを生成する。 (宣言時): services.AddHttpClient("name", client => { ... }); (使用時): HttpClient client = _clientFactory.CreateClient("name"); |
3. 型付きクライアント (接続先や構成パターンが限定な場合、送受信時の業務ロジックを実装してその内容を隠蔽したい場合等) | 独自のクライアントクラスを使用する方法。 Startup.csでクラス名を指定してクライアントを登録・設定を宣言し、コントローラ等で使用する場合はコンストラクタでクライアントをDIして使用する。(IHttpClientFactoryのDIは不要)(宣言時): services.AddHttpClient<GitHubService>(); (使用時): public SomeConstructor(GitHubService client){ ... }; |
4. 生成されたクライアント (要件がライブラリで満たされる場合、実装量を削減したい場合等) | サードパーティーライブラリのクライアントを使用する方法。 IHttpClientFactoryにサードパーティーライブラリを登録することで実現する。クライアントの使用方法はライブラリに依存するが、基本的にはDIで使用可能となる。下記はRefitを使用する場合の例です。(宣言時): services.AddHttpClient("hello", client => { .. }) |
参考として、MSが提供する型付きクライアントの構成イメージを説明します。
IHttpClientFactoryにClientServiceを登録することで、コントローラやクライアントにClientServiceをDIできます。(コントローラやクライアントはClientServiceを直接DIできるので、IHttpClientFactoryをDIする必要はありません。)
ClientService内ではHttpClientインスタンスを使用して通信を行いますが、実際の通信処理はプールから取得したHttpMessageHandlerインスタンスを使って行われます。送信の前後で、クライアントに登録されているデリゲートハンドラが要求や応答を編集・検証します。エラーハンドリングに関しては、ポリシーとして登録されているPollyのエラーハンドリングが行われます。
カスタマイズ方法
ここでは前述のようにHttpClientをどう使うか?ではなく、HttpClientそのものの動作のカスタマイズについて説明します。カスタマイズ可能なポイントは次の通りです。
- HttpClientのデフォルトプロパティ
- Pollyによるエラーハンドリング
- デリゲートハンドラの登録
- HttpMessageHandlerライフサイクルの管理
- HttpMessageHandlerのプロパティ
HttpClientのデフォルトプロパティ
クライアント登録時に、HttpClientの既定のプロパティを設定できます。
設定可能なHTTPヘッダはMSのリファレンスを参照のこと。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("github", client => { client.BaseAddress = new Uri("https://example.com/"); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(){ NoCache = true, NoStore = true, MaxAge = new TimeSpan(0) }; client.DefaultRequestHeaders.Add("Authorization", "Bearer xxx"); client.Timeout = TimeSpan.FromSeconds(3 * 60); }); } |
Pollyによるエラーハンドリング
応答コードやエラーに対するエラーハンドリング方法をポリシーとして指定します。
応答コード5xxや408、例外(HttpRequestException)を纏めてエラーハンドリングする場合はAddTransientHttpErrorPolicyを使用します。
1 2 3 4 5 | public void ConfigureServices(IServiceCollection services) { services.AddHttpClient<UnreliableEndpointCallerService>() .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600))); |
応答コードや例外のそれぞれに異なるエラーハンドリングを行いたい場合はAddPolicyHandlerを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public void ConfigureServices(IServiceCollection services) { services.AddHttpClient<ICatalogService, CatalogService>() .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(...); } static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } |
デリゲートハンドラの登録
デリゲートハンドラクラスを作成し、Startupで登録します。
デリゲートハンドラクラスを作成するためにはDelegatingHandlerを継承したクラスを作成し、SendAsyncメソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class SecureRequestHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.RequestUri.Scheme == Uri.UriSchemeHttp) { var builder = new UriBuilder(request.RequestUri) { Scheme = Uri.UriSchemeHttps }; request.RequestUri = builder.Uri; } return base.SendAsync(request, cancellationToken); } } |
Startup.csにて、定義したデリゲートハンドラクラスをシングルトンとして登録し、特定のクライアントに対してAddHttpMessageHandlerメソッドでデリゲートハンドラクラスを登録します。
1 2 3 4 5 6 7 8 | public void ConfigureServices(IServiceCollection services) { services.AddTransient<SecureRequestHandler>(); services.AddTransient<RequestDataHandler>(); services.AddHttpClient("clientwithhandlers") .AddHttpMessageHandler<SecureRequestHandler>() .AddHttpMessageHandler<RequestDataHandler>(); |
デリゲートハンドラの完全なコードはMSのサンプルを参照のこと。
HttpMessageHandlerライフサイクルの管理
HttpMessageHandlerのプール上での生存時間を指定します。(既定は2分です。)
1 2 3 4 | public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("extendedhandlerlifetime") .SetHandlerLifetime(TimeSpan.FromMinutes(5)); |
HttpMessageHandlerのプロパティ
HttpClientの詳細のカスタマイズを行いたい場合、内部で使用されるHttpMessageHandlerをカスタマイズします。Cookieの使用についてもカスタマイズできます。
使用できるプロパティはMSのリファレンスを参照のこと。(以前はHttpClientHandlerで構成しましたが、.NET Core 2.1以降ではSocketsHttpHandlerを使用するそうです。)
1 2 3 4 5 6 7 8 9 10 11 12 | public void ConfigureServices(IServiceCollection services) { services.AddHttpClient("configured-inner-handler") .ConfigurePrimaryHttpMessageHandler(() => { return new HttpClientHandler() { AllowAutoRedirect = false, UseDefaultCredentials = true }; } ); |
その他の参考情報
サンプル
私が作成したサンプルの説明記事です。
Pollyについて
Polly関連の補足説明です。
- Pollyで実装されているエラーハンドリングのパターンはAzureのクラウド設計パターンが参考になると思います。
- 再試行: 一時的な障害をアプリケーションが処理できるようにする。Azureサービスを使用する際の再試行ポリシーはこちらを参考のこと。
- サーキットブレーカー: 直ぐに回復しない障害に対して再試行すると状況を悪化させる場合があります。このような状況にならないよう、失敗する可能性がある操作をアプリケーションが実行しないようにする。
- バルクヘッド: 接続プールを分離し、特定サービスへの接続で障害が起きても、別のサービスへの接続に影響ないようにする。
接続プールを共有する場合、例えばあるサービスに対して大量の接続が行われると、接続プール上の接続が空になってしまい、他のサービスへの接続ができなくなってしまう。接続プールを分離することで、あるサービスの影響が他サービスの利用に影響しないようにできる。
バルクヘッドとは船の区画(隔壁)の意で、船体の特定区画が破損して浸水しても、他の区画には浸水させず船が沈むのを防ぐ。
- IHttpClientFactory ポリシーと Polly ポリシーで指数バックオフを含む HTTP 呼び出しの再試行を実装する
- サーキット ブレーカー パターンを実装する