IHttpClientFactory と .NET

この記事では、IHttpClientFactory インターフェイスを使用して HttpClient 型を作成する方法と、依存関係の挿入 (DI)、ログ記録、構成などのさまざまな .NET の基礎について説明します。 HttpClient 型は、2012 年にリリースされた .NET Framework 4.5 で導入されました。 つまり、使われるようになってしばらく経ちます。 HttpClient は、HTTP 要求を行い、Uri によって識別される Web リソースからの HTTP 応答を処理するために使用されます。 HTTP プロトコルによって、すべてのインターネット トラフィックの大部分が構成されます。

ベスト プラクティスを推進する最新のアプリケーション開発原則に従うことにより、IHttpClientFactory は、カスタム構成で HttpClient インスタンスを作成できるファクトリ抽象化として機能します。 IHttpClientFactory は .NET Core 2.1 で導入されました。 一般的な HTTP ベースの .NET ワークロードでは、回復性のある、一時的な障害処理用のサードパーティ ミドルウェアを簡単に利用できます。

Note

アプリに Cookie が必要な場合は、アプリで IHttpClientFactory を使用しない方がよい場合があります。 クライアントを管理する別の方法については、「HTTP クライアントの使用に関するガイドライン」を参照してください。

重要

IHttpClientFactory によって作成された HttpClient インスタンスの有効期間の管理は、手動で作成されたインスタンスとは完全に異なります。 その方法は、IHttpClientFactory によって作成された有効期間の短いクライアントを使うか、PooledConnectionLifetime が設定された有効期間の長いクライアントを使うことです。 詳細については、「HttpClient の有効期間の管理」セクションと「HTTP クライアントの使用に関するガイドライン」を参照してください。

IHttpClientFactory

この記事で提供されているすべてのサンプル ソース コードで Microsoft.Extensions.Http NuGet パッケージのインストールが必要です。 さらに、コード サンプルでは、無料の {JSON} Placeholder API からユーザー Todo オブジェクトを取得するための HTTP GET 要求の使用方法を紹介しています。

AddHttpClient 拡張メソッドのいずれかを呼び出すと、IHttpClientFactory と関連サービスが IServiceCollection に追加されます。 IHttpClientFactory 型には次のような利点があります。

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

利用パターン

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

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

基本的な使用方法

IHttpClientFactory を登録するには、AddHttpClient を呼び出します。

using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

サービスを利用する場合、DI を使用してコンストラクター パラメーターとして IHttpClientFactory を要求できます。 次のコードでは、IHttpClientFactory を使用して HttpClient インスタンスを作成しています。

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        using HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

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

名前付きクライアント

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

  • アプリで、多くの異なる HttpClient を使用する必要がある。
  • 多くの HttpClient インスタンスには異なる構成があります。

名前付き HttpClient の構成は、IServiceCollection での登録時に指定できます:

using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

  • "TodoHttpClientName" の下の構成からプルされた名前。
  • ベース アドレス https://jsonplaceholder.typicode.com/
  • "User-Agent" ヘッダー。

構成を使用して HTTP クライアント名を指定することができます。これは、追加時や作成時にクライアントに誤った名前を付けないようにするのに役立ちます。 この例では、HTTP クライアント名を構成するために appsettings.js ファイルが使用されています。

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

この構成を拡張して、HTTP クライアントがどのように機能するかについての詳細を格納するのは簡単です。 詳細については、「.NET での構成」を参照してください。

クライアントの作成

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

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

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

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public sealed class TodoService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<TodoService> _logger = null!;

    public TodoService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<TodoService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

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

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

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

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

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

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger) : IDisposable
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }

    public void Dispose() => httpClient?.Dispose();
}

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

  • この構成は、型指定されたクライアントがサービス コレクションに追加されるときに設定されます。
  • HttpClient は、クラススコープの変数 (フィールド) として割り当てられ、公開された API と共に使用されます。

HttpClient の機能を公開する API 固有のメソッドを作成できます。 たとえば、GetUserTodosAsync メソッドでは、ユーザー固有のTodo オブジェクトを取得するためのコードがカプセル化されます。

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

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

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

重要

シングルトン サービスで型指定されたクライアントを使うと、危険な場合があります。 詳細については、シングルトン サービスでの型指定されたクライアントの回避に関するセクションを参照してください。

Note

型指定されたクライアントを AddHttpClient<TClient> メソッドを使用して登録する場合、TClient 型には、パラメータとして HttpClient を許可するコンストラクタが必要です。 さらに、TClient 型は DI コンテナを使用して個別に登録しないでください。登録すると、前登録が後の登録にとり上書きされるためです。

生成されたクライアント

IHttpClientFactory は、Refit などのサードパーティ製ライブラリと組み合わせて使用できます。 Refit は、.NET 用の REST ライブラリです。 これにより、インターフェイス メソッドをエンドポイントにマッピングする宣言型の REST API 定義が可能になります。 インターフェイスの実装は RestService によって動的に生成され、HttpClient を使用して外部 HTTP の呼び出しを行います。

次の record 型があるとします:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

次の例は、Refit.HttpClientFactory NuGet パッケージに依存しており、単純なインターフェイスです。

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

上記の C# インターフェイスでは、次のことを行います。

  • Task<Todo[]> インスタンスを返す GetUserTodosAsync という名前のメソッドを定義します。
  • 外部 API に対するパスとクエリ文字列を使用して、Refit.GetAttribute 属性を宣言します。

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

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

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

POST、PUT、DELETE の要求を行う

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

  • POST
  • PUT
  • DELETE
  • PATCH

サポートされている HTTP 動詞の一覧については、「HttpMethod」を参照してください。 HTTP 要求の作成の詳細については、「HttpClient を使用して要求を送信する」 を参照してください。

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

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

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

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

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

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

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

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

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

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

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

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

HttpClient 有効期間管理

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

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

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

ハンドラーの既定の有効期間は 2 分です。 既定値をオーバーライドするには、次のように、IServiceCollection で各クライアントに対して SetHandlerLifetime を呼び出します。

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

重要

IHttpClientFactory によって作成される HttpClient インスタンスは、短い有効期間であることが想定されます。

  • HttpMessageHandler の有効期限が切れたときにそのリサイクルと再作成を行うことは、IHttpClientFactory にとってハンドラーが DNS の変更に対応できるようにするために不可欠です。 HttpClient は作成時に特定のハンドラー インスタンスに関連付けられるため、新しい HttpClient インスタンスを適切なタイミングで要求し、クライアントが更新されたハンドラーを確実に取得できるようにする必要があります。

  • ファクトリによって作成されたこのような HttpClient インスタンスを破棄してもソケットは枯渇しません。破棄しても HttpMessageHandler は破棄されないためです。 IHttpClientFactoryHttpClient インスタンスの作成に使用されたリソース (具体的には HttpMessageHandler インスタンス) を追跡し、破棄します。それらの有効期間はすぐに期限切れになり、それらを使う HttpClient がなくなるからです。

1 つの HttpClient インスタンスを長期間存続させることは、IHttpClientFactory代替手段として使用できる一般的なパターンですが、このパターンには PooledConnectionLifetime のような追加のセットアップが必要です。 PooledConnectionLifetime を指定した有効期間の長いクライアントを使うか、IHttpClientFactory によって作成された有効期間の短いクライアントを使うことができます。 アプリで使用する戦略の詳細については、「HTTP クライアントの使用に関するガイドライン」を参照してください。

HttpMessageHandler を構成する

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

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

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

HttClientHandler を構成すると、ハンドラーのさまざまな他のプロパティの中から HttpClient インスタンスのプロキシを指定できます。 詳細については、クライアントごとのプロキシに関する記事を参照してください。

追加の設定

IHttpClientHandler を制御するための追加の構成オプションがいくつかあります。

メソッド 説明
AddHttpMessageHandler 名前付き HttpClient のメッセージ ハンドラーをさらに追加します。
AddTypedClient TClient と、IHttpClientBuilder に関連付けられている名前付き HttpClient との間のバインディングを構成します。
ConfigureHttpClient 名前付き HttpClient の構成に使用されるデリゲートを追加します。
ConfigurePrimaryHttpMessageHandler 名前付き HttpClient に対して、依存関係挿入コンテナーからプライマリ HttpMessageHandler を構成します。
RedactLoggedHeaders ログ記録する前に値を編集する必要がある HTTP ヘッダー名のコレクションを設定します。
SetHandlerLifetime HttpMessageHandler を再利用できる時間を設定します。 名前付きの各クライアントには、独自の構成済みハンドラーの有効期間の値を設定できます。
UseSocketsHttpHandler 名前付き HttpClient のプライマリ ハンドラとして使用されるように依存性の注入コンテナから新しいまたは前に追加した SocketsHttpHandler インスタンスを構成します。 (.NET 5+ のみ)

SocketsHttpHandler と共に HttpClientFactory を使用する

HttpMessageHandlerSocketsHttpHandler 実装は .NET Core 2.1 で追加されました。これにより PooledConnectionLifetime を構成できます。 この設定は、ハンドラーが DNS の変更に対応できるようにするために使用されます。そのため、SocketsHttpHandler の使用は IHttpClientFactory の使用の代替手段と見なされます。 詳細については、「HttpClient の使用に関するガイドライン」を参照してください。

ただし、SocketsHttpHandlerIHttpClientFactory を一緒に使うと、構成可能性が向上します。 これらの API の両方を使うことで、低レベル (たとえば、動的な証明書の選択に LocalCertificateSelectionCallback を使う) と高レベル (たとえば、DI 統合や複数のクライアント構成を活用する) の両方で構成可能性の利点を得ることができます。

両方の API を使うには:

  1. ConfigurePrimaryHttpMessageHandler または UseSocketsHttpHandler 経由で PrimaryHandler として SocketsHttpHandler を指定します (.NET 5+ のみ)。
  2. DNS を更新する間隔に基づいて SocketsHttpHandler.PooledConnectionLifetime を設定します。以前 HandlerLifetime にあった値などが例として挙げられます。
  3. (オプション) SocketsHttpHandler が接続ポールとリサイクルを処理するため、IHttpClientFactory レベルのハンドラ リサイクルは不要になりました。 HandlerLifetimeTimeout.InfiniteTimeSpan に設定して、サイズ変更を無効にすることもできます。
services.AddHttpClient(name)
    .UseSocketsHttpHandler((handler, _) =>
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

上の例では、既定の HandlerLifetime 値に合わせて、図解の目的で 2 分を任意に選択しました。 DNS またはその他のネットワーク変更の予想される頻度に基づいて値を選択する必要があります。 詳細については、HttpClient ガイドラインの「DNS の動作」セクションと PooledConnectionLifetime API ドキュメントの「備考」セクションを参照してください。

シングルトン サービスで型指定されたクライアントを使う

"名前付きクライアント" の方法を使用する場合、IHttpClientFactory はサービスに挿入され、HttpClient インスタンスは HttpClient が必要になるたびに CreateClient を呼び出すことによって作成されます。

ただし、"型指定されたクライアント" の方法では、型指定されたクライアントは、通常はサービスに挿入される一時的なオブジェクトになります。 これにより問題が発生するおそれがあります。型指定されたクライアントをシングルトン サービスに挿入できるためです。

重要

型指定されたクライアントは、IHttpClientFactory によって作成された HttpClient インスタンスと同じ意味で、短い有効期間であることが想定されます (詳細については、「HttpClient の有効期間の管理」を参照)。 型指定されたクライアント インスタンスが作成されるとすぐに、IHttpClientFactory ではそれを制御できなくなります。 型指定されたクライアント インスタンスがシングルトンで取得された場合、DNS の変更に対応できなくなり、IHttpClientFactory の目的の 1 つが失われるおそれがあります。

シングルトン サービスで HttpClient インスタンスを使う必要がある場合は、次のオプションを検討してください。

  • 代わりに "名前付きクライアント" の方法を使い、シングルトン サービスに IHttpClientFactory を挿入し、必要に応じて HttpClient インスタンスを再作成します。
  • "型指定されたクライアント" の方法が必要な場合は、構成済みの PooledConnectionLifetime と共にプライマリ ハンドラーとして SocketsHttpHandler を使います。 SocketsHttpHandlerIHttpClientFactory を使う方法について詳しくは、セクション「SocketsHttpHandler と共に IHttpClientFactory を使用する」 を参照してください。

IHttpClientFactory のメッセージ ハンドラーのスコープ

IHttpClientFactory は、各 HttpMessageHandler インスタンスごとに個別の DI スコープを作成します。 これらの DI スコープは、アプリケーション DI スコープ (たとえば、ASP.NET 受信要求スコープやユーザーが作成した手動 DI スコープなど) とは別であるため、スコープ付きサービス インスタンスは共有されません。 メッセージ ハンドラーのスコープはハンドラーの有効期間に関連付けられ、アプリケーション スコープを上回る可能性があります。これにより、たとえば、複数の受信要求間で、同じ挿入されたスコープ付き依存関係を持つ同じ HttpMessageHandler インスタンスが再利用される可能性があります。

2 つのアプリケーション DI スコープと個別のメッセージ ハンドラー スコープを示す図

HttpMessageHandler インスタンス内で (HttpContext のデータなどの) スコープ関連の情報をキャッシュしないことと、機密情報を漏洩しないように注意してスコープ付き依存関係を使うことを強くお勧めします。

メッセージ ハンドラーからアプリ DI スコープにアクセスする必要がある場合は、認証の例として、スコープ対応ロジックを別の一時的な DelegatingHandler にカプセル化し、IHttpClientFactory キャッシュから HttpMessageHandler インスタンスの周囲にラップします。 登録済みの 名前付きクライアント のハンドラー呼び出し IHttpMessageHandlerFactory.CreateHandler にアクセスします。 その場合は、構築されたハンドラーを使用して自分で HttpClient インスタンスを作成します。

別の一時的なメッセージハンドラと IH ttpMessageHandlerFactory を介してアプリ DI スコープにアクセスすることを示すダイアグラム

次の例は、スコープ対応の DelegatingHandler を使用して HttpClient を作成する方法を示しています:

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

さらに回避策として、スコープ対応 DelegatingHandler を登録し、現在のアプリ スコープにアクセスできる一時的なサービスによる既定 IHttpClientFactory の登録をオーバーライドするための拡張メソッドを使用できます:

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

詳細については、完全な例を参照してください。

関連項目