やっとコードよりの話になれる!!過去の 2 記事は言語ごとの事情や、その人の経験などで色々ちょっとずつ異なることがあるので「〇〇の場合は違う」とか「こういう側面もある」とか色々コメントしやすい感じだったのですが、そのおかげで初めての Qiita のデイリーで No1 取れました。やったね!
ということで、自分の主戦場の C# での DI コンテナ事情について書いてみたいと思います。
Microsoft.Extensions.DependencyInjection
ASP.NET Core などで何も考えないと使うことになる、事実上の標準の DI コンテナです。
https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection
非常にシンプルで DI コンテナとして最低限これくらいは持ってるだろうと思われる機能だけ持ってます。
例えば、以下のようなクラスがあったとします。
interfaceIMyService{voidGreet();}classMyService:IMyService{privatereadonlyIMessagePrinter_messagePrinter;privatereadonlyIMessageGenerator_messageGenerator;publicMyService(IMessagePrintermessagePrinter,IMessageGeneratormessageGenerator){_messagePrinter=messagePrinter;_messageGenerator=messageGenerator;}publicvoidGreet()=>_messagePrinter.Print(_messageGenerator.Generate());}interfaceIMessagePrinter{voidPrint(stringmessage);}classConsoleMessagePrinter:IMessagePrinter{publicvoidPrint(stringmessage)=>Console.WriteLine(message);}interfaceIMessageGenerator{stringGenerate();}classMyMessageGenerator:IMessageGenerator{publicstringGenerate()=>"Hello world";}
Microsoft.Extensions.DependencyInjection を使うと上記のクラスを組み立て可能なコンテナを作って IMyService を取得して Greet を呼び出すコードは以下のような感じになります。
classProgram{staticvoidMain(string[]args){// 型の登録varservices=newServiceCollection();services.AddTransient<IMyService,MyService>();services.AddTransient<IMessagePrinter,ConsoleMessagePrinter>();services.AddTransient<IMessageGenerator,MyMessageGenerator>();// インスタンスを提供してくれる人を作るusingvarprovider=services.BuildServiceProvider();varmyService=provider.GetService<IMyService>();myService.Greet();}}
実行結果は Hello world
と表示されるだけです。
インスタンス管理
AddTransient で登録するとコンテナから取得するたびに別のインスタンスを返します。AddSingleton で登録すると毎回同じインスタンスになります。AddScoped で登録すると同じスコープ内だと同じインスタンスになります。
スコープを作るには ServiceCollection に BuildServiceProvider をした結果の ServiceProvider の CreateScope メソッドを使います。各クラスのコンストラクタが呼ばれたときにわかりやすいように標準出力にメッセージを出すように手を加えた後に以下のようにコードを書き替えてみました。
classProgram{staticvoidMain(string[]args){// 型の登録varservices=newServiceCollection();services.AddScoped<IMyService,MyService>();services.AddSingleton<IMessagePrinter,ConsoleMessagePrinter>();services.AddSingleton<IMessageGenerator,MyMessageGenerator>();// インスタンスを提供してくれる人を作るusingvarprovider=services.BuildServiceProvider();Console.WriteLine("Scope1");using(varscope=provider.CreateScope()){vars=scope.ServiceProvider.GetService<IMyService>();s.Greet();}Console.WriteLine("Scope2");using(varscope=provider.CreateScope()){vars=scope.ServiceProvider.GetService<IMyService>();s.Greet();}}}
MyService が AddScoped で残りは AddSingleton にしてみました。
実行すると以下のようになります。
Scope1
ConsoleMessagePrinter のコンストラクタ
MyMessageGenerator のコンストラクタ
MyService のコンストラクタ
Hello world
Scope2
MyService のコンストラクタ
Hello world
Singleton のものはスコープが変わってもインスタンスは新たに作られなくて、AddScoped で登録したものはスコープが変わると再生成されてることがわかります。
生成処理をカスタマイズしたい
AddScoped や AddTransient や AddSingleton はラムダ式を受け取るオーバーライドがあって、それを使うとオブジェクトの生成処理をカスタマイズできるようになっています。
例えば MyService の生成ロジックを自前のものに置き換えたコードを以下に示します。ちなみに、このコードの場合は別に生成処理を変えたところで意味はありません。単純に new してるだけなので。
classProgram{staticvoidMain(string[]args){// 型の登録varservices=newServiceCollection();services.AddScoped<IMyService,MyService>(provider=>{// ここで任意の生成ロジックを入れることが出来るvarprinter=provider.GetRequiredService<IMessagePrinter>();vargenerator=provider.GetRequiredService<IMessageGenerator>();returnnewMyService(printer,generator);});services.AddSingleton<IMessagePrinter,ConsoleMessagePrinter>();services.AddSingleton<IMessageGenerator,MyMessageGenerator>();// インスタンスを提供してくれる人を作るusingvarprovider=services.BuildServiceProvider();Console.WriteLine("Scope1");using(varscope=provider.CreateScope()){vars=scope.ServiceProvider.GetService<IMyService>();s.Greet();}Console.WriteLine("Scope2");using(varscope=provider.CreateScope()){vars=scope.ServiceProvider.GetService<IMyService>();s.Greet();}}}
実行結果は同じです。
Microsoft.Extensions.DependencyInjection について深く知りたい人は、Microsoft.Extensions.DependencyInjection Deep Diveを見てみるといいと思います。
他の DI コンテナと使いたい
とまぁ、こんな感じで必要最低限の機能セット(登録と取得とシンプルなライフサイクル管理とインスタンス生成のカスタマイズ)がある程度なのですが、もうちょっと高度な機能を持った DI コンテナを使いたいという要望に応えられるようになっています。
以下にリストがあります。
https://github.com/dotnet/runtime/tree/master/src/libraries/Microsoft.Extensions.DependencyInjection
試しに Unity を使ってみましょう。Unity は昔は Microsoft がメンテナンスしてた OSS の DI コンテナで、今は完全に Microsoft から離れてメンテナンスされています。
Unity.Microsoft.DependencyInjection
パッケージを追加することで Unity が使えるようになります。ただの DI コンテナとして使うだけなら別に Unity をあえて使う必要はないので、追加で Unity.Interception
パッケージも追加してみようと思います。
ということでこんな感じで IMyService はログを出すような追加処理が入るようにしてみました。
usingMicrosoft.Extensions.DependencyInjection;usingSystem;usingSystem.Collections.Generic;usingUnity;usingUnity.Interception;usingUnity.Interception.ContainerIntegration;usingUnity.Interception.InterceptionBehaviors;usingUnity.Interception.Interceptors.InstanceInterceptors.InterfaceInterception;usingUnity.Interception.PolicyInjection.Pipeline;usingUnity.Lifetime;usingUnity.Microsoft.DependencyInjection;namespaceUnityLab{publicinterfaceIMyService{voidGreet();}publicclassMyService:IMyService{privatereadonlyIMessagePrinter_messagePrinter;privatereadonlyIMessageGenerator_messageGenerator;publicMyService(IMessagePrintermessagePrinter,IMessageGeneratormessageGenerator){_messagePrinter=messagePrinter;_messageGenerator=messageGenerator;}publicvoidGreet()=>_messagePrinter.Print(_messageGenerator.Generate());}publicinterfaceIMessagePrinter{voidPrint(stringmessage);}publicclassConsoleMessagePrinter:IMessagePrinter{publicvoidPrint(stringmessage)=>Console.WriteLine(message);}publicinterfaceIMessageGenerator{stringGenerate();}publicclassMyMessageGenerator:IMessageGenerator{publicstringGenerate()=>"Hello world";}publicclassLogBehavior:IInterceptionBehavior{privatereadonlyIMessagePrinter_messagePrinter;publicboolWillExecute=>true;publicLogBehavior(IMessagePrintermessagePrinter){_messagePrinter=messagePrinter;}publicIEnumerable<Type>GetRequiredInterfaces()=>Type.EmptyTypes;publicIMethodReturnInvoke(IMethodInvocationinput,GetNextInterceptionBehaviorDelegategetNext){_messagePrinter.Print($"Begin: {input.MethodBase.Name}");try{varresult=getNext()(input,getNext);_messagePrinter.Print($"End: {input.MethodBase.Name}");returnresult;}catch(Exceptionex){_messagePrinter.Print($"Exception: {input.MethodBase.Name}, {ex}");throw;}}}classProgram{staticvoidMain(string[]args){// 型の登録varservices=newServiceCollection();services.AddSingleton<IMessagePrinter,ConsoleMessagePrinter>();services.AddSingleton<IMessageGenerator,MyMessageGenerator>();// Unity のコンテナに登録してログ機能も追加varcontainer=newUnityContainer().AddNewExtension<Interception>();container.RegisterType<IMyService,MyService>(newSingletonLifetimeManager(),newInterceptor<InterfaceInterceptor>(),newInterceptionBehavior<LogBehavior>());// インスタンスを提供してくれる人を作るvarprovider=services.BuildServiceProvider(container);vars=provider.GetService<IMyService>();s.Greet();}}}
実行すると以下のような感じになります。
Begin: Greet
Hello world
End: Greet
内部的には ServiceCollection に登録されている情報を見て UnityContainer に登録処理をして、UnityContainer をラップする IServiceProvider が作られてる感じです。
なので、ServiceCollection で登録したやつも UnityContainer で登録したやつも同じコンテナにあるように(実際同じコンテナにあるので)インジェクション出来ます。
まとめ
ここら辺まで出来たら、あとは ASP.NET Core あたりのドキュメントを見ながらぽちぽちやってみるのがいいと思います。