web-dev-qa-db-ja.com

ASP.NET Core 2での同じタイプの複数のインスタンスの依存性注入

ASP.NET Core 2 Web Apiでは、httpClientAHttpClientインスタンスをControllerAに、およびhttpClientBのインスタンスにインジェクションするために、依存性注入を使用します。 HttpClientからControllerBへ。

DI登録コードは次のようになります。

HttpClient httpClientA = new HttpClient();
httpClientA.BaseAddress = endPointA;
services.AddSingleton<HttpClient>(httpClientA);

HttpClient httpClientB = new HttpClient();
httpClientB.BaseAddress = endPointB;
services.AddSingleton<HttpClient>(httpClientB);

HttpClientをサブクラス化して各コントローラーに固有の型を作成できることは知っていますが、それはあまりうまく拡張できません。

より良い方法は何ですか?

[〜#〜] update [〜#〜]特にHttpClientに関しては、Microsoftは何か作業中のようです

https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 -これを指摘してくれた@ mountain-traveller(Dylan)に感謝します。

15
Bryan

注:この回答では、HttpClientHttpClientFactoryexampleとして使用していますが、簡単に適用できます他の種類のものに。特にHttpClientの場合、 新しいIHttpClientFactoryを使用 from Microsoft.Extensions.Http が推奨されます。


組み込みの依存関係注入コンテナーは、名前付きの依存関係登録をサポートしていません。また、 現時点ではこれを追加する予定はありません があります。

この理由の1つは、依存性注入では、必要な名前付きインスタンスの種類を指定するタイプセーフな方法がないことです。コンストラクターのパラメーター属性(またはプロパティインジェクションのプロパティの属性)のようなものを使用できますが、それは価値のない別の種類の複雑さです。確かに裏付けられません型システムによる。これは依存性注入の仕組みの重要な部分です。

一般に、名前付き依存関係は、依存関係を適切に設計していないことを示しています。同じタイプの2つの異なる依存関係がある場合、これはそれらが互換的に使用される可能性があることを意味するはずです。それが当てはまらず、他の1つが有効ではない場合にそれらの1つが有効である場合、それは Liskov置換原理 に違反している可能性があることを示しています。

さらに、これらの依存性注入にdo support名前付き依存性が含まれている場合、これらの依存性を取得する唯一の方法は依存性注入ではなく サービスロケーターパターン を使用することです。 =代わりに 制御の反転 の正反対です== DIが促進します。

シンプルなインジェクター、より大きな依存関係注入コンテナーの1つ このような名前付きの依存関係がないことを説明します

キーによるインスタンスの解決は、Simple Injectorから意図的に除外された機能です。これは、常にアプリケーションがDIコンテナー自体に多数の依存関係を持つ傾向がある設計につながるためです。キー付きインスタンスを解決するには、おそらくContainerインスタンスを直接呼び出す必要があり、これは Service Locator anti-pattern につながります。

これは、キーによるインスタンスの解決が決して有用ではないという意味ではありません。キーによるインスタンスの解決は、通常、Containerではなく、特定のファクトリーのジョブです。このアプローチにより、設計がよりクリーンになり、DIライブラリに多数の依存関係をとる必要がなくなり、DIコンテナの作成者が単に考慮しなかった多くのシナリオが可能になります。


言われていることすべてで、時々あなたは本当にこのようなものを望みます、そして、多数のサブタイプと別々の登録を持つことは単に実行不可能です。その場合、これにアプローチする適切な方法があります。

ASP.NET Coreのフレームワークコードでこれに似たものがあると思う特定の状況が1つあります。認証フレームワークの名前付き構成オプションです。概念をすばやく説明してみましょう(ご容赦ください)。

ASP.NET Coreの認証スタックは、同じタイプの複数の認証プロバイダーの登録をサポートします。たとえば、アプリケーションで使用できる OpenID Connectプロバイダー が複数ある場合があります。しかし、これらはすべて同じプロトコルの技術的実装を共有していますが、それらが独立して動作し、インスタンスを個別に構成する方法が必要です。

これは、各「認証スキーム」に一意の名前を付けることで解決されます。スキームを追加するときは、基本的に新しい名前を登録し、使用するハンドラータイプを登録に伝えます。さらに、 IConfigureNamedOptions<T> を使用して各スキームを設定します。これは、実装時に、名前が一致する場合、基本的に未設定のオプションオブジェクトを渡して設定されます。そのため、各認証タイプTについて、最終的に複数IConfigureNamedOptions<T>の登録があり、スキームの個々のオプションオブジェクトを設定できます。

ある時点で、特定のスキームの認証ハンドラーが実行され、実際に構成されたオプションオブジェクトが必要になります。このため、それはIOptionsFactory<T>に依存します。これは デフォルトの実装 により、すべてのIConfigureNamedOptions<T>ハンドラーによって設定される具体的なオプションオブジェクトを作成することができます。

そして、オプションファクトリのその正確なロジックは、一種の「名前付き依存関係」を達成するために利用できるものです。特定の例に変換すると、たとえば次のようになります。

// container type to hold the client and give it a name
public class NamedHttpClient
{
    public string Name { get; private set; }
    public HttpClient Client { get; private set; }

    public NamedHttpClient (string name, HttpClient client)
    {
        Name = name;
        Client = client;
    }
}

// factory to retrieve the named clients
public class HttpClientFactory
{
    private readonly IDictionary<string, HttpClient> _clients;

    public HttpClientFactory(IEnumerable<NamedHttpClient> clients)
    {
        _clients = clients.ToDictionary(n => n.Key, n => n.Value);
    }

    public HttpClient GetClient(string name)
    {
        if (_clients.TryGet(name, out var client))
            return client;

        // handle error
        throw new ArgumentException(nameof(name));
    }
}


// register those named clients
services.AddSingleton<NamedHttpClient>(new NamedHttpClient("A", httpClientA));
services.AddSingleton<NamedHttpClient>(new NamedHttpClient("B", httpClientB));

次にHttpClientFactoryをどこかに挿入し、そのGetClientメソッドを使用して名前付きクライアントを取得します。

明らかに、この実装と私が以前に書いたことについて考えると、これはサービスロケーターパターンと非常によく似ています。ある意味では、既存の依存性注入コンテナの上に構築されていますが、実際にはこの場合の1つです。これで改善されますか?おそらくそうではありませんが、それは既存のコンテナーで要件を実装する方法なので、重要です。完全な防御の場合、上記の認証オプションの場合、オプションファクトリはrealファクトリであるため、実際のオブジェクトを構築し、既存の事前登録済みインスタンスを使用しないため、技術的にはnotサービスロケーションパターン。


明らかに、他の選択肢は、上で書いたものを完全に無視し、ASP.NET Coreで別の依存性注入コンテナーを使用することです。たとえば、 Autofac は名前付き依存関係をサポートし、 ASP.NET Coreのデフォルトコンテナを簡単に置き換える を使用できます。

25
poke

名前付き登録を使用する

これがまさに 名前付き登録 の目的です。

次のように登録します。

container.RegisterInstance<HttpClient>(new HttpClient(), "ClientA");
container.RegisterInstance<HttpClient>(new HttpClient(), "ClientB");

そして、この方法を取得します。

var clientA = container.Resolve<HttpClient>("ClientA");
var clientB = container.Resolve<HttpClient>("ClientB");

ClientAまたはClientBを別の登録済みタイプに自動的に挿入する場合は、 この質問 を参照してください。例:

container.RegisterType<ControllerA, ControllerA>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientA") // Resolve parameter of type HttpClient using name "ClientA"
    )
);
container.RegisterType<ControllerB, ControllerB>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientB") // Resolve parameter of type HttpClient using name "ClientB"
    )
);

工場を使用する

IoCコンテナーに名前付き登録を処理する機能がない場合は、ファクトリーを注入し、コントローラーにインスタンスの取得方法を決定させることができます。これは本当に簡単な例です:

class HttpClientFactory : IHttpClientFactory
{
    private readonly Dictionary<string, HttpClient> _clients;

    public void Register(string name, HttpClient client)
    {
        _clients[name] = client;
    }

    public HttpClient Resolve(string name)
    {
        return _clients[name];
    }
}

そして、あなたのコントローラーで:

class ControllerA
{
    private readonly HttpClient _httpClient;

    public ControllerA(IHttpClientFactory factory)
    {
        _httpClient = factory.Resolve("ClientA");
    }
}

そして、コンポジションのルートで:

var factory = new HttpClientFactory();
factory.Register("ClientA", new HttpClient());
factory.Register("ClientB", new HttpClient());
container.AddSingleton<IHttpClientFactory>(factory);
4
John Wu

本当に、サービスの利用者は、それが使用しているインスタンスの実装についてどこを気にするべきではありません。あなたの場合、HttpClientの多くの異なるインスタンスを手動で登録する理由はありません。型を一度登録すれば、インスタンスを必要とする消費インスタンスは、HttpClientの独自のインスタンスを取得できます。あなたはAddTransientでそれを行うことができます。

AddTransientメソッドは、抽象型を、それを必要とするすべてのオブジェクトに対して個別にインスタンス化される具体的なサービスにマッピングするために使用されます

services.AddTransient<HttpClient, HttpClient>();
0
Igor

別のオプションは

  • インターフェイスで追加のジェネリック型パラメーターを使用するか、非ジェネリックインターフェイスを実装する新しいインターフェイスを使用します。
  • マーカータイプを追加するアダプター/インターセプタークラスを実装してから、
  • 「名前」として汎用タイプを使用します

詳細を記載した記事を書きました: .NETでの依存性注入:欠落している名前付き登録を回避する方法

0
Rico Suter