EntityFramework Coreのバージョンアップに想定外に工数を取られた話

情報システム部では社内システムを .NET Core + EntityFramework Core (以下 EFCore) + SQLServer という組み合わせで実装しています。これらの中には既に EOL を迎えてしまっている .NET Core 2.1 や 2.2 で実装されたままバージョン更新できていないものが存在しました。本記事はそんな古いアプリを更新する際に、 EFCore のドキュメントに無い挙動の変更に直面し、対応にかなりの時間がかかってしまったという内容になります。

環境情報

  • Visual Studio 2022
  • EntityFramework Core 2.1.14 / 2.2.6 / 3.1.22 / 5.0.14 / 6.0.2

今回試したソースはこちらに置いてあります。

何が起こったのか

冒頭に記した通り、 .NET Core 2.1 や 2.2 で実装されているアプリケーションを段階的な更新の一環として .NET Core 3.1 に更新を行っています。その際に EFCore を使用している箇所で以下のエラーが発生しました。

There is already an open DataReader associated with this Connection which must be closed first.

EFCore 2.1 や 2.2 でも非同期メソッドを Task.WhenAll などを使用して同時に実行しようとすると上記エラーが発生していた様な気がしますが、今回は元々動いていた箇所がバージョンアップによって動かなくなりました。 Microsoft のドキュメントにまとめられている破壊的変更の一覧には該当しそうな変更点は見当たりません。

docs.microsoft.com

エラーが発生した該当箇所のソースは以下のようになっていました。

foreach (var post in context.Entity1.Where(x => ...))
{
    var result = context.Entity2.Where(x => ...).ToList();
}

1つ目のクエリを foreach で順番に処理している間に、もう1つ別のクエリを発行しています。 Data Provider の実装や EFCore の処理の方法によっては、同時実行でエラーになりそうな臭いがしています。 EFCore 3.1 の破壊的変更の一覧に System.Data.SqlClient ではなく Microsoft.Data.SqlClient が使用される というものがあり、実際にエラーが発生しているのも Microsoft.Data.SqlClient の内部でしたので、きっとこの項目に引っかかったのでしょう。具体的にソースを追うことまではしていないため想像の域は出ないのですが。

試しに SQLServer と SQLite に対して同様のコードを実行してみたところ、以下のようになりました。 EFCore 3.1 以降の SQLServer だけエラーが発生しています。今回の原因は EFCore の実装では無く Microsoft.Data.SqlClient の実装によるものであながち間違いではないのだと思います。

各 EFCore バージョンで SQLServer と SQLite に同じ処理をした結果

行った対応

原因になんとなくの当たりは付いたところで、続いて修正対応についてです。対応自体は foreach で処理されている最初のクエリに ToList()First()FirstOrDefault() などの呼び出しを付け加えるだけです。 foreach のループ処理前に結果を確定してしまえば今回のエラーは発生しません。

修正方針はとてもシンプルなのですが、問題は修正箇所の特定です。
テストコードでカバーされている箇所はテストを実行すれば分かりますが、残念ながら全ての実装にテストが書かれているわけではありません。また厄介なことに今回のエラーは SQLite では発生せず SQLServer のみで発生しますので、 DB接続先をダミーとして SQLite InMemory に変更している箇所はエラーになりません

最初はエラーを見つけてはパッチする対応を行っていたのですがモグラ叩きにも限界があり、 DbContext を使用している全ての処理を確認することとなりました。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧