Razor Pages Patterns について
ASP.NET Core Razor Pages のコード生成、レビュー、リファクタリングに使う実践ガイドです。構成、検証、セキュリティ、性能、テストを扱い、保守性と本番向けの規約を重視します。
主な機能
- 01ページと PageModel の責務分離
- 02モデルバインディングとサーバー側検証
- 03ルーティング、ナビゲーション、PRG
- 04不正リクエスト対策、認可、入力保護
- 05エラー処理、ログ、キャッシュ
- 06ユニットテストと統合テストの指針
活用シーン
- 01Razor Pages のプロジェクト構成設計
- 02フォーム検証と過剰バインディングの確認
- 03POST ハンドラーへの不正リクエスト対策追加
- 04PageModel とページリクエストのテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill razor-pages-patternsインストール後、Claude Code で/razor-pages-patternsを入力して読み込まれたか確認します。
詳細
あなたは、Razor Pages を専門とする ASP.NET Core のシニアアーキテクトです。Razor Pages のコードを生成、レビュー、またはリファクタリングする際は、これらのパターンを厳守してください。関心の分離、テスト容易性、セキュリティ、パフォーマンスを優先してください。最小ホスティングや null 許容参照型を有効にするなど、最新の機能を備えた .NET 8 以降を対象とします。
理由
Razor Pages は、コントローラーとビューを PageModel に統合することで MVC を簡素化し、ページ中心の Web アプリ向けモデルを提供します。本番環境では、不適切なパターンにより、コードの複雑化、セキュリティの脆弱性(CSRF など)、検証の不備、スケーラビリティの問題が発生します。これらのプラクティスは、Microsoft の規約、OWASP のガイドライン、コミュニティで検証された慣用パターンに準拠し、堅牢で保守しやすいアプリの構築を支援します。
ベストプラクティス
-
プロジェクト構成
- ページは
/Pagesフォルダー内に、論理的なサブフォルダー(例:/Pages/Account、/Pages/Admin)を作成して整理します。 _ViewImports.cshtmlを使用して、タグヘルパー、using ディレクティブ、モデルのインポートをグローバルに設定します。- プロジェクト全体で nullable 参照型を有効にし(
<Nullable>enable</Nullable>)、null を早期に検出します。 - ハイブリッドアプリの場合を除き、Razor Pages とコントローラー/API を同じプロジェクトに混在させないでください。area またはマイクロサービスを使用して関心事を分離します。
- ページは
-
PageModel の設計
- PageModel は簡潔に保ちます。依存関係(例:サービス、DbContext)はコンストラクター経由で注入します。
- アクションにはハンドラーメソッド(例:
OnGetAsync、OnPostAsync)を使用します。シンプルにするため、1ページあたりのハンドラーは1~2個に制限します。 - I/O バウンドの処理(例:DB 呼び出し)には、非同期ハンドラーを優先します。
[BindProperty]によるプロパティのバインドは必要最小限にします。過剰なデータのバインドによる攻撃を避けるため、複雑なフォームでは明示的なモデルバインディングを使用します。
-
モデルバインディングと検証
- バインドするモデルにはデータ注釈(例:
[Required]、[StringLength]、[EmailAddress])を使用します。 - 常にサーバー側で検証します。クライアント側の検証(jQuery Validation によるもの)は、任意の機能強化です。
- POST ハンドラーで
ModelState.IsValidを確認し、無効な場合はエラーを再表示するためにPage()を返します。 - カスタム検証には
IValidatableObjectを実装するか、FluentValidation との統合を使用します。
- バインドするモデルにはデータ注釈(例:
-
ルーティングとナビゲーション
- ルートテンプレート(例:
@page "/{id:int}")を指定した@pageディレクティブを使用します。 <a asp-page="/Index">のようなタグヘルパーを活用し、型安全なリンクを作成します。- 必要に応じて、カスタムルートハンドラーを使用し、スラッグや SEO に適した URL に対応します。
- 重複送信を防ぐ PRG(Post-Redirect-Get)パターンには、
RedirectToPageによるリダイレクトを使用します。
- ルートテンプレート(例:
-
ビューと Razor 構文
- .cshtml ファイルはビュー専用にし、ビジネスロジックを記述しないでください。再利用可能な UI にはパーシャル(
_Partial.cshtml)を使用します。 - HTML の生成にはタグヘルパー(例:
<input asp-for="Model.Property" />)を使用します。 - ページ固有の JS/CSS にはセクション(
@section Scripts { ... })を使用します。 - 本番環境では、
webOptimizerまたは組み込みミドルウェアを使用してバンドルと縮小化を有効にします。
- .cshtml ファイルはビュー専用にし、ビジネスロジックを記述しないでください。再利用可能な UI にはパーシャル(
-
セキュリティ対策
- フォームには
@Html.AntiForgeryToken()を含め、POST ハンドラーでは[ValidateAntiForgeryToken]を使用して検証し、常にアンチフォージェリトークンを適用します。 - 認証には PageModel に
[Authorize]を適用し、きめ細かなアクセス制御にはポリシーを使用します。 - XSS を防ぐために入力をサニタイズします。Razor はデフォルトでエスケープしますが、ユーザー生成コンテンツは検証してください。
app.UseHsts()とapp.UseHttpsRedirection()を使用して HTTPS を強制します。
- フォームには
-
エラー処理とロギング
- グローバルエラーには
app.UseExceptionHandler("/Error")を使用し、ユーザーフレンドリーなメッセージを表示する/Errorページを作成します。 - 注入した
ILogger<PageModel>で例外をログに記録します。 - 開発環境と本番環境を区別し、
app.UseDeveloperExceptionPage()を使用してスタックトレースを表示するのは開発環境のみにします。 NotFound()やBadRequest()などを適切に使用してステータスコードを返します。
- グローバルエラーには
-
パフォーマンスとスケーラビリティ
- ページに
[ResponseCache]を適用して出力キャッシュを使用します。 - 可能であればセッション状態を避け、1回限りのメッセージには TempData を優先します。
- 関連データを事前に読み込み、プロジェクションを使用して DB アクセスを最適化します。
- dotnet-trace や MiniProfiler などのツールでプロファイリングします。
- ページに
-
テスト
- 依存関係をモック(例:Moq を使用)して PageModel の単体テストを行います。
WebApplicationFactoryを使用してリクエストをシミュレートし、統合テストを行います。- Playwright または Selenium を使用して、フォームと送信の UI テストを行います。
- ハンドラーとモデルのカバレッジは 80% 以上を目指します。
例
適切に構成された PageModel(Index.cshtml.cs):
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace MyApp.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly IMyService _service;
public IndexModel(ILogger<IndexModel> logger, IMyService service)
{
_logger = logger;
_service = service;
}
[BindProperty]
public InputModel Input { get; set; } = new();
public string Message { get; set; } = string.Empty;
public async Task OnGetAsync()
{
Message = await _service.GetWelcomeMessageAsync();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
try
{
await _service.ProcessInputAsync(Input);
return RedirectToPage("/Success");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing input");
ModelState.AddModelError(string.Empty, "An error occurred.");
return Page();
}
}
public class InputModel
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
}
}
}
対応する Razor ビュー(Index.cshtml):
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>@Model.Message</p>
</div>
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Name" class="control-label"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
@Html.AntiForgeryToken()
</form>
アンチパターン
- 巨大な PageModel:無関係な複数のハンドラーを 1 つのモデルに詰め込む → ページを分割します。
- マジックストリング:ルートや URL をハードコードする → asp-page タグヘルパーまたは IUrlHelper を使用します。
- バリデーションの無視:ModelState.IsValid の確認を省略する → 無効なデータを防ぐため、必ず検証します。
- 非同期処理を同期的に実行する:ハンドラーで同期 DB 呼び出しを使用する → スレッドプールの枯渇を避けるため、非同期処理にします。
- ログなし:例外を黙って握りつぶす → コンテキスト情報を添えて、必ずログに記録します。
- 過剰なデータバインディング:許可リストを設けずにモデル全体をバインドする → [Bind("Property1,Property2")] またはビューモデルを使用します。
参考資料
- Microsoft Docs:https://learn.microsoft.com/en-us/aspnet/core/razor-pages/
- OWASP チートシート:https://cheatsheetseries.owasp.org/cheatsheets/DotNet_Security_Cheat_Sheet.html
- コミュニティ:https://github.com/dotnet/aspnetcore(サンプル)
- ツール:FluentValidation、MiniProfiler、テスト用の xUnit。
このスキルは必要な場合にのみ適用してください。タスクが Razor Pages に関係する場合に限ります。データアクセスには efcore-patterns、DI には dependency-injection-patterns など、他のスキルも参照してください。