ASP.NET(Razor Pages)からSQL Serverを検索すると、SSMSでは大量に返るのに画面は0件――そんなときは「SQLが間違い」ではなく「例外で止まっているのに気づけていない」ケースが珍しくありません。本記事では“0件に見える”原因の切り分け、SqlDataReaderの安全な読み取り、件数表示までを一気に整理します。
現象:SQL Serverでは1500件以上返るのに、Razor Pagesでは0件表示
今回の状況を整理すると、次のような“よくある落とし穴”にハマっていました。
- SQL Serverの Info テーブルを読み取り、List<MovieInfo> に詰めてページに表示したい
- 列はすべて nvarchar / NOT NULL
- 例:where Format = ‘Ultra HD’ order by Title のような検索をしたい
- SSMSなどで同じ条件を実行すると1500件以上返るのに、Webプロジェクト実行だと 0件に見える
- 読み取りループ(
while (reader.Read()))やGetChar/GetStringを疑っていた
結論から言うと、今回の本質原因は「読み取り処理」ではなく、接続文字列(Trust Server Certificate関連)で例外が出て処理が途中で止まっていたことでした。例外が握りつぶされると、リストが空のまま戻ってくるため、見た目としては“0件”になります。
まずやるべきこと:0件の前に例外が起きていないかを“見える化”する
データが0件のとき、やりがちなのが「SQLの条件が厳しすぎる?」「reader.Read()の書き方が違う?」と取得ロジックだけを疑い続けることです。しかしWebアプリでは、例外が出ても画面に出ず、ログも追っていないと“静かに失敗”しているように見えます。
| チェック項目 | 0件に見せる典型パターン | 対策(見える化) |
|---|---|---|
| 例外の扱い | try...catchで握りつぶし、Consoleにだけ出している | 開発中はcatchを外す/ログへ出す/デバッガで停止させる |
| 接続が成功しているか | Open()やExecuteReader()で落ちているのに気づかない | 直後にブレークポイント、HasRowsやList件数を監視 |
| 接続先の環境 | SSMSは本番DB、アプリは別DB(または別ユーザー)に接続していた | 接続文字列のDB名・サーバー名・資格情報を必ず画面/ログで確認 |
“Consoleにだけ例外”はWeb開発では気づけないことがある
コンソールアプリならConsole.WriteLineでも気づけますが、Razor Pagesの実行では、出力先が見えていなかったり、Visual Studioの出力ウィンドウを見ていなかったりして、例外に気づきにくいことがあります。開発中は次のどれかを徹底すると、原因特定が一気に早くなります。
- 開発中はいったん try…catch を外す(例外で止めてスタックトレースを見る)
- ILoggerに出す(Output/ログに残す)
- 例外は再スローする(握りつぶさない)
例えば「例外を握りつぶして0件に見せる」典型例はこうです。
try
{
using var conn = new SqlConnection(connString);
conn.Open();
using var cmd = new SqlCommand(sql, conn);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
// ここに到達する前に例外が出ているのに…
list.Add(new MovieInfo { Title = reader.GetString(0) });
}
}
catch (Exception ex)
{
// Web実行だとConsoleが見えず、結果だけ空に見える
Console.WriteLine(ex);
}
開発時は最低限、ILoggerで記録し、例外を再スローして“失敗した”ことを表に出します。
try
{
using var conn = new SqlConnection(connString);
conn.Open();
using var cmd = new SqlCommand(sql, conn);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
// 読み取り処理
}
}
catch (Exception ex)
{
_logger.LogError(ex, "DBアクセスで例外が発生しました。接続文字列やTLS設定を確認してください。");
throw; // 重要:握りつぶさない
}
デバッガで確認するなら「ExecuteReaderの直後」が最も効く
実際の調査では、次の順で“事実”を積み上げると迷子になりません。
conn.Open()を通過しているか(ここで落ちるなら接続/証明書/資格情報)ExecuteReader()を通過しているか(権限/SQL構文/タイムアウトなど)- 通過しているなら
reader.HasRowsはどうか while (reader.Read())が1回でも回るか- 回るのにListが増えないならマッピング処理を疑う
読み取りコードの改善ポイント:GetCharはnvarcharに不適切、列順依存も避ける
今回の本質原因は接続例外でしたが、読み取りコードにも改善余地がありました。特にGetChar()は“落とし穴”になりやすいので、ここで整理します。
| やりがち | 起きること | おすすめ |
|---|---|---|
GetChar()でnvarcharを読む | 取得できるのは先頭1文字。文字列全体は取れない | GetString()を使う |
select * | 列順変更や列追加で別の列を読んでしまう | select Title, Year, Format, ...のように列名を明示 |
| インデックス番号で読む | 列順の変更に弱い | GetOrdinal("Title")で列名から位置を取る |
| 文字列連結でwhere条件を作る | SQLインジェクションやエスケープ漏れの原因 | パラメータ(@format)を使う |
安全で壊れにくい読み取り例(列名指定+GetOrdinal+パラメータ)
nvarcharでNOT NULLならGetStringで素直に読めます。列名で取得する形にしておくと、将来のスキーマ変更にも強くなります。
public class MovieInfo
{
public string Title { get; set; } = "";
public string Year { get; set; } = "";
public string Format { get; set; } = "";
// 必要な列を追加
}
public List<MovieInfo> LoadMovies(string format)
{
var list = new List<MovieInfo>();
const string sql = @"
select
Title,
Year,
Format
from Info
where Format = @format
order by Title;";
using var conn = new SqlConnection(_connString);
conn.Open();
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.Add("@format", SqlDbType.NVarChar, 50).Value = format;
using var reader = cmd.ExecuteReader();
// まずはHasRowsで“本当に0件か”を確認できる
if (!reader.HasRows)
{
return list;
}
var ordTitle = reader.GetOrdinal("Title");
var ordYear = reader.GetOrdinal("Year");
var ordFormat = reader.GetOrdinal("Format");
while (reader.Read())
{
list.Add(new MovieInfo
{
Title = reader.GetString(ordTitle),
Year = reader.GetString(ordYear),
Format = reader.GetString(ordFormat),
});
}
return list;
}
この形にしておくと、「列順が変わった」「列が追加された」などの変更があっても、取得コードが壊れにくくなります。
真の原因:Trust Server Certificate関連の例外で処理が停止し、結果的に0件に見えていた
今回の決め手は、後から「trust server certificate」関連の例外が出ていることに気づいた点です。SQL Serverへの接続は、環境やドライバのバージョンによってはTLS暗号化(Encrypt)や証明書検証が絡み、接続時点で失敗することがあります。
そして、接続・実行で例外が出たのに、アプリ側でそれを握りつぶしてしまうと、最終的に画面へ渡るリストが空のままになり、“0件取得できた”ように見えるわけです。
なぜTrust Server Certificateで落ちるのか(現場で起きがちなパターン)
- 接続文字列のキーが誤っている/値が不正(例:True/False以外)
- Encryptが有効なのに、SQL Server側の証明書がクライアントで信頼されていない
- 開発PCと本番サーバーで、Windowsの証明書ストアやポリシーが違う
- 使用しているプロバイダ(
Microsoft.Data.SqlClient/System.Data.SqlClient)やバージョン差で既定値が変わっている
今回の解決:接続文字列からTrust Server Certificateを削除して正常化
質問者の環境では、接続文字列に含まれていた Trust Server Certificate=…(または同等のキー)が例外の起点になっており、その設定を削除したところ解消しました。つまり、アプリが“0件を取得していた”のではなく、取得処理が走り切っていなかったということです。
接続文字列は appsettings.json / appsettings.Development.json / 環境変数など、複数箇所に分散しがちです。まずは「Web実行時に読まれている接続文字列がどれか」を特定するのが近道です。
| 見るべき場所 | チェック内容 | よくある罠 |
|---|---|---|
appsettings.json | 本番相当の既定値 | 開発環境では上書きされていて参照されていない |
appsettings.Development.json | ローカル開発の接続先 | こちらだけ古い設定が残っている |
| 環境変数 / シークレット | CI/CDやIISの上書き | 意図せず別DBに接続している |
運用面の補足:開発の暫定回避と、本番の安全設計は分けて考える
証明書やTLSが原因の場合、やり方によっては“動くけれど安全ではない”設定になります。開発では一時的に回避したくなる一方で、本番では通信の暗号化と証明書検証を適切に行うのが基本です。方針の目安を表にまとめます。
| 項目 | 開発(ローカル) | 本番(推奨) |
|---|---|---|
| Encrypt | 環境により要検討(社内規約・クライアント既定値に従う) | 基本は有効(ネットワーク越しの通信を想定) |
| TrustServerCertificate | 暫定回避として使う場合は“いつまでか”を決める | 安易に使わず、信頼できる証明書を用意する |
| 証明書 | 自己署名/社内CAなど、最小で回すこともある | クライアントが検証できる証明書を整備し、運用手順も含めて管理 |
今回のように「特定の設定が原因で例外になる」場合は、まずは誤設定を取り除くのが最優先です。そのうえで、暗号化や証明書の扱いをどうするかは、開発と本番で分けて設計すると事故が減ります。
取得したレコード件数をWebページに表示する方法(Razor Pages)
SQLの取得が安定したら、次に「何件取れたか」を画面に出すとデバッグにも運用にも役立ちます。Razor Pagesで ListMovies が PageModel のプロパティなら、.cshtml 側はシンプルです。
最短の書き方
<div>
件数: @Model.ListMovies.Count
</div>
List<T> なら Count はプロパティなので高速です。もし IEnumerable<T> で受けているなら Count() でも構いませんが、状況によっては列挙が走ることがあるため、表示だけの目的なら Listにしておくのが扱いやすいです。
Nullの可能性がある場合(落とさない表示)
PageModelの初期化タイミングによっては、リストがnullのままのこともあります。落とさないための書き方は次のとおりです。
<div>
件数: @(Model.ListMovies?.Count ?? 0)
</div>
「本当に取得処理まで到達しているか」を確認する最小テスト
原因が読めないときは、画面表示やViewModelの都合を一度捨てて、DB接続~1行取得だけに絞ったテストを作ると切り分けが早くなります。例えばRazor PagesのOnGetで、まずはSELECT TOP (1)だけを実行し、タイトルが取れるかを確認します。
public string? DebugTitle { get; private set; }
public void OnGet()
{
const string sql = "select top (1) Title from Info order by Title;";
using var conn = new SqlConnection(_connString);
conn.Open();
using var cmd = new SqlCommand(sql, conn);
DebugTitle = (string?)cmd.ExecuteScalar();
}
ここで落ちるなら、原因は読み取りループではなく接続・認証・TLS・接続先にあります。逆にここが通れば「検索条件」「マッピング」「表示」の層へ順に疑いを移せます。
開発時におすすめのログ出力ポイント
例外のログだけでなく、次の値を“開発時だけ”ログに出すと、環境差の発見が簡単になります(ただしパスワード等の機密情報は出さない)。
- 接続先サーバー名(
Data Source)とDB名(Initial Catalog) - 検索条件(例:Formatの値)
- 取得件数(ListのCount)
- エラー発生時の例外メッセージとスタックトレース
接続文字列のミスを減らすコツ:SqlConnectionStringBuilderで検証する
接続文字列は「スペルが1文字違う」「値がTrue/Falseではない」「余計な空白が混ざっている」など、人間が手で編集する以上ミスが起きます。そこで便利なのが SqlConnectionStringBuilder です。文字列をパースしてプロパティとして扱えるので、設定の意図が明確になり、ミスにも気づきやすくなります。
var builder = new SqlConnectionStringBuilder(_connString);
// 例:開発用に設定を確認する(必要なら上書きも可能)
bool encrypt = builder.Encrypt;
bool trustCert = builder.TrustServerCertificate;
// ログに出すなら機密情報はマスクする
builder.Password = "********";
_logger.LogInformation("SQL接続先: {DataSource} / DB: {Catalog} / Encrypt: {Encrypt} / TrustServerCertificate: {Trust}",
builder.DataSource, builder.InitialCatalog, builder.Encrypt, builder.TrustServerCertificate);
“Trust Server Certificate”系の例外に遭遇したときも、まずはBuilderで現在の値を確認すると「そもそも意図しない値になっていないか」をすぐ見抜けます。特にアプリ設定が複数経路(JSON/環境変数/シークレット)で上書きされるプロジェクトほど効果的です。
Razor Pages側の実装例:取得・表示・件数の三点セット
最後に、今回の要件(Infoテーブルを検索して一覧表示し、件数も出す)を最小構成でまとめた例です。実際の列やプロパティ名は環境に合わせて調整してください。
PageModel(.cs)
public class MoviesModel : PageModel
{
private readonly IConfiguration _config;
private readonly ILogger<MoviesModel> _logger;
public MoviesModel(IConfiguration config, ILogger<MoviesModel> logger)
{
_config = config;
_logger = logger;
}
public List<MovieInfo> ListMovies { get; private set; } = new();
public void OnGet()
{
var connString = _config.GetConnectionString("DefaultConnection");
try
{
ListMovies = LoadMovies(connString, "Ultra HD");
_logger.LogInformation("取得件数: {Count}", ListMovies.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Moviesの取得で例外が発生しました。接続文字列とTLS設定を確認してください。");
throw;
}
}
private static List<MovieInfo> LoadMovies(string? connString, string format)
{
if (string.IsNullOrWhiteSpace(connString))
throw new InvalidOperationException("接続文字列が設定されていません。");
var list = new List<MovieInfo>();
const string sql = @"
select Title, Year, Format
from Info
where Format = @format
order by Title;";
using var conn = new SqlConnection(connString);
conn.Open();
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.Add("@format", SqlDbType.NVarChar, 50).Value = format;
using var reader = cmd.ExecuteReader();
var ordTitle = reader.GetOrdinal("Title");
var ordYear = reader.GetOrdinal("Year");
var ordFormat = reader.GetOrdinal("Format");
while (reader.Read())
{
list.Add(new MovieInfo
{
Title = reader.GetString(ordTitle),
Year = reader.GetString(ordYear),
Format = reader.GetString(ordFormat),
});
}
return list;
}
}
View(.cshtml)
<div>件数: @Model.ListMovies.Count</div>
@foreach (var m in Model.ListMovies)
{
}
Title
Year
Format
@m.Title
@m.Year
@m.Format
この“三点セット”をベースに、列を増やしたり検索条件を増やしたりしていくと、後から問題が起きても「どの層で止まっているか」が追いやすくなります。
“0件”に見えるトラブルを最短で潰すチェックリスト
最後に、同様のトラブルを再発させないためのチェックリストをまとめます。特にRazor PagesのようなWeb実行では「例外の見え方」が変わるため、ここを押さえるだけで切り分け速度が上がります。
- 例外は握りつぶさない(開発中は再スロー、ILoggerで必ず記録)
Open()とExecuteReader()の直後でブレークし、HasRowsとList件数を確認- 接続文字列は
appsettings.json/appsettings.Development.json/ 環境変数のどれが効いているかを確定 - 読み取りは
GetString、SQLは列名を明示、列位置はGetOrdinalで取得 - 条件値はパラメータ化して、意図せぬ文字列差分やインジェクションを防ぐ
- TLS/証明書系のエラーが出たら、まずは誤設定(TrustServerCertificate等)を疑い、開発と本番の方針を分けて整理する
「SSMSでは返るのにアプリは0件」は、SQLの問題よりも接続・例外・環境差が原因であることが多いです。まずは“0件の前に落ちていないか”を徹底的に見える化し、原因が見つかったら読み取りコードも堅くしていく──この順序で進めると、最短で解決に辿り着けます。

コメント