DIは、その考え方は理解しやすいです。そして、DIを簡単に実現するためにDIコンテナ(IoCコンテナ)があります。しかし、そのDIコンテナをどう使うと良いのか、判断に迷うことがあります。それが難しい部分です。
課題
Microsoft.Extensions.DependencyInjection
での課題の1つは、次です。
Transient
で登録した(解決する)クラスは、DIコンテナのScope
に紐付きます。しかし、Transient
のクラスにIDisposable
があると、Scope
をDispose
するまで、そのクラスのインスタンスは破棄されません。その結果、意図しないメモリーリークになります。
しかし、これは仕様です。そして、仕様を決める時に検討した上でこの仕様にしたそうです。
解決方法
では、IDisposable
のクラスを、Transient
で利用するにはどうすると良いか?
最適な解決策は、調べた限りではありませんでした。困っている人多数です。
私も困ったため、次のような方法を考えました。テスト上では機能します。
static
な、ServiceScopeManager
で、scope
のDispose
を行うのがポイントです。
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddTransient<ISub3, Sub3>() //IDisposable
.AddSingleton<ISub3Factory, Sub3Factory>()
.BuildServiceProvider());
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");
}
}
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コンテナでは、不要かもしれません。
サンプルプロジェクト
Sub
-
Sub1
IDisposable
ではない -
Sub2
IDisposable
-
Sub3
IDisposable
Add Sub Instance
Sub1, Sub2, Sub3のインスタンスをDIコンテナから生成し、リストに追加。
Remove Sub Instance
それぞれのインスタンスをリストから削除。
Sub2
は、リストから削除しても、インスタンスが残りメモリーリークする。
SubView
-
SubView1, SubView2
は、どちらも、IDisposable
Show SubView1
はメモリーリークする
Show SubView2
はメモリーリークしない。
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公式では、次のように書かれていますが、これを読んだだけで理解できるとは思えません。
メモリーリークについて
Disposable transients are referenced inside the scope so they can be disposed when the scope ends.
scope on the disposal will dispose the services which implement IDisposable