Razor Pages
The page-focused alternative to MVC: a .cshtml file plus a PageModel with OnGet and OnPost handlers, and when it is simpler.
public IActionResult OnPost() { ... return RedirectToPage("/Index"); }Razor Pages keeps the markup and the code for one page next to each other. Pages/Contact.cshtml holds the HTML and begins with the @page directive, which must be its first directive. Pages/Contact.cshtml.cs holds a class deriving from PageModel. The URL comes from the file path, so Pages/Contact.cshtml answers /Contact and Pages/Index.cshtml answers /. There is no route pattern to write and no controller.
Handlers
A method named after the HTTP method runs for that request. OnGet handles GET and OnPost handles POST; OnGetAsync and OnPostAsync are the asynchronous forms, used when the handler awaits anything. Returning Page() renders this page, and RedirectToPage("/Index") sends the browser elsewhere. A property marked [BindProperty] is filled from the posted form, and [BindProperties] on the class does the same for all of its properties.
A page may have several POST handlers. The name after OnPost is the handler name, with any Async suffix dropped, so OnPostDeleteAsync is reached by a button carrying asp-page-handler="Delete".
Setting it up, and choosing it
builder.Services.AddRazorPages() and app.MapRazorPages() are the two lines needed. Razor Pages fits pages: a form, a list, a settings screen. Controllers fit one class serving a family of related URLs, and remain the normal choice for a JSON API. Both can live in the same project. Razor Pages also validates the antiforgery token on every unsafe request without being asked, which an MVC controller does not.
What trips people up
Handler methods must be public and spelled exactly, and a page's markup and its PageModel class must stay in matching files.
Example
// Pages/Contact.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace Shop.Pages;
public class ContactModel : PageModel
{
// Filled from the posted form. Not bound on GET.
[BindProperty]
[Required]
[EmailAddress]
public string Email { get; set; } = "";
// Read by the markup when the page renders.
public bool Sent { get; private set; }
// GET /Contact renders the form.
public void OnGet()
{
Sent = false;
}
// POST /Contact. A bad address returns 200 with the message beside
// the field; a good one returns 200 with the thank-you text.
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
return Page();
}
Sent = true;
return Page();
}
}When to use it
- Build a contact or settings screen where the form and its code belong to one page.
- Add a small server-rendered area to a project that otherwise serves JSON from controllers.
- Give one list page several buttons, each posting to its own named handler.
More examples
The page that goes with it
Note that @Model here is the PageModel object itself, not a separate view model, which is why the page reads Model.Sent and Model.Email. The asp-for tag helpers work exactly as they do in MVC views.
@page
@model Shop.Pages.ContactModel
@{
ViewData["Title"] = "Contact";
}
<h1>Contact</h1>
@if (Model.Sent)
{
<p>Thank you. We will reply to @Model.Email.</p>
}
else
{
@* No action attribute, so the antiforgery token is written into
the form, and Razor Pages checks it on post by itself. *@
<form method="post">
<label asp-for="Email"></label>
<input asp-for="Email" />
<span asp-validation-for="Email"></span>
<button type="submit">Send</button>
</form>
}The whole Program.cs for a Razor Pages app
Two lines turn the feature on, and adding AddControllersWithViews and MapControllerRoute beside them lets pages and controllers serve the same app. A request for /Contact is then matched to Pages/Contact.cshtml with no route pattern anywhere in the project.
// Program.cs of a web project (<Project Sdk="Microsoft.NET.Sdk.Web">).
var builder = WebApplication.CreateBuilder(args);
// Services for Razor Pages, including the Razor view engine.
builder.Services.AddRazorPages();
var app = builder.Build();
// Serves files from wwwroot, such as the stylesheet the layout links to.
app.UseStaticFiles();
// One endpoint per .cshtml file under Pages, named by its path.
app.MapRazorPages();
app.Run();Two POST handlers on one page
Reach for named handlers when one screen has several buttons that do different things, which is common on list pages. Redirecting after a successful post, rather than returning Page(), means a refresh does not repeat the action.
// Pages/Todo.cshtml.cs, with the same usings as the primary example.
public class TodoModel : PageModel
{
[BindProperty]
public string Title { get; set; } = "";
public void OnGet()
{
}
// Handler name: Add. The Async suffix is not part of the name.
public async Task<IActionResult> OnPostAddAsync()
{
await Task.CompletedTask; // stands in for saving the item
return RedirectToPage(); // reload this page with a GET
}
// Handler name: Clear.
public IActionResult OnPostClear() => RedirectToPage();
}
// In Pages/Todo.cshtml, inside <form method="post">:
// <button type="submit" asp-page-handler="Add">Add</button>
// <button type="submit" asp-page-handler="Clear">Clear all</button>
Discussion