• システム開発に関わる内容をざっくりと書いていく

ObjectDisposedException 発生原因と対策

ObjectDisposedException は、「すでに破棄(Dispose)されたオブジェクトを使った」ときに出る。HttpClient・Stream・DbContext・CancellationTokenSource などでよく見る。

この記事では、よくある発生原因対策の型をざっくりまとめる。


先に結論

  • 本体は「寿命が終わったオブジェクトへの操作」
  • 典型は using を抜けたあと・DI スコープ終了後・親が子をまとめて Dispose したあと
  • 対策は「誰が寿命を持つか」をはっきりさせる(所有権)
  • HttpClient は都度 Dispose しすぎない(使い回し/IHttpClientFactory
  • 非同期では「await のあとに disposed」になりやすいので注意

よくある発生原因

1. using を抜けたあとに触る

Stream stream;
using (var fs = File.OpenRead(path))
{
    stream = fs;
}
var b = stream.ReadByte(); // ObjectDisposedException

参照は残っていても、中身は破棄済み。

2. DI スコープ終了後に Scoped を使う

リクエスト終了後に DbContext をキャッシュして使い回す、バックグラウンドに渡したままスコープが終わる、などが典型。

// イメージ: リクエストスコープの DbContext をフィールドに保存して後で使う
_db = context; // リクエスト終了後に Dispose される
// ...
await _db.SaveChangesAsync(); // ObjectDisposedException

3. HttpClient / ハンドラの寿命ミス

  • 短命すぎる Dispose → ソケット枯渇側の問題にもつながる
  • 破棄済みハンドラを共有していると ObjectDisposed が出る
  • 基本は IHttpClientFactory か、長寿命の使い回し

4. 非同期: await の前後で寿命が変わる

async Task<string> ReadAsync()
{
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
    // using はメソッド終了時に Dispose
    // 続きの処理で reader を使うと危険
}

「起動したタスクが、親の using 終了後に動く」パターンが特に危険。タスクの完了まで Dispose しない。

5. Dispose 後の再利用

Dispose 自体は複数回呼ばれてもよい実装が多いが、Dispose 後に通常操作すると ObjectDisposed になる。キャンセル済みの CancellationTokenSource 再使用なども同類。


対策の型

所有権を決める

  • 作った側が Dispose する
  • 受け取った側は「借りているだけ」なら Dispose しない
  • DI に任せているなら、自分で Dispose しない(特に Scoped)

using の範囲をタスク完了まで広げる

await using var stream = File.OpenRead(path);
var bytes = new byte[stream.Length];
await stream.ReadExactlyAsync(bytes);
// ここで初めて Dispose

Scoped はスコープ内だけで使う

長寿命サービスから Scoped が必要なら、IServiceScopeFactory で短いスコープを作ってその中で使う。

HttpClient は Factory 経由

builder.Services.AddHttpClient("backend", c =>
{
    c.BaseAddress = new Uri("https://example.invalid/");
});

var client = _httpClientFactory.CreateClient("backend");

切り分け手順

  1. 例外メッセージの型名(どのオブジェクトか)を確認
  2. そのオブジェクトを Dispose/using/スコープ終了している箇所を探す
  3. 例外時刻より前に寿命が終わっていないか(特に await 前後)を見る
  4. 所有権を見直し、使う側の寿命に合わせて Dispose を遅らせる/移す

ざっくりまとめ

  • ObjectDisposedException = 捨てたあとに触った
  • using・DI スコープ・親の Dispose が寿命の区切り
  • 非同期は「await 完了まで Dispose しない」
  • HttpClient は都度 new + Dispose を避け、Factory か長寿命共有
  • 誰が Dispose するかをコードレビュー観点に入れる