ASP.NET Scaffolding と T4 テンプレートを用いた CRUD コードの自動生成戦略

開発効率化におけるコード生成の選択

企業規模のアプリケーション開発において、標準的な CRUD 操作、ページネーション、検索機能、および批量操作の実装は繰り返しの多い作業です。開発コストを抑制し、生産性を向上させるために、ASP.NET Scaffolding と T4 テンプレート技術を組み合わせたコード生成アプローチを採用しました。この手法により、作成、閲覧、編集、削除に加え、検索、ページ分割、削除確認ダイアログ、批量削除などの機能を自動的に実装することが可能になります。

スキャフォールディングテンプレートのカスタマイズ

ASP.NET のコード基架機能は、特定のディレクトリ構造に基づいてテンプレートを解釈します。生成ロジックを業務要件に適合させるためには、以下のテンプレートディレクトリを修正する必要があります。

  • MvcControllerWithContext: Entity Framework を使用した MVC コントローラーを生成する際の T4 テンプレートです。ここを編集することで、アクションメソッドの内部ロジックを制御できます。
  • MvcView: 対応するビュー(View)ファイルを生成するテンプレートです。フォーム要素や JavaScript ロジックのカスタマイズはここで行います。

コントローラーコードの生成実装

コントローラーの Index アクションでは、検索クエリとページネーション処理を自動的に含めるように設定します。以下の例では、キーワード検索とページング処理を非同期で実行する実装を示しています。

public async Task<ActionResult> Search(string keyword = "", int currentPage = 1, int pageSize = 20)
{
    // データソースの取得と関連エンティティの読み込み
    var source = _context.NotificationMessages
                         .Include(m => m.Author)
                         .Include(m => m.Editor)
                         .AsQueryable();

    // 検索フィルターの適用
    if (!string.IsNullOrEmpty(keyword))
    {
        source = source.Where(m => m.Title.Contains(keyword) 
                                || m.Body.Contains(keyword) 
                                || m.Author.Name.Contains(keyword));
    }

    // ページネーション処理の実行
    var totalRecords = await source.CountAsync();
    var items = await source.OrderBy(m => m.CreatedAt)
                            .Skip((currentPage - 1) * pageSize)
                            .Take(pageSize)
                            .ToListAsync();

    var viewModel = new PagedResult<NotificationMessage>
    {
        Items = items,
        CurrentPage = currentPage,
        PageSize = pageSize,
        TotalCount = totalRecords
    };

    return View(viewModel);
}

このコードにより、基本的な検索と分页ロジックが備わります。複雑な検索条件が必要な場合は、Where 節を拡張することで対応可能です。

批量操作の実装

複数のアイテムに対して一括で処理を行う機能は管理画面で頻繁に利用されます。コントローラー側では、操作タイプと ID リストを受け取り、一括削除などの処理を行います。

[HttpPost]
[Route("api/messages/bulk/{actionType}")]
public async Task<JsonResult> ProcessBulkAction(string actionType, params string[] selectedKeys)
{
    var result = new ApiResult();
    
    if (selectedKeys == null || selectedKeys.Length == 0)
    {
        result.IsSuccess = false;
        result.ErrorMessage = "処理対象が選択されていません。";
        return Json(result);
    }

    try
    {
        // 対象データの取得
        var targets = await _context.NotificationMessages
                                    .Where(m => selectedKeys.Contains(m.MessageId))
                                    .ToListAsync();

        if (!targets.Any())
        {
            result.IsSuccess = false;
            result.ErrorMessage = "対象データが見つかりません。";
            return Json(result);
        }

        // 操作タイプによる分岐
        switch (actionType.ToUpperInvariant())
        {
            case "REMOVE":
                _context.NotificationMessages.RemoveRange(targets);
                await _context.SaveChangesAsync();
                result.IsSuccess = true;
                result.Message = $"{targets.Count} 件のデータを削除しました。";
                break;
            default:
                result.IsSuccess = false;
                result.ErrorMessage = "未定義の操作です。";
                break;
        }
    }
    catch (Exception ex)
    {
        result.IsSuccess = false;
        result.ErrorMessage = ex.Message;
    }

    return Json(result);
}

フロントエンド側の批量操作ロジック

ビュー側では、チェックボックスで選択された ID を収集し、サーバーへ送信する JavaScript 関数を用意します。

function handleBulkAction(actionType, confirmText) {
    var $selected = $("input[name='selectedIds']:checked");
    
    if ($selected.length === 0) {
        alert("少なくとも 1 つ選択してください。");
        return;
    }

    var execute = function() {
        var idList = [];
        $selected.each(function() {
            idList.push($(this).val());
        });

        $.ajax({
            url: "/api/messages/bulk/" + actionType,
            type: "POST",
            data: { selectedKeys: idList },
            success: function(response) {
                if (response.IsSuccess) {
                    location.reload();
                } else {
                    alert(response.ErrorMessage);
                }
            }
        });
    };

    if (confirmText) {
        if (confirm(confirmText)) {
            execute();
        }
    } else {
        execute();
    }
}

// イベントバインディング
$(document).ready(function() {
    $("#bulkActionBtn").on("click", function() {
        var action = $(this).data("action");
        var msg = $(this).data("confirm");
        handleBulkAction(action, msg);
    });
});

複合主鍵への対応

単一の主鍵ではなく、複数の列を組み合わせて主鍵とするテーブル也存在します。この場合、ID を文字列として結合して渡す方式を採用します。例えば、注文 ID と行番号をアンダースコアで結合します。

ビュー側のチェックボックス値:

<input type="checkbox" name="selectedIds" value="@(item.OrderId + "_" + item.LineNum)" />

コントローラー側での受信と分解:

public async Task<ActionResult> Edit(string id)
{
    var keys = id.Split('_');
    if (keys.Length != 2) return HttpNotFound();

    var orderId = int.Parse(keys[0]);
    var lineNum = int.Parse(keys[1]);

    var entity = await _context.OrderDetails.FindAsync(orderId, lineNum);
    // 編集処理...
}

批量操作においても、同様に文字列配列として ID を受け取り、内部で結合キーを構築して検索条件に使用することで、複合主鍵を持つエンティティの一括処理を実現できます。

タグ: ASP.NET-MVC Entity-Framework T4-Templates Code-Scaffolding C-Sharp

5月20日 15:19 投稿