はじめに
コードレビューをしていて、IDisposeを実装しているわりにTransientでサービスが登録されている子がいました。
これはTransientで登録したインスタンスのDisposeが呼び出されるか確認しましょうね。という話です。
サービスクラス
こんなクラスがあるとします。実際にはファイルやネットワークコネクション、データベースコネクションやらを持っていて、Disposeで解放するイメージです。
public class TransientService : IDisposable
{
public void WriteServiceName(int num) => Console.WriteLine($"{num} TransientService Method {GetHashCode()}");
public void Dispose() => Console.WriteLine($"TransientService Disposed! {GetHashCode()}");
}
利用側
こんな感じでScope有りと無しで5回連続で呼び出してみます。
using Microsoft.Extensions.DependencyInjection;
var builder = new ServiceCollection();
builder.AddTransient<TransientService>();
var services = builder.BuildServiceProvider();
Console.WriteLine("スコープありでTransientServiceを5回取り出して実行する");
{
using var scope = services.CreateScope();
foreach (var i in Enumerable.Range(20, 5))
{
var service = scope.ServiceProvider.GetRequiredService<TransientService>();
service.WriteServiceName(i);
}
}
Console.WriteLine("スコープなしでTransientServiceを5回取り出して実行する");
{
foreach (var i in Enumerable.Range(0, 5))
{
var service = services.GetRequiredService<TransientService>();
service.WriteServiceName(i);
}
}
実行結果
上のブロックではforeachの外でスコープを作っているので、CreateScope()で作った変数のスコープを抜けた段階で まとめて Disposeが呼ばれています。また、下のブロックはスコープを作っていないのでDisposeは呼ばれていません。
スコープありでTransientServiceを5回取り出して実行する
TransientService Method 4094363 20
TransientService Method 36849274 21
TransientService Method 63208015 22
TransientService Method 32001227 23
TransientService Method 19575591 24
TransientService Disposed! 19575591
TransientService Disposed! 32001227
TransientService Disposed! 63208015
TransientService Disposed! 36849274
TransientService Disposed! 4094363
スコープなしでTransientServiceを5回取り出して実行する
TransientService Method 21083178 0
TransientService Method 55530882 1
TransientService Method 30015890 2
TransientService Method 1707556 3
TransientService Method 15368010 4
となると?
ASP.NET Coreなどではリクエストの単位でスコープが作られるので、TransientServiceが一度のリクエストで数十回~数百回注入されるような場合でなければ問題は表面化はしないかもしれませんが(高負荷になったら発生しそうですが)、コンソールアプリケーションではアプリケーションの終了までDisposeが呼ばれないので容易にアンマネージドリソースが枯渇することになります。
どうすれば?
一時的なインスタンスと共有インスタンスのための IDisposable ガイダンスに記載がありますが、IDisposableを実装するような子はAddTransientではなくAddScopedで依存関係を登録するか、AddScopedで登録したファクトリー経由でアンマネージドリソースを供給するような仕組みにするのがよさそうです。
おわりに
HTTPのコネクションやデータベースのコネクションなどはフレームワーク側で考慮してくれているので、HttpClientFactoryを使った実装になると思いますが、独自のアンマネージドリソースを管理するようなクラスを作る場合はスコープやそのクラスで管理するものがいつ破棄されるかに気を付けて実装できると良いですね。