Prism.Unity の環境で アプリ終了時に DIコンテナ の登録インスタンスを Dispose したく、UnityContainer のライフサイクル について調査した記事です。
ゲームエンジンの "unity" とは全く関係ありません。
本記事の大半は Prism.Unity 限定ではなく、UnityContainer で共通の内容です。
はじめに
私は業務で稀に WPF アプリを作る機会があり、その際は Prism + UnityContainer をベースに開発しています。
Prism では UnityContainer 以外にもいくつかの DIライブラリ(Autofac, Ninject, DryIoc) をサポートおり、特別 UnityContainer に拘っていないのですが「Microsoft が関わっていた」との理由から なんとなーくで選んでいます。
と言っても私の場合は、ModelクラスのいくつかをDIコンテナに登録する程度のライトな用途なので、Prism が提供してくれているインターフェイス越しの操作で十分でした。
先日、アプリ終了時に DIコンテナに登録したインスタンスを Dispose したい 機会があり、Prism提供の共通インターフェイス に物足りなさを感じましたので、正月休みを利用して UnityContainer の ライフサイクル を調査してみました。
Prismでの生DIコンテナの取得
かずき先生が Prism 7.x で DI コンテナ固有の機能を使いたい にて詳しくまとめて下さっていますので、そちらに案内させて頂きます。毎度お世話になっております🙇♂️
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
IUnityContainer unityContainer = containerRegistry.GetContainer();
}
DIコンテナへの登録方法
UnityContainer への登録方法は大きく分けて 3つ あります。
- 型からの登録
- Factoryを使用した登録
- インスタンスの登録
何れもDIコンテナの一般的な概念だと思いますので詳細は割愛します。
今回は最もベタな登録方法だと思われる 『1.型からの登録』のライフサイクルのみを調査しました。
型登録時のライフライクル
型登録時のライフサイクルは、RegisterType<T>()
の引数 ITypeLifetimeManager lifetimeManager
から指定できます。
以降は ITypeLifetimeManager
の取得メソッドを提供する static class TypeLifetime
のメソッド名でまとめてみました。(TypeLifetimeクラスのソースコード)
丸括弧は取得メソッドから返却される実クラス名です。
それでは順に見ていきましょう。
1. Transient (TransientLifetimeManager)
-
Resolve<T>()
の呼び出しの度に、新しいインスタンスが生成されて返却されます。 -
RegisterType<T>()
のデフォルトのライフタイムで、ライフタイムを管理しないことと同義です。// 明示的に TypeLifetime.Transient を渡さないようにましょう(公式より) container.RegisterType<Foo>(); container.RegisterType<IService, Service>(); var s1 = container.Resolve<IService>(); var s2 = container.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s2)); // "false"
2. Singleton (SingletonLifetimeManager)
-
ガチのシングルトンです。
-
後続の
Resolve<T>()
の呼び出し や コンストラクタインジェクションなどのインスタンス挿入で 同一インスタンスが返却されます。 -
RegisterType<T>()
の登録結果は コンテナの親子ツリー全体に反映されます。(子コンテナ に登録した場合、ルート親コンテナに登録されます。) -
コンテナが生成したインスタンスの参照を保持します。インスタンスが
IDisposable
を継承している場合、コンテナ自体のDispose()
時に保持インスタンスを Dispose してくれます。 -
登録済みの型を再登録した場合は、新しい登録が優先されます。
container.RegisterType<IService, DisposableService>(TypeLifetime.Singleton); var s1 = container.Resolve<IService>(); var s2 = container.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s2)); // "true" container.Dispose(); // s1,s2 を Dispose してくれる
3. ContainerControlled / PerContainer (ContainerControlledLifetimeManager)
-
Singleton と同じ挙動です。
-
こちらが Singleton の基底クラスであり、Singletonクラスは実装がほぼ空なので (ソースコード)、迷った場合はこちらを使用しましょう。
-
ちなみに、
RegisterSingleton<T>()
のライフタイム にも Singleton ではなく、こちらが指定されています。(ソースコード)container.RegisterType<IService, Service>(TypeLifetime.PerContainer); var s1 = container.Resolve<IService>(); var s2 = container.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s2)); // "true" container.Dispose(); // s1,s2 を Dispose してくれる
4. PerContainerTransient (ContainerControlledTransientManager)
-
Transient と同様に
Resolve<T>()
の呼び出しの度に、新しいインスタンスが生成されて返却されます。 -
Transient の違いとして(PerContainer と同様に)コンテナが生成したインスタンスの参照を保持します。
container.RegisterType<IService, Service>(TypeLifetime.PerContainerTransient); var s1 = container.Resolve<IService>(); var s2 = container.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s2)); // "false" container.Dispose(); // s1,s2 を Dispose してくれる
5. Hierarchical / Scoped (HierarchicalLifetimeManager)
-
同一のコンテナであれば PerContainer と同じ挙動です。(シングルトン、参照保持)
-
PerContainer との違いとして、親コンテナ と 各子コンテナ で情報が共有されません。
Resolve<T>()
を呼び出したコンテナに対応するインスタンスが返却されます。container.RegisterType<IService, Service>(TypeLifetime.Hierarchical); var s1 = container.Resolve<IService>(); var s2 = container.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s2)); // "true" using (var child = container.CreateChildContainer()) { child.RegisterType<IService, Service>(TypeLifetime.Hierarchical); var s3 = child.Resolve<IService>(); Console.WriteLine(ReferenceEquals(s1, s3)); // "false" } // s3 を Dispose してくれる
6. PerResolve (PerResolveLifetimeManager)
-
1回の解決呼び出し中だけインスタンスへの参照を保持します。
-
文字での説明が難しいのですが、下記の依存関係の場合に ClassC を生成した場合、"ClassB 内の ClassA" と "ClassC 内の ClassA" を同一インスタンスにしてくれます。
// クラスの依存関係 class ClassA {} class ClassB { public ClassB(ClassA a) => _a = a; } class ClassC { public ClassC(ClassA a, ClassB b) => (_a, _b) = (a, b); }
container.RegisterType<ClassA>(TypeLifetime.PerResolve); container.RegisterType<ClassB>(); container.RegisterType<ClassC>(); var c = container.Resolve<ClassC>(); Console.WriteLine(ReferenceEquals(c.b.a, c.a)); // "true"
-
参照の保持は1回の解決呼び出し中だけなので、異なる ClassC の ClassA は別インスタンスとなります。
var c1 = container.Resolve<ClassC>(); var c2 = container.Resolve<ClassC>(); Console.WriteLine(ReferenceEquals(c1.a, c2.a)); // "false"
7. PerThread (PerThreadLifetimeManager)
-
スレッドごとのシングルトンです。
-
参照は保持されません。コンテナを
Dispose()
しても各インスタンスは Dispose されません。container.RegisterType<IService, Service>(TypeLifetime.PerThread); var s1 = container.Resolve<IService>(); var s2 = await Task.Run(() => container.Resolve<IService>()); Console.WriteLine(ReferenceEquals(s1, s2)); // "false"
Prism の DIコンテナ インターフェイス
Prism の DIコンテナ インターフェイスである IContainerRegistry
を使用して登録した場合に、UnityContainer のどの Lifetime が使用されるかを整理しました。ソースコード
IContainerRegistry の登録メソッド | UnityContainer Lifetime |
---|---|
Register<T>() | Transient |
RegisterSingleton<T>() | ContainerControlled |
RegisterScoped<T>() | Hierarchical |
ここまでのライフサイクル調査のおかげで、RegisterSingleton<T>()
と RegisterScoped<T>()
の場合、アプリ終了時に DIコンテナ自体 を Dispose()
することで、参照を保持しているインスタンス達 を Dispose してくれることが分かりました。
少しやっつけですが、以下の実装で良いかと思われます。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IService, Service>();
containerRegistry.RegisterScoped<IReader, Reader>();
// Exit時に Service と Reader も Dispose される。
this.Exit += (_, _) => containerRegistry.GetContainer().Dispose();
}
Register<T>()
(Transient)の場合は DIコンテナが何もしてくれないので、Resolve<T>()
でインスタンスを取得した人が Dispose を行う必要があります。
おわりに
Prism.Unity の環境で アプリ終了時に DIコンテナ の登録インスタンスを Dispose するため、UnityContainer のライフサイクル について調査しました。
もう少し詳しく調べたい部分もあり 後ろ髪を引かれましたが、年内に完了させてスッキリしたく公開しました。それでは良いお年を🎍
確認環境
VisualStudio 2019 16.8.3
.NET 5.0 + C# 9.0
Prism.Unity 8.0.0.1909
Unity.Container 5.11.9
参考にさせて頂いたページ
Disposing needed in Unity? - stackoverflow