ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う

作成者: Kirk LarkinSteve GordonGlenn CondronRyan Nowak

アプリ内で HttpClient インスタンスを構成して作成するために、IHttpClientFactory を登録して使用できます。 IHttpClientFactory には次のような利点があります。

  • 論理 HttpClient インスタンスの名前付けと構成を一元化します。 たとえば、GitHub にアクセスするために、github という名前のクライアントを登録して構成できます。 一般的なアクセスのために、既定のクライアントを登録できます。
  • HttpClient でのハンドラーのデリゲートにより、送信ミドルウェアの概念が体系化されます。 HttpClient でのハンドラーのデリゲートを利用するために、Polly ベースのミドルウェアに対する拡張機能が提供されます。
  • 基になっている HttpClientMessageHandler インスタンスのプールと有効期間が管理されます。 自動管理により、HttpClient の有効期間を手動で管理するときの一般的な DNS (ドメイン ネーム システム) の問題が発生しなくなります。
  • ファクトリによって作成されたクライアントから送信されるすべての要求に対し、(ILogger によって) 構成可能なログ エクスペリエンスを追加します。

このトピックのバージョンのサンプル コードでは、HTTP 応答で返された JSON コンテンツを、System.Text.Json を使用して逆シリアル化します。 Json.NET および ReadAsAsync<T> を使用するサンプルについては、バージョン セレクターを使用して、このトピックの 2.x バージョンを選択してください。

利用パターン

アプリで IHttpClientFactory を使用するには複数の方法があります。

どの方法が最善かは、アプリの要件によって異なります。

基本的な使用方法

Program.csAddHttpClient を呼び出すことにより、IHttpClientFactory を登録します。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

IHttpClientFactory は、依存関係の挿入 (DI) を使用して要求できます。 次のコードでは、IHttpClientFactory を使用して HttpClient インスタンスを作成しています。

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

前の例のように IHttpClientFactory を使用するのは、既存のアプリをリファクタリングするのに適した方法です。 HttpClient の使用方法に対する影響はありません。 既存のアプリで HttpClient のインスタンスが作成されている場所を、CreateClient の呼び出しに置き換えます。

名前付きクライアント

名前付きクライアントは、次の場合に適しています。

  • アプリで、多くの異なる HttpClient を使用する必要がある。
  • 多くの HttpClient の構成が異なる。

Program.cs での登録時に、特定の名前の HttpClient に対する構成を指定します。

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

上記のコードでは、クライアントに次のものが構成されます。

  • ベース アドレス https://api.github.com/
  • GitHub API を使用するために必要な 2 つのヘッダー。

CreateClient

CreateClient が呼び出されるたびに、次のことが行われます。

  • HttpClient の新しいインスタンスが作成されます。
  • 構成アクションが呼び出されます。

名前付きクライアントを作成するには、その名前を CreateClient に渡します。

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

上記のコードでは、要求でホスト名を指定する必要はありません。 クライアントに構成されているベース アドレスが使用されるため、コードではパスを渡すだけで済みます。

型指定されたクライアント

型指定されたクライアント:

  • キーとして文字列を使用する必要なしに、名前付きクライアントと同じ機能を提供します。
  • クライアントを使用するときに、IntelliSense とコンパイラのヘルプが提供されます。
  • 特定の HttpClient を構成してそれと対話する 1 つの場所を提供します。 たとえば、単一の型指定されたクライアントは、次のために使用される場合があります。
    • 単一のバックエンド エンドポイント用。
    • エンドポイントを処理するすべてのロジックをカプセル化するため。
  • DI に対応しており、アプリ内の必要な場所に挿入できます。

型指定されたクライアントは、そのコンストラクターで HttpClient パラメーターを受け取ります。

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

上のコードでは以下の操作が行われます。

  • 構成が型指定されたクライアントに移動されます。
  • 指定された HttpClient インスタンスは、プライベート フィールドとして格納されます。

HttpClient の機能を公開する API 固有のメソッドを作成できます。 たとえば、GetAspNetCoreDocsBranches メソッドでは、docs GitHub ブランチを取得するためのコードがカプセル化されます。

次のコードでは、Program.csAddHttpClient を呼び出して、GitHubService 型指定されたクライアント クラスを登録します。

builder.Services.AddHttpClient<GitHubService>();

型指定されたクライアントは、DI で一時的として登録されます。 上記のコードで、AddHttpClientGitHubService を一時的なサービスとして登録します。 この登録では、ファクトリ メソッドを使用して次のことを行います。

  1. HttpClient のインスタンスを作成します。
  2. GitHubService のインスタンスを作成し、HttpClient のインスタンスをそのコンストラクターに渡します。

型指定されたクライアントは、直接挿入して使用できます。

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

型指定されたクライアントの構成は、型指定されたクライアントのコンストラクターではなく、Program.cs での登録時に指定することもできます。

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

生成されたクライアント

IHttpClientFactory は、Refit などのサードパーティ製ライブラリと組み合わせて使用できます。 Refit は、.NET 用の REST ライブラリです。 REST API をライブ インターフェイスに変換します。 インターフェイスの動的実装を生成するには、AddRefitClient を呼び出します。これにより、HttpClient を使用して外部 HTTP 呼び出しが行われます。

外部 API はカスタム インターフェイスによって表されます。

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

AddRefitClient を呼び出して動的実装を生成した後、ConfigureHttpClient を呼び出して、基になる HttpClient を構成します。

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

IGitHubClient の動的実装にアクセスするには、DI を使用します。

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

POST、PUT、DELETE の要求を行う

前の例では、すべての HTTP 要求で GET HTTP 動詞が使用されています。 HttpClient では、次のような他の HTTP 動詞もサポートされています。

  • POST
  • PUT
  • Del
  • PATCH

サポートされている HTTP 動詞の一覧については、「HttpMethod」を参照してください。

次の例は、HTTP POST 要求の方法を示しています。

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

上記のコードの CreateItemAsync メソッドは:

  • System.Text.Json を使用して TodoItem パラメーターを JSON にシリアル化します。
  • HTTP 要求の本文で送信するためにシリアル化された JSON をパッケージ化する StringContent のインスタンスを作成します。
  • PostAsync を呼び出し、指定した URL に JSON コンテンツを送信します。 これは HttpClient.BaseAddress に追加される相対 URL です。
  • 応答状態コードが成功を示していない場合に、EnsureSuccessStatusCode を呼び出して例外をスローします。

HttpClient は、他の種類のコンテンツもサポートしています。 たとえば、MultipartContentStreamContent です。 サポートされているコンテンツの一覧については、「HttpContent」を参照してください。

HTTP PUT 要求の例を次に示します。

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

上記のコードは、POST の例に似ています。 SaveItemAsync メソッドは PostAsync ではなく PutAsync を呼び出します。

HTTP DELETE 要求の例を次に示します。

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.EnsureSuccessStatusCode();
}

上記のコードの DeleteItemAsync メソッドは DeleteAsync を呼び出します。 HTTP DELETE 要求には通常、本文が含まれていないため、DeleteAsync メソッドは HttpContent のインスタンスを受け入れるオーバーロードを提供しません。

HttpClient でのさまざまな HTTP 動詞の使用の詳細については、「HttpClient」を参照してください。

送信要求ミドルウェア

HttpClient は、送信 HTTP 要求用にリンクできるハンドラーのデリゲートの概念を備えています。 IHttpClientFactory:

  • 各名前付きクライアントに適用するハンドラーの定義が簡単になります。
  • 送信要求ミドルウェア パイプラインを構築するための複数のハンドラーの登録とチェーン化がサポートされています。 各ハンドラーは、送信要求の前と後に処理を実行できます。 このパターンは次のようなものです。
    • ASP.NET Core での受信ミドルウェア パイプラインに似ています。
    • 次のような HTTP 要求に関する横断的な問題を管理するためのメカニズムが提供されます。
      • キャッシュ
      • エラー処理
      • シリアル化
      • ログ

デリゲート ハンドラーを作成するには、次のようにします。

  • DelegatingHandler から派生します。
  • SendAsync をオーバーライドします。 パイプライン内の次のハンドラーに要求を渡す前に、コードを実行します。
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

上記のコードでは、X-API-KEY ヘッダーが要求に含まれているかどうかが確認されます。 X-API-KEY がない場合は、BadRequest が返されます。

Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler を使用して HttpClient の構成に複数のハンドラーを追加できます。

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

上記のコードでは、ValidateHeaderHandler が DI で登録されます。 登録が済むと、AddHttpMessageHandler を呼び出してハンドラーの型を渡すことができます。

実行する順序で、複数のハンドラーを登録することができます。 最後の HttpClientHandler が要求を実行するまで、各ハンドラーは次のハンドラーをラップします。

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

上のコードでは、SampleHandler1 が最初に (SampleHandler2 より先に) 実行されています。

送信要求ミドルウェアで DI を使用する

IHttpClientFactory によって新しいデリゲート ハンドラーが作成される際、ハンドラーのコンストラクター パラメーターを満たすために DI が使用されます。 IHttpClientFactory により、ハンドラーごとに個別の DI スコープが作成されます。これにより、ハンドラーによって "スコープ付き" サービスが使用されるときに、予期しない動作が発生する可能性があります。

たとえば、識別子 OperationId を持つ操作としてタスクを表す、次のインターフェイスとその実装について考えてみます。

public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

その名前が示すように、IOperationScoped は "スコープ付き" 有効期間を使用して DI に登録されます。

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

次のデリゲート ハンドラーでは、IOperationScoped を使用して、送信要求用の X-OPERATION-ID ヘッダーを設定します。

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

HttpRequestsSample ダウンロードで、/Operation に移動してページを更新します。 要求スコープの値は要求ごとに変更されますが、ハンドラーのスコープの値は 5 秒ごとに変更されるだけです。

ハンドラーは、任意のスコープのサービスに依存することができます。 ハンドラーが依存するサービスは、ハンドラーが破棄されるときに破棄されます。

次の方法のいずれかを使って、要求ごとの状態をメッセージ ハンドラーと共有します。

Polly ベースのハンドラーを使用する

IHttpClientFactory は、サードパーティのライブラリ Polly と統合します。 Polly は、.NET 用の包括的な回復力および一時的エラー処理ライブラリです。 開発者は、自然でスレッド セーフな方法を使用して、Retry、Circuit Breaker、Timeout、Bulkhead Isolation、Fallback などのポリシーを表現できます。

構成されている HttpClient インスタンスで Polly ポリシーを使用できるようにするための、拡張メソッドが提供されています。 Polly の拡張機能では、クライアントへの Polly ベースのハンドラーの追加がサポートされています。 Polly では、Microsoft.Extensions.Http.Polly NuGet パッケージが必要です。

一時的な障害を処理する

通常、外部 HTTP を呼び出すときに発生する障害は、一時的なものです。 AddTransientHttpErrorPolicy では、一時的なエラーを処理するためのポリシーを定義できます。 AddTransientHttpErrorPolicy で構成されたポリシーでは、次の応答が処理されます。

AddTransientHttpErrorPolicy では、可能性のある一時的障害を表すエラーを処理するために構成された PolicyBuilder オブジェクトへのアクセスが提供されます。

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

上記のコードでは、WaitAndRetryAsync ポリシーが定義されています。 失敗した要求は最大 3 回再試行され、再試行の間には 600 ミリ秒の遅延が設けられます。

ポリシーを動的に選択する

Polly ベースのハンドラーを追加するための拡張メソッドが提供されています (AddPolicyHandler など)。 次の AddPolicyHandler のオーバーロードでは、要求が検査されて、適用するポリシーが決定されます。

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

上記のコードでは、送信要求が HTTP GET の場合は、10 秒のタイムアウトが適用されます。 他の HTTP メソッドの場合は、30 秒のタイムアウトが使用されます。

複数の Polly ハンドラーを追加する

Polly のポリシーは入れ子にするのが一般的です。

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

前の例の場合:

  • 2 つのハンドラーが追加されます。
  • 1 つ目のハンドラーでは、AddTransientHttpErrorPolicy を使用して再試行ポリシーが追加されます。 失敗した要求は 3 回まで再試行されます。
  • 2 つ目の AddTransientHttpErrorPolicy の呼び出しでは、サーキット ブレーカー ポリシーが追加されます。 試行が連続して 5 回失敗した場合、それ以上の外部要求は 30 秒間ブロックされます。 サーキット ブレーカー ポリシーはステートフルです。 このクライアントからのすべての呼び出しは、同じサーキット状態を共有します。

Polly レジストリからポリシーを追加する

定期的に使用されるポリシーを管理するには、ポリシーを 1 回定義した後、PolicyRegistry でポリシーを登録します。 次に例を示します。

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

上のコードでは以下の操作が行われます。

  • 2 つのポリシー (RegularLong) が Polly レジストリに追加されます。
  • これらのポリシーを Polly から使用するために、AddPolicyHandlerFromRegistry によって個々の名前付きクライアントを構成します。

IHttpClientFactory と Polly の統合の詳細については、Polly に関する wiki を参照してください。

HttpClient と有効期間の管理

IHttpClientFactoryCreateClient を呼び出すたびに、HttpClient の新しいインスタンスが返されます。 HttpMessageHandler は、名前付きクライアントごとに作成されます。 ファクトリによって HttpMessageHandler インスタンスの有効期間が管理されます。

IHttpClientFactory は、リソースの消費量を減らすため、ファクトリによって作成された HttpMessageHandler のインスタンスをプールします。 新しい HttpClient インスタンスを作成するときに、プールの HttpMessageHandler インスタンスの有効期間が切れていない場合はそれを再利用する場合があります。

通常、各ハンドラーでは基になる HTTP 接続が独自に管理されるため、ハンドラーはプールすることが望まれます。 必要以上のハンドラーを作成すると、接続に遅延が発生する可能性があります。 また、一部のハンドラーでは接続が無期限に開かれており、DNS (ドメイン ネーム システム) の変更にハンドラーが対応できないことがあります。

ハンドラーの既定の有効期間は 2 分です。 名前付きクライアントごとに、既定値をオーバーライドすることができます。

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

通常、HttpClient インスタンスは、破棄する必要のない .NET オブジェクトとして扱うことができます。 破棄すると送信要求がキャンセルされ、Dispose の呼び出し後には指定された HttpClient インスタンスを使用できなくなります。 IHttpClientFactory は、HttpClient インスタンスによって使用されたリソースの追跡と破棄を行います。

IHttpClientFactory の登場以前は、1 つの HttpClient インスタンスを長い期間使い続けるパターンが一般的に使用されていました。 IHttpClientFactory に移行した後は、このパターンは不要になります。

IHttpClientFactory の代替手段

DI 対応のアプリ内で IHttpClientFactory を使用すれば、次のことを回避できます。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題。
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題。

有効期間の長い SocketsHttpHandler インスタンスを使用して、上記の問題を解決する別の方法があります。

  • アプリの起動時に SocketsHttpHandler インスタンスを作成し、アプリの有効期間中、それを使用します。
  • DNS の更新時間に基づいて、PooledConnectionLifetime を適切な値に構成します。
  • 必要に応じて new HttpClient(handler, disposeHandler: false) を使用して HttpClient インスタンスを作成します。

上記の方法を使用すると、IHttpClientFactory が同様の方法で解決するリソース管理の問題を解決できます。

  • SocketsHttpHandler を使用すると、HttpClient インスタンス間で接続を共有できます。 この共有によってソケットの枯渇が防止されます。
  • SocketsHttpHandler では、古くなった DNS の問題を回避するために PooledConnectionLifetime に従って接続を循環されます。

ログの記録

IHttpClientFactory によって作成されたクライアントは、すべての要求のログ メッセージを記録します。 既定のログ メッセージを見るには、ログの構成で適切な情報レベルを有効にします。 要求ヘッダーのログなどの追加ログは、トレース レベルでのみ含まれます。

各クライアントに使用されるログのカテゴリには、クライアントの名前が含まれます。 たとえば、MyNamedClient という名前のクライアントでは、"System.Net.Http.HttpClient.MyNamedClient.LogicalHandler" というカテゴリでメッセージがログに記録されます。 LogicalHandler というサフィックスが付いたメッセージが、要求ハンドラーのパイプラインの外部で発生します。 要求では、パイプライン内の他のハンドラーが要求を処理する前に、メッセージがログに記録されます。 応答では、他のパイプライン ハンドラーが応答を受け取った後で、メッセージがログに記録されます。

ログ記録は、要求ハンドラーのパイプラインの内部でも行われます。 MyNamedClient の例では、これらのメッセージはログ カテゴリ "System.Net.Http.HttpClient.MyNamedClient.ClientHandler" でログに記録されます。 要求では、他のすべてのハンドラーが実行された後、要求が送信される直前に、これが行われます。 応答では、このログ記録には、ハンドラー パイプラインを通じ戻される前の応答の状態が含まれます。

パイプラインの外部と内部でログを有効にすると、他のパイプライン ハンドラーによる変更を検査できます。 これには、要求ヘッダーや応答状態コードへの変更を含めることができます。

ログのカテゴリにクライアントの名前を含めると、特定の名前付きクライアントでログをフィルター処理できます。

HttpMessageHandler を構成する

クライアントによって使用される内部 HttpMessageHandler の構成を制御することが必要な場合があります。

名前付きクライアントまたは型指定されたクライアントを追加すると、IHttpClientBuilder が返されます。 ConfigurePrimaryHttpMessageHandler 拡張メソッドを使用して、デリゲートを定義することができます。 デリゲートは、そのクライアントによって使用されるプライマリ HttpMessageHandler の作成と構成に使用されます。

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Cookies

HttpMessageHandler インスタンスをプールすると、CookieContainer オブジェクトが共有されます。 予期せぬ CookieContainer オブジェクト共有があると、多くの場合、コードは不適切なものとなります。 cookie を必要とするアプリの場合は、次のいずれかを検討してください。

  • 自動的な cookie 処理の無効化
  • IHttpClientFactory の回避

ConfigurePrimaryHttpMessageHandler を呼び出して、自動的な cookie 処理を無効にします。

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

コンソール アプリで IHttpClientFactory を使用する

コンソール アプリで、次のパッケージ参照をプロジェクトに追加します。

次に例を示します。

  • IHttpClientFactoryGitHubService は、汎用ホストのサービス コンテナーに登録されます。
  • GitHubService は DI から要求され、その後、IHttpClientFactory のインスタンスを要求します。
  • GitHubServiceIHttpClientFactory を使用して HttpClient のインスタンスを作成します。これは、docs GitHub ブランチを取得するために使用されます。
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

ヘッダー伝達ミドルウェア

ヘッダー伝達は、受信要求から送信 HttpClient 要求に HTTP ヘッダーを伝達するための ASP.NET Core ミドルウェアです。 ヘッダー伝達を使用するには、次を行います。

  • Microsoft.AspNetCore.HeaderPropagation パッケージをインストールします。

  • Program.csHttpClient とミドルウェア パイプラインを構成します。

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • 構成済みの HttpClient インスタンス (追加されたヘッダーを含む) を使用して、送信要求を行います。

その他のリソース

作成者: Kirk LarkinSteve GordonGlenn CondronRyan Nowak

アプリ内で HttpClient インスタンスを構成して作成するために、IHttpClientFactory を登録して使用できます。 IHttpClientFactory には次のような利点があります。

  • 論理 HttpClient インスタンスの名前付けと構成を一元化します。 たとえば、GitHub にアクセスするために、github という名前のクライアントを登録して構成できます。 一般的なアクセスのために、既定のクライアントを登録できます。
  • HttpClient でのハンドラーのデリゲートにより、送信ミドルウェアの概念が体系化されます。 HttpClient でのハンドラーのデリゲートを利用するために、Polly ベースのミドルウェアに対する拡張機能が提供されます。
  • 基になっている HttpClientMessageHandler インスタンスのプールと有効期間が管理されます。 自動管理により、HttpClient の有効期間を手動で管理するときの一般的な DNS (ドメイン ネーム システム) の問題が発生しなくなります。
  • ファクトリによって作成されたクライアントから送信されるすべての要求に対し、(ILogger によって) 構成可能なログ エクスペリエンスを追加します。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

このトピックのバージョンのサンプル コードでは、HTTP 応答で返された JSON コンテンツを、System.Text.Json を使用して逆シリアル化します。 Json.NET および ReadAsAsync<T> を使用するサンプルについては、バージョン セレクターを使用して、このトピックの 2.x バージョンを選択してください。

利用パターン

アプリで IHttpClientFactory を使用するには複数の方法があります。

どの方法が最善かは、アプリの要件によって異なります。

基本的な使用方法

IHttpClientFactory は、AddHttpClient を呼び出すことによって登録できます。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

IHttpClientFactory は、依存関係の挿入 (DI) を使用して要求できます。 次のコードでは、IHttpClientFactory を使用して HttpClient インスタンスを作成しています。

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

前の例のように IHttpClientFactory を使用するのは、既存のアプリをリファクタリングするのに適した方法です。 HttpClient の使用方法に対する影響はありません。 既存のアプリで HttpClient のインスタンスが作成されている場所を、CreateClient の呼び出しに置き換えます。

名前付きクライアント

名前付きクライアントは、次の場合に適しています。

  • アプリで、多くの異なる HttpClient を使用する必要がある。
  • 多くの HttpClient の構成が異なる。

名前付き HttpClient の構成は、Startup.ConfigureServices での登録時に指定できます。

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

上記のコードでは、クライアントに次のものが構成されます。

  • ベース アドレス https://api.github.com/
  • GitHub API を使用するために必要な 2 つのヘッダー。

CreateClient

CreateClient が呼び出されるたびに、次のことが行われます。

  • HttpClient の新しいインスタンスが作成されます。
  • 構成アクションが呼び出されます。

名前付きクライアントを作成するには、その名前を CreateClient に渡します。

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

上記のコードでは、要求でホスト名を指定する必要はありません。 クライアントに構成されているベース アドレスが使用されるため、コードではパスを渡すだけで済みます。

型指定されたクライアント

型指定されたクライアント:

  • キーとして文字列を使用する必要なしに、名前付きクライアントと同じ機能を提供します。
  • クライアントを使用するときに、IntelliSense とコンパイラのヘルプが提供されます。
  • 特定の HttpClient を構成してそれと対話する 1 つの場所を提供します。 たとえば、単一の型指定されたクライアントは、次のために使用される場合があります。
    • 単一のバックエンド エンドポイント用。
    • エンドポイントを処理するすべてのロジックをカプセル化するため。
  • DI に対応しており、アプリ内の必要な場所に挿入できます。

型指定されたクライアントは、そのコンストラクターで HttpClient パラメーターを受け取ります。

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

上のコードでは以下の操作が行われます。

  • 構成が型指定されたクライアントに移動されます。
  • HttpClient オブジェクトは、パブリック プロパティとして公開されます。

HttpClient の機能を公開する API 固有のメソッドを作成できます。 たとえば、GetAspNetDocsIssues メソッドでは、未解決の問題を取得するためのコードがカプセル化されています。

次のコードでは、Startup.ConfigureServices 内で AddHttpClient を呼び出して、型指定されたクライアント クラスを登録しています。

services.AddHttpClient<GitHubService>();

型指定されたクライアントは、DI で一時的として登録されます。 上記のコードで、AddHttpClientGitHubService を一時的なサービスとして登録します。 この登録では、ファクトリ メソッドを使用して次のことを行います。

  1. HttpClient のインスタンスを作成します。
  2. GitHubService のインスタンスを作成し、HttpClient のインスタンスをそのコンストラクターに渡します。

型指定されたクライアントは、直接挿入して使用できます。

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

型指定されたクライアントのコンストラクターではなく、Startup.ConfigureServices での登録時に型指定されたクライアントの構成を指定できます。

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient は、型指定されたクライアント内にカプセル化できます。 プロパティとして公開するのではなく、HttpClient インスタンスを内部的に呼び出すパブリック メソッドを定義します。

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

上記のコードでは、HttpClient はプライベート フィールドに格納されます。 HttpClient へのアクセスは、パブリック GetRepos メソッドによって行われます。

生成されたクライアント

IHttpClientFactory は、Refit などのサードパーティ製ライブラリと組み合わせて使用できます。 Refit は、.NET 用の REST ライブラリです。 REST API をライブ インターフェイスに変換します。 インターフェイスの実装は RestService によって動的に生成され、HttpClient を使用して外部 HTTP の呼び出しを行います。

インターフェイスと応答は、外部の API とその応答を表すように定義されます。

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

型指定されたクライアントを追加し、Refit を使用して実装を生成できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

定義されたインターフェイスは、DI および Refit によって提供される実装で必要に応じて使用できます。

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

POST、PUT、DELETE の要求を行う

前の例では、すべての HTTP 要求で GET HTTP 動詞が使用されています。 HttpClient では、次のような他の HTTP 動詞もサポートされています。

  • POST
  • PUT
  • Del
  • PATCH

サポートされている HTTP 動詞の一覧については、「HttpMethod」を参照してください。

次の例は、HTTP POST 要求の方法を示しています。

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードの CreateItemAsync メソッドは:

  • System.Text.Json を使用して TodoItem パラメーターを JSON にシリアル化します。 これは、JsonSerializerOptions のインスタンスを使用して、シリアル化プロセスを構成します。
  • HTTP 要求の本文で送信するためにシリアル化された JSON をパッケージ化する StringContent のインスタンスを作成します。
  • PostAsync を呼び出し、指定した URL に JSON コンテンツを送信します。 これは HttpClient.BaseAddress に追加される相対 URL です。
  • 応答状態コードが成功を示していない場合に、EnsureSuccessStatusCode を呼び出して例外をスローします。

HttpClient は、他の種類のコンテンツもサポートしています。 たとえば、MultipartContentStreamContent です。 サポートされているコンテンツの一覧については、「HttpContent」を参照してください。

HTTP PUT 要求の例を次に示します。

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードは、POST の例によく似ています。 SaveItemAsync メソッドは PostAsync ではなく PutAsync を呼び出します。

HTTP DELETE 要求の例を次に示します。

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードの DeleteItemAsync メソッドは DeleteAsync を呼び出します。 HTTP DELETE 要求には通常、本文が含まれていないため、DeleteAsync メソッドは HttpContent のインスタンスを受け入れるオーバーロードを提供しません。

HttpClient でのさまざまな HTTP 動詞の使用の詳細については、「HttpClient」を参照してください。

送信要求ミドルウェア

HttpClient は、送信 HTTP 要求用にリンクできるハンドラーのデリゲートの概念を備えています。 IHttpClientFactory:

  • 各名前付きクライアントに適用するハンドラーの定義が簡単になります。
  • 送信要求ミドルウェア パイプラインを構築するための複数のハンドラーの登録とチェーン化がサポートされています。 各ハンドラーは、送信要求の前と後に処理を実行できます。 このパターンは次のようなものです。
    • ASP.NET Core での受信ミドルウェア パイプラインに似ています。
    • 次のような HTTP 要求に関する横断的な問題を管理するためのメカニズムが提供されます。
      • キャッシュ
      • エラー処理
      • シリアル化
      • ログ

デリゲート ハンドラーを作成するには、次のようにします。

  • DelegatingHandler から派生します。
  • SendAsync をオーバーライドします。 パイプライン内の次のハンドラーに要求を渡す前に、コードを実行します。
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

上記のコードでは、X-API-KEY ヘッダーが要求に含まれているかどうかが確認されます。 X-API-KEY がない場合は、BadRequest が返されます。

Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler を使用して HttpClient の構成に複数のハンドラーを追加できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

上記のコードでは、ValidateHeaderHandler が DI で登録されます。 登録が済むと、AddHttpMessageHandler を呼び出してハンドラーの型を渡すことができます。

実行する順序で、複数のハンドラーを登録することができます。 最後の HttpClientHandler が要求を実行するまで、各ハンドラーは次のハンドラーをラップします。

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

送信要求ミドルウェアで DI を使用する

IHttpClientFactory によって新しいデリゲート ハンドラーが作成される際、ハンドラーのコンストラクター パラメーターを満たすために DI が使用されます。 IHttpClientFactory により、ハンドラーごとに個別の DI スコープが作成されます。これにより、ハンドラーによって "スコープ付き" サービスが使用されるときに、予期しない動作が発生する可能性があります。

たとえば、識別子 OperationId を持つ操作としてタスクを表す、次のインターフェイスとその実装について考えてみます。

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

その名前が示すように、IOperationScoped は "スコープ付き" 有効期間を使用して DI に登録されます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

次のデリゲート ハンドラーでは、IOperationScoped を使用して、送信要求用の X-OPERATION-ID ヘッダーを設定します。

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

HttpRequestsSample ダウンロード] で、/Operation に移動してページを更新します。 要求スコープの値は要求ごとに変更されますが、ハンドラーのスコープの値は 5 秒ごとに変更されるだけです。

ハンドラーは、任意のスコープのサービスに依存することができます。 ハンドラーが依存するサービスは、ハンドラーが破棄されるときに破棄されます。

次の方法のいずれかを使って、要求ごとの状態をメッセージ ハンドラーと共有します。

Polly ベースのハンドラーを使用する

IHttpClientFactory は、サードパーティのライブラリ Polly と統合します。 Polly は、.NET 用の包括的な回復力および一時的エラー処理ライブラリです。 開発者は、自然でスレッド セーフな方法を使用して、Retry、Circuit Breaker、Timeout、Bulkhead Isolation、Fallback などのポリシーを表現できます。

構成されている HttpClient インスタンスで Polly ポリシーを使用できるようにするための、拡張メソッドが提供されています。 Polly の拡張機能では、クライアントへの Polly ベースのハンドラーの追加がサポートされています。 Polly では、Microsoft.Extensions.Http.Polly NuGet パッケージが必要です。

一時的な障害を処理する

通常、外部 HTTP を呼び出すときに発生する障害は、一時的なものです。 AddTransientHttpErrorPolicy では、一時的なエラーを処理するためのポリシーを定義できます。 AddTransientHttpErrorPolicy で構成されたポリシーでは、次の応答が処理されます。

AddTransientHttpErrorPolicy では、可能性のある一時的障害を表すエラーを処理するために構成された PolicyBuilder オブジェクトへのアクセスが提供されます。

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

上記のコードでは、WaitAndRetryAsync ポリシーが定義されています。 失敗した要求は最大 3 回再試行され、再試行の間には 600 ミリ秒の遅延が設けられます。

ポリシーを動的に選択する

Polly ベースのハンドラーを追加するための拡張メソッドが提供されています (AddPolicyHandler など)。 次の AddPolicyHandler のオーバーロードでは、要求が検査されて、適用するポリシーが決定されます。

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

上記のコードでは、送信要求が HTTP GET の場合は、10 秒のタイムアウトが適用されます。 他の HTTP メソッドの場合は、30 秒のタイムアウトが使用されます。

複数の Polly ハンドラーを追加する

Polly のポリシーは入れ子にするのが一般的です。

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

前の例の場合:

  • 2 つのハンドラーが追加されます。
  • 1 つ目のハンドラーでは、AddTransientHttpErrorPolicy を使用して再試行ポリシーが追加されます。 失敗した要求は 3 回まで再試行されます。
  • 2 つ目の AddTransientHttpErrorPolicy の呼び出しでは、サーキット ブレーカー ポリシーが追加されます。 試行が連続して 5 回失敗した場合、それ以上の外部要求は 30 秒間ブロックされます。 サーキット ブレーカー ポリシーはステートフルです。 このクライアントからのすべての呼び出しは、同じサーキット状態を共有します。

Polly レジストリからポリシーを追加する

定期的に使用されるポリシーを管理するには、ポリシーを 1 回定義した後、PolicyRegistry でポリシーを登録します。

次のコードの内容は以下のとおりです。

  • "regular" ポリシーと "long" ポリシーが追加されます。
  • AddPolicyHandlerFromRegistry では、レジストリから "regular" ポリシーと "long" ポリシーが追加されます。
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

IHttpClientFactory と Polly の統合の詳細については、Polly に関する wiki を参照してください。

HttpClient と有効期間の管理

IHttpClientFactoryCreateClient を呼び出すたびに、HttpClient の新しいインスタンスが返されます。 HttpMessageHandler は、名前付きクライアントごとに作成されます。 ファクトリによって HttpMessageHandler インスタンスの有効期間が管理されます。

IHttpClientFactory は、リソースの消費量を減らすため、ファクトリによって作成された HttpMessageHandler のインスタンスをプールします。 新しい HttpClient インスタンスを作成するときに、プールの HttpMessageHandler インスタンスの有効期間が切れていない場合はそれを再利用する場合があります。

通常、各ハンドラーでは基になる HTTP 接続が独自に管理されるため、ハンドラーはプールすることが望まれます。 必要以上のハンドラーを作成すると、接続に遅延が発生する可能性があります。 また、一部のハンドラーでは接続が無期限に開かれており、DNS (ドメイン ネーム システム) の変更にハンドラーが対応できないことがあります。

ハンドラーの既定の有効期間は 2 分です。 名前付きクライアントごとに、既定値をオーバーライドすることができます。

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

通常、HttpClient インスタンスは、破棄する必要のない .NET オブジェクトとして扱うことができます。 破棄すると送信要求がキャンセルされ、Dispose の呼び出し後には指定された HttpClient インスタンスを使用できなくなります。 IHttpClientFactory は、HttpClient インスタンスによって使用されたリソースの追跡と破棄を行います。

IHttpClientFactory の登場以前は、1 つの HttpClient インスタンスを長い期間使い続けるパターンが一般的に使用されていました。 IHttpClientFactory に移行した後は、このパターンは不要になります。

IHttpClientFactory の代替手段

DI 対応のアプリ内で IHttpClientFactory を使用すれば、次のことを回避できます。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題。
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題。

有効期間の長い SocketsHttpHandler インスタンスを使用して、上記の問題を解決する別の方法があります。

  • アプリの起動時に SocketsHttpHandler インスタンスを作成し、アプリの有効期間中、それを使用します。
  • DNS の更新時間に基づいて、PooledConnectionLifetime を適切な値に構成します。
  • 必要に応じて new HttpClient(handler, disposeHandler: false) を使用して HttpClient インスタンスを作成します。

上記の方法を使用すると、IHttpClientFactory が同様の方法で解決するリソース管理の問題を解決できます。

  • SocketsHttpHandler を使用すると、HttpClient インスタンス間で接続を共有できます。 この共有によってソケットの枯渇が防止されます。
  • SocketsHttpHandler では、古くなった DNS の問題を回避するために PooledConnectionLifetime に従って接続を循環されます。

Cookies

HttpMessageHandler インスタンスをプールすると、CookieContainer オブジェクトが共有されます。 予期せぬ CookieContainer オブジェクト共有があると、多くの場合、コードは不適切なものとなります。 cookie を必要とするアプリの場合は、次のいずれかを検討してください。

  • 自動的な cookie 処理の無効化
  • IHttpClientFactory の回避

ConfigurePrimaryHttpMessageHandler を呼び出して、自動的な cookie 処理を無効にします。

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

ログの記録

IHttpClientFactory によって作成されたクライアントは、すべての要求のログ メッセージを記録します。 既定のログ メッセージを見るには、ログの構成で適切な情報レベルを有効にします。 要求ヘッダーのログなどの追加ログは、トレース レベルでのみ含まれます。

各クライアントに使用されるログのカテゴリには、クライアントの名前が含まれます。 たとえば、MyNamedClient という名前のクライアントでは、"System.Net.Http.HttpClient.MyNamedClient.LogicalHandler" というカテゴリでメッセージがログに記録されます。 LogicalHandler というサフィックスが付いたメッセージが、要求ハンドラーのパイプラインの外部で発生します。 要求では、パイプライン内の他のハンドラーが要求を処理する前に、メッセージがログに記録されます。 応答では、他のパイプライン ハンドラーが応答を受け取った後で、メッセージがログに記録されます。

ログ記録は、要求ハンドラーのパイプラインの内部でも行われます。 MyNamedClient の例では、これらのメッセージはログ カテゴリ "System.Net.Http.HttpClient.MyNamedClient.ClientHandler" でログに記録されます。 要求では、他のすべてのハンドラーが実行された後、要求が送信される直前に、これが行われます。 応答では、このログ記録には、ハンドラー パイプラインを通じ戻される前の応答の状態が含まれます。

パイプラインの外部と内部でログを有効にすると、他のパイプライン ハンドラーによる変更を検査できます。 これには、要求ヘッダーや応答状態コードへの変更を含めることができます。

ログのカテゴリにクライアントの名前を含めると、特定の名前付きクライアントでログをフィルター処理できます。

HttpMessageHandler を構成する

クライアントによって使用される内部 HttpMessageHandler の構成を制御することが必要な場合があります。

名前付きクライアントまたは型指定されたクライアントを追加すると、IHttpClientBuilder が返されます。 ConfigurePrimaryHttpMessageHandler 拡張メソッドを使用して、デリゲートを定義することができます。 デリゲートは、そのクライアントによって使用されるプライマリ HttpMessageHandler の作成と構成に使用されます。

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

コンソール アプリで IHttpClientFactory を使用する

コンソール アプリで、次のパッケージ参照をプロジェクトに追加します。

次に例を示します。

  • IHttpClientFactory汎用ホストのサービス コンテナーに登録されます。
  • MyService により、クライアント ファクトリ インスタンスがサービスから作成され、それが HttpClient の作成に使用されます。 HttpClient は Web ページを取得する目的で使用されます。
  • Main により、サービスの GetPage メソッドを実行し、Web ページ コンテンツの最初の 500 文字をコンソールに書き込むためのスコープが作成されます。
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ヘッダー伝達ミドルウェア

ヘッダー伝達は、受信要求から送信 HTTP クライアント要求に HTTP ヘッダーを伝達するための ASP.NET Core ミドルウェアです。 ヘッダー伝達を使用するには、次を行います。

  • Microsoft.AspNetCore.HeaderPropagation パッケージを参照します。

  • Startup でミドルウェアと HttpClient を構成します。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • クライアントで、構成されたヘッダーが送信要求に含まれます。

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

その他の技術情報

作成者: Kirk LarkinSteve GordonGlenn CondronRyan Nowak

アプリ内で HttpClient インスタンスを構成して作成するために、IHttpClientFactory を登録して使用できます。 IHttpClientFactory には次のような利点があります。

  • 論理 HttpClient インスタンスの名前付けと構成を一元化します。 たとえば、GitHub にアクセスするために、github という名前のクライアントを登録して構成できます。 一般的なアクセスのために、既定のクライアントを登録できます。
  • HttpClient でのハンドラーのデリゲートにより、送信ミドルウェアの概念が体系化されます。 HttpClient でのハンドラーのデリゲートを利用するために、Polly ベースのミドルウェアに対する拡張機能が提供されます。
  • 基になっている HttpClientMessageHandler インスタンスのプールと有効期間が管理されます。 自動管理により、HttpClient の有効期間を手動で管理するときの一般的な DNS (ドメイン ネーム システム) の問題が発生しなくなります。
  • ファクトリによって作成されたクライアントから送信されるすべての要求に対し、(ILogger によって) 構成可能なログ エクスペリエンスを追加します。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

このトピックのバージョンのサンプル コードでは、HTTP 応答で返された JSON コンテンツを、System.Text.Json を使用して逆シリアル化します。 Json.NET および ReadAsAsync<T> を使用するサンプルについては、バージョン セレクターを使用して、このトピックの 2.x バージョンを選択してください。

利用パターン

アプリで IHttpClientFactory を使用するには複数の方法があります。

どの方法が最善かは、アプリの要件によって異なります。

基本的な使用方法

IHttpClientFactory は、AddHttpClient を呼び出すことによって登録できます。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

IHttpClientFactory は、依存関係の挿入 (DI) を使用して要求できます。 次のコードでは、IHttpClientFactory を使用して HttpClient インスタンスを作成しています。

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

前の例のように IHttpClientFactory を使用するのは、既存のアプリをリファクタリングするのに適した方法です。 HttpClient の使用方法に対する影響はありません。 既存のアプリで HttpClient のインスタンスが作成されている場所を、CreateClient の呼び出しに置き換えます。

名前付きクライアント

名前付きクライアントは、次の場合に適しています。

  • アプリで、多くの異なる HttpClient を使用する必要がある。
  • 多くの HttpClient の構成が異なる。

名前付き HttpClient の構成は、Startup.ConfigureServices での登録時に指定できます。

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

上記のコードでは、クライアントに次のものが構成されます。

  • ベース アドレス https://api.github.com/
  • GitHub API を使用するために必要な 2 つのヘッダー。

CreateClient

CreateClient が呼び出されるたびに、次のことが行われます。

  • HttpClient の新しいインスタンスが作成されます。
  • 構成アクションが呼び出されます。

名前付きクライアントを作成するには、その名前を CreateClient に渡します。

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

上記のコードでは、要求でホスト名を指定する必要はありません。 クライアントに構成されているベース アドレスが使用されるため、コードではパスを渡すだけで済みます。

型指定されたクライアント

型指定されたクライアント:

  • キーとして文字列を使用する必要なしに、名前付きクライアントと同じ機能を提供します。
  • クライアントを使用するときに、IntelliSense とコンパイラのヘルプが提供されます。
  • 特定の HttpClient を構成してそれと対話する 1 つの場所を提供します。 たとえば、単一の型指定されたクライアントは、次のために使用される場合があります。
    • 単一のバックエンド エンドポイント用。
    • エンドポイントを処理するすべてのロジックをカプセル化するため。
  • DI に対応しており、アプリ内の必要な場所に挿入できます。

型指定されたクライアントは、そのコンストラクターで HttpClient パラメーターを受け取ります。

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

コードのコメントを英語以外の言語に翻訳し表示したい場合、こちらの GitHub ディスカッション イシューにてお知らせください。

上のコードでは以下の操作が行われます。

  • 構成が型指定されたクライアントに移動されます。
  • HttpClient オブジェクトは、パブリック プロパティとして公開されます。

HttpClient の機能を公開する API 固有のメソッドを作成できます。 たとえば、GetAspNetDocsIssues メソッドでは、未解決の問題を取得するためのコードがカプセル化されています。

次のコードでは、Startup.ConfigureServices 内で AddHttpClient を呼び出して、型指定されたクライアント クラスを登録しています。

services.AddHttpClient<GitHubService>();

型指定されたクライアントは、DI で一時的として登録されます。 上記のコードで、AddHttpClientGitHubService を一時的なサービスとして登録します。 この登録では、ファクトリ メソッドを使用して次のことを行います。

  1. HttpClient のインスタンスを作成します。
  2. GitHubService のインスタンスを作成し、HttpClient のインスタンスをそのコンストラクターに渡します。

型指定されたクライアントは、直接挿入して使用できます。

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

型指定されたクライアントのコンストラクターではなく、Startup.ConfigureServices での登録時に型指定されたクライアントの構成を指定できます。

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient は、型指定されたクライアント内にカプセル化できます。 プロパティとして公開するのではなく、HttpClient インスタンスを内部的に呼び出すパブリック メソッドを定義します。

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

上記のコードでは、HttpClient はプライベート フィールドに格納されます。 HttpClient へのアクセスは、パブリック GetRepos メソッドによって行われます。

生成されたクライアント

IHttpClientFactory は、Refit などのサードパーティ製ライブラリと組み合わせて使用できます。 Refit は、.NET 用の REST ライブラリです。 REST API をライブ インターフェイスに変換します。 インターフェイスの実装は RestService によって動的に生成され、HttpClient を使用して外部 HTTP の呼び出しを行います。

インターフェイスと応答は、外部の API とその応答を表すように定義されます。

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

型指定されたクライアントを追加し、Refit を使用して実装を生成できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

定義されたインターフェイスは、DI および Refit によって提供される実装で必要に応じて使用できます。

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

POST、PUT、DELETE の要求を行う

前の例では、すべての HTTP 要求で GET HTTP 動詞が使用されています。 HttpClient では、次のような他の HTTP 動詞もサポートされています。

  • POST
  • PUT
  • Del
  • PATCH

サポートされている HTTP 動詞の一覧については、「HttpMethod」を参照してください。

次の例は、HTTP POST 要求の方法を示しています。

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードの CreateItemAsync メソッドは:

  • System.Text.Json を使用して TodoItem パラメーターを JSON にシリアル化します。 これは、JsonSerializerOptions のインスタンスを使用して、シリアル化プロセスを構成します。
  • HTTP 要求の本文で送信するためにシリアル化された JSON をパッケージ化する StringContent のインスタンスを作成します。
  • PostAsync を呼び出し、指定した URL に JSON コンテンツを送信します。 これは HttpClient.BaseAddress に追加される相対 URL です。
  • 応答状態コードが成功を示していない場合に、EnsureSuccessStatusCode を呼び出して例外をスローします。

HttpClient は、他の種類のコンテンツもサポートしています。 たとえば、MultipartContentStreamContent です。 サポートされているコンテンツの一覧については、「HttpContent」を参照してください。

HTTP PUT 要求の例を次に示します。

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードは、POST の例によく似ています。 SaveItemAsync メソッドは PostAsync ではなく PutAsync を呼び出します。

HTTP DELETE 要求の例を次に示します。

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

上記のコードの DeleteItemAsync メソッドは DeleteAsync を呼び出します。 HTTP DELETE 要求には通常、本文が含まれていないため、DeleteAsync メソッドは HttpContent のインスタンスを受け入れるオーバーロードを提供しません。

HttpClient でのさまざまな HTTP 動詞の使用の詳細については、「HttpClient」を参照してください。

送信要求ミドルウェア

HttpClient は、送信 HTTP 要求用にリンクできるハンドラーのデリゲートの概念を備えています。 IHttpClientFactory:

  • 各名前付きクライアントに適用するハンドラーの定義が簡単になります。
  • 送信要求ミドルウェア パイプラインを構築するための複数のハンドラーの登録とチェーン化がサポートされています。 各ハンドラーは、送信要求の前と後に処理を実行できます。 このパターンは次のようなものです。
    • ASP.NET Core での受信ミドルウェア パイプラインに似ています。
    • 次のような HTTP 要求に関する横断的な問題を管理するためのメカニズムが提供されます。
      • キャッシュ
      • エラー処理
      • シリアル化
      • ログ

デリゲート ハンドラーを作成するには、次のようにします。

  • DelegatingHandler から派生します。
  • SendAsync をオーバーライドします。 パイプライン内の次のハンドラーに要求を渡す前に、コードを実行します。
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

上記のコードでは、X-API-KEY ヘッダーが要求に含まれているかどうかが確認されます。 X-API-KEY がない場合は、BadRequest が返されます。

Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler を使用して HttpClient の構成に複数のハンドラーを追加できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

上記のコードでは、ValidateHeaderHandler が DI で登録されます。 登録が済むと、AddHttpMessageHandler を呼び出してハンドラーの型を渡すことができます。

実行する順序で、複数のハンドラーを登録することができます。 最後の HttpClientHandler が要求を実行するまで、各ハンドラーは次のハンドラーをラップします。

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

送信要求ミドルウェアで DI を使用する

IHttpClientFactory によって新しいデリゲート ハンドラーが作成される際、ハンドラーのコンストラクター パラメーターを満たすために DI が使用されます。 IHttpClientFactory により、ハンドラーごとに個別の DI スコープが作成されます。これにより、ハンドラーによって "スコープ付き" サービスが使用されるときに、予期しない動作が発生する可能性があります。

たとえば、識別子 OperationId を持つ操作としてタスクを表す、次のインターフェイスとその実装について考えてみます。

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

その名前が示すように、IOperationScoped は "スコープ付き" 有効期間を使用して DI に登録されます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

次のデリゲート ハンドラーでは、IOperationScoped を使用して、送信要求用の X-OPERATION-ID ヘッダーを設定します。

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

HttpRequestsSample ダウンロード] で、/Operation に移動してページを更新します。 要求スコープの値は要求ごとに変更されますが、ハンドラーのスコープの値は 5 秒ごとに変更されるだけです。

ハンドラーは、任意のスコープのサービスに依存することができます。 ハンドラーが依存するサービスは、ハンドラーが破棄されるときに破棄されます。

次の方法のいずれかを使って、要求ごとの状態をメッセージ ハンドラーと共有します。

Polly ベースのハンドラーを使用する

IHttpClientFactory は、サードパーティのライブラリ Polly と統合します。 Polly は、.NET 用の包括的な回復力および一時的エラー処理ライブラリです。 開発者は、自然でスレッド セーフな方法を使用して、Retry、Circuit Breaker、Timeout、Bulkhead Isolation、Fallback などのポリシーを表現できます。

構成されている HttpClient インスタンスで Polly ポリシーを使用できるようにするための、拡張メソッドが提供されています。 Polly の拡張機能では、クライアントへの Polly ベースのハンドラーの追加がサポートされています。 Polly では、Microsoft.Extensions.Http.Polly NuGet パッケージが必要です。

一時的な障害を処理する

通常、外部 HTTP を呼び出すときに発生する障害は、一時的なものです。 AddTransientHttpErrorPolicy では、一時的なエラーを処理するためのポリシーを定義できます。 AddTransientHttpErrorPolicy で構成されたポリシーでは、次の応答が処理されます。

AddTransientHttpErrorPolicy では、可能性のある一時的障害を表すエラーを処理するために構成された PolicyBuilder オブジェクトへのアクセスが提供されます。

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

上記のコードでは、WaitAndRetryAsync ポリシーが定義されています。 失敗した要求は最大 3 回再試行され、再試行の間には 600 ミリ秒の遅延が設けられます。

ポリシーを動的に選択する

Polly ベースのハンドラーを追加するための拡張メソッドが提供されています (AddPolicyHandler など)。 次の AddPolicyHandler のオーバーロードでは、要求が検査されて、適用するポリシーが決定されます。

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

上記のコードでは、送信要求が HTTP GET の場合は、10 秒のタイムアウトが適用されます。 他の HTTP メソッドの場合は、30 秒のタイムアウトが使用されます。

複数の Polly ハンドラーを追加する

Polly のポリシーは入れ子にするのが一般的です。

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

前の例の場合:

  • 2 つのハンドラーが追加されます。
  • 1 つ目のハンドラーでは、AddTransientHttpErrorPolicy を使用して再試行ポリシーが追加されます。 失敗した要求は 3 回まで再試行されます。
  • 2 つ目の AddTransientHttpErrorPolicy の呼び出しでは、サーキット ブレーカー ポリシーが追加されます。 試行が連続して 5 回失敗した場合、それ以上の外部要求は 30 秒間ブロックされます。 サーキット ブレーカー ポリシーはステートフルです。 このクライアントからのすべての呼び出しは、同じサーキット状態を共有します。

Polly レジストリからポリシーを追加する

定期的に使用されるポリシーを管理するには、ポリシーを 1 回定義した後、PolicyRegistry でポリシーを登録します。

次のコードの内容は以下のとおりです。

  • "regular" ポリシーと "long" ポリシーが追加されます。
  • AddPolicyHandlerFromRegistry では、レジストリから "regular" ポリシーと "long" ポリシーが追加されます。
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

IHttpClientFactory と Polly の統合の詳細については、Polly に関する wiki を参照してください。

HttpClient と有効期間の管理

IHttpClientFactoryCreateClient を呼び出すたびに、HttpClient の新しいインスタンスが返されます。 HttpMessageHandler は、名前付きクライアントごとに作成されます。 ファクトリによって HttpMessageHandler インスタンスの有効期間が管理されます。

IHttpClientFactory は、リソースの消費量を減らすため、ファクトリによって作成された HttpMessageHandler のインスタンスをプールします。 新しい HttpClient インスタンスを作成するときに、プールの HttpMessageHandler インスタンスの有効期間が切れていない場合はそれを再利用する場合があります。

通常、各ハンドラーでは基になる HTTP 接続が独自に管理されるため、ハンドラーはプールすることが望まれます。 必要以上のハンドラーを作成すると、接続に遅延が発生する可能性があります。 また、一部のハンドラーでは接続が無期限に開かれており、DNS (ドメイン ネーム システム) の変更にハンドラーが対応できないことがあります。

ハンドラーの既定の有効期間は 2 分です。 名前付きクライアントごとに、既定値をオーバーライドすることができます。

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

通常、HttpClient インスタンスは、破棄する必要のない .NET オブジェクトとして扱うことができます。 破棄すると送信要求がキャンセルされ、Dispose の呼び出し後には指定された HttpClient インスタンスを使用できなくなります。 IHttpClientFactory は、HttpClient インスタンスによって使用されたリソースの追跡と破棄を行います。

IHttpClientFactory の登場以前は、1 つの HttpClient インスタンスを長い期間使い続けるパターンが一般的に使用されていました。 IHttpClientFactory に移行した後は、このパターンは不要になります。

IHttpClientFactory の代替手段

DI 対応のアプリ内で IHttpClientFactory を使用すれば、次のことを回避できます。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題。
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題。

有効期間の長い SocketsHttpHandler インスタンスを使用して、上記の問題を解決する別の方法があります。

  • アプリの起動時に SocketsHttpHandler インスタンスを作成し、アプリの有効期間中、それを使用します。
  • DNS の更新時間に基づいて、PooledConnectionLifetime を適切な値に構成します。
  • 必要に応じて new HttpClient(handler, disposeHandler: false) を使用して HttpClient インスタンスを作成します。

上記の方法を使用すると、IHttpClientFactory が同様の方法で解決するリソース管理の問題を解決できます。

  • SocketsHttpHandler を使用すると、HttpClient インスタンス間で接続を共有できます。 この共有によってソケットの枯渇が防止されます。
  • SocketsHttpHandler では、古くなった DNS の問題を回避するために PooledConnectionLifetime に従って接続を循環されます。

Cookies

HttpMessageHandler インスタンスをプールすると、CookieContainer オブジェクトが共有されます。 予期せぬ CookieContainer オブジェクト共有があると、多くの場合、コードは不適切なものとなります。 cookie を必要とするアプリの場合は、次のいずれかを検討してください。

  • 自動的な cookie 処理の無効化
  • IHttpClientFactory の回避

ConfigurePrimaryHttpMessageHandler を呼び出して、自動的な cookie 処理を無効にします。

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

ログの記録

IHttpClientFactory によって作成されたクライアントは、すべての要求のログ メッセージを記録します。 既定のログ メッセージを見るには、ログの構成で適切な情報レベルを有効にします。 要求ヘッダーのログなどの追加ログは、トレース レベルでのみ含まれます。

各クライアントに使用されるログのカテゴリには、クライアントの名前が含まれます。 たとえば、MyNamedClient という名前のクライアントでは、"System.Net.Http.HttpClient.MyNamedClient.LogicalHandler" というカテゴリでメッセージがログに記録されます。 LogicalHandler というサフィックスが付いたメッセージが、要求ハンドラーのパイプラインの外部で発生します。 要求では、パイプライン内の他のハンドラーが要求を処理する前に、メッセージがログに記録されます。 応答では、他のパイプライン ハンドラーが応答を受け取った後で、メッセージがログに記録されます。

ログ記録は、要求ハンドラーのパイプラインの内部でも行われます。 MyNamedClient の例では、これらのメッセージはログ カテゴリ "System.Net.Http.HttpClient.MyNamedClient.ClientHandler" でログに記録されます。 要求では、他のすべてのハンドラーが実行された後、要求が送信される直前に、これが行われます。 応答では、このログ記録には、ハンドラー パイプラインを通じ戻される前の応答の状態が含まれます。

パイプラインの外部と内部でログを有効にすると、他のパイプライン ハンドラーによる変更を検査できます。 これには、要求ヘッダーや応答状態コードへの変更を含めることができます。

ログのカテゴリにクライアントの名前を含めると、特定の名前付きクライアントでログをフィルター処理できます。

HttpMessageHandler を構成する

クライアントによって使用される内部 HttpMessageHandler の構成を制御することが必要な場合があります。

名前付きクライアントまたは型指定されたクライアントを追加すると、IHttpClientBuilder が返されます。 ConfigurePrimaryHttpMessageHandler 拡張メソッドを使用して、デリゲートを定義することができます。 デリゲートは、そのクライアントによって使用されるプライマリ HttpMessageHandler の作成と構成に使用されます。

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

コンソール アプリで IHttpClientFactory を使用する

コンソール アプリで、次のパッケージ参照をプロジェクトに追加します。

次に例を示します。

  • IHttpClientFactory汎用ホストのサービス コンテナーに登録されます。
  • MyService により、クライアント ファクトリ インスタンスがサービスから作成され、それが HttpClient の作成に使用されます。 HttpClient は Web ページを取得する目的で使用されます。
  • Main により、サービスの GetPage メソッドを実行し、Web ページ コンテンツの最初の 500 文字をコンソールに書き込むためのスコープが作成されます。
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ヘッダー伝達ミドルウェア

ヘッダー伝達は、受信要求から送信 HTTP クライアント要求に HTTP ヘッダーを伝達するための ASP.NET Core ミドルウェアです。 ヘッダー伝達を使用するには、次を行います。

  • Microsoft.AspNetCore.HeaderPropagation パッケージを参照します。

  • Startup でミドルウェアと HttpClient を構成します。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • クライアントで、構成されたヘッダーが送信要求に含まれます。

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

その他の技術情報

寄稿者: Glenn CondronRyan NowakSteve Gordon

アプリ内で HttpClient インスタンスを構成して作成するために、IHttpClientFactory を登録して使用できます。 次のような利点があります。

  • 論理 HttpClient インスタンスの名前付けと構成を一元化します。 たとえば、github クライアントを登録して、GitHub にアクセスするように構成できます。 既定のクライアントは、他の目的に登録できます。
  • HttpClient でのハンドラーのデリゲートにより送信ミドルウェアの概念を体系化し、Polly ベースのミドルウェアでそれを利用するための拡張機能を提供します。
  • 基になっている HttpClientMessageHandler インスタンスのプールと有効期間を管理し、HttpClient の有効期間を手動で管理するときに発生する一般的な DNS の問題を防ぎます。
  • ファクトリによって作成されたクライアントから送信されるすべての要求に対し、(ILogger によって) 構成可能なログ エクスペリエンスを追加します。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

必須コンポーネント

.NET Framework をターゲットとするプロジェクトでは、Microsoft.Extensions.Http NuGet パッケージのインストールが必要です。 .NET Core をターゲットとし、Microsoft.AspNetCore.App メタパッケージを参照するプロジェクトには、既に Microsoft.Extensions.Http パッケージが含まれています。

利用パターン

アプリで IHttpClientFactory を使用するには複数の方法があります。

これらの間に厳密な優劣はありません。 どの方法が最善かは、アプリの制約に依存します。

基本的な使用方法

IHttpClientFactory は、Startup.ConfigureServices メソッドの内部で IServiceCollectionAddHttpClient 拡張メソッドを呼び出すことによって登録できます。

services.AddHttpClient();

登録が済むと、コードは、依存関係の挿入 (DI) を使用してサービスを挿入できる任意の場所で、IHttpClientFactory を受け取ることができます。 IHttpClientFactory を使用して、HttpClient インスタンスを作成できます。

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

このように IHttpClientFactory を使用するのは、既存のアプリをリファクタリングする優れた方法です。 HttpClient の使用方法に影響はありません。 現在 HttpClient インスタンスが作成されている場所で、それを CreateClient の呼び出しに置き換えます。

名前付きクライアント

それぞれ構成が異なる多数の HttpClient をアプリで個別に使用する必要がある場合は、名前付きクライアントを使用することができます。 名前付き HttpClient の構成は、Startup.ConfigureServices での登録時に指定できます。

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

上記のコードでは、github という名前を指定して AddHttpClient を呼び出しています。 このクライアントには既定の構成がいくつか適用されています。つまり、GitHub API を使用するために必要なベース アドレスと 2 つのヘッダーです。

CreateClient を呼び出すたびに、HttpClient の新しいインスタンスが作成されて、構成アクションが呼び出されます。

名前付きクライアントを使用するには、文字列パラメーターを CreateClient に渡すことができます。 作成するクライアントの名前を指定します。

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

上記のコードでは、要求でホスト名を指定する必要はありません。 クライアントに構成されているベース アドレスが使用されるため、パスだけを渡すことができます。

型指定されたクライアント

型指定されたクライアント:

  • キーとして文字列を使用する必要なしに、名前付きクライアントと同じ機能を提供します。
  • クライアントを使用するときに、IntelliSense とコンパイラのヘルプが提供されます。
  • 特定の HttpClient を構成してそれと対話する 1 つの場所を提供します。 たとえば、単一の型指定されたクライアントを単一のバックエンド エンドポイントに対して使用し、そのエンドポイントを操作するすべてのロジックをカプセル化できます。
  • DI に対応しており、アプリ内の必要な場所に挿入できます。

型指定されたクライアントは、そのコンストラクターで HttpClient パラメーターを受け取ります。

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

上記のコードでは、構成が型指定されたクライアントに移動されています。 HttpClient オブジェクトは、パブリック プロパティとして公開されます。 HttpClient 機能を公開する API 固有のメソッドを定義することができます。 GetAspNetDocsIssues メソッドは、GitHub リポジトリで最新の未解決の問題をクエリして解析するために必要なコードをカプセル化します。

型指定されたクライアントを登録するには、ジェネリック AddHttpClient 拡張メソッドを Startup.ConfigureServices 内で使用して、型指定されたクライアントのクラスを指定します。

services.AddHttpClient<GitHubService>();

型指定されたクライアントは、DI で一時的として登録されます。 型指定されたクライアントは、直接挿入して使用できます。

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

好みに応じて、型指定されたクライアントのコンストラクターではなく、Startup.ConfigureServices での登録時に型指定されたクライアントの構成を指定できます。

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

型指定されたクライアントの内部に HttpClient を完全にカプセル化することができます。 プロパティとして公開するのではなく、HttpClient インスタンスを内部的に呼び出すパブリック メソッドとして提供することができます。

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

上記のコードでは、HttpClient はプライベート フィールドとして格納されます。 外部呼び出しを行うすべてのアクセスは、GetRepos メソッドを通して行われます。

生成されたクライアント

IHttpClientFactory は、Refit などの他のサードパーティ製ライブラリと組み合わせて使用できます。 Refit は、.NET 用の REST ライブラリです。 REST API をライブ インターフェイスに変換します。 インターフェイスの実装は RestService によって動的に生成され、HttpClient を使用して外部 HTTP の呼び出しを行います。

インターフェイスと応答は、外部の API とその応答を表すように定義されます。

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

型指定されたクライアントを追加し、Refit を使用して実装を生成できます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}

定義されたインターフェイスは、DI および Refit によって提供される実装で必要に応じて使用できます。

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

送信要求ミドルウェア

HttpClient は既に、送信 HTTP 要求用にリンクできるハンドラーのデリゲートの概念を備えています。 IHttpClientFactory を使用すると、各名前付きクライアントに適用するハンドラーを簡単に定義できます。 複数のハンドラーを登録してチェーン化し、送信要求ミドルウェア パイプラインを構築できます。 各ハンドラーは、送信要求の前と後に処理を実行できます。 このパターンは、ASP.NET Core での受信ミドルウェア パイプラインに似ています。 このパターンは、キャッシュ、エラー処理、シリアル化、ログ記録など、HTTP 要求に関する横断的関心事を管理するためのメカニズムを提供します。

ハンドラーを作成するには、DelegatingHandler の派生クラスを定義します。 パイプライン内の次のハンドラーに要求を渡す前にコードを実行するように、SendAsync メソッドをオーバーライドします。

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

上記のコードでは、基本的なハンドラーが定義されています。 このコードは、X-API-KEY ヘッダーが要求に含まれていたかどうかを確認します。 ヘッダーがない場合、HTTP 呼び出しを行わずに適切な応答を返すことができます。

登録時には、1 つまたは複数のハンドラーを HttpClient の構成に追加することができます。 このタスクは、IHttpClientBuilder の拡張メソッドを使用して実行されます。

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

上記のコードでは、ValidateHeaderHandler が DI で登録されます。 ハンドラーは、スコープではなく、一時的なサービスとして DI で登録する必要があります。 ハンドラーがスコープ付きサービスとして登録されており、ハンドラーが依存するサービスを破棄できる場合:

  • ハンドラーのサービスは、ハンドラーがスコープ外に出る前に破棄されることがあります。
  • 破棄されたハンドラー サービスが原因でハンドラーが失敗します。

登録が済むと、AddHttpMessageHandler を呼び出してハンドラーの型を渡すことができます。

実行する順序で、複数のハンドラーを登録することができます。 最後の HttpClientHandler が要求を実行するまで、各ハンドラーは次のハンドラーをラップします。

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

次の方法のいずれかを使って、要求ごとの状態をメッセージ ハンドラーと共有します。

  • HttpRequestMessage.Properties を使ってデータをハンドラーに渡します。
  • IHttpContextAccessor を使って現在の要求にアクセスします。
  • データを渡すカスタムの AsyncLocal ストレージ オブジェクトを作成します。

Polly ベースのハンドラーを使用する

IHttpClientFactory は、Polly という名前の人気のあるサードパーティ製ライブラリと統合します。 Polly は、.NET 用の包括的な回復力および一時的エラー処理ライブラリです。 開発者は、自然でスレッド セーフな方法を使用して、Retry、Circuit Breaker、Timeout、Bulkhead Isolation、Fallback などのポリシーを表現できます。

構成されている HttpClient インスタンスで Polly ポリシーを使用できるようにするための、拡張メソッドが提供されています。 Polly の拡張機能は、次のようになっています。

  • クライアントへの Polly ベースのハンドラーの追加がサポートされます。
  • Microsoft.Extensions.Http.Polly NuGet パッケージのインストール後に使用できます。 このパッケージは、ASP.NET Core 共有フレームワークに含まれていません。

一時的な障害を処理する

外部 HTTP を呼び出す際に発生する一般的な障害のほとんどは、一時的なものです。 AddTransientHttpErrorPolicy という便利な拡張メソッドが含まれており、一時的なエラーを処理するためのポリシーを定義できます。 この拡張メソッドで構成されたポリシーは、HttpRequestException、HTTP 5xx 応答、および HTTP 408 応答を処理します。

AddTransientHttpErrorPolicy 拡張メソッドは、Startup.ConfigureServices 内で使用できます。 この拡張メソッドは、可能性のある一時的障害を表すエラーを処理するように構成された PolicyBuilder オブジェクトへのアクセスを提供します。

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

上記のコードでは、WaitAndRetryAsync ポリシーが定義されています。 失敗した要求は最大 3 回再試行され、再試行の間には 600 ミリ秒の遅延が設けられます。

ポリシーを動的に選択する

Polly ベースのハンドラーを追加するために使用できる追加の拡張メソッドが存在します。 そのような拡張メソッドの 1 つは AddPolicyHandler であり、複数のオーバーロードを持ちます。 1 つのオーバーロードでは、適用するポリシーを定義するときに要求を検査できます。

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

上記のコードでは、送信要求が HTTP GET の場合は、10 秒のタイムアウトが適用されます。 他の HTTP メソッドの場合は、30 秒のタイムアウトが使用されます。

複数の Polly ハンドラーを追加する

Polly ポリシーを入れ子にして拡張機能を提供するのが一般的です。

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

前の例では、2 つのハンドラーが追加されています。 最初のハンドラーは、AddTransientHttpErrorPolicy 拡張メソッドを使用して再試行ポリシーを追加します。 失敗した要求は 3 回まで再試行されます。 AddTransientHttpErrorPolicy の 2 番目の呼び出しでは、サーキット ブレーカー ポリシーが追加されています。 試行が連続して 5 回失敗した場合、それ以上の外部要求は 30 秒間ブロックされます。 サーキット ブレーカー ポリシーはステートフルです。 このクライアントからのすべての呼び出しは、同じサーキット状態を共有します。

Polly レジストリからポリシーを追加する

定期的に使用されるポリシーを管理するには、ポリシーを 1 回定義した後、PolicyRegistry でポリシーを登録します。 提供されている拡張メソッドを使用することで、レジストリからポリシーを使用してハンドラーを追加できます。

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

上記のコードでは、PolicyRegistryServiceCollection に追加されるときに 2 つのポリシーが登録されています。 レジストリからポリシーを使用するには、適用するポリシーの名前を渡して AddPolicyHandlerFromRegistry メソッドを使用します。

IHttpClientFactory および Polly の統合について詳しくは、Polly の Wiki をご覧ください。

HttpClient と有効期間の管理

IHttpClientFactoryCreateClient を呼び出すたびに、HttpClient の新しいインスタンスが返されます。 名前付きクライアントごとに HttpMessageHandler が存在します。 ファクトリによって HttpMessageHandler インスタンスの有効期間が管理されます。

IHttpClientFactory は、リソースの消費量を減らすため、ファクトリによって作成された HttpMessageHandler のインスタンスをプールします。 新しい HttpClient インスタンスを作成するときに、プールの HttpMessageHandler インスタンスの有効期間が切れていない場合はそれを再利用する場合があります。

通常、各ハンドラーでは基になる HTTP 接続が独自に管理されるため、ハンドラーはプールすることが望まれます。 必要以上のハンドラーを作成すると、接続に遅延が発生する可能性があります。 また、一部のハンドラーは接続を無期限に開いており、DNS の変更にハンドラーが対応できないことがあります。

ハンドラーの既定の有効期間は 2 分です。 名前付きクライアントごとに、既定値をオーバーライドすることができます。 オーバーライドするには、クライアント作成時に返された IHttpClientBuilderSetHandlerLifetime を呼び出します。

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

クライアントを破棄する必要はありません。 破棄すると送信要求がキャンセルされ、Dispose の呼び出し後には指定された HttpClient インスタンスを使用できなくなります。 IHttpClientFactory は、HttpClient インスタンスによって使用されたリソースの追跡と破棄を行います。 通常、HttpClient インスタンスは、破棄を必要としない .NET オブジェクトとして扱うことができます。

IHttpClientFactory の登場以前は、1 つの HttpClient インスタンスを長い期間使い続けるパターンが一般的に使用されていました。 IHttpClientFactory に移行した後は、このパターンは不要になります。

IHttpClientFactory の代替手段

DI 対応のアプリ内で IHttpClientFactory を使用すれば、次のことを回避できます。

  • HttpMessageHandler インスタンスをプールすることによるリソース枯渇の問題。
  • 一定の間隔で HttpMessageHandler インスタンスを循環させることによって発生する古くなった DNS の問題。

有効期間の長い SocketsHttpHandler インスタンスを使用して、上記の問題を解決する別の方法があります。

  • アプリの起動時に SocketsHttpHandler インスタンスを作成し、アプリの有効期間中、それを使用します。
  • DNS の更新時間に基づいて、PooledConnectionLifetime を適切な値に構成します。
  • 必要に応じて new HttpClient(handler, disposeHandler: false) を使用して HttpClient インスタンスを作成します。

上記の方法を使用すると、IHttpClientFactory が同様の方法で解決するリソース管理の問題を解決できます。

  • SocketsHttpHandler を使用すると、HttpClient インスタンス間で接続を共有できます。 この共有によってソケットの枯渇が防止されます。
  • SocketsHttpHandler では、古くなった DNS の問題を回避するために PooledConnectionLifetime に従って接続を循環されます。

Cookies

HttpMessageHandler インスタンスをプールすると、CookieContainer オブジェクトが共有されます。 予期せぬ CookieContainer オブジェクト共有があると、多くの場合、コードは不適切なものとなります。 cookie を必要とするアプリの場合は、次のいずれかを検討してください。

  • 自動的な cookie 処理の無効化
  • IHttpClientFactory の回避

ConfigurePrimaryHttpMessageHandler を呼び出して、自動的な cookie 処理を無効にします。

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

ログの記録

IHttpClientFactory によって作成されたクライアントは、すべての要求のログ メッセージを記録します。 既定のログ メッセージを見るには、ログの構成で適切な情報レベルを有効にします。 要求ヘッダーのログなどの追加ログは、トレース レベルでのみ含まれます。

各クライアントに使用されるログのカテゴリには、クライアントの名前が含まれます。 たとえば、MyNamedClient という名前のクライアントは、カテゴリ System.Net.Http.HttpClient.MyNamedClient.LogicalHandler でメッセージをログに記録します。 LogicalHandler というサフィックスが付いたメッセージが、要求ハンドラーのパイプラインの外部で発生します。 要求では、パイプライン内の他のハンドラーが要求を処理する前に、メッセージがログに記録されます。 応答では、他のパイプライン ハンドラーが応答を受け取った後で、メッセージがログに記録されます。

ログ記録は、要求ハンドラーのパイプラインの内部でも行われます。 MyNamedClient の例では、それらのメッセージはログ カテゴリ System.Net.Http.HttpClient.MyNamedClient.ClientHandler に対して記録されます。 要求では、他のすべてのハンドラーが実行した後、要求がネットワークに送信される直前に、これが行われます。 応答では、このログ記録には、ハンドラー パイプラインを通じ戻される前の応答の状態が含まれます。

パイプラインの外部と内部でログを有効にすると、他のパイプライン ハンドラーによる変更を検査できます。 これには、たとえば、要求ヘッダーや応答状態コードへの変更を含めることができます。

ログのカテゴリにクライアントの名前を含めると、必要に応じて、特定の名前付きクライアントでログをフィルター処理できます。

HttpMessageHandler を構成する

クライアントによって使用される内部 HttpMessageHandler の構成を制御することが必要な場合があります。

名前付きクライアントまたは型指定されたクライアントを追加すると、IHttpClientBuilder が返されます。 ConfigurePrimaryHttpMessageHandler 拡張メソッドを使用して、デリゲートを定義することができます。 デリゲートは、そのクライアントによって使用されるプライマリ HttpMessageHandler の作成と構成に使用されます。

services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

コンソール アプリで IHttpClientFactory を使用する

コンソール アプリで、次のパッケージ参照をプロジェクトに追加します。

次に例を示します。

  • IHttpClientFactory汎用ホストのサービス コンテナーに登録されます。
  • MyService により、クライアント ファクトリ インスタンスがサービスから作成され、それが HttpClient の作成に使用されます。 HttpClient は Web ページを取得する目的で使用されます。
  • サービスの GetPage メソッドが実行されて、Web ページ コンテンツの最初の 500 文字をコンソールに書き込みます。 Program.Main からサービスを呼び出す方法について詳しくは、「ASP.NET Core での依存関係の挿入」を参照してください。
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

ヘッダー伝達ミドルウェア

ヘッダー伝達は、受信要求から送信 HTTP クライアント要求に HTTP ヘッダーを伝達するためのコミュニティでサポートされたミドルウェアです。 ヘッダー伝達を使用するには、次を行います。

  • パッケージ HeaderPropagation のコミュニティでサポートされているポートを参照します。 ASP.NET Core 3.1 以降では、Microsoft.AspNetCore.HeaderPropagation がサポートされています。

  • Startup でミドルウェアと HttpClient を構成します。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • クライアントで、構成されたヘッダーが送信要求に含まれます。

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

その他の技術情報