本文示例コードは、すべて .NET 6 を使用しています。具体的なコードはこのリポジトリ Articles.DI から取得できます。
前の文章を通じて、サービスの 3 つのライフサイクルについて理解しました。それでは、サービスを登録する際に、フレームワークはどのような API を提供しているのでしょうか?自分のニーズに基づいてサービスを宣言し、具体的な実装を宣言するには、どのようにコードを書くべきでしょうか?この記事では、.NET に組み込まれている DI フレームワークが提供する API とその使用方法、具体的な機能について探ります。
.NET API ブラウザは、詳細な API ドキュメントを提供しています。このリンクでは、サービス登録に関連する API が示されています。
登録方法#
サービスを登録する方法は、必ずしも Add{LifeTime}<TService, TImplementation>()
だけではありません。具体的な方法の種類は、このリンクをクリックして、ドキュメントに提供されている表を確認できます。以下にその表を直接貼り付けます。
方法 | 自動解放 | 多種実装 | 伝達パラメータ |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | はい | はい | いいえ |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | はい | はい | はい |
Add{LIFETIME}<{IMPLEMENTATION}>() | はい | いいえ | いいえ |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | いいえ | はい | はい |
AddSingleton(new {IMPLEMENTATION}) | いいえ | いいえ | はい |
上の表では、ジェネリックメソッドをサポートする方法のみを示していますが、Add{LIFETIME}(typeof({SERVICE}), typeof({IMPLEMENTATION}))
のような方法もあり、これは本質的にジェネリックメソッドと同じですので、ここでは省略します。表の右側の 3 列は、これらの方法の制限範囲をそれぞれ説明しています。
サービス解放#
コンテナはサービスの構築を担当しますが、サービスの有効期限が切れた場合、サービスの解放は誰が担当するのでしょうか?サービスの依存者でしょうか?それともコンテナでしょうか?
前の表の 自動解放
列が はい
の場合、これはこのような方法で登録されたサービスがコンテナによって一括して解放されることを示しています。これらのサービスの中に IDisposable
インターフェースを実装しているものがあれば、コンテナが自動的に Dispose
メソッドを呼び出し、コード内で明示的に解放メソッドを呼び出す必要はありません。
表を確認すると、AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
と AddSingleton(new {IMPLEMENTATION})
の 2 種類の方法では、自動解放は行われません。コンテナフレームワークは自動的に解放しないため、開発者がサービスの解放を自分で担当する必要があります。
多種実装#
前の例では、登録時に 1 つのインターフェースと 1 つの実装クラスを使用しましたが、依存関数注入時には単一のサービスが注入されます。もし複数の実装を同じインターフェースとして登録したい場合、どの方法を使用すればよいでしょうか?
前の表の 多種実装
列を見ると、2 つの方法が多種実装をサポートしていないことがわかります。Add{LIFETIME}<{IMPLEMENTATION}>()
と AddSingleton(new {IMPLEMENTATION})
です。実際、方法の説明を確認すればわかりますが、前者は IMPLEMENTATION
クラスの実装を直接 IMPLEMENTATION
型のサービスとして登録するため、多種実装を実現できません。後者も同様に、登録されたインスタンスを直接使用するため、多種実装を実現できません。
ここでの 多種実装
は、同じインターフェースを実装する複数の実装クラスを指し、サービスを登録する際に同時にこのインターフェースに登録されます。前回の記事では、1 つの実装クラスが複数のインターフェースを実装し、サービスを登録する際にこのクラスが複数のインターフェースに同時に登録されましたが、これらは異なる状況であり、後者は 多種実装
とは見なされません。
次に、コードを使用して 多種実装
を実現する方法を示します:
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService4
// 1. インターフェース IMyDependency を定義し、このインターフェースを実装する複数の実装クラスを作成します
public interface IMyDependency
{
string Id { get; }
bool Flag { get; }
}
public class MyDependency0 : IMyDependency
{
public string Id { get; } = Guid.NewGuid().ToString()[^4..];
public bool Flag { get; init; } = false;
}
public class MyDependency1 : MyDependency0 {}
public class MyDependency2 : MyDependency0 {}
public class MyDependency3 : MyDependency0 {}
public class MyDependency4 : MyDependency0 {}
// 2. サービスを登録します
using Microsoft.Extensions.DependencyInjection.Extensions;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// 同時に1つのインターフェースの多種実装を登録します
services.AddTransient<IMyDependency, MyDependency1>();
services.AddTransient<IMyDependency, MyDependency2>();
// TryAdd は、インターフェースがすでに登録されている場合、再登録しません
// IMyDependency はすでに登録されているため、MyDependency3 はその実装として登録されません
services.TryAddTransient<IMyDependency, MyDependency3>();
// TryAddEnumerable は、同じ種類の実装がすでに登録されている場合、重複登録しません
// IMyDependency -> MyDependency4、この関係はまだ登録されていないため、成功裏に登録されます
var descriptor = new ServiceDescriptor(
typeof(IMyDependency),
typeof(MyDependency4),
ServiceLifetime.Transient);
services.TryAddEnumerable(descriptor);
// このインターフェースが登録されているかどうかは確認せず、この関係が登録されているかどうかも確認せず、重複登録します
// MyDependency2 は2回登録されています
// そのため、IEnumerable<IMyDependency> を取得する際に
// MyDependency2 型のインスタンスが2つ存在します
services.AddTransient<IMyDependency, MyDependency2>(_ => new MyDependency2
{
Flag = true // このサービスを工場メソッドで構築して、最後に登録されたサービスを区別します
});
})
.Build();
Fun(host.Services);
static void Fun(IServiceProvider serviceProvider)
{
using var scopeServices = serviceProvider.CreateScope();
var services = scopeServices.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
GetData(myDependency);
Console.WriteLine();
var list = services.GetRequiredService<IEnumerable<IMyDependency>>();
foreach (var dependency in list)
{
GetData(dependency);
}
Console.WriteLine();
}
static void GetData<T>(T item) where T : IMyDependency
{
Console.WriteLine($"{item.GetType().Name} {item.Id}, {item.Flag}");
}
プログラムの実行結果は以下の通りです:
MyDependency2 c432, True
MyDependency1 ea48, False
MyDependency2 9b9a, False
MyDependency4 c4ce, False
MyDependency2 77e9, True
プログラム全体の出力結果を確認すると、すべてのサービスが Transient
型のライフサイクルとして登録されているため、各インスタンスの Id は異なります。
最初の行の出力では、item.Flag が True
であることから、IMyDependency
型を解決する際に、最後に成功裏に登録された実装クラスが構築されて注入されることがわかります(この例では myDependency
変数)。
以下の 4 行の出力を比較すると、その順序はサービス登録時の順序と一致しており、IEnumerable<IMyDependency>
型の変数を取得する際には、すべての成功裏に登録されたタイプが順番に注入されます。この中の 2 行目と 4 行目はどちらも MyDependency2
であり、サービス登録は同じ実装を重複して登録できることを示しています。
伝達パラメータ#
方法 | 自動解放 | 多種実装 | 伝達パラメータ |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | はい | はい | いいえ |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | はい | はい | はい |
Add{LIFETIME}<{IMPLEMENTATION}>() | はい | いいえ | いいえ |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | いいえ | はい | はい |
AddSingleton(new {IMPLEMENTATION}) | いいえ | いいえ | はい |
伝達パラメータは、コンテナがサービスインスタンスを構築する際に、工場メソッドを使用して多様な方法でインスタンスを構築できるかどうかを示します。表を確認すると、工場メソッドが伝達された場合、その 伝達パラメータ
列は はい
となり、そうでない場合は いいえ
となります。実際には、工場メソッドを伝達しなくても、他の手段を用いて伝達パラメータの目的を達成することができます。
たとえば、コンストラクタ内で IOperation<>
型を使用することができます。必要な設定データを IOperation<MyOperation>
に登録すれば、コンストラクタ内で必要なリアルタイムデータを取得でき、伝達パラメータの目的を達成できます。具体的な設定データの取得は、IOperation<MyOperation>
の登録メソッドに置くことができます。