-
Notifications
You must be signed in to change notification settings - Fork 0
MS_ASPNETMVCDataTable
- 戻る(ASP.NET MVCの利用方法)
ASP.NET MVC で Entity 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 を返す共通基盤)が
ある場合は、本ページの内容が今も必要になる。
-
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. - 「ASP.NET MVC で DataTable を使用するのがベストプラクティスでは無い。」
-
補足(原文の読解は正しい): 引用元の英文を
「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 という混在は、
移行途中の構成として現実的である。
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> に変換可能。
var result = dt.AsEnumerable();しかし、この result は、
@{
・・・
var grid = new WebGrid(erc);
・・・
}
・・・
@grid.GetHtml()
・・・として使用できない。
理由は、
DataRow の public property が、
- RowError property
- HasError property
であるためのもよう。
詳しくは、下記を参照のこと。
- DataTableExtensions.AsEnumerable メソッド (DataTable) (System.Data)
https://learn.microsoft.com/ja-jp/dotnet/api/system.data.datatableextensions.asenumerable - WebGrid and DataTable | The ASP.NET Forums
https://forums.asp.net/t/1673391.aspx?WebGrid%20and%20DataTable
この場合、
-
後述のように、DataTable を、List<dynamic> に変換するか、
-
grid.Column() メソッドで以下のように、format を明示する、
grid.Column("Name", format: (item) => item.Value["Name"].ToString()),
-
なお、ここで HTML ヘルパーを使用するには、以下のように処理する。
- asp.net mvc 3 - how to format html helpers in webgrid column - Stack Overflow
https://stackoverflow.com/questions/15158183/how-to-format-html-helpers-in-webgrid-column
format: item => Html.TextBox((item) => item.Value["Name"].ToString())
- asp.net mvc 3 - how to format html helpers in webgrid column - Stack Overflow
-
と、イイ感じに処理できるもよう。
補足(原因の説明を正確にしておく): 原文の「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> に、(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 として扱える ★
削除しないとメモリを食う。
Hidden にバイナリ・シリアライズした DataTable の Base64 エンコーディングを保存。
オーバーヘッドがある。
移行メモ(原文の見出しの誤字): 原文は「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 に持たせると、
・ブックマークできる、共有できる ・戻るボタンが正しく動く ・サーバに状態を持たないという副次的な利点も得られる。
- MVC Web Grid using Dynamic Data Table
https://www.c-sharpcorner.com/code/2910/mvc-web-grid-using-dynamic-data-table.aspx
- c# - Displaying standard DataTables in MVC
https://stackoverflow.com/questions/2243898/displaying-standard-datatables-in-mvc - asp.net mvc 3 - Populate MVC Webgrid from DataTable
https://stackoverflow.com/questions/6168548/populate-mvc-webgrid-from-datatable - asp.net mvc 3 webgrid bound to List<dynamic> is exceedingly slow
https://stackoverflow.com/questions/17322239/asp-net-mvc-3-webgrid-bound-to-listdynamic-is-exceedingly-slow
- DataTableExtensions.AsEnumerable メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.data.datatableextensions.asenumerable - DataRowExtensions.Field<T> メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.data.datarowextensions.field - BinaryFormatter のシリアル化メソッドは互換性がありません(.NET 9 で削除)
https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/serialization/9.0/binaryformatter-removed
Tags: 移行, .NET開発, ASP.NET, ASP.NET MVC, ADO.NET
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。