Skip to content

付録M Web 社員管理アプリ 完成度アップデー ― リリース品質へ

第 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_userTrainingDB に接続できる(第 27 章 27-2)
  • (並べ替え・ページングをやるなら)社員データが 50 件入っている

M-2 の見積もり(リリース計画づくり用)

Section titled “M-2 の見積もり(リリース計画づくり用)”
レベル課題難易度新しい学び
1M-3-1 メッセージ(TempData)★☆☆
1M-3-2 DataAnnotations★★☆◎ 宣言的バリデーション
1M-3-3 エラー時のやさしい画面★★☆○ 例外処理の入口
2M-4-1 並べ替え(ソート)★★☆○ 安全な動的 ORDER BY
2M-4-2 ページング★★☆◎ OFFSET / FETCH
3M-5-1 トランザクション★★★◎◎ 原子性・Rollback
3M-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 回だけ残る置き場所が TempData
TempData["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 で消えますが、TempDataRedirect 後の画面で 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(属性を付ける)

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 文(またはカスタム検証)が向きます。本章の Email(空ならスキップ、入れたら @ を確認)が、まさに 属性に移せない条件付き の実例です。両方を知っておく のが現場では大事です。

確認すること

  • 姓を空・メールを 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 と結び付けて説明できる

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 インジェクション になります。switchdefault で安全側(既定の並び)に倒すのがコツ。

確認すること

  • 列ヘッダーのクリックで並びが変わる(昇順/降順)
  • 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=10
cmd.Parameters.AddWithValue("@skip", (page - 1) * pageSize);
cmd.Parameters.AddWithValue("@take", pageSize);

ヒント:OFFSET ... FETCHORDER 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 が必要です。

  1. UPDATE employees SET department_id = A WHERE department_id = B(社員を移す)
  2. 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 を壊して(例:存在しない列名にして)実行すると、catchRollback が走り、1 つ目の UPDATE も取り消されて データが元のままなことを確認できます。これが「全部か、ゼロか」です。

using と Rollback:CommitRollback も呼ばずにメソッドを抜けると、usingDispose自動的に Rollback されます(確定していない取引は破棄)。だから「確定できたときだけ Commit」が安全です。

確認すること

  • 部署統合が動く(社員が移り、空部署が消える)
  • わざと 2 文目を失敗させると、1 文目も取り消される(原子性)ことを確認した
  • Commit / Rollback / 各コマンドへの tran 結び付けの役割を「なぜ」コメントで書いた

M-5-2 操作のログを残す(ILogger)入門

Section titled “M-5-2 操作のログを残す(ILogger)入門”

「いつ・誰が・何をしたか」を記録すると、トラブル時に原因を追えます。 ASP.NET Core には 標準のログ機能(ILogger) が組み込まれています。

仕様

  • EmployeesControllerILogger<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 の「出力」やコンソールで確認)
  • _loggernew していないこと(= 外から渡されている)に気づいた

レベル4:さらにその先へ(読み物・実装は任意)

Section titled “レベル4:さらにその先へ(読み物・実装は任意)”

同時に同じ社員を編集したら?(楽観的排他)

Section titled “同時に同じ社員を編集したら?(楽観的排他)”

2 人が同じ社員を同時に開いて、別々に保存したら ── 後から保存したほうで上書きされ、先の変更が 消えます(ロストアップデート)。これを防ぐ代表的な方法が 楽観的排他 です。

  • 各行に「版(バージョン)」を持たせる(SQLServer なら rowversion 型)
  • 保存時に「開いたときの版」と「今の版」が同じかを WHERE で確認し、違えば「他の人が更新しました」と知らせる

第 31 章 31-7 で触れた「同時編集問題」の、技術的な答えのひとつです。 実装は本研修の範囲外(読み物) ですが、「こういう備えがある」と知っておくと、現場で設計を読めるようになります。


  • リリース計画(何を入れるか)を作業前にコミットした
  • 選んだ機能が動く(中途半端なものはフリーズして外した)
  • 追加・変更した難所に「なぜ」コメントを残した
  • (M-3-2)DataAnnotations で検証が効く
  • (M-4-1)sort を SQL に直接連結していない
  • (M-4-2)OFFSET / FETCH の値もパラメータ化した
  • (M-5-1)Commit / Rollback が正しく、各コマンドに tran を結び付けた
  • binobj.vs フォルダが Git 管理に入っていない

Terminal window
git status
git add .
git commit -m "AppendixM タイマー提出: <リリースした機能> / <詰まった点>"
git push origin main

納期(発表開始時刻)を過ぎたら手を止めてリリースします。Git が使えないときは付録 C のコピー提出に従い、リリースメモ.txt に「入れた機能 / 詰まった点」を書いてください。


  • 「動く」アプリを リリース品質 へ磨く工程を体験した(メッセージ・入力チェック・エラー画面・一覧の使い勝手・データの安全)
  • DataAnnotations:入力チェックを 属性で宣言的に 書ける(if 文方式と使い分け)
  • 並べ替え許可リスト(switch) で安全に、ページングOFFSET/FETCH
  • トランザクション:複数の更新を 「全部か、ゼロか」(BeginTransaction/Commit/Rollback)。SQL 研修の力が Web で活きる
  • ILogger自分で new しないのに渡ってくる ── これが次の 付録 N(DI) につながる

ここまで磨けたら、あなたのアプリは「課題」から「小さな業務アプリ」に近づいています。 さらに現場のお作法へ踏み込みたい人は、付録 N「依存性注入(DI)入門」 へ進みましょう。