Razor pages is a new aspect of ASP.NET Core MVC introduced in ASP.NET Core 2.0. It offers a "page-based" approach for building server-side rendered apps in ASP.NET Core and can coexist with "traditional" MVC or Web API controllers. In this post I provide an introduction to Razor Pages, the basics of getting started, and how Razor Pages differs from MVC.
Razor Pages vs MVC
If you've used ASP.NET Core for building server-side rendered apps, then you'll be familiar with the traditional Model-View-Controller (MVC) pattern. Razor Pages provides an abstraction over the top of MVC, which can make it better suited to some page-based apps.
In MVC, controllers are used to group similar actions together. When a request is received, routing directs the request to a single action method. This method typically performs some processing or business logic, and returns an IActionResult
, commonly a ViewResult
or a RedirectResult
. If a ViewResult
is returned, a Razor view is rendered using the provided view model.
MVC provides a lot of flexibility, so the grouping of actions into controllers can be highly discretionary, but you commonly group actions that are related in some way, such as by URL route or by function. For example, you might group by domain components so that in an ecommerce app actions related to "products" would be in the ProductController
, while "cart" actions would be in the CartController
. Alternatively, actions may be grouped based on technical aspects; for example, where all actions on a controller share a common set of authorization requirements.
A common pattern you'll find is to have pairs of related actions inside a controller. This is especially true where you are using HTML forms, where you would typically have one action to handle the initial GET
request, and another action for the POST
request. Both of these actions use the same URL route and the same Razor view. From the point of view of a user (or the developer), they're logically two aspects of the same "page".
In some cases, you may find that your controllers are filled with these action method pairs. For example, the default ASP.NET Core Identity AccountController
for an MVC app contains many such pairs:
The GET and POST pair of actions are highly coupled, as they both return the same view model, may need similar initialization logic, and use the same Razor view. The pair of actions are also related to the overall controller in which they're located (they're all related to identity and accounts), but they're more closely related to each other.
Razor Pages offers much the same functionality as traditional MVC, but using a slightly different model by taking advantage of this pairing. Each route (each pair of actions) becomes a separate Razor Page, instead of grouping many similar actions together under a single controller. That page can have multiple handlers that each respond to a different HTTP verb, but use the same view. The same Identity AccountController
from above could therefore be rewritten in Razor Pages as shown below. In fact, as of ASP.NET Core 2.1, the new project templates use Razor Pages for Identity, even in an MVC app.
Razor Pages have the advantage of being highly cohesive. Everything related to a given page in your app is in one place. Contrast that with MVC controllers where some actions are highly correlated, but the controller as a whole is less cohesive.
Another good indicator for using Razor Pages is when your MVC controllers are just returning a Razor view with no significant processing required. A classic example is the HomeController
from the ASP.NET Core 2.0 templates, which includes four actions:
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
These actions are not really related, but every action needs a controller, and the HomeController
is a somewhat convenient location to put them. The Razor Pages equivalent places the Index
(Home), About
, Contact
, and Error
pages in the root directory, removing the implicit links between them. As an added bonus, everything related to the About
page (for example) can be found in the files About.cshtml
and About.cshtml.cs
, which are located together on disk and in your solution explorer. That is in contrast to the MVC approach where controllers, view model, and view files are often located in completely different folders.
However, both of these approaches are functionally identical, so which one should you choose?
When should you use Razor Pages?
It's important to realize that you don't have to go all-in with Razor Pages. Razor Pages uses exactly the same infrastructure as traditional MVC, so you can mix Razor Pages with MVC and Web API controllers all in the same app. Razor Pages also uses the same ASP.NET Core primitives as traditional MVC, so you still get model binding, validation, and action results.
From a maintainability point of view, I find the extra cohesion afforded by Razor Pages makes it preferable for new development. Not having to jump back and forth between the controller, view model, and view files is surprisingly refreshing!
Having said that, there are some situations where it may be preferable to stick with traditional MVC controllers:
- When you have a lot of MVC action filters on your controllers. You can use filters in Razor Pages too, but they generally provide less fine-grained control than for traditional MVC. For example, you can't apply Razor Page filters to individual handlers (for example,
GET
vsPOST
handlers) within a Razor Page. - When your MVC controllers aren't rendering views. Razor Pages are focused around a "page" model, where you're rending a view for the user. If your controllers are either Web API controllers, or aren't designed to provide pages a user navigates through, then Razor Pages don't really make sense.
- When your controller is already highly cohesive, and it makes sense to centralise the action methods in one file.
On the other hand, there are some situations where Razor Pages really shines:
- When your action methods have little or no logic and are just returning views (for example, the
HomeController
shown previously). - When you have HTML forms with pairs of
GET
andPOST
actions. Razor Pages makes each pair a cohesive page, which I find requires less cognitive overhead when developing, rather than having to jump between multiple files. - When you were previously using ASP.NET Web Pages (WebMatrix). This framework provided a lightweight page-based model, but it was completely separate from ASP.NET. In contrast, Razor Pages has a similar level of simplicity, but also the full power of ASP.NET Core when required.
Now you've seen the high-level differences between MVC and Razor Pages, it's time to dive into the specifics. How do you create a Razor Page, and how does it differ from a traditional Razor View?
The Razor Page Model
Razor Pages are built on top of MVC, but they use a slightly different paradigm than the MVC pattern. With MVC the controller typically provides the logic and behavior for an action, ultimately producing a view model which contains data that is used to render the view. Razor Pages takes a slightly different approach, by using a Page Model.
Compared to MVC, the page model acts as both a mini-controller and the view model for the view. It's responsible for both the behavior of the page and for exposing the data used to generate the view. This pattern is closer to the Model-View-ViewModel (MVVM) pattern used in some desktop and mobile frameworks, especially if the business logic is pushed out of the page model and into your "business" model.
[Diagram of Model, View, View Model, with view model calling into the Model to get the required data, make changes etc, and exposing data to View]
In technical terms a Razor Page is very similar to a Razor view, except it has an @page
directive at the top of the file:
@page
<div>The time is @DateTime.Now</div>
As with Razor views, any HTML in the Razor page is rendered to the client, and you can use the @
symbol to render C# values or use C# control structures. See the documentation for a complete reference guide to Razor syntax.
Adding @page
is all that's required to expose your page, but this page doesn't use a page model yet. More typically you create a class that derives from PageModel
and associate it with your cshtml
file. You can include your PageModel
and Razor view in the same cshtml
file if you wish, but best practice is to keep the PageModel
in a "code-behind" file, and only include presentational data in the cshtml
file. By convention, if your razor page is called MyPage.cshtml
, the code-behind file should be named MyPage.cshtml.cs
:
The PageModel
class is the page model for the Razor view. When the Razor page is rendered, properties exposed on the PageModel
are available in the .cshtml
view. For example, you could expose a property for the current time in your Index.cshtml.cs
file:
using System;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel: PageModel
{
public DateTime CurrentTime => DateTime.UtcNow;
}
And render it in your Index.cshtml
file using standard Razor syntax:
@page
@model IndexModel
<div>The current time is @Model.CurrentTime.ToShortTimeString()</div>
If you're familiar with Razor views, this should be quite familiar. You declare the type of the model using the @model
directive, which is exposed on the Model
property. The difference is that instead of your MVC controller passing in a View Model of type IndexModel
, the PageModel
itself is exposed as the Model
property.
Routing in Razor Pages
Razor Pages, like MVC, uses a mixture of conventions, configuration, and declarative directives to control how your app behaves. They use the same routing infrastructure as MVC under the hood; the difference is in how routing is configured.
- For MVC and Web API, you use either attribute or convention-based routing to match an incoming URL to a controller and action.
- For Razor Pages, the path to the file on disk is used to calculate the URL at which the page can be reached. By convention, all Razor Pages are nested in the
Pages
directory.
For example if you create a Razor Page in your app at /Pages/MyFolder/Test.cshtml
, it would be exposed at the URL /MyFolder/Test
. This is definitely a positive feature for working with Razor Pages—navigating and visualizing the URLs exposed by your app is as easy as viewing the file structure.
Having said that, Razor Page routes are fully customizable, so if you need to expose your page at a route that doesn't correspond to it's path on disk, simply provide a route template in the @page
directive. This can also include other route parameters, as shown below:
@page "/customroute/customized/{id?}"
This page would be exposed at the URL /customroute/customized
, and it could also bind an optional id
segment in the URL, /customroute/customized/123
, for example. The id
value can be bound to a PageModel
property using model binding.
Model binding in Razor Pages
In MVC the method parameters of an action in a controller are bound to the incoming request by matching values in the URL, query string, or body of the request as appropriate (see the documentation for details). In Razor Pages the incoming request is bound to the properties of the PageModel
instead.
For security reasons, you have to explicitly opt-in to the properties to bind, by decorating properties to bind with the [BindProperty]
attribute:
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
[BindProperty]
public string Search { get;set; }
public DateTime CurrentTime { get; set; };
}
In this example, the Search
property would be bound to the request as it's decorated with [BindProperty]
, but CurrentTime
would not be bound. For GET requests, you have to go one step further and set the SupportsGet
property on the attribute:
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
[BindProperty(SupportsGet = true)]
public string Search { get;set; }
}
If you're binding complex models, such as a form post-back, then adding the [BindProperty]
attribute everywhere could get tedious. Instead I like to create a single property as the "input model" and decorate this with [BindProperty]
. This keeps your PageModel
public surface area explicit and controlled. A common extension to this approach is to make your input model a nested class. This often makes sense as you commonly don't want to use your UI-layer models elsewhere in your app:
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
public class IndexModel : PageModel
{
public bool IsEmailConfirmed { get; set; }
[BindProperty]
public InputModel Input { get; set; }
public class InputModel
{
[Required, EmailAddress]
public string Email { get; set; }
[Required, Phone, Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
}
In this example only the property Input
is bound to the incoming request. This uses the nested InputModel
class, to define all the expected values to bind. If you need to bind another value, you can add another property to InputModel
.
Handling multiple HTTP verbs with Razor Page Handlers
One of the main selling points of Razor Pages is the extra cohesion they can bring over using MVC controllers. A single Razor Page contains all of the UI code associated with a given Razor view by using page handlers to respond to requests.
Page handlers are analogous to action methods in MVC. When a Razor Page receives a request, a single handler is selected to run, based on the incoming request and the handler names. Handlers are matched via a naming convention, On{Verb}[Async]
, where {Verb}
is the HTTP method, and [async]
is optional. For example:
OnPost
andOnPostAsync
run in response toPOST
requestsOnDelete
andOnDeleteAsync
run in response toDELETE
requestsOnGet
andOnGetAsync
run in response toGET
requests (and optionallyHEAD
requests in ASP.NET Core 2.1+).
For HTML forms, it's very common to have an OnGet
handler that displays the initial empty form, and an OnPost
handler which handles the POST
back from the client. For example the following form shows the code-behind for a Razor Page that updates a user's display name.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
public class UpdateDisplayNameModel : PageModel
{
private readonly IUserService _userService;
public IndexModel(IUserService userService)
{
_userService = userService;
}
[BindProperty]
public InputModel Input { get; set; }
public void OnGet()
{
Input.DisplayName = _userService.GetDefaultDisplayName();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
await _userService.UpdateDisplayName(User, Input.DisplayName);
return RedirectToPage("/Index");
}
public class InputModel
{
[Required, StringLength(50)]
public string DisplayName { get; set; }
}
}
This Razor Page uses a fictional IUserService
which is injected into the PageModel
constructor. The OnGet
page handler runs when the form is initially requested, and sets the default display name to a value obtained from the IUserService
. The form is sent to the client, who fills in the details and posts it back.
The OnPostAsync
handler runs in response to the POST
, and follows a similar pattern to MVC action methods. You should first check that the PageModel
passes model validation using ModelState.IsValid
, and if not re-display the form using Page()
. Page()
is semantically equivalent to the View()
method in MVC controllers; it's used to render the view and return a response.
If the PageModel
is valid, the form uses the provided DisplayName
value to update the name of the currently logged in user, User
. The PageModel
provides access to many of the same properties that the Controller
base class does, such as HttpContext
, Request
, and in this case, User
. Finally, the handler redirects to another Razor Page using the RedirectToPage()
method. This is functionally equivalent to the MVC RedirectToAction()
method.
The OnGet
and OnPost
pair of handlers are common when a form only has a single possible role, but it's also possible to have a single Razor Page with multiple handlers for the same verb. To create a named handler, use the naming convention On{Verb}{Handler}[Async]
. For example, maybe we want to add a handler to the UpdateDisplayName
Razor page that allows users to reset their username to the default. We could add the following ResetName
handler to the existing Razor page:
public async Task<IActionResult> OnPostResetNameAsync()
{
await _userService.ResetDisplayName(User);
return RedirectToPage("/Index");
}
To invoke the handler, you pass the handler name in the query string for the POST
, for example ?handler=resetName
. This ensures the named handler is invoked instead of the default OnPostAsync
handler. If you don't like the use of query strings here, you can use a custom route and include the handler name in a path segment instead.
This section showed handlers for GET
and POST
but it’s also possible to have page handlers for other HTTP verbs like DELETE
, PUT
, and PATCH
. These verbs are generally not used by HTML forms, so will not commonly be required in a page-oriented Razor Pages app. However they follow the same naming conventions and behaviour as other page handlers should you need them to be called by an API for some reason.
Using tag helpers in Razor Pages
When you're working with MVC actions and views, ASP.NET Core provides various tag helpers such as asp-action
and asp-controller
for generating links to your actions from Razor views. Razor Pages have equivalent tag helpers, where you can use asp-page
to generate the path to a specific Razor page based, and asp-page-handler
to set a specific handler.
For example, you could create a "reset name" button in your form using both the asp-page
and asp-page-handler
tags:
<button asp-page="/Index" asp-page-handler="ResetName" type="submit">Reset Display Name</button>
Summary
Razor Pages are a new aspect of ASP.NET Core MVC that were introduced in ASP.NET Core 2.0. They are built on top of existing ASP.NET Core primitives and provide the same overall functionality as traditional MVC, but with a page-based model. For many apps, the page-based approach using a PageModel
can result in more cohesive code than traditional MVC. Razor Pages can be used seamlessly in the same app as traditional MVC or Web API controllers, so you only need to use it where it is a good fit.
If you're creating a new app using Razor, I strongly suggest considering Razor Pages as the default approach. It may feel strange for experienced MVC developers at first, but I've been pleasantly surprised by the improved development experience. For existing MVC apps, adding new Razor Pages is easy—but it's unlikely to be worth migrating a whole MVC app to use them. They're functionally identical to MVC, so the main benefit is a workflow that's more convenient for common development tasks.
Additional Resources
Learn Razor Pages is a tutorial website setup by Microsoft MVP Mike Brind. It's a great resource that I highly recommend in addition to the official documentation.