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

InvalidOperationException よくある発生パターン(LINQ / DI / コレクション)

InvalidOperationException は「今の状態ではその操作はできない」ときに出る。メッセージだけ見ると抽象的で、現場では LINQ・DI・コレクション周りでよく当たる。

この記事では、よくある発生パターン対処の型をざっくり逆引きする。


先に結論

  • 本体は「オブジェクトの状態と操作が噛み合っていない」
  • LINQ なら Single / First の件数想定ミスが多い
  • コレクションなら「列挙中の変更」が定番
  • DI / ホストなら「ビルド後の変更」「スコープ外」などライフサイクル違反
  • 対処は「想定をコードに落とす」(OrDefault、コピーしてから変更、正しいスコープ)

1. LINQ: Single / First の件数ミス

Single / SingleOrDefault

// 件数が想定外だと InvalidOperationException
var user = users.Single(u => u.Id == id);
  • Single: ちょうど1件必須。0件・複数件で例外
  • SingleOrDefault: 0件は default。複数件は例外
  • データ不整合の検知には向く。検索 UI には向かないことが多い

First / FirstOrDefault

// 0件で InvalidOperationException
var user = users.First(u => u.Id == id);

// 0件は null(参照型)
var userOrNull = users.FirstOrDefault(u => u.Id == id);

複数件あっても先頭を取る。Single と違い「複数=異常」にはならない。

対処の型

  • 無いことがある → FirstOrDefault / SingleOrDefault
  • 必ず1件のはず → Single
  • DB 取得ならクエリ側でも件数を1件に抑える

2. コレクション: 列挙中に変更

foreach (var item in list)
{
    if (item.IsExpired)
        list.Remove(item); // InvalidOperationException
}

List<T> などを foreach 中に Add/Remove すると例外になる。

対処の型

// 残すものを作り直す
list = list.Where(x => !x.IsExpired).ToList();

// または後ろから削除
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i].IsExpired)
        list.RemoveAt(i);
}

3. DI / ホスト: ライフサイクル違反

ビルド後にサービス登録

var app = builder.Build();
builder.Services.AddSingleton<IMyService, MyService>(); // もう遅い

Build() 後のコンテナ変更は基本 NG。登録は Build() 前。

スコープ外で Scoped を解決

シングルトンから Scoped(例: DbContext)をコンストラクタ注入すると、実行時に InvalidOperation 系で怒られることがある。正しいスコープ(IServiceScopeFactory など)で取る。

サービス未登録

GetRequiredService で未登録だと例外。Unable to resolve service for type ... も同系統。


4. その他でよく見るパターン

  • IEnumerator.Current を MoveNext 前に読む
  • 閉じた/未オープンの接続・リーダーに対する操作
  • 「一度しか呼べない」初期化 API の再実行

切り分け手順

  1. メッセージを読む(no elements / more than one / collection was modified など)
  2. スタックトレースで LINQ / コレクション / DI のどれかを特定
  3. 「件数」「変更タイミング」「ライフサイクル」の想定をコードと突き合わせる
  4. OrDefault・コピー・正しいスコープに直す

ざっくりまとめ

  • InvalidOperationException = 状態と操作の不一致
  • LINQ は Single/First の件数想定を疑う
  • コレクションは列挙中変更を疑う
  • DI は Build 前後と Scoped/Singleton の組み合わせを疑う
  • メッセージが道しるべ。先にメッセージ、次にスタック