Razor Pages vs MVC — Choosing the Right Pattern

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>
Note: The @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.
Tip: Mix Razor Pages and MVC in the same application — 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).
Warning: [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.

🧠 Test Yourself

A Razor Page has a [BindProperty] on a CreatePostInput property. A GET request arrives. Is the property populated from query string parameters?