C#のDI ContainerのIDisposableへの参照保持について

C#のDI Containerはいろいろ (https://github.com/danielpalme/IocPerformance) 存在します。一部のコンテナでは自動DisposeのためにIDisposableインスタンスへの参照が保持(Tracking)され、注意しないとMemoryOverflowExceptionが発生することがあります。Trackingするコンテナとそうでないコンテナを調査してみました。

# 結果

各種DI Containerをできるだけデフォルト状態で調査したところ、以下のようになりました。

|DI Container   |Normal|Disposable|
|:--------------|:-----|:---------|
|Autofac        |    No|     Track|
|LightInject    |    No|        No|
|abioc          |    No|        No|
|DryIoc(NoTrack)|    No|        No|
|DryIoc(Track)  |    No|     Track|
|Grace          |    No|     Track|
|MicroResolver  |    No|        No|
|Unity.Container|    No|        No|

AutofacとGraceがIDisposableをTrackingするようです。

DryIocは２つありますが、DryIocだとどちらの挙動がよいか設定で選択できます。なにも選択しない状態だとIDisposableを実装する型を登録するとエラーが発生し、Trackingするかどうかを指示するように警告されるので、結構親切ですね。

なお、調査したときのバージョンは以下のとおりです。

* Autofac, Version=4.9.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da
* LightInject, Version=5.4.0.0, Culture=neutral, PublicKeyToken=null
* abioc, Version=0.7.0.0, Culture=neutral, PublicKeyToken=null
* DryIoc, Version=4.0.0.0
* Grace, Version=6.4.2.0, Culture=neutral, PublicKeyToken=b7d24c6367970497
* MicroResolver, Version=2.3.5.0, Culture=neutral, PublicKeyToken=null
* Unity.Container, Version=5.10.2.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0

# 調査の発端

ここからは余談ですが、

C#アプリでAutofacを使用していたのですが、バッチ処理とかで大量のクラス生成を繰り返す処理を実行すると、アプリ内ではどこからも参照されていないのに、インスタンスが解放されず、OutOfMemoryExceptionが発生する問題に遭遇しました。

検索すると以下の記事がヒットします。

https://nblumhardt.com/2011/01/an-autofac-lifetime-primer/

> Autofac **holds references** to all the disposable components it creates

「Autofacは生成したすべてのIDsposableインスタンスへの**参照を保持**する」ので、

以下のようにLifetimeScopeで囲ってあげないとMemoryLeakしてMemoryOverflowExceptionになる。`lifetimeScope.Resolve<IMyResource>();`を`container.Resolve<IMyResource>();`と書いてしまうとMemoryLeakになるということですね。

```c#
// var container = …
while (true)
{
    using (var lifetimeScope = container.BeginLifetimeScope())
    {
        var r = lifetimeScope.Resolve<IMyResource>();
        // r, all of its dependencies and any other components
        // created indirectly will be released here
    }
}
```

> Don’t resolve from the root container. Always resolve from and then release a lifetime scope.

「常に**lifetime scope**からインスタンスを生成し、タスク完了時にscopeをdisposeしなければならない」

Autofacの思想としては、大量データの生成、破棄をともなう繰り返し処理はコンテナに依存するようにし、厳密にLifetime管理せよ、ということなのでしょう。Autofacは参照しているIDisposableインスタンスのDispose()を自動で呼び出してくれるようで、それも便利なケースもありそうですね。

Autofacより後発のLightInjectではこのようなメモリリークは発生しません。前掲の記事によると、TrackingなDI ContainerとそうでないDI Containerがあるようです。そこで、C#のDI ContainerをTrackingなDI Containerとそれ以外で分類してみました。

# 調査方法

`InnerRun`メソッドのスコープ内でResolveしたオブジェクトのFinalizeをcallbackで通知を受け、`GC.Collect()`後にFinalizeされたかどうかを返します。

```csharp
public bool Run(Type t)
{
    var container = CreateContainer(t);
    var finalized = false;
    InnerRun(container, t, () => {finalized = true;});

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    return finalized;
}

private void InnerRun(object container, Type t, Action callback)
{
    // Resolve in this scope.
    var obj = Resolve(container, t);
    obj.FinalizeCallback = callback;

    // Scope out, then no reference to obj.
}
```

Finalizeを監視する対象のBaseクラスは以下のようにしました。これを通常のオブジェクト(Normal)とIDisposableを実装したオブジェクトそれぞれに継承させるようにしています。

```csharp
public abstract class FinalizeCallbackable
{
    public Action FinalizeCallback {get; set;} = null;

    ~FinalizeCallbackable()
    {
        Debug.WriteLine($"Finalize({this.GetType()})");
        FinalizeCallback?.Invoke();
    }
}
```

ソースコードはこちらです。.NET Core 2.2で動作確認しました。
https://github.com/youmts/dicontainer-tracking-checker/