5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Prism.Unity のライフサイクル調査

Posted at

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つ あります。

  1. 型からの登録
  2. Factoryを使用した登録
  3. インスタンスの登録

何れも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

参考にさせて頂いたページ

Lifetime - UnityContainer

Unity Container - GitHub

Disposing needed in Unity? - stackoverflow

PRISM/Unity IDisposable - stackoverflow

Prism 7.x で DI コンテナ固有の機能を使いたい

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?