.NET 8 / EF Coreで出力パラメータ付きストアドプロシージャを呼ぶ方法|SqlQueryRawで値が取れない原因と解決策

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&lt;int&gt;("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
{
    /// &lt;summary&gt;
    /// レシピの平均評価(該当なしは 0)を、出力パラメータ付きストアドから取得する。
    /// &lt;/summary&gt;
    public async Task&lt;int&gt; 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&lt;int&gt;($"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 からのストアド呼び出しが安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次