そんなに深くない気がしますが。Microsoft.Extensions.DependencyInjection の DI についてざっくりまとめた記事です。なお、長いので、Microsoft.Extensions.DependencyInjection を M.E.DI と略します。
例によって、公式ドキュメント にすべて書いてある、はずですが、ここでは少し別の観点でまとめてみます。また、ドキュメントに書いてない(と思う)内部実装についてもいくつか書いてあります。
IServiceCollectionの拡張メソッドを使用して、フレームワークやランタイムが提供するIServiceCollectionの実装に対して登録していきます。IServiceCollectionはServiceDescriptorというサービスについてのスペックを表すオブジェクトのコレクションである。ServiceDescriptorはサービス型をキーにして、ライフタイムと実装を持ちます。- 実装は、実装の型か、カスタムファクトリとなるデリゲートか、実装のインスタンスの形で与えられる。実装の型を与えられた場合、既定のファクトリによってインスタンス化されます。
- カスタムファクトリは、
IServiceProviderを引数として受け取るので、必要な依存先サービスを取得できます。なお、このIServiceProviderは後述のスコープの概念を実装しているので、現在のスコープのインスタンスが取得できます。そのため、カスタムファクトリでは必ずこのIServiceProvider経由でインスタンスを取得しなければなりません。
- カスタムファクトリは、
IServiceCollectionは、BuildServiceProvider()拡張メソッドでIServiceProviderを構築します。このIServiceProviderを使用して、サービス型のインスタンスを取得します。
Microsoft.Extensions.DependencyInjection は以下のような構造を持っています。
Microsoft.Extensions.DependencyInjection.Abstraction。DI を設定するコード(いわゆる「貧者の DI」とか「合成ルート(composition root)」とか言われるコード)で使うためのインターフェイスと拡張メソッドが定義されています。M.E.DI対応のクラスライブラリを作りたい(ロガーとか)場合や、がっつりカスタムの DI コンテナーを作りたい場合にはこれを参照すれば OK。このパッケージには以下のような型が含まれています。- メインとなる API 群。
ServiceDescriptor。DI で注入するサービスとその仕様を表します(具体的には、サービス型、ライフタイム、そして、実装型、実装オブジェクトのインスタンス、または実装オブジェクトのファクトリデリゲートのいずれかの組み合わせ)。IServiceCollection。DI で注入するサービスのリスト。IList<ServiceDescritor>型にすぎないのですが、このIServiceCollectionに対して、様々な拡張メソッドが定義されます。
- DI コンテナーの実装に必要な SPI(Service Provider Interface)群。
IServiceProviderFactory。IServiceCollectionからIServiceProviderを生成します。IServiceScope。スコープを表します。現在のスコープに対応するIServiceProviderへのアクセス手段を提供します。IServiceScopeFactory。新しいIServiceScopeを生成します。ISupportRequiredService。必須のサービスを要求されたときに通常とは別の振る舞いをするIServiceProviderを表現します。- 実装しない場合、
GetRequiredService拡張メソッドは、必須のサービスが見つからない場合(IServiceProvider.GetService(Type)がnullを返した場合)、InvalidOperationExceptionをスローします。
- 実装しない場合、
IServiceProvoviderの拡張メソッド。IServiceProviderをサービスロケーターとして使用するうえで便利なメソッド群を追加しています。ActivatorUtilities。サービスロケーターとしてのIServiceProviderを使用して、コンストラクターインジェクションを使用したインスタンス化を実装します。ActivatorUtilitiesConstructorAttribute。コンストラクターのオーバーロードが複数ある場合に、ActivatorUtilitiesが使用するコンストラクターであることを宣言します。
- メインとなる API 群。
Microsoft.Extensions.DependencyInjection。M.E.DIの既定の実装で、以下のような API が用意されています。ServiceCollection。IServiceCollectionの実体です。単なるリストです。ServiceProvider。IServiceProviderの実装です。DefaultServiceProviderFactory。`
DI コンテナーへの登録は、コンポジションルート(ASP.NET Core や GenericHost を使用しているコードであれば StartUp、そうでなければ Main から呼び出す適当な初期化処理)で、IServiceCollection(StartUp がないのであれば、誰も IServiceCollection をインスタンス化してくれないので、ServiceCollection を new して作成します)に対して追加していきます。
追加時には、Add{ライフタイム} メソッドで追加します。ライフタイムについては後述しますが、わからなければこれを使え、というものはありません。シングルトン、スコープ付き、一時的の 3 種類を適切に使い分ける必要があります。いずれにせよ、サービス型(DI される側で受け取る型)と、それに対応する実装(サービス型と互換性のある、サービスを実装するオブジェクト)のペアを登録します。なお、実装は、型(ジェネリック引数で指定)、ファクトリ(Func<IServiceProvider, T> 型のデリゲートを、ラムダ式やメソッド参照で指定)、オブジェクト(初期化済みのシングルトンオブジェクトを定数的に渡す)のいずれかで指定できます。なお、Add{ライフタイム} は、内部的には ServiceDescriptor を作成して、IServiceCollection.Add() を呼び出しているだけです。
ライブラリやフレームワークを使用する場合、ライブラリやフレームワークの作成者が DI 登録用の IServiceCollection の拡張メソッド(だいたい Configure{機能名}() とか Add{機能名}() になっています)を呼び出せばよいでしょう。アプリケーションのドメインオブジェクト(ドメインモデルとか、トランザクションスクリプトとかテーブルモジュールとかいうあれ)やら、その先で呼び出すやつ(リポジトリとかデータアクセスオブジェクトとかいうあれ)は、Add{ライフタイム}() で追加するか、似たような共通の仕組みを用意するといいでしょう。
なお、実装の追加方法として、Add{ライフタイム} と TryAdd{ライフタイム} があります。これは、指定したサービス型が、追加先の IServiceCollection に既に登録済みである場合に、気にせずに追加するのか(Add{ライフタイム})、追加しないのか(TryAdd{ライフタイム})の違いがあります。複数の実装があっても構わない場合は Add{ライフタイム}、二重登録してはまずいものは TryAdd{ライフタイム} を使用します。通常、二重登録はコンポジションルートのバグだと思いますが、クラスライブラリなどをの作成するときに、DI の登録処理(つまりは IServiceCollection の拡張メソッド)がどのように呼び出されるかわからないので、TryAdd{ライフタイム} を呼ぶといいでしょう。迷ったら TryAdd{ライフタイム} でいいとは思います。また、複数登録したいが、二重登録はしたくないというケースもあるでしょう。言い換えると、あるサービス型に対し、複数の実装を登録したいが、サービス型と実装のペアは二重登録したくない場合です。この場合、TryAddEnumerable() 拡張メソッドを使うことができます。残念なことに、TryAddEnumerable() は ServiceDescriptor を受け取るオーバーロードしか用意されていませんが、ServiceDescriptor には、ライフタイムの名前が付いた静的なファクトリメソッドが用意されているので、それを活用できます。
さらに、既存の実装を差し替えたい場合もあるでしょう。たとえば、デバッグモードのときだけ実装を差し替えるなどです。この場合、IServiceCollection.Replace() 拡張メソッドを使用できます。これは、指定した ServiceDescriptor のサービス型と一致するサービス型を置き換える処理……に見えますが、実際には先頭のものを削除して、末尾に追加するという挙動になるので注意してください。なお、Remove() や RemoveAll() といった拡張メソッドもあります。
TryAdd{ライフタイム}()、TryAddEnumerable()、Replace()、Remove()、RemoveAll() を使うには、Microsoft.Extensions.DependencyInjection.Extensions 名前空間を using ディレクティブで宣言する必要があります。なお、パッケージは Microsoft.Extensions.DependencyInjection.Abstraction になります。
余談ですが、サービス型と実装の型は同一でもまったく問題ありません。実装を DI で入れ替えたい型というのは、多くの場合外部に依存した型(ファイルアクセスとか DB アクセスとか)のみで、それを使うだけの型は実装が 1 つだけというのはよくある話です。レイヤー構造であれば一番下のレイヤー、ヘキサゴナルやオニオン型なら一番外側のみが切り替えたい対象でしょう。逆に言えば、レイヤー構造の一番下以外や、ヘキサゴナルやオニオンの内側は、依存先が(連鎖的に)注入されてほしいだけで、サービス型と実装を区別する必要はないはずです。メソッドの追加や変更のたびに、複数の箇所に分散したシグネチャを直さないで済むならその方が良いですよね。
また、サービス型と実装の型は同一でも良いということは、サービス型がインターフェイスである必要はないということです。抽象クラスである必要すらありません。たとえば、外部アクセスの処理で共通の部分があり、サービス型をインターフェイスではなく基底クラスにしたい場合はそれでもかまいません。
- ライフタイムに応じて、既定またはカスタムファクトリが呼び出されます。
- ライフタイムは以下の 3 種類です。
- シングルトンは、最初に呼び出されたときに生成され、以降はキャッシュされたシングルトンオブジェクトが返ります。
- スコープ付き(scoped)は、スコープごとに作成されます。スコープはフレームワークやランタイムが
IServicProvider.CreateScope()拡張メソッド(またはその実装であるIServiceScopeFactory.CreateScope()メソッド)を呼び出すことで作成され、スコープにキャッシュされます。 - 一時的(transient)の場合は、
IServiceProviderから要求されるたびにインスタンス化されます。ただし、破棄はスコープと一緒に行われます。
- このとき、スコープ付きまたは一時的の場合はスコープ(スコープが開始されていない場合はルートスコープ)に、インスタンスが登録される。このインスタンスは、スコープが破棄されるときに一緒に破棄されます。
IAsyncDisposableを実装されている場合は、DisposeAsyncが呼び出される。IDisposableとIAsyncDisposableが両方実装されている場合は、IAsyncDisposableが優先されます。
- ライフタイムは以下の 3 種類です。
シングルトンライフタイムのオブジェクトと、ルートスコープに生成されたオブジェクトは、ServiceProvider が Dispose されたタイミングで破棄されます。それ以外のオブジェクトは、スコープが Dispose されたタイミングで破棄されます。言い換えると、ルートスコープの中で生成されたスコープ付きオブジェクトはシングルトンとして動作します。また、ルートスコープの中で生成された一時的オブジェクトは、ルートスコープが破棄されるまで破棄されません。さらに、ルートスコープがそれらの一時的オブジェクトへの参照を保持するので、スコープを実装していない実行環境(たとえば単純なコンソールアプリケーションや WebJob)で一時的なオブジェクトを取得すると、メモリリークを引き起こします。
M.E.DI はコンストラクターインジェクションのみ実装しています。つまり、依存先のサービスはコンストラクターのパラメーターとして受け取ります。コンストラクターパラメーターの型と一致するサービス型の実装が注入されます。
ver 3.0 時点では、以下のようになっています。
publicなコンストラクターが対象となります。- 候補が 1 つの場合、そのコンストラクターが得らればれます。
- 候補が複数ある場合、DI なのか(
IServiceProviderから取得するのか)、ActivatorUtilitiesを使用するのかで異なります。 IServiceProviderから取得する場合:- 最も多くのパラメーターのある、正確に言えば最上位のスーパーセットを持つコンストラクターが選ばれます。
- コンストラクターが選べない場合はエラーです。
ActivatorUtilitiesの場合:[ActivatorUtilitiesConstructor]が付与されたコンストラクターがある場合、そのコンストラクターが対象となります。- そうではない場合、
ActivatorUtilitiesに渡したカスタム引数の一致度が最も高いものが対象となります。 - そうではない場合、メタデータ上、最も後に宣言されたものが使用されます。これは明らかに実装詳細なので、この動作には依存しない方が良いでしょう。
つまり、IServiceProvider と ActivatorUtilities の両方でうまくいくコンストラクターの解決方法がありません。そのため、ベストプラクティスをまとめると、以下のようになります。
publicなコンストラクターを 1 つ用意する。- テストを楽にするなどの目的でコンストラクターのオーバーロードを加える場合、
[InternalsVisibleTo]などを用意して非publicなコンストラクターにする。
前述のように、IServiceCollection の Add{ライフタイム} を呼び出した場合、1 つのサービス型に複数の実装が登録されている場合があります。その場合の挙動は以下のようになります。
- コンストラクターパラメーターの型がサービス型の場合、最後に
IServiceCollectionに登録した実装が渡されます。 - コンストラクターパラメーターの型が
IEnumerable<サービス型>の場合、登録した順に実装のリストが渡されます。
ServiceProvider は、その実装としていくつかの「エンジン」を持ちます。
ServiceProviderEngine。基底クラス。RuntimeServiceProviderEngine。RuntimeResolverによる実装型の生成を行います。- これは、
Array.CreateInstanceとConstructorInfo.Invokeによるサービス生成になります。
- これは、
CompiledServiceProviderEngine。M.E.DIアセンブリのビルドオプションに応じて、Emit API または式木による実行時 IL 生成によって実装型の生成を行います。DynamicServiceProviderEngine。1 回目はRuntimeResolverによる実装型の生成を行うが、2 回目はCompiledServiceProviderEngineによる生成を行います。
ExpressionsServiceProviderEngine。ベンチマーク用。式木による実行時 IL 生成によって実装型の生成を行います。ILEmitServiceProviderEngine。ベンチマーク用。Emit API による実行時 IL 生成によって実装型の生成を行います。
どのエンジンが使われるのかは以下のようになります。
- .NET Standard または .NET Framework 向けの場合、
DynamicServiceProviderEngine - .NET Core 向けの場合、
IsDynamicCodeCompiledがRuntimeFeatureとしてサポートされていれば、DynamicServiceProviderEngine。そうでなければRuntimeServiceProviderEngine。
実装型の生成は、ファクトリによって実行されます。
- サービススコープが渡されていなければ、ルートスコープを使用します。
- ファクトリがない場合、実体化(realize)を行います。
- サービス型から、コールサイト(callsite)を作成します。これは、
CallSiteFactoryで実装されます。 - コールサイトが作成できたなら、それを使用してサービスの実体化を行います。これは
RealizeService仮想メソッドで行い、ここがエンジンごとの実装差異になります。
- サービス型から、コールサイト(callsite)を作成します。これは、
- スコープを引数にして、ファクトリを呼び出します。
コールサイトは、サービスディスクリプターの種類ごとに異なります。なお、IServiceProvider と IServiceScopeFactory については、固定の実装になります。
IServiceProviderの場合はServiceProviderを返すServiceProviderCallSite。IServiceScopeFactoryの場合はエンジンそのものを返すServiceScopeFactoryCallSite。- インスタンスを持つ場合は
ConstantCallSite - カスタムファクトリを持つ場合は
FactoryCallSite - そうではなく、サービス型が登録されている場合は
ConstructorCallSite- 引数については
ServiceCallSite。- 引数がサービスとして解決できれば、そのコールサイト。
- そうではないが、既定値が指定されているか、または値型であれば、その既定値を使用した
ConstantCallSite。
- 引数については
- そうではなく、サービス型のジェネリック型定義が登録されている場合は、サービス型のジェネリック型引数を使用して実装型のクローズドジェネリック型を構築し、その構築したクローズドジェネリック型のインスタンスを生成する
ConstructorCallSite。 - そうではなく、
IEnumerable<T>が要求されている場合は、IEnumerableCallSite。
なお、実体化される処理は以下の疑似コードで表現できます。
// resolve<T> がコールサイトごとの処理。
scope =>
{
IDictionary<ServiceCacheKey, object> resolvedServices = scope.ResolvedServices;
ServiceCacheKey key = new ServiceCacheKey(typeof(T), {slot#});
bool lockWasTaken;
try
{
Monitor.Enter(resolvedServices, ref lockWasTaken);
object resolved;
if (!resolvedServices.TryGetValue(key, out resolved))
{
resolved = CaptureDisposable!!
resolved =
typeof(IDisposable).IsAssignableFrom(typeof(T))
|| typeof(IAsyncDisposable).IsAssignableFrom(typeof(T))
? scope.CaptureDisposable(
resolve<T>(scope);
)
: resolve<T>(scope)
reslovedServices.Add(key, resolved);
}
return resolved;
}
finally
{
if (lockWasTaken)
{
Monitor.Exit();
}
}
}// サービスの場合
_ => serviceDescriptor.Instance;
// パラメーターの既定値の場合
_ => defaultValue;scope =>
{
Func<IServiceProvider, object> factory = serviceDescriptor.Factory;
return factory((IServiceProvider)scope);
}// resolve<T>() は基本呼び出し(コールサイトごとに異なる)
scope =>
new T((T)resolve<T1>(scope), (T)resolve<T2>(scope))// resolve<T>() は基本呼び出し(コールサイトごとに異なる)
scope => new T[] {(T)resolve<T1>(scope), (T)resolve<T2>(scope), ...}scope => scope;_ => _engine; // 最終的には new ServiceProviderEngineScope(_engine)ちなみに、コールサイトの構築時には、以下のようなこともやってます。
- 型の循環参照による無限再帰の検出(循環参照があれば
InvalidOperationException) - スレッドのスタックが不足しているかチェックし、不足している場合には別スレッドで処理を実行。