SQL Server のストアドプロシージャを EF Core から呼ぶとき、出力パラメータ(OUTPUT)だけで値を返す設計だと、SqlQueryRaw で読もうとして「Sequence contains no elements」になることがあります。本記事では原因と、.NET 8 / EF Core で確実に値を受け取る3つの方法を整理します。
結論:行を返さないストアドは「行取得API」で読めない
今回のポイントはとてもシンプルです。SQL Server のストアドプロシージャ RecipeAverageRating は、平均値を @AverageRating int OUTPUT に代入して終わる設計で、結果セット(SELECT の行)を返していません。そのため、EF Core の SqlQueryRaw() や FromSqlRaw() のような「行を読む」APIで呼ぶと、返ってくるのは空のシーケンスになり、
First()/Single()を呼んだ瞬間にSequence contains no elementsで落ちる- 例外を避けても、期待した平均値が取れない(そもそも行が来ていない)
という挙動になります。
解決するには、「このストアドは“クエリ”ではなく“コマンド”」という扱いに切り替え、出力パラメータを正しく受け取る実装にします。具体的には次の3パターンです。
| 方針 | DB側の変更 | EF Core側の主なAPI | こんな時に向く |
|---|---|---|---|
| 解決策A:OUTPUT をそのまま読む(採用例) | 不要 | ExecuteSqlRaw / ExecuteSqlRawAsync | 既存ストアドを変えられない、最短で直したい |
| 解決策B:1行1列を返す形に変更して読む | 必要 | SqlQuery<T> / FromSql | 仕様変更OKで、EF Core から自然に扱いたい |
| 解決策C:ADO.NET(または Dapper)で呼ぶ | 不要 | Database.GetDbConnection() | OUTPUT/RETURN を確実に扱いたい、EXEC 文字列を避けたい |
前提:SQL Server が「値を返す」方法は3種類ある
まず混乱しやすいので整理します。SQL Server のストアドは、呼び出し元に値を返す方法が主に3つあります。EF Core はこの3つを同じようには扱えません。
| 返し方 | SQL Server 側の例 | 呼び出し側での受け取り | EF Core との相性 |
|---|---|---|---|
| 結果セット(行) | SELECT ... で 1行以上返す | DataReader で行を読む | 非常に良い(FromSql / SqlQuery で扱える) |
| 出力パラメータ(OUTPUT) | @AverageRating int OUTPUT に代入 | パラメータの .Value を読む | 読み分けが必要(ExecuteSqlRaw など「コマンド」側が分かりやすい) |
| RETURN 値 | RETURN 0 のような戻りコード | Direction = ReturnValue のパラメータで受ける | OUTPUT と同様。用途は「成功/失敗コード」向き |
今回つまずく原因は、OUTPUT パラメータで返しているのに、結果セットとして読もうとしていることです。
今回のストアド:OUTPUT に値を入れるが、行は返さない
イメージとして、ストアドが次のような構造だとします(要点だけ簡略化しています)。
CREATE OR ALTER PROCEDURE dbo.RecipeAverageRating
@RecipeId int,
@AverageRating int OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- レビューが無ければ AVG は NULL なので 0 に寄せる
SELECT
@AverageRating =
ISNULL(CAST(AVG(ReviewNumber * 1.0) AS int), 0)
FROM dbo.Reviews
WHERE RecipeId = @RecipeId;
END
ここで重要なのは、SELECT @AverageRating = ... は結果セットを返す SELECT ではなく、変数(パラメータ)への代入だという点です。つまり、呼び出し側が受け取れるのは「行」ではなく「パラメータ」です。
症状:SqlQueryRaw() で読もうとすると空になり、First/Single で落ちる
やりがちな(そして今回起きている)パターンを、わざと書くと次のようになります。
// ※これは「ダメな例」:行が返ってこないストアドを、行として読んでいる
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
var recipeIdParam = new SqlParameter("@RecipeId", recipeId);
var avgParam = new SqlParameter
{
ParameterName = "@AverageRating",
SqlDbType = SqlDbType.Int,
Direction = ParameterDirection.Output
};
var rows = await context.Database
.SqlQueryRaw<int>("EXEC dbo.RecipeAverageRating @RecipeId, @AverageRating OUT", recipeIdParam, avgParam)
.ToListAsync(cancellationToken);
// rows は空になるので、ここで例外
var average = rows.First();
SqlQueryRaw<int> は「SQL を実行して、結果セットの1列を int にマッピングして列挙する」APIです。ストアドが1行も返さない以上、列挙結果は空になります。
その空の列挙に対して First() / Single() を呼ぶと、LINQ の規約通り Sequence contains no elements になります。これは SQL Server の例外ではなく、.NET 側が“空の結果に対して先頭要素を要求した”という意味の例外です。
解決策A:ExecuteSqlRaw で実行し、OUTPUT パラメータから値を読む(採用された解)
DB側はそのまま、EF Core 側だけを正して最短で直すならこの方法です。考え方は次の通りです。
- 「行を読む」のではなく「コマンドを実行する」
@AverageRatingはDirection = Outputで渡す- 実行後に
SqlParameter.Valueを読む
まずは SQL Server 側で期待通りに動いているか確認する
アプリの問題かストアドの問題かを切り分けるため、SSMS 等で次のように実行して、出力パラメータが想定通り入ってくることを確認します。
DECLARE @AverageRating int;
EXEC dbo.RecipeAverageRating
@RecipeId = 10,
@AverageRating = @AverageRating OUTPUT;
SELECT @AverageRating AS AverageRating;
レビューが無いレシピで 0 が返る、レビューがあるレシピで平均が返る、という動きが確認できれば、あとは EF Core 側の呼び出し方の問題です。
実装例:DbContext にメソッドを生やして「正しい呼び方」を封じ込める
呼び出し側で毎回 Output パラメータを組み立てると、どこかでミスが起きやすくなります。DbContext に専用メソッドとして切り出し、アプリ内での呼び方を統一するのが実務的には一番安全です。
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
public partial class AppDbContext : DbContext
{
/// <summary>
/// レシピの平均評価(該当なしは 0)を、出力パラメータ付きストアドから取得する。
/// </summary>
public async Task<int> GetRecipeAverageRatingAsync(int recipeId, CancellationToken cancellationToken = default)
{
// 入力パラメータ
var recipeIdParam = new SqlParameter("@RecipeId", recipeId);
// 出力パラメータ(int なので Size は不要)
var averageRatingParam = new SqlParameter
{
ParameterName = "@AverageRating",
SqlDbType = SqlDbType.Int,
Direction = ParameterDirection.Output
};
// ExecuteSqlRaw は「行を読む」のではなく「実行する」ためのAPI
// ※ OUT / OUTPUT はどちらでも良いが、EXEC 呼び出し側は OUT を使うことが多い
await Database.ExecuteSqlRawAsync(
"EXEC dbo.RecipeAverageRating @RecipeId, @AverageRating OUT",
parameters: new[] { recipeIdParam, averageRatingParam },
cancellationToken: cancellationToken);
// 出力が NULL の場合は DBNull.Value が入ることがあるので吸収
if (averageRatingParam.Value == DBNull.Value || averageRatingParam.Value is null)
{
return 0;
}
return (int)averageRatingParam.Value;
}
}
この実装の良い点は、呼び出し側が「平均を取る」という意図だけを表現できることです。生SQLの組み立てや Output パラメータの作法は、DbContext 内に閉じ込められます。
呼び出し例:アプリケーション層からは普通のメソッドとして使う
var avg = await _db.GetRecipeAverageRatingAsync(recipeId, cancellationToken);
// avg を画面やAPIレスポンスに利用
解決策A のチェックポイント
| チェック項目 | よくある失敗 | 対処 |
|---|---|---|
| ストアドが行を返していない | SqlQueryRaw / FromSql で読もうとする | ExecuteSqlRaw(NonQuery)で実行し、OUTPUT を読む |
| 出力パラメータの Direction | 既定の Input のまま | Direction = ParameterDirection.Output |
| 型・サイズ | 文字列出力で Size 未設定、切り詰め | SqlDbType と必要なら Size を指定 |
| DBNull の扱い | キャストで例外になる | DBNull.Value を 0 に寄せるなど吸収 |
| 戻り値の誤解 | ExecuteSqlRaw の戻り値が平均だと思う | 戻り値は「影響行数」。平均は Output パラメータから取る |
補足:OUT パラメータを “名前付き” で呼ぶとさらに読みやすい
パラメータの順番ミスを避けたい場合は、EXEC 文字列側を名前付きで書くのも有効です。
EXEC dbo.RecipeAverageRating
@RecipeId = @RecipeId,
@AverageRating = @AverageRating OUT;
EF Core に渡す SqlParameter の順序は通常そこまで問題になりませんが、長いストアドになるほど「どれがどれだっけ?」が起きやすいので、可読性の投資としておすすめです。
解決策B:ストアドを「1行1列返す」形に変更して、EF の “行取得” で読む
もし仕様として変更できるなら、EF Core(に限らず ORM 全般)で扱いやすいのは、値を OUTPUT ではなく SELECT で返す形です。理由は単純で、「ORM の強みは結果セットのマッピング」だからです。
例:平均値を 1行1列で返すストアドにする
CREATE OR ALTER PROCEDURE dbo.RecipeAverageRating_Select
@RecipeId int
AS
BEGIN
SET NOCOUNT ON;
SELECT
ISNULL(CONVERT(int, ROUND(AVG(CAST(ReviewNumber AS float)), 0)), 0) AS AverageRating
FROM dbo.Reviews
WHERE RecipeId = @RecipeId;
END
この形なら、呼び出し側は「クエリを実行して 1 行を読む」だけで済みます。OUTPUT パラメータの方向や DBNull を意識する場面が減り、実装が短くなります。
EF Core での取得例:スカラーとして読む
EF Core 7/8 以降で使える SqlQuery<T> 系(環境によっては SqlQueryRaw<T>)を使うと、1列の結果をそのまま int にマッピングできます。
var avg = await context.Database
.SqlQuery<int>($"EXEC dbo.RecipeAverageRating_Select @RecipeId = {recipeId}")
.SingleAsync(cancellationToken);
ストアドが必ず 1 行返す設計(レビュー無しでも 0 を返す)なら SingleAsync が安全です。逆に「返らない可能性がある」設計なら SingleOrDefaultAsync を選びます。
もう一歩:そもそもストアドを使わず、SQL をスカラーで返す
平均値の取得が単純で、権限や監査の都合でストアドが必須ではない場合、SQL を直接書いてスカラーを返す方が見通しが良いこともあります。
SELECT ISNULL(CONVERT(int, ROUND(AVG(CAST(ReviewNumber AS float)), 0)), 0) AS AverageRating
FROM dbo.Reviews
WHERE RecipeId = @RecipeId;
ただし、アプリ側に生SQLが散らばると保守性が落ちるので、「DBで集中管理したい」ならストアド、「アプリ側で完結させたい」ならクエリ、とチームの方針で揃えるのがおすすめです。
解決策C:EF から ADO.NET(または Dapper)で “ストアド呼び出し” を行う
OUTPUT パラメータを確実に扱いたい、EXEC 文字列で呼びたくない、将来的に戻り値や入力が増えそう、という場合は ADO.NET の王道のやり方が堅いです。EF Core でも Database.GetDbConnection() から同じことができます。
ADO.NET 実装例:CommandType.StoredProcedure で呼ぶ
using System.Data;
using Microsoft.EntityFrameworkCore;
public async Task GetRecipeAverageRatingWithAdoAsync(
AppDbContext context,
int recipeId,
CancellationToken cancellationToken = default)
{
await using var connection = context.Database.GetDbConnection();
// DbCommand は provider(SQL Server なら Microsoft.Data.SqlClient)に応じた実体になる
await using var command = connection.CreateCommand();
command.CommandText = "dbo.RecipeAverageRating";
command.CommandType = CommandType.StoredProcedure;
var pRecipeId = command.CreateParameter();
pRecipeId.ParameterName = "@RecipeId";
pRecipeId.DbType = DbType.Int32;
pRecipeId.Value = recipeId;
command.Parameters.Add(pRecipeId);
var pAverage = command.CreateParameter();
pAverage.ParameterName = "@AverageRating";
pAverage.DbType = DbType.Int32;
pAverage.Direction = ParameterDirection.Output;
command.Parameters.Add(pAverage);
if (connection.State != ConnectionState.Open)
{
await connection.OpenAsync(cancellationToken);
}
await command.ExecuteNonQueryAsync(cancellationToken);
if (pAverage.Value == DBNull.Value || pAverage.Value is null)
{
return 0;
}
return Convert.ToInt32(pAverage.Value);
}
この方法のメリットは、
- ストアドをRPCとして呼ぶので、EXEC 文字列の書き間違いが減る
- OUTPUT だけでなく RETURN 値も同じノリで受け取れる
- ストアドが結果セットも返す場合、DataReader で行を読みつつ Output も扱える(Reader を閉じた後に Output が確定)
といった点です。反面、EF Core の “LINQ で全部書ける” 体験からは少し離れるため、アプリの方針に合わせて局所的に採用するのが現実的です。
どの方法を選ぶべきか:実務目線の判断基準
「正しい呼び方」は1つですが、「どれを採用するか」は状況で変わります。迷ったら次の観点で決めるとブレにくいです。
| 観点 | 解決策A | 解決策B | 解決策C |
|---|---|---|---|
| DB変更の可否 | 不要 | 必要 | 不要 |
| 呼び出しコードの短さ | 中(Output パラメータ設定が必要) | 短(クエリとして読むだけ) | 中〜長(Command 構築が必要) |
| EXEC 文字列への依存 | あり | あり(ただし結果セットでシンプル) | なし(StoredProcedure 呼び出し) |
| OUTPUT/RETURN の扱いやすさ | 良い | そもそも OUTPUT を廃止できる | 非常に良い |
| 結果セット+OUTPUT の両取り | 不得意(行は読まない前提) | 結果セットに寄せる | 得意(Reader と Output を併用できる) |
| おすすめの場面 | 既存ストアドのまま最短修正 | 今後も EF Core から頻繁に呼ぶ | 複雑なストアド、将来拡張、厳密な制御 |
よくある勘違いと、再発防止のコツ
FirstOrDefault にすれば直る?
FirstOrDefault() に変えると例外は消えるかもしれませんが、値が取れない原因は解決していません。行が返っていない以上、既定値(0)が返るだけで、実際の平均値を受け取ったわけではありません。今回のように「OUTPUT で返す設計」なら、OUTPUT を読む実装に変える必要があります。
ExecuteSqlRaw の戻り値が平均値だと思ってしまう
ExecuteSqlRaw / ExecuteSqlRawAsync の戻り値は、基本的に「影響行数(rows affected)」です。ストアドの場合は -1 が返ることもあり得ます。ここを平均値として扱うとバグになります。平均値は必ず Output パラメータの .Value から取り出します。
文字列の OUTPUT で切り詰めが起きる
今回は int なので気にしなくて良いのですが、varchar や nvarchar を OUTPUT で受け取る場合は、呼び出し側のパラメータで Size を設定しないと切り詰めや空になる原因になります。OUTPUT を使う設計ほど、パラメータ定義(型・サイズ)を“SQL と C# で揃える”意識が重要です。
例外が出ないのに値が変
平均の計算は、型変換や丸めで意図しない結果になりやすい箇所です。例えば AVG(int) の戻りは小数になり得るので、CAST(... AS int) で単純に落とすと「切り捨て」になります。四捨五入したいなら ROUND を使う、評価が 1〜5 のように小さいなら decimal(10,2) に寄せる、など、求めたい仕様(切り捨て/四捨五入/小数保持)を明文化すると後から揉めません。
行を返さないストアドは “ORM と相性が悪い” を前提に設計する
ストアドを「何でもできる便利箱」にすると、呼び出し側は “クエリ” なのか “コマンド” なのかを常に意識しなければならず、今回のような取り違えが発生します。再発を防ぐには、次のいずれかに寄せるのが効果的です。
- 値を返したいストアドは、基本 SELECT で結果セットを返す(解決策B の方向)
- OUTPUT を使うストアドは、アプリ側に 専用ラッパーメソッドを用意して呼び方を統一する(解決策A の方向)
- 複雑なストアドや OUTPUT 多用は、ADO.NET/Dapper に寄せる(解決策C の方向)
まとめ:OUTPUT は「結果セット」と別物。EF Core では読み分ける
RecipeAverageRating のように OUTPUT パラメータだけで値を返すストアドは、EF Core の SqlQueryRaw() で“行”として読もうとしても空になります。Sequence contains no elements は「SQLが失敗した」のではなく「行が無いのに First() した」サインです。
既存のストアドを変えずに直すなら ExecuteSqlRaw で実行して OUTPUT を読む(解決策A)。変更できるなら 1行1列を返す形に寄せる(解決策B)。OUTPUT/RETURN を堅牢に扱いたいなら ADO.NET/Dapper でストアド呼び出し(解決策C)。この整理で、EF Core からのストアド呼び出しが安定します。

コメント