Skip to content

MS_ASPNETMVCDataTable

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

ASP.NET MVCでDataTableを使用する。

概要

ASP.NET MVCEntity Framework をキャンセルした場合の選択肢として
ASP.NET MVC + DataTable が必要なため、本項にコレを纏めた。

補足(この選択の背景): 「Entity Framework
キャンセルした場合」という前提が本ページの出発点である。
その判断理由は Entity Framework の懸念
ADO.NET vs ORM で論じられている。

【ORM を採らない場合のデータ表現の選択肢】

 ① DataTable / DataSet     … 型なし。列名は文字列 ★ 本ページ
 ② POCO(自前のクラス)     … 型あり。マッピングを自分で書く
 ③ dynamic / ExpandoObject  … 型なし。動的
 ④ Dapper 等の軽量マッパー   … 型あり。SQL は自分で書く ★ 現在の主流

現在なら ④(Dapper 等)を第一に検討する

// Dapper:SQL は自分で書き、結果は POCO で受ける
var products = conn.Query<Product>(
    "SELECT Id, Name, Price FROM Products WHERE CategoryId = @cid",
    new { cid = 3 }).ToList();

「EF は重いが、DataTable は型がなくて辛い」という
当時のジレンマは、軽量マッパーが埋めた
——というのが
その後の流れである。

ただし、既存の ADO.NET 資産(DataTable を返す共通基盤)が
ある場合は、本ページの内容が今も必要
になる。

通常の処理

通常通りViewに持って行って処理可能。

  • c# - Displaying standard DataTables in MVC - Stack Overflow
    https://stackoverflow.com/questions/2243898/displaying-standard-datatables-in-mvc

    • ASP.NET MVC で DataTable の併用は問題ない。

      There is nothing about MVC that prevents you from using ADO.NET.
      
    • 以下は、

      • ASP.NET MVC で DataTable を使用するのがベストプラクティスでは無い。」
        と言っているのでは無くて、
      • 「Controller でデータを作成するのがベストプラクティスでは無い。」と言っているので、

      お間違いなきよう。

      Now, I'm violating a whole lot of principles and "best-practices"
      of ASP.NET MVC here, so please understand this is just a simple demonstration.
      

補足(原文の読解は正しい): 引用元の英文を
「DataTable が悪い」ではなく「Controller にデータ生成を書くのが悪い」
と読み取っている点は、正確である。

MVC の責務分担:

【Controller】 リクエストを受け、【サービス層を呼び】、View を選ぶ
                → データ取得ロジックそのものは書かない
【Model】      データとビジネス ロジック
                → ここで DataTable を返しても、MVC の原則には反しない
【View】       表示だけ

DataTable を使うことの実際の欠点は、MVC の原則とは別の話である。

欠点 内容
コンパイル時に検証されない row["Nmae"] の打ち間違いが実行時まで分からない
リファクタリングが効かない 列名の変更を IDE が追跡できない
メモリを食う 変更追跡・スキーマ情報を持つため、POCO の数倍
単体テストが書きにくい テスト データの構築が冗長
View が読みにくい @item["Name"] が並ぶ

一方、DataTable が有利な場面もある。

・【列が動的に決まる】(帳票、CSV エクスポート、汎用検索画面)
・スキーマが実行時まで分からない
・既存の共通基盤が DataTable を前提にしている
・Excel 出力(ClosedXML 等は DataTable を直接受けられる)

ただし、双方向バインディング等は不可能

単一レコードの処理を行う場合等は POCO の ViewModel を作成する。

補足(この方針は現在も正しい): 「表示は DataTable、
入力は POCO の ViewModel
」という使い分けは、
実務的な折衷案として妥当である。

【なぜ DataTable では双方向バインディングできないのか】

   ModelBinder は【プロパティ名】でフォームの値を対応付ける
     <input name="Name" value="…">  →  model.Name

   DataTable は列を【インデクサ】で持つ
     row["Name"]  ← プロパティではない
     → ModelBinder が対応付けられない

   また、DataRow の public プロパティは
     RowError / HasErrors / Table / ItemArray / RowState
     ← 【業務データとは無関係なものばかり】★
     (後述の WebGrid の問題と同じ根)
// 入力画面用の ViewModel(POCO)
public class ProductEditViewModel
{
    public int Id { get; set; }

    [Required(ErrorMessage = "名前は必須です")]
    [StringLength(50)]
    public string Name { get; set; }

    [Range(0, 999999)]
    public decimal Price { get; set; }
}

POCO にすると、以下が同時に手に入る

・双方向バインディング([HttpPost] のメソッド引数で受け取れる)
・データ注釈による【入力検証】(サーバ・クライアント両方)
・IntelliSense と コンパイル時チェック
・単体テストの容易さ

一覧は DataTable、明細は POCO という混在は、
移行途中の構成として現実的である。

Grid系の処理

foreach

foreach で処理可能。

補足(現在はこれが第一選択): WebGrid が廃れた現在、
foreach で書くのが最も確実である
ASP.NET MVCのWebGrid)。

@model System.Data.DataTable

<table class="table">
  <thead>
    <tr>
      @foreach (System.Data.DataColumn col in Model.Columns)
      {
        <th>@col.ColumnName</th>
      }
    </tr>
  </thead>
  <tbody>
  @foreach (System.Data.DataRow row in Model.Rows)
  {
    <tr>
      @foreach (System.Data.DataColumn col in Model.Columns)
      {
        <td>@row[col]</td>
      }
    </tr>
  }
  </tbody>
</table>

列が動的な画面(汎用検索など)では、
むしろ DataTable の方が素直に書ける
——という利点が
ここで効いてくる。

Razor の @ は自動的に HTML エスケープされるため、
XSS の心配は要らないエンコーディング)。
@Html.Raw() を使わない限り安全である。

WebGrid のコンストラクタの第一引数の
source は、IEnumerable をサポートする必要があるので、
DataTable 使用時は、型の変換が必要がになるもよう。

IEnumerable<DataRow> ではダメなケースがある。

以下で IEnumerable<DataRow> に変換可能。

var result = dt.AsEnumerable();

しかし、この result は、

@{
  ・・・
  var grid = new WebGrid(erc);
  ・・・
}
・・・
@grid.GetHtml()
・・・

として使用できない。

理由は、

DataRow の public property が、

  • RowError property
  • HasError property

であるためのもよう。

詳しくは、下記を参照のこと。

この場合、

  • 後述のように、DataTable を、List<dynamic> に変換するか、

  • grid.Column() メソッドで以下のように、format を明示する、

    grid.Column("Name", format: (item) => item.Value["Name"].ToString()),

と、イイ感じに処理できるもよう。

補足(原因の説明を正確にしておく): 原文の「DataRow の
public property が RowError / HasError であるため
」という診断は
正しい方向だが、少し補足すると分かりやすい。

【WebGrid が列を決める仕組み】
   columns を指定しない場合、
   source の要素の型を【リフレクションで調べ】、
   その【public プロパティ】を列にする

【DataRow の public プロパティ】
   ・ItemArray
   ・HasErrors      ← 原文の「HasError」は正しくは HasErrors
   ・RowError
   ・RowState
   ・Table

   → 【業務データの列は 1 つも出てこない】★
     row["Name"] はプロパティではなくインデクサのため、
     リフレクションからは「列」として見えない

移行メモ: 原文の「HasError property」は、
正しくは HasErrors(複数形)である。

原文が挙げる 2 つの回避策は、どちらも有効である。

方法 性質
List<dynamic> に変換 列が動的でも動く。ただし遅い(後述)
format: を明示 変換が不要で速い。ただし列を先に知っている必要がある

列が固定なら format: 明示の方が良い
変換のコストもメモリも増えないためである。

List<dynamic> に変換する。

List<dynamic> に、(IDictionary<string, object>) ExpandoObject を追加していく。

var result = new List<dynamic>();
foreach (DataRow row in table.Rows)
{
  var obj = (IDictionary<string, object>)new ExpandoObject();
  foreach (DataColumn col in table.Columns)
  {
    obj.Add(col.ColumnName, row[col.ColumnName]);
  }
  result.Add(obj);
}

補足(この方法の性能上の注意): 原文の参考リンクにも
exceedingly slow」という Stack Overflow の投稿が挙がっている通り、
この変換は遅い。理由を明確にしておく。

【遅い理由】
 ① ExpandoObject は内部で辞書を持つ
      → 行数 × 列数 の分だけ辞書エントリを作る
 ② dynamic のアクセスは【実行時バインディング】
      → 呼び出しごとに DLR がメンバを解決する(キャッシュはあるが重い)
 ③ WebGrid が列を決める際、【1 行目をリフレクションで調べる】
 ④ ソート・ページングも dynamic 経由で行われる

 → 1,000 行程度でも体感できる遅さになる

性能を優先するなら:

// ① 表示する列だけを POCO に詰め替える(最も速い)
var rows = table.AsEnumerable()
    .Select(r => new ProductRow {
        Id = r.Field<int>("Id"),
        Name = r.Field<string>("Name") })
    .ToList();

// ② ページングを DB 側で行う(そもそも全件持ってこない)
//    SELECT ... ORDER BY Id OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY

② が本質的な対策である。
1 万件を取ってきて画面で 20 件表示するという設計自体が問題で、
DB 側でページングするのが正しい
SQL Server)。

row.Field<T>() を使う利点:

(int)row["Id"]           // ✗ DBNull で InvalidCastException
row.Field<int?>("Id")    // ○ DBNull を null として扱える ★

ポストバック的な動作がある場合

Sessionに格納しておく。

削除しないとメモリを食う。

Hiddenに格納しておく。

Hidden にバイナリ・シリアライズした DataTable の Base64 エンコーディングを保存。

都度、DBから取得する。

オーバーヘッドがある。

移行メモ(原文の見出しの誤字): 原文は「Hidennに格納しておく」と
なっていたため、「Hidden」に修正した。

補足(3 つの選択肢の評価と、現在の推奨): 3 つとも実在する手法だが、
現在は評価が大きく変わっている

方法 当時の評価 現在の評価
Session に格納 メモリを食う スケールアウトで問題。分散キャッシュなら可
Hidden にシリアライズ サイズが増える 使ってはならない ★(後述)
都度 DB から取得 オーバーヘッド これが正解。DB 側ページングと併用

② Hidden への格納は、現在は明確に非推奨である。

【問題点】
 ① 【セキュリティ】 利用者に改竄される
      → Base64 を戻せば中身が読める([エンコーディング] 参照)
      → 改竄されたデータをそのまま信じると重大な脆弱性
 ② 【BinaryFormatter が .NET 5 以降で無効化された】★
      → DataTable のバイナリ シリアライズは既定で使えない
      → 既知の脆弱性(任意コード実行)のため
 ③ ページ サイズが肥大する([ASP.NET ViewState] と同じ問題)

BinaryFormatter は .NET 9 で完全に削除された
DataTable.ReadXml / WriteXml は使えるが、
そもそも Hidden に業務データを載せないのが正しい。

① Session の現在の扱い:

・インプロセス Session は【スケールアウトできない】
   → 複数サーバに分散すると、別サーバに飛んだ時に失われる
・分散キャッシュ(Redis / SQL Server)に置けば共有できるが、
   大きな DataTable を置くと【シリアライズのコストが支配的】になる
     → [ASP.NET CoreのSession利用方法] / [分散キャッシュ] 参照

③ 都度 DB から取得が正解である理由:

・状態を持たない(ステートレス)=【水平にスケールする】
・データが常に最新
・「オーバーヘッド」は、【DB 側でページングすれば小さい】
    → 20 件だけ取れば、往復 1 回で数 ms
    → 全件を Session に載せる方が、よほど高くつく

現在の実装の形:

// 検索条件だけを持ち回る(URL のクエリ文字列)
// /products?keyword=abc&page=3&sort=price
public IActionResult Index(string keyword, int page = 1, string sort = "id")
{
    var (rows, total) = _repository.Search(keyword, page, pageSize: 20, sort);
    return View(new ProductListViewModel { Rows = rows, Total = total, Page = page });
}

条件を URL に持たせると、

・ブックマークできる、共有できる
・戻るボタンが正しく動く
・サーバに状態を持たない

という副次的な利点も得られる。

参考

Stack Overflow

Microsoft Learn


Tags: 移行, .NET開発, ASP.NET, ASP.NET MVC, ADO.NET

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally