IDisposableのクラスをTransientで生成した時のメモリーリークを解決する

DIは、その考え方は理解しやすいです。そして、DIを簡単に実現するためにDIコンテナ（IoCコンテナ）があります。しかし、そのDIコンテナをどう使うと良いのか、判断に迷うことがあります。それが難しい部分です。

## 課題
`Microsoft.Extensions.DependencyInjection`での課題の１つは、次です。
`Transient`で登録した（解決する）クラスは、DIコンテナの`Scope`に紐付きます。しかし、`Transient`のクラスに`IDisposable`があると、`Scope`を`Dispose`するまで、そのクラスのインスタンスは破棄されません。その結果、意図しないメモリーリークになります。
しかし、これは仕様です。そして、仕様を決める時に検討した上でこの仕様にしたそうです。

- [Disposable transient services captured by container](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#disposable-transient-services-captured-by-container)
- [コンテナーによってキャプチャされる破棄可能な一時サービス](https://learn.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection-guidelines#disposable-transient-services-captured-by-container)

## 解決方法
では、`IDisposable`のクラスを、`Transient`で利用するにはどうすると良いか？
最適な解決策は、調べた限りではありませんでした。困っている人多数です。
私も困ったため、次のような方法を考えました。テスト上では機能します。

`static`な、`ServiceScopeManager`で、`scope`の`Dispose`を行うのがポイントです。

```csharp
      Ioc.Default.ConfigureServices(
              new ServiceCollection()
              .AddTransient<ISub3, Sub3>() //IDisposable
              .AddSingleton<ISub3Factory, Sub3Factory>()
              .BuildServiceProvider());
```
```csharp
internal class Sub3Factory : ISub3Factory
{
    public ISub3 Create()
    {
        var serviceScope = Ioc.Default.CreateScope();
        var sub3 = serviceScope.ServiceProvider.GetRequiredService<ISub3>();
        ServiceScopeManager.ServiceScopeDict.Add((IDisposable)sub3, serviceScope);
        return sub3;
    }
}

public interface ISub3
{
    public int SomeMethod();
}

public interface ISub3Factory
{
    ISub3 Create();
}

internal class Sub3 : ISub3, IDisposable
{
    public Sub3()
    {
        Debug.WriteLine("new Sub3");
    }

    public int SomeMethod()
    {
        return 0;
    }

    public void Dispose()
    {
        Debug.WriteLine("Dispose Sub3");
    }
}
```
```csharp
public static class ServiceScopeManager
{
    public static Dictionary<IDisposable, IServiceScope> ServiceScopeDict { get; } = [];

    public static void DisposeInstance(IDisposable obj)
    {
        if (ServiceScopeDict.TryGetValue(obj, out IServiceScope? serviceScope))
        {
            ServiceScopeDict.Remove(obj);
            serviceScope.Dispose();
        }
    }
}
```

このスコープに登録された`IDisposable`のインスタンスを破棄するタイミングで、次を実行します。
```
ServiceScopeManager.DisposeInstance((IDisposable)sub3);
```
すると、`serviceScope.Dispose()`された時点で、それぞれのインスタンスの`Dispose`が実行されます。
つまり、`Scope`の`Dispose`のタイミングを、コントロールするのです。


## デメリット
アプリ本体のコードは、DIについて知る必要はない、という考え方もあるようです。
しかし、この方法では、`Microsoft.Extensions.DependencyInjection`に対応するためだけのコードを、アプリケーション本体のコードに追加する必要があります。これは、他のDIコンテナでは、不要かもしれません。

## サンプルプロジェクト

https://github.com/mikihiro-t/DITransientStudy/

#### Sub
- `Sub1` `IDisposable`ではない
- `Sub2` `IDisposable`
- `Sub3` `IDisposable`

`Add Sub Instance`
Sub1, Sub2, Sub3のインスタンスをDIコンテナから生成し、リストに追加。
 
`Remove Sub Instance`
それぞれのインスタンスをリストから削除。

`Sub2`は、リストから削除しても、インスタンスが残りメモリーリークする。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132059/051d3375-d08b-404f-842e-aa6757094b13.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132059/3294dd48-f827-40c1-9357-6341f82d7c98.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132059/e21c2ccc-6f4c-4d18-8f0d-bd2ba04ab549.png)

#### SubView
- `SubView1, SubView2`は、どちらも、`IDisposable`

`Show SubView1`　はメモリーリークする
`Show SubView2`　はメモリーリークしない。

```csharp
internal class SubView1Factory : ISubView1Factory
{
    public SubView1 Instance() => Ioc.Default.GetRequiredService<SubView1>();
}

internal class SubView2Factory : ISubView2Factory
{
    public SubView2 Instance()
    {
        var serviceScope = Ioc.Default.CreateScope();
        var subView2 = serviceScope.ServiceProvider.GetRequiredService<SubView2>();
        ServiceScopeManager.ServiceScopeDict.Add((IDisposable)subView2, serviceScope);
        return subView2;
    }
}
```

## その他
- `Microsoft.Extensions.DependencyInjection`は、コンストラクターで、パラメーターを渡す(`IOptions`の利用では間に合わない場合）機能がありません。これを行うには、別途、実装する必要があります。しかし、試しにコードを書いてみると、さらに別の課題が出てきました。
私の場合、他のDIコンテナの利用を調査中です。
- `Microsoft.Extensions.DependencyInjection`を使って、相当作り込んでから、これでは間に合わないことがわかりました。
あるパッケージを利用する時に、良さそうだったのでそれを使って作り込んでいくと、課題が出てきて解決できず、他のパッケージの利用に切り替える。こうしたことは、時々あります。

## 参考

##### Microsoft公式
Microsoft公式では、次のように書かれていますが、これを読んだだけで理解できるとは思えません。
- [IDisposable guidance for transient and shared instances](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#idisposable-guidance-for-transient-and-shared-instances)

- [一時的なインスタンスと共有インスタンスのための IDisposable ガイダンス](https://learn.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection-guidelines#idisposable-guidance-for-transient-and-shared-instances)

##### メモリーリークについて
- [Microsoft DI Container _transientDisposables not disposed (memory leak)](https://stackoverflow.com/a/58266542/)
> Disposable transients are referenced inside the scope so they can be disposed when the scope ends.

- [Transient IDisposable Memory Leak](https://stackoverflow.com/a/75602280/)

>scope on the disposal will dispose the services which implement IDisposable

##### Transientの有効期間
- [Design for solving transient disposables on Blazor Server](https://github.com/dotnet/aspnetcore/issues/26676)

- [Validate scope for transient inside singleton in .net core?](https://stackoverflow.com/a/68069128/)


##### 詳しい解説
- [Microsoft.Extensions.DependencyInjection Deep Dive](https://gist.github.com/yfakariya/11e40a5ebf537ee545cfdd3b6e8c5fbe)