Razor Pages is an alternative ASP.NET Core page model that co-locates the page’s code and markup in a single unit — a .cshtml file with a PageModel class. Unlike MVC, where controllers and views are in separate files, a Razor Page handles its own GET and POST requests through OnGet and OnPost handlers. Razor Pages often simplifies CRUD-heavy pages: the overhead of a controller class, a service registration, and separate view files for a simple create form can feel disproportionate. Understanding both patterns and knowing when to choose each is essential for building production ASP.NET Core applications.
Razor Pages vs MVC — Side by Side
// ── MVC approach — three files for a create post page ─────────────────────
// 1. Controllers/PostsController.cs
public class PostsController(IPostService service) : Controller
{
[HttpGet]
public IActionResult Create() => View(new CreatePostViewModel());
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreatePostViewModel model)
{
if (!ModelState.IsValid) return View(model);
await service.CreateAsync(model.ToRequest());
return RedirectToAction(nameof(Index));
}
}
// 2. Views/Posts/Create.cshtml — separate view file
// 3. ViewModels/CreatePostViewModel.cs — separate ViewModel
// ── Razor Pages approach — one self-contained file ─────────────────────────
// Pages/Posts/Create.cshtml (and Create.cshtml.cs PageModel)
// OR: fully inline with @functions or separate code-behind class
// Create.cshtml.cs — PageModel
public class CreateModel(IPostService service) : PageModel
{
// [BindProperty] tells model binding to populate this on POST
[BindProperty]
public CreatePostViewModel Post { get; set; } = new();
public void OnGet()
{
// Nothing needed — Post is initialised with defaults
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page(); // re-display the page
await service.CreateAsync(Post.ToRequest());
TempData["Success"] = "Post created!";
return RedirectToPage("Index"); // redirect to Index.cshtml page
}
}
// Create.cshtml — the view (with @page directive for routing)
// @page
// @model CreateModel
//
// <h1>Create Post</h1>
// <form method="post">
// <input asp-for="Post.Title" />
// <button type="submit">Create</button>
// </form>
@page directive at the top of a Razor Page makes it an endpoint that can handle HTTP requests directly. Without @page, the file is treated as a regular Razor view (like in MVC) and is not addressable as a standalone URL. The page’s URL is derived from its file path: Pages/Posts/Create.cshtml maps to /Posts/Create. Razor Pages routing is file-system-based by default, unlike MVC’s pattern-based routing.AddControllersWithViews() and AddRazorPages() can be registered together. A common pattern: use Razor Pages for simple CRUD admin pages (where one handler per page is clean and self-contained) and MVC controllers for complex pages with multiple views, shared view logic, or non-page-centric concerns like API endpoints. The two co-exist with separate routing conventions (MapRazorPages() and MapControllerRoute() both in Program.cs).[BindProperty] in Razor Pages binds the property from the HTTP request on POST (and optionally GET with [BindProperty(SupportsGet = true)]). Overly broad [BindProperty] declarations create the same mass assignment risk as in MVC: if the PageModel has public properties without [BindProperty] that share names with POST fields, they are not bound — but if you add [BindProperty] to a property that has sensitive values (AuthorId, IsAdmin), those become user-settable. Only use [BindProperty] on properties that should legitimately accept user input.When to Choose Each Pattern
| Scenario | Razor Pages | MVC |
|---|---|---|
| Simple CRUD forms | ✅ Simpler (fewer files) | Works but more ceremony |
| Complex controller with many actions | Gets unwieldy | ✅ Controllers organise multiple actions |
| API endpoints | Not designed for this | ✅ ControllerBase for JSON APIs |
| Complex routing needs | Limited (file-based) | ✅ Full route template control |
| Shared views across actions | No shared handler concept | ✅ Multiple actions, one shared view |
| Admin CRUD dashboards | ✅ One page per operation | Works but more files |
Common Mistakes
Mistake 1 — Using Razor Pages for complex multi-step workflows
❌ Wrong — wizard-style multi-step form spread across many PageModel classes with complex state passing.
✅ Correct — use MVC controllers for complex workflows where a single controller orchestrates multiple views and state.
Mistake 2 — Missing @page directive (page not addressable as URL)
❌ Wrong — Razor Page without @page is treated as a regular Razor view, not a page endpoint.
✅ Correct — every Razor Pages file must start with @page as the first directive.