かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

DIコンテナのUnityのLifetimeManagerを拡張して任意のタイミングでインスタンスの破棄をする

UnityのLifetimeManagerはシングルトンで管理するContainerControlledLifetimeManagerか、デフォルトの毎回newする‘PerResolveLifetimeManager‘か、スレッド単位のPerThreadLifetimeManagerが用意されています。あとマニアックなところだと、ExternallyControlledLifetimeManagerとかいう弱参照で管理されるものもあります。

今回は、LifetimeManagerを拡張して任意のタイミングで破棄できるLifetimeManagerを作ってみようと思います。

破棄を通知するインターフェースの定義

まず、オブジェクトが破棄されたことを通知するインターフェースを定義します。とりあえず今回はイベントの発火でライフサイクルの終了を通知するようにしてみようと思うので、シンプルにCompletedイベントだけを持ったインターフェースを定義しました。

interface ITransactionPolicy
{
    event EventHandler Completed;
}

ITransactionPolicyを使ったLifetimeManagerの定義

後は、ITransactionPolicyCompletedイベントが起きたらインスタンスを破棄するようにするLifetimeManagerの実装を作るだけです。

sealed class TransactionLifetimeManager : LifetimeManager, IDisposable
{
    private object value;

    public override object GetValue()
    {
        return value;
    }

    public override void RemoveValue()
    {
        this.Dispose();
    }

    public override void SetValue(object newValue)
    {
        value = newValue;
        var tx = value as ITransactionPolicy;
        if (tx != null)
        {
            tx.Completed += this.Tx_Completed;
        }
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposable)
    {
        if (disposable)
        {
            (value as IDisposable)?.Dispose();
            this.value = null;
        }
    }


    private void Tx_Completed(object sender, EventArgs e)
    {
        ((ITransactionPolicy)this.value).Completed -= this.Tx_Completed;
        this.RemoveValue();
    }
}

使ってみよう

使い方は簡単です。ITransactionPolicyを実装してTransactionLifetimeManagerで登録するだけ。

// こっちはライフサイクルを制御する
class Person : ITransactionPolicy, IDisposable
{
    private Service Service { get; }
    public Person(Service service)
    {
        this.Service = service;
        Console.WriteLine("Person#Constructor");
    }

    public event EventHandler Completed;

    public void Complete()
    {
        this.Completed?.Invoke(this, EventArgs.Empty);
    }

    public void Dispose()
    {
        Console.WriteLine("Person#Dispose");
    }
}

// こっちは普通のクラス
class Service
{
    public Service()
    {
        Console.WriteLine("Service#Constructor");
    }
}

コンテナに登録して使ってみましょう。

var c = new UnityContainer();
// Serviceはシングルトン
c.RegisterType<Service>(new ContainerControlledLifetimeManager());
// Personは任意のタイミングで破棄
c.RegisterType<Person>(new TransactionLifetimeManager());

// とりあえずインスタンス取得
Console.WriteLine("Resolve<Person>()");
var p = c.Resolve<Person>();

// 2回目取得しても同じインスタンスが取れることを確認
Console.WriteLine("Resolve<Person>()");
Console.WriteLine(p == c.Resolve<Person>() ? "同じインスタンス" : "違うインスタンス");

// インスタンスを明示的に破棄
Console.WriteLine("RaiseCompleted");
p.Complete();

// 違うインスタンスが取れることを確認
Console.WriteLine("Resolve<Person>()");
Console.WriteLine(p == c.Resolve<Person>() ? "同じインスタンス" : "違うインスタンス");

// コンテナの破棄時にDisposeが呼ばれることも確認
Console.WriteLine("Container#Dispose");
c.Dispose();

実行すると以下のような出力が得られます。

Resolve<Person>()
Service#Constructor
Person#Constructor
Resolve<Person>()
同じインスタンス
RaiseCompleted
Person#Dispose
Resolve<Person>()
Person#Constructor
違うインスタンス
Container#Dispose
Person#Dispose

ちゃんとServiceはシングルトンでPersonCompletedイベントの発火とともに削除されてることが確認できます。

注意点

スレッドセーフではないのでWebアプリケーションみたいに不特定多数のスレッドからコンテナにアクセスするような環境下では使わないほうが幸せです。クライアントアプリとかでUIスレッド上からしかコンテナにアクセスしないような環境下で使いましょう。

まとめ

Unityはいいぞ。

LifetimeManagerの拡張は、はじめてやったけど結構簡単でした。

追記

3年前にやってた。

code.msdn.microsoft.com