付録M Web 社員管理アプリ 完成度アップデー ― リリース品質へ
この付録の位置づけ
Section titled “この付録の位置づけ”第 28〜30 章で、Web 社員管理アプリ(MvcEmployeeApp)の 一覧・検索・CRUD が完成しました。
ここまでは「動くこと」を目標にしてきましたが、現場のアプリは 動いたあと に磨く工程があります。エラー時の見せ方、入力チェック、一覧の使い勝手、データを安全に変更するしくみ ── こうした「リリース品質」への仕上げを、この付録で体験します。
これは、Windows フォーム編のあとに行った 付録 J(Windows 拡張課題集)の Web 版 です。 J と同じ リリース演習スタイル(自分で計画を立て、納期=発表時刻までにできたところをリリースする)で進めます。
新しい学びも入っています
仕上げのなかに、ここまで出てこなかった 新しい道具 を 2 つ入れています。
- トランザクション(レベル3):複数の更新を「全部成功か、全部なかったことに」する(SQL 研修の知識が Web で活きます)
- DataAnnotations(レベル1):入力チェックを「属性」で宣言的に書く
さらに DI(依存性注入) に触れたい人は、続けて 付録 N へ進んでください。
第 26〜30 章は再説明しません
asp-*/@model/ Model Binding / PRG などは、第 26 章 26-12 早見表や各章を参照しながら進めてください。本付録は「仕上げの差分」だけを示します。
M-1 取り組み方(リリース演習のルール)
Section titled “M-1 取り組み方(リリース演習のルール)”進め方は 付録 J「J-1 取り組み方」と同じ です。要点だけ再掲します。
- 個人で「リリース計画」を立て、作業前に自分のリポジトリにコミットしておく(何をどこまで入れるか先に宣言)
- チームは納期(発表開始時刻)を共有し、支え合い・報告の単位。個人の成果物を個人で出す
- 時間が来たら、できたところまでをリリース(中途半端な機能はフィーチャーフリーズで外す)
- 写経しない:仕様とヒントだけ見て、自分で組み立てる
- 難所には 「なぜ」コメント を残す(第 23 章「課題 23-1」・第 7 章コラム)
対象プロジェクト:本体
MvcEmployeeAppを育てるこの付録の機能追加は、第 28〜30 章で育ててきた
MvcEmployeeApp(ソリューションKadaiWebApp)にそのまま足します(新しいプロジェクトは作りません)。 ただし、付録 N の DI 化だけは本体ではなくコピーで行います(本体の構造を壊さないため。詳細は付録 N)。
M-2 準備:拡張データ(50 件)と完成済みアプリ
Section titled “M-2 準備:拡張データ(50 件)と完成済みアプリ”並べ替え・ページングを体感するには、社員が 10 人では少し物足りません。 付録 J「J-2」で入れた拡張データ(社員 50 件・部署 8 件)をそのまま使います。
- すでに付録 J を実施済みなら、データは入っているので 追加作業は不要 です。
- まだなら、付録 J「J-2:演習用の拡張データを入れる(SSMS で実行)」の
INSERTを実行してください(正本の 10 名・5 部署は変えず、追加するだけ)。
作業前チェック:
-
MvcEmployeeAppが/Employeesで動く(一覧・検索・新規・編集・削除が一通り動作) -
app_userでTrainingDBに接続できる(第 27 章 27-2) - (並べ替え・ページングをやるなら)社員データが 50 件入っている
M-2 の見積もり(リリース計画づくり用)
Section titled “M-2 の見積もり(リリース計画づくり用)”| レベル | 課題 | 難易度 | 新しい学び |
|---|---|---|---|
| 1 | M-3-1 メッセージ(TempData) | ★☆☆ | |
| 1 | M-3-2 DataAnnotations | ★★☆ | ◎ 宣言的バリデーション |
| 1 | M-3-3 エラー時のやさしい画面 | ★★☆ | ○ 例外処理の入口 |
| 2 | M-4-1 並べ替え(ソート) | ★★☆ | ○ 安全な動的 ORDER BY |
| 2 | M-4-2 ページング | ★★☆ | ◎ OFFSET / FETCH |
| 3 | M-5-1 トランザクション | ★★★ | ◎◎ 原子性・Rollback |
| 3 | M-5-2 ログ(ILogger) | ★★☆ | ○ DI の予告 |
全部はやりません。 残り時間に合わせて 「リリースに含めるもの」を選んで 計画してください。 迷ったら、M-3-2(DataAnnotations)と M-5-1(トランザクション)= 新しい学びの核 を優先するのがおすすめです。合計の目安は 2〜4 個(レベル3 は重いので入れるなら 1 つ)。
レベル1:仕上げの定番(リリースに含めたい)
Section titled “レベル1:仕上げの定番(リリースに含めたい)”M-3-1 登録・更新後にメッセージを出す(TempData)
Section titled “M-3-1 登録・更新後にメッセージを出す(TempData)”新規登録や更新のあと、一覧に戻ったときに「社員 〇〇 を登録しました」と一言出します。
PRG(第 30 章 30-6)で Redirect をまたいでも消えない 値の置き場所が TempData です。
仕様
Create/Editの保存成功後、TempDataにメッセージを入れてからRedirectToAction("Index")- 一覧画面(
Index.cshtml)の上部に、メッセージがあれば表示する
Controller(保存成功後に 1 行足す)
empRepo.Insert(employee);
// PRG の Redirect をまたいでも 1 回だけ残る置き場所が TempDataTempData["Message"] = $"社員 {employee.LastName} {employee.FirstName} を登録しました。";
return RedirectToAction("Index");View(Index.cshtml の上部)
@if (TempData["Message"] != null){ <div class="alert alert-success">@TempData["Message"]</div>}ヒント:
TempDataは「次の 1 リクエストまで」だけ残る特別な入れ物です。ViewBagは Redirect で消えますが、TempDataは Redirect 後の画面で 1 回だけ 読めます。更新・削除のメッセージも同じ要領で出せます。
確認すること
- 登録 → 一覧で「登録しました」が出る / もう一度 F5 すると 消える(1 回だけ)
- なぜ
ViewBagでなくTempDataか(PRG の Redirect をまたぐ)を説明できる
M-3-2 入力チェックを DataAnnotations にする(新しい学び)
Section titled “M-3-2 入力チェックを DataAnnotations にする(新しい学び)”第 30 章では、ValidateEmployeeInput の中で if 文を並べて 入力チェックをしていました。
これを、プロパティに「属性」を付けるだけ の宣言的な書き方(DataAnnotations)に置き換えます。
仕様
EmployeeEditViewModelの 姓・名・給与・部署 に検証属性を付けるValidateEmployeeInputは、属性に移せないEmail(任意だが、入れたら形式チェック)だけ を残す- 画面の
asp-validation-for(第 30 章 30-7)はそのまま使える
EmployeeEditViewModel.cs(属性を付ける)
using System.ComponentModel.DataAnnotations;
namespace MvcEmployeeApp.Models;
public class EmployeeEditViewModel{ public int EmployeeId { get; set; }
[Required(ErrorMessage = "「姓」は必須です。")] public string LastName { get; set; } = "";
[Required(ErrorMessage = "「名」は必須です。")] public string FirstName { get; set; } = "";
// メールは「任意。ただし入れたら @ を含む形式」という条件付き。 // 単純な属性では表せないので、チェックは Controller 側に残す(下記)。 // ※ [EmailAddress] を付けると、空欄("")が「形式エラー」と判定され、 // メールが事実上「必須」になってしまう(空文字には @ が無いため)。 public string Email { get; set; } = "";
public DateTime HireDate { get; set; }
[Range(typeof(decimal), "0", "100000000", ErrorMessage = "「給与」は 0 以上で入力してください。")] public decimal Salary { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "「部署」を選択してください。")] public int DepartmentId { get; set; }
public List<Department> Departments { get; set; } = new List<Department>();}Controller(ValidateEmployeeInput の呼び出しを消す)
[HttpPost]public IActionResult Create(EmployeeEditViewModel model){ // 姓・名・給与・部署は属性が自動でチェック。 // 「メールは入れたときだけ形式チェック」という条件付きだけ手動で残す。 ValidateEmployeeInput(model);
if (!ModelState.IsValid) { DepartmentRepository deptRepo = new DepartmentRepository(); model.Departments = deptRepo.GetAll(); return View(model); } // ... 以下は同じ}
// 属性に移せない「条件付き」のチェックだけを残す(姓名・給与・部署は属性へ移った)private void ValidateEmployeeInput(EmployeeEditViewModel model){ if (!string.IsNullOrEmpty(model.Email) && !model.Email.Contains('@')) { ModelState.AddModelError("Email", "「メール」は @ を含む形式で入力してください。"); }}| 書き方 | 検証の置き場所 | 特徴 |
|---|---|---|
if 文方式(第 30 章) | Controller の ValidateEmployeeInput | ロジックが目に見える・自由が利く |
| DataAnnotations 方式 | Model の 属性 | 短い・宣言的・View の asp-validation-for と直結 |
使い分けの判断軸(ペア確認のネタ):単純な「必須・範囲」は属性が短くて済みます。「他の項目と突き合わせる」「DB を見て重複チェック」「任意だが入れたら形式チェック」など 条件が付く ものは
if文(またはカスタム検証)が向きます。本章の@を確認)が、まさに 属性に移せない条件付き の実例です。両方を知っておく のが現場では大事です。
確認すること
- 姓を空・メールを
abc(@ なし)・部署未選択 で保存 → それぞれエラーが画面に出る - メールを 空のまま でも保存できる(任意項目 ── だから属性にしなかった)
- 単純な検証は属性へ移り、条件付きの
Emailだけifに残った理由を説明できる
M-3-3 エラー時にやさしい画面を出す
Section titled “M-3-3 エラー時にやさしい画面を出す”存在しない URL や、サーバー側の例外が起きたとき、素っ気ないエラーのまま だと不親切です。 利用者向けの やさしいエラー画面 を出します(第 26 章で学んだ 404 / 500 の話の続きです)。
仕様(どれか 1 つでも可)
- (a) 開発用の詳細エラーではなく、本番向けの やさしいエラーページ に切り替える
- (b) 例外が起きうる操作を
try-catchし、専用のエラー画面 に誘導する(付録 L 発展 D のreturn View("Error")と同じ発想)
(a) Program.cs(本番時のエラーページ)
ASP.NET Core のテンプレートには、最初から次のような分岐があります。確認して意味を読み取りましょう。
if (!app.Environment.IsDevelopment()){ app.UseExceptionHandler("/Home/Error"); // 例外時にこの画面へ}開発中(IsDevelopment)は詳細エラーが出ますが、本番では /Home/Error(テンプレートの Error.cshtml)に飛びます。
(b) Controller で try-catch して専用画面へ
try{ empRepo.Delete(id); TempData["Message"] = "削除しました。";}catch (SqlException){ // 例:他テーブルから参照されていて削除できない 等 TempData["Message"] = "削除できませんでした(関連データがある可能性があります)。";}return RedirectToAction("Index");ヒント:何でもかんでも
catchで握りつぶすのは禁物です。「利用者に見せてよいメッセージに変える」ことと「開発者が原因を追える」こと(次の M-5-2 ログ)の両立を意識しましょう。
確認すること
- (a) 本番モードのエラーページの行き先(
/Home/Error)を説明できる、または (b) 例外時にやさしいメッセージが出る - 第 26 章の 404 / 500 と結び付けて説明できる
レベル2:一覧を実用的にする
Section titled “レベル2:一覧を実用的にする”M-4-1 一覧を並べ替える(ソート)
Section titled “M-4-1 一覧を並べ替える(ソート)”列ヘッダー(氏名・給与・入社日 など)をクリックすると、その列で 昇順/降順 に並べ替えます。
仕様
- 一覧に
sortパラメータを足す(例:?sort=salary_desc) ORDER BYは 許可した値だけ をswitchで当てる(sortを SQL に 直接連結しない)- 列ヘッダーは
<a asp-action="Index" asp-route-sort="salary_desc">給与</a>のようにリンクにする
Repository(switch で許可値だけを ORDER BY に)
private static string ToOrderBy(string sort){ // ★ ここがインジェクション対策:外から来た文字列を SQL に直接入れない。 // 許可した組み合わせだけを返し、それ以外は既定(社員ID 昇順)にする。 switch (sort) { case "name_asc": return "ORDER BY e.last_name, e.first_name"; case "name_desc": return "ORDER BY e.last_name DESC, e.first_name DESC"; case "salary_asc": return "ORDER BY e.salary"; case "salary_desc": return "ORDER BY e.salary DESC"; case "hire_asc": return "ORDER BY e.hire_date"; case "hire_desc": return "ORDER BY e.hire_date DESC"; default: return "ORDER BY e.employee_id"; }}組み立てた ORDER BY 句を、GetAll(または Search)の SQL 末尾に 連結 します。値ではなく 句の選択 なので、ユーザー入力がそのまま SQL になることはありません。
ヒント:第 29 章 発展 29-2 D と同じ「許可リスト方式」です。
sortを"ORDER BY " + sortのように 直接つなぐと SQL インジェクション になります。switchのdefaultで安全側(既定の並び)に倒すのがコツ。
確認すること
- 列ヘッダーのクリックで並びが変わる(昇順/降順)
-
sortを SQL に直接連結していない(switchの許可値だけ)ことを説明できる
M-4-2 一覧をページに分ける(ページング)
Section titled “M-4-2 一覧をページに分ける(ページング)”50 件を 1 画面に全部出すのではなく、1 ページ 10 件ずつ に分けて「前へ/次へ」で移動します。
仕様
pageパラメータを足す(例:?page=2、既定は 1)- SQL で
OFFSET(読み飛ばす件数)とFETCH NEXT(取る件数)を使う - 画面下に「前へ / 次へ」リンク
Repository(OFFSET / FETCH ― これも必ずパラメータ化)
const string sql = @" SELECT e.employee_id, e.last_name, e.first_name, e.email, e.hire_date, e.salary, e.department_id, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.department_id ORDER BY e.employee_id OFFSET @skip ROWS FETCH NEXT @take ROWS ONLY";
// 2 ページ目・1 ページ 10 件なら skip=10, take=10cmd.Parameters.AddWithValue("@skip", (page - 1) * pageSize);cmd.Parameters.AddWithValue("@take", pageSize);ヒント:
OFFSET ... FETCHはORDER BYが必須 です(順序が無いと「何件目」が決まらないため)。M-4-1 のソートと併用するなら、M-4-1 で組み立てたORDER BY句の後ろにOFFSET ... FETCHを付けます(順序の確定が先、ページ切り出しが後)。「次へ」が押せるかは「次のページがあるか」=総件数SELECT COUNT(*)で判断できます。総件数の取得はExecuteScalar(第 27 章)。
確認すること
- 1 ページ 10 件で表示され、「次へ」で続きが見える
-
OFFSET/FETCHの値もパラメータで渡している(直接埋め込まない)
レベル3:データを安全に扱う(★★★・新しい学びの核)
Section titled “レベル3:データを安全に扱う(★★★・新しい学びの核)”M-5-1 トランザクションで複数の更新をまとめる
Section titled “M-5-1 トランザクションで複数の更新をまとめる”ここが この付録のいちばんの新しい学び です。
「全部成功するか、全部なかったことにするか(原子性)」を保証するのが トランザクション です。SQL 研修で COMMIT / ROLLBACK を学んだ人は、それを C# から制御 する形になります。
題材:部署の統合(2 つの文が一体でないと困る操作)
「部署 B を廃止して、その社員を部署 A に移す」を考えます。これは 2 つの SQL が必要です。
UPDATE employees SET department_id = A WHERE department_id = B(社員を移す)DELETE FROM departments WHERE department_id = B(空になった部署を消す)
もし 1 が成功して 2 が失敗(あるいは逆)すると、データが 中途半端 になります。 2 つを 1 つのトランザクションにまとめ、両方成功したときだけ確定 します。
Repository(新しいメソッドを足す)
public void MergeDepartment(int fromDepartmentId, int toDepartmentId){ using SqlConnection conn = new SqlConnection(ConnectionString); conn.Open();
// ① トランザクション開始 using SqlTransaction tran = conn.BeginTransaction(); try { // ② 各コマンドに同じトランザクションを結び付ける(第 3 引数) using (SqlCommand move = new SqlCommand( "UPDATE employees SET department_id = @to WHERE department_id = @from", conn, tran)) { move.Parameters.AddWithValue("@to", toDepartmentId); move.Parameters.AddWithValue("@from", fromDepartmentId); move.ExecuteNonQuery(); }
using (SqlCommand del = new SqlCommand( "DELETE FROM departments WHERE department_id = @from", conn, tran)) { del.Parameters.AddWithValue("@from", fromDepartmentId); del.ExecuteNonQuery(); }
// ③ ここまで来たら両方成功 → 確定 tran.Commit(); } catch { // ④ 途中でエラー → 全部なかったことに tran.Rollback(); throw; // 原因は上(Controller)に伝える(ログや画面表示のため) }}ポイント:
| 番号 | コード | 意味 |
|---|---|---|
| ① | conn.BeginTransaction() | 取引を始める。以降の変更は「仮」の状態 |
| ② | new SqlCommand(sql, conn, tran) | 各コマンドにトランザクションを結び付ける(忘れるとエラー) |
| ③ | tran.Commit() | 全部成功 → まとめて確定 |
| ④ | tran.Rollback() | 途中で失敗 → 全部取り消し(①以降の変更が消える) |
試してみよう(Rollback の体感):わざと 2 つ目の SQL を壊して(例:存在しない列名にして)実行すると、
catch→Rollbackが走り、1 つ目のUPDATEも取り消されて データが元のままなことを確認できます。これが「全部か、ゼロか」です。
usingと Rollback:CommitもRollbackも呼ばずにメソッドを抜けると、usingのDisposeで 自動的に Rollback されます(確定していない取引は破棄)。だから「確定できたときだけCommit」が安全です。
確認すること
- 部署統合が動く(社員が移り、空部署が消える)
- わざと 2 文目を失敗させると、1 文目も取り消される(原子性)ことを確認した
-
Commit/Rollback/ 各コマンドへのtran結び付けの役割を「なぜ」コメントで書いた
M-5-2 操作のログを残す(ILogger)入門
Section titled “M-5-2 操作のログを残す(ILogger)入門”「いつ・誰が・何をしたか」を記録すると、トラブル時に原因を追えます。
ASP.NET Core には 標準のログ機能(ILogger) が組み込まれています。
仕様
EmployeesControllerでILogger<EmployeesController>を受け取り、登録・更新・削除のときにログを出す
Controller(コンストラクタで受け取る)
public class EmployeesController : Controller{ private readonly ILogger<EmployeesController> _logger;
// ★ この _logger は、自分で new していないのに渡ってくる。これが「DI(依存性注入)」。 public EmployeesController(ILogger<EmployeesController> logger) { _logger = logger; }
// ... アクションの中で // _logger.LogInformation("社員を登録: {Name}", employee.LastName);}これが DI の入口です
ILoggerは 自分でnewしていないのに、コンストラクタの引数として渡ってきます。これは ASP.NET Core が用意した DI(依存性注入) のしくみです。 「自分で登録しなくても来る DI」を体感したら、次は 自分の Repository も DI で渡す 付録 N に進みましょう。new EmployeeRepository()をやめる話です。
確認すること
- 登録・更新・削除でログが出る(Visual Studio の「出力」やコンソールで確認)
-
_loggerをnewしていないこと(= 外から渡されている)に気づいた
レベル4:さらにその先へ(読み物・実装は任意)
Section titled “レベル4:さらにその先へ(読み物・実装は任意)”同時に同じ社員を編集したら?(楽観的排他)
Section titled “同時に同じ社員を編集したら?(楽観的排他)”2 人が同じ社員を同時に開いて、別々に保存したら ── 後から保存したほうで上書きされ、先の変更が 消えます(ロストアップデート)。これを防ぐ代表的な方法が 楽観的排他 です。
- 各行に「版(バージョン)」を持たせる(SQLServer なら
rowversion型) - 保存時に「開いたときの版」と「今の版」が同じかを
WHEREで確認し、違えば「他の人が更新しました」と知らせる
第 31 章 31-7 で触れた「同時編集問題」の、技術的な答えのひとつです。 実装は本研修の範囲外(読み物) ですが、「こういう備えがある」と知っておくと、現場で設計を読めるようになります。
リリース前チェックリスト
Section titled “リリース前チェックリスト”- リリース計画(何を入れるか)を作業前にコミットした
- 選んだ機能が動く(中途半端なものはフリーズして外した)
- 追加・変更した難所に「なぜ」コメントを残した
- (M-3-2)DataAnnotations で検証が効く
- (M-4-1)
sortを SQL に直接連結していない - (M-4-2)
OFFSET/FETCHの値もパラメータ化した - (M-5-1)
Commit/Rollbackが正しく、各コマンドにtranを結び付けた -
bin・obj・.vsフォルダが Git 管理に入っていない
Git への提出(リリース)
Section titled “Git への提出(リリース)”git statusgit add .git commit -m "AppendixM タイマー提出: <リリースした機能> / <詰まった点>"git push origin main納期(発表開始時刻)を過ぎたら手を止めてリリースします。Git が使えないときは付録 C のコピー提出に従い、
リリースメモ.txtに「入れた機能 / 詰まった点」を書いてください。
この付録のまとめ
Section titled “この付録のまとめ”- 「動く」アプリを リリース品質 へ磨く工程を体験した(メッセージ・入力チェック・エラー画面・一覧の使い勝手・データの安全)
- DataAnnotations:入力チェックを 属性で宣言的に 書ける(
if文方式と使い分け) - 並べ替えは 許可リスト(
switch) で安全に、ページングはOFFSET/FETCHで - トランザクション:複数の更新を 「全部か、ゼロか」(
BeginTransaction/Commit/Rollback)。SQL 研修の力が Web で活きる ILoggerは 自分でnewしないのに渡ってくる ── これが次の 付録 N(DI) につながる
ここまで磨けたら、あなたのアプリは「課題」から「小さな業務アプリ」に近づいています。 さらに現場のお作法へ踏み込みたい人は、付録 N「依存性注入(DI)入門」 へ進みましょう。