Razor Pages and MVC views

Page models group a URL's handlers with its markup, and tag helpers remove the stringly-typed parts of a view.

Page models and handlers

// Pages/Orders/Edit.cshtml.cs
public sealed class EditModel : PageModel
{
    private readonly AppDbContext _db;
    public EditModel(AppDbContext db) => _db = db;

    [BindProperty]
    public OrderInput Input { get; set; } = new();

    [TempData] public string? StatusMessage { get; set; }

    public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
    {
        var order = await _db.Orders.FindAsync(new object[] { id }, ct);
        if (order is null) return NotFound();

        Input = new OrderInput(order.Id, order.Note);
        return Page();
    }

    public async Task<IActionResult> OnPostAsync(int id, CancellationToken ct)
    {
        if (!ModelState.IsValid) return Page();

        var order = await _db.Orders.FindAsync(new object[] { id }, ct);
        if (order is null) return NotFound();

        order.Note = Input.Note;
        await _db.SaveChangesAsync(ct);
        StatusMessage = "Saved.";

        // Post, redirect, get: refresh does not resubmit the form
        return RedirectToPage("./Edit", new { id });
    }
}
<form method="post">
  <!-- The anti-forgery token is added by the form tag helper -->
  <input asp-for="Input.Note" class="form-control" />
  <span asp-validation-for="Input.Note" class="text-danger"></span>

  <button type="submit" asp-page-handler="Save">Save</button>
  <a asp-page="/Orders/Index" asp-route-id="@Model.Input.Id">Cancel</a>
</form>

@section Scripts {
  <partial name="_ValidationScriptsPartial" />
}
  • Handlers are named OnGet, OnPost and named variants like OnPostSaveAsync selected by asp-page-handler.
  • Rendering a page directly from a POST without redirecting means a refresh re-submits. Redirect after every successful state change.
  • asp-for generates the name, the value and the validation attributes from the model, which is why a renamed property updates the view.
  • The @section Scripts block requires the layout to render it; a partial that is never called is a silent source of missing client-side validation.

Layouts, partials and view components

<!-- Views/Shared/_Layout.cshtml -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>@ViewData["Title"] - Example</title>
  <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
  <header><partial name="_Nav" model="Model.Nav" /></header>
  <main>
    @RenderBody()
  </main>
  <footer><partial name="_Footer" /></footer>
  @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
// A view component is a partial with logic and its own DI
public sealed class OrderSummaryViewComponent : ViewComponent
{
    private readonly AppDbContext _db;
    public OrderSummaryViewComponent(AppDbContext db) => _db = db;

    public async Task<IViewComponentResult> InvokeAsync(int customerId,
                                                        CancellationToken ct)
    {
        var summary = await _db.Orders
            .Where(o => o.CustomerId == customerId)
            .GroupBy(o => o.Currency)
            .Select(g => new CurrencyTotal(g.Key, g.Sum(o => o.Total)))
            .ToListAsync(ct);

        return View(summary);
    }
}

// Used from a page or a view
// @await Component.InvokeAsync("OrderSummary", new { customerId = 7 })
💡
Choose Razor Pages when the URL maps to one page with its own handlers; choose MVC controllers when one URL shape serves several related views; choose Blazor when the interaction is genuinely stateful on the client. Mixing all three in one application is possible and usually reflects a migration in progress.

FAQ

Why is my validation message not showing?
Usually because the validation script partial is missing, or because client-side validation is disabled and the server-side message is not rendered by a asp-validation-for tag.
Should TempData or a query string carry the status message?
TempData for a redirect after a POST, because it survives exactly one redirect and is then cleared. A query string leaks into logs and into bookmarks.

Interactive UI with Blazor Controllers, dependency injection and middleware

Last refreshed 2026-09-18.