やっとコードよりの話になれる!!過去の 2 記事は言語ごとの事情や、その人の経験などで色々ちょっとずつ異なることがあるので「〇〇の場合は違う」とか「こういう側面もある」とか色々コメントしやすい感じだったのですが、そのおかげで初めての Qiita のデイリーで No1 取れました。やったね!
ということで、自分の主戦場の C# での DI コンテナ事情について書いてみたいと思います。
Microsoft.Extensions.DependencyInjection
ASP.NET Core などで何も考えないと使うことになる、事実上の標準の DI コンテナです。
非常にシンプルで DI コンテナとして最低限これくらいは持ってるだろうと思われる機能だけ持ってます。
例えば、以下のようなクラスがあったとします。
interface IMyService
{
void Greet();
}
class MyService : IMyService
{
private readonly IMessagePrinter _messagePrinter;
private readonly IMessageGenerator _messageGenerator;
public MyService(IMessagePrinter messagePrinter,
IMessageGenerator messageGenerator)
{
_messagePrinter = messagePrinter;
_messageGenerator = messageGenerator;
}
public void Greet() => _messagePrinter.Print(_messageGenerator.Generate());
}
interface IMessagePrinter
{
void Print(string message);
}
class ConsoleMessagePrinter : IMessagePrinter
{
public void Print(string message) => Console.WriteLine(message);
}
interface IMessageGenerator
{
string Generate();
}
class MyMessageGenerator : IMessageGenerator
{
public string Generate() => "Hello world";
}
Microsoft.Extensions.DependencyInjection を使うと上記のクラスを組み立て可能なコンテナを作って IMyService を取得して Greet を呼び出すコードは以下のような感じになります。
class Program
{
static void Main(string[] args)
{
// 型の登録
var services = new ServiceCollection();
services.AddTransient<IMyService, MyService>();
services.AddTransient<IMessagePrinter, ConsoleMessagePrinter>();
services.AddTransient<IMessageGenerator, MyMessageGenerator>();
// インスタンスを提供してくれる人を作る
using var provider = services.BuildServiceProvider();
var myService = provider.GetService<IMyService>();
myService.Greet();
}
}
実行結果は Hello world
と表示されるだけです。
インスタンス管理
AddTransient で登録するとコンテナから取得するたびに別のインスタンスを返します。AddSingleton で登録すると毎回同じインスタンスになります。AddScoped で登録すると同じスコープ内だと同じインスタンスになります。
スコープを作るには ServiceCollection に BuildServiceProvider をした結果の ServiceProvider の CreateScope メソッドを使います。各クラスのコンストラクタが呼ばれたときにわかりやすいように標準出力にメッセージを出すように手を加えた後に以下のようにコードを書き替えてみました。
class Program
{
static void Main(string[] args)
{
// 型の登録
var services = new ServiceCollection();
services.AddScoped<IMyService, MyService>();
services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>();
services.AddSingleton<IMessageGenerator, MyMessageGenerator>();
// インスタンスを提供してくれる人を作る
using var provider = services.BuildServiceProvider();
Console.WriteLine("Scope1");
using (var scope = provider.CreateScope())
{
var s = scope.ServiceProvider.GetService<IMyService>();
s.Greet();
}
Console.WriteLine("Scope2");
using (var scope = provider.CreateScope())
{
var s = 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 してるだけなので。
class Program
{
static void Main(string[] args)
{
// 型の登録
var services = new ServiceCollection();
services.AddScoped<IMyService, MyService>(provider =>
{
// ここで任意の生成ロジックを入れることが出来る
var printer = provider.GetRequiredService<IMessagePrinter>();
var generator = provider.GetRequiredService<IMessageGenerator>();
return new MyService(printer, generator);
});
services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>();
services.AddSingleton<IMessageGenerator, MyMessageGenerator>();
// インスタンスを提供してくれる人を作る
using var provider = services.BuildServiceProvider();
Console.WriteLine("Scope1");
using (var scope = provider.CreateScope())
{
var s = scope.ServiceProvider.GetService<IMyService>();
s.Greet();
}
Console.WriteLine("Scope2");
using (var scope = provider.CreateScope())
{
var s = scope.ServiceProvider.GetService<IMyService>();
s.Greet();
}
}
}
実行結果は同じです。
Microsoft.Extensions.DependencyInjection について深く知りたい人は、Microsoft.Extensions.DependencyInjection Deep Dive を見てみるといいと思います。
他の DI コンテナと使いたい
とまぁ、こんな感じで必要最低限の機能セット(登録と取得とシンプルなライフサイクル管理とインスタンス生成のカスタマイズ)がある程度なのですが、もうちょっと高度な機能を持った DI コンテナを使いたいという要望に応えられるようになっています。
以下にリストがあります。
試しに Unity を使ってみましょう。Unity は昔は Microsoft がメンテナンスしてた OSS の DI コンテナで、今は完全に Microsoft から離れてメンテナンスされています。
Unity.Microsoft.DependencyInjection
パッケージを追加することで Unity が使えるようになります。ただの DI コンテナとして使うだけなら別に Unity をあえて使う必要はないので、追加で Unity.Interception
パッケージも追加してみようと思います。
ということでこんな感じで IMyService はログを出すような追加処理が入るようにしてみました。
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using Unity;
using Unity.Interception;
using Unity.Interception.ContainerIntegration;
using Unity.Interception.InterceptionBehaviors;
using Unity.Interception.Interceptors.InstanceInterceptors.InterfaceInterception;
using Unity.Interception.PolicyInjection.Pipeline;
using Unity.Lifetime;
using Unity.Microsoft.DependencyInjection;
namespace UnityLab
{
public interface IMyService
{
void Greet();
}
public class MyService : IMyService
{
private readonly IMessagePrinter _messagePrinter;
private readonly IMessageGenerator _messageGenerator;
public MyService(IMessagePrinter messagePrinter,
IMessageGenerator messageGenerator)
{
_messagePrinter = messagePrinter;
_messageGenerator = messageGenerator;
}
public void Greet() => _messagePrinter.Print(_messageGenerator.Generate());
}
public interface IMessagePrinter
{
void Print(string message);
}
public class ConsoleMessagePrinter : IMessagePrinter
{
public void Print(string message) => Console.WriteLine(message);
}
public interface IMessageGenerator
{
string Generate();
}
public class MyMessageGenerator : IMessageGenerator
{
public string Generate() => "Hello world";
}
public class LogBehavior : IInterceptionBehavior
{
private readonly IMessagePrinter _messagePrinter;
public bool WillExecute => true;
public LogBehavior(IMessagePrinter messagePrinter)
{
_messagePrinter = messagePrinter;
}
public IEnumerable<Type> GetRequiredInterfaces() => Type.EmptyTypes;
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
_messagePrinter.Print($"Begin: {input.MethodBase.Name}");
try
{
var result = getNext()(input, getNext);
_messagePrinter.Print($"End: {input.MethodBase.Name}");
return result;
}
catch (Exception ex)
{
_messagePrinter.Print($"Exception: {input.MethodBase.Name}, {ex}");
throw;
}
}
}
class Program
{
static void Main(string[] args)
{
// 型の登録
var services = new ServiceCollection();
services.AddSingleton<IMessagePrinter, ConsoleMessagePrinter>();
services.AddSingleton<IMessageGenerator, MyMessageGenerator>();
// Unity のコンテナに登録してログ機能も追加
var container = new UnityContainer()
.AddNewExtension<Interception>();
container.RegisterType<IMyService, MyService>(
new SingletonLifetimeManager(),
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<LogBehavior>());
// インスタンスを提供してくれる人を作る
var provider = services.BuildServiceProvider(container);
var s = provider.GetService<IMyService>();
s.Greet();
}
}
}
実行すると以下のような感じになります。
Begin: Greet
Hello world
End: Greet
内部的には ServiceCollection に登録されている情報を見て UnityContainer に登録処理をして、UnityContainer をラップする IServiceProvider が作られてる感じです。
なので、ServiceCollection で登録したやつも UnityContainer で登録したやつも同じコンテナにあるように(実際同じコンテナにあるので)インジェクション出来ます。
まとめ
ここら辺まで出来たら、あとは ASP.NET Core あたりのドキュメントを見ながらぽちぽちやってみるのがいいと思います。