Dependency Injection は Autofac を使っていたのだけど、.NET Core を使い始めると、Microsoft製のMicrosoft.Extensions.DependencyInjection がシンプルでよいという話を同僚から聞いたので、その基本をまとめておきたい。
Microsoft.Extensions.DependencyInjection
公式ページはこちら
実際に私はASP.NET というより、Azure Functions で使うことを想定していますので生の基本的な挙動を理解したいと思っています。
オブジェクトの注入
次のようなクライアントとそのインターフェイスがあるとします。
public interface ISomeClient
{
string Id { get; set; }
}
public class SomeClient : ISomeClient
{
public string Id {get; set;}
public SomeClient()
{
this.Id = Guid.NewGuid().ToString();
}
public SomeOption SomeOptions {get; set;}
}
依存性の注入
DI コンテナにISomeClient
インターフェイスをキーに、SomeClient
のインスタンスを返却するには次の通り。DIの情報が格納されるコンテナはServiceCollectionクラスです。これが主要なパーツになります。そこに登録していきます。ここでは、Singleton
というスコープで登録していますが、スコープに関しては次に説明します。AddSingleton<ISomeClient, SomeClient>()
によって、ISomeClient
でインスタンスを要求されたら、SomeClinet
が返却されるという作りです。
var services = new ServiceCollection();
services.AddSingleton<ISomeClient, SomeClient>();
利用する側は次のとおりです。BuildServiceProvider()
メソッドを実行してIServiceProvider
を獲得し、GetRequiredService
もしくは、GetService
メソッドを使って登録したインスタンスを獲得します。GetRequiredService
の場合、もしインスタンスがなければ Exception がスローされ、GetService
の場合は、nullが返ってきます。
var provider = services.BuildServiceProvider();
var someClient1 = provider.GetRequiredService<ISomeClient>();
Console.WriteLine($"SomeClient1(Singleton): Type: {someClient1.GetType()} Id: {someClient1.Id}");
実行結果
とても簡単でした。
SomeClient1(Singleton): Type: DIFundamental.SomeClient Id: b248f5ca-21d5-4106-a2f6-6940bed988a4
スコープ
依存性を注入する場合、3つのスコープを使うことができます。
- AddSingleton(...): シングルトンのスコープつまり、インスタンスは1回だけ生成される
- AddScoped(...): 1回のリクエストに対して、インスタンスが1回生成される
- AddTransient(...): 毎回新しいインスタンスが生成される
上記のことをテストしてみましょう。OtherClient の方は、SomeClient と全く同じつくりにしています。ちなみにOtherClient の方は書き方を変えていますが、意味は同じで、このように、返却したいインスタンスを作るファクトリの関数を持たせることもできます。
// Basic Registration
services.AddSingleton<ISomeClient, SomeClient>();
// Registration with Function
services.AddTransient<IOtherClient>(client =>
{
return new OtherClient();
});
呼び出してみましょう。
// Singleton
var someClient1 = provider.GetRequiredService<ISomeClient>();
Console.WriteLine($"SomeClient1(Singleton): Type: {someClient1.GetType()} Id: {someClient1.Id}");
var someClient2 = provider.GetRequiredService<ISomeClient>();
Console.WriteLine($"SomeClient2(Singleton): Type: {someClient2.GetType()} Id: {someClient2.Id}");
var someClient3 = (ISomeClient)provider.GetRequiredService(typeof(ISomeClient));
Console.WriteLine($"SomeClient3(Singleton): Type: {someClient3.GetType()} Id: {someClient3.Id}");
// Transient
var otherClient1 = provider.GetService<IOtherClient>();
Console.WriteLine($"OtherClient1(Transient): Type: {otherClient1.GetType()} Id: {otherClient1.Id}");
var otherClient2 = provider.GetService<IOtherClient>();
Console.WriteLine($"OtherClient2(Transient): Type: {otherClient2.GetType()} Id: {otherClient2.Id}");
結果
Scoped は ASP.NET がないと検証が厳しいので省いて、今回は2つに絞ってやっててみました。Singleton の場合は、インスタンスが一つですね。
SomeClient1(Singleton): Type: DIFundamental.SomeClient Id: b248f5ca-21d5-4106-a2f6-6940bed988a4
SomeClient2(Singleton): Type: DIFundamental.SomeClient Id: b248f5ca-21d5-4106-a2f6-6940bed988a4
SomeClient3(Singleton): Type: DIFundamental.SomeClient Id: b248f5ca-21d5-4106-a2f6-6940bed988a4
OtherClient1(Transient): Type: DIFundamental.OtherClient Id: e0bd12f1-a8c7-4438-9c87-b3af630c14f1
OtherClient2(Transient): Type: DIFundamental.OtherClient Id: 967d2af8-7b63-46f9-8237-a301838fbffc
ちなみに、スコープの他に、TryAdd...
系のメソッドも用意されていて、もし、すでに登録されていたら登録をスキップ的な使い方をするようです(試してませんが)
Config
実際にDI のコードを書いていると、ある特定の時に、コンフィグレーションオプションを使いたくなる時があります。そのようなときのための機構も用意されています。 ご覧の通り、登録する時に使うオプションをコンフィグできて、BuildServiceProvider()
が実行される前にコンフィグを注入しています。
// Register Option value object
services.Configure<SomeOption>(o => new SomeOption
{
Value1 = "Hello",
Value2 = "World"
});
// Options
services.AddSingleton<SomeClient>(serviceProvider =>
{
var options = serviceProvider.GetRequiredService<IOptions<SomeOption>>();
var client = new SomeClient();
client.SomeOptions = options.Value;
return client;
});
コンストラクターインジェクション
複数のクラスがあって、それを組み合わせてインジェクションするのも簡単です。例えば、SomeClient をもった SomeService をインジェクションしたい場合、コンストラクターインジェクションは何も意識せず実行できます。
public interface ISomeService
{
void Greeting();
}
public class SomeService : ISomeService
{
private ISomeClient client;
public SomeService (ISomeClient client)
{
this.client = client;
}
public void Greeting()
{
Console.WriteLine($"Type: {this.client.GetType()} Id: {this.client.Id}");
}
}
上記のようにコンストラクタにISomeClient をしておいて普通に登録します。すでに、ISomeClientが登録ずみならこれでOK.
services.AddSingleton<ISomeService, SomeService>();
名前の解決
var service = provider.GetRequiredService<ISomeService>();
Console.WriteLine($"SomeService Greeting calling...");
service.Greeting();
結果
SomeService Greeting calling...
Type: DIFundamental.SomeClient Id: b248f5ca-21d5-4106-a2f6-6940bed988a4
ServiceCollection の拡張
次のような形で自分独自のメソッドを定義したい場合はServiceCollection の拡張メソッドを使います。ちょっとかっこいい!特に、複雑なインジェクションをしたい場合、それを隠蔽するために、特別なメソッドを書いたりすると利用するほうが便利です。
services.AddSpecialClient();
拡張メソッドを定義しましょう。今回は単にSingletonにデリゲートしているだけです。
public static class ServiceCollectionExteions
{
public static void AddSpecialClient(this IServiceCollection services)
{
services.AddSingleton<ISpecialClient, SpecialClient>();
}
}
まとめ
基本的な ServiceCollection
の使い方は大体カバーできているのではないでしょうか?ASP.NET もDIを基本的に使うようになっているので、テスタビリティの向上が期待できます。(本当は Azure Functions もそうしてよと思いますw)ちなみに、このライブラリを拡張して、もっと便利にインジェクションできるようにしたライブラリもあります。
というものです。海外のイケメン系の人はだいたい次にこれに流れるっぽいですが、次の機会に。また、英語ですが、ちょっと凝ったユースケースに出会ったので、そのやり方もブログに書いておきました。