Razor Pages is a new feature to ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive.
Razor Pages is included in version 2.0.0 of ASP.NET Core. Tooling support for Razor Pages in Visual Studio ships in Visual Studio 2017 Update 3.
Razor Pages is on by default by MVC. If you are using a typical Startup.cs
like the following, Razor Pages is already enabled.
public class Startup
{
public void ConfigureServices(IServiceCollections services)
{
services.AddMvc(); // includes support for pages as well as controllers
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
All of the new Razor Pages types and features are included in the Microsoft.AspNetCore.Mvc.RazorPages
assembly. If you are referencing the Microsoft.AspNetCore.Mvc
package, then a reference to the Razor Pages assembly is already included.
Let's look at a basic page.
@page
@{
var message = "Hello, World!";
}
<html>
<body>
<p>@message</p>
</body>
</html>
So far this is looking a lot like a regular old Razor view file. What makes it different is the new @page
directive. Using @page
makes this file into an MVC action - which means that it can handle requests directly, without going through a controller. @page
must occur first where it is used, as it affects the behavior of other Razor constructs.
If you save this file as /Pages/Index.cshtml
in the project directory, this it will match the URL path /
or /Index
and render Hello, World!
. If instead you name the file /Pages/Contact.cshtml
, it will match /Contact
. If you name it Contact
and put it in a folder /Pages/Store
then it will match /Store/Contact
. The associations of URL paths to pages is determined by the page's location in the file system.
The new Razor Pages features are designed to make common patterns used with web browsers simple. Let's take a look at a page that implements a basic 'contact us' form for a simple model:
For these examples this model class, and a database/DbContext
are already set up.
MyApp/Contact.cs
using System.ComponentModel.DataAnnotations;
namespace MyApp
{
public class Contact
{
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
}
}
MyApp/Pages/Contact.cs
@page
@using MyApp
@using Microsoft.AspNetCore.Mvc.RazorPages
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@inject ApplicationDbContext Db
@functions {
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
Db.Contacts.Add(Contact);
await Db.SaveChangesAsync();
return RedirectToPage();
}
return Page();
}
}
<html>
<body>
<p>Enter your contact info here and we will email you about our fine products!</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Contact.Name" /></div>
<div>Email: <input asp-for="Contact.Email" /></div>
<input type="submit" />
</form>
</body>
</html>
This page now has what we call a handler method. In this case the handler method (OnPostAsync
) runs for POST
requests, when the user submits the form. You can add handler methods for any HTTP verb, but you'll most frequently use an OnGet
handler to initialize any state a needed to show the HTML and an OnPost
to handle form submissions. The Async
naming suffix is optional, you can use it if you like to follow that convention. The code that's in OnPostAsync
in this case looks very similar to what you would normally write in a controller. This is typical for pages; most of the MVC primitives like model binding, validation, and action results are shared.
The basic flow of OnPostAsync
is:
- Check for validation errors
- If there are no errors, save the data and redirect
- Else, show the page again this time with the validation errors
When the data is entered successfully the, the OnPostAsync
handler method uses the new RedirctToPage()
helper method to return an instance of RedirectToPageResult
. This is new action result similar to RedirectToAction()
or RedirectToRoute()
but customized for pages. In this case it's going to redirect back to the same URL as the current page (/Contact
).
When the submitted data has validation errors, OnPostAsync
handler method uses the new Page()
helper method to return an instance of PageResult
. This is similar to have actions in controllers return View()
to execute the show. PageResult
is also the default for a handler method. A handler method that returns void
will render the page.
The Contact
property is using the new [BindProperty]
attribute to opt-in to model binding. For pages, properties are only bound for a POST (or any other non-GET verb) by default. Binding to properties can reduce the amount of code you have to write by using the same property to render form fields (<input asp-for="Contacts.Name" />
) and accept the input.
Rather than using @model
here, we're taking advantage of a special new feature for pages. By default, the generated Page
-derived class is the model. This means that features like model binding, tag helpers and HTML helpers all just work with the properties defined in @functions
. If you've every read articles that recommend using a view model with Razor views, you're getting a view model for free.
Notice that this Page also uses @inject
for dependency injection, which is the same as traditional Razor views - this generates the Db
property that is used in OnPostAsync
. @inject
-ed properties will be set before handler methods run.
We also didn't have to write any extra code for antiforgery validation. Antiforgery token generation and validation is automatic for pages. No additional code or attributes are needed, secure by default.
Another way to write this simple form would be separate the view code and the handler method into separate files, a 'code-behind' for the view code.
MyApp/Pages/Contact.chsml
@page
@using MyApp
@using MyApp.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@model ContactModel
<html>
<body>
<p>Enter your contact info here and we will email you about our fine products!</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Contact.Name" /></div>
<div>Email: <input asp-for="Contact.Email" /></div>
<input type="submit" />
</form>
</body>
</html>
MyApp/Pages/Contact.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyApp.Pages
{
public class ContactModel : PageModel
{
public ContactModel(ApplicationDbContext db)
{
Db = db;
}
[BindProperty]
public Contact Contact { get; set; }
private ApplicationDbContext Db { get; }
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
Db.Contacts.Add(Contact);
await Db.SaveChangesAsync();
return RedirectToPage();
}
return Page();
}
}
}
By convention the PageModel
class is called <PageName>Model
and is in the same namespace as the page. Not much change is needed to convert from a page using @functions
to define handlers and a page using a PageModel
class. The main change you would have to make is to add constructor injection for all of your @inject
-ed properties.
Using a PageModel
supports unit testing, but will require you to write an explicit constructor and class. Pages without PageModel
s have the advantage at development time that they support runtime compilation (and recompilation). Choose whichever way fits your style and needs.
Pages work with all of the features that you're already familar with from the Razor view engine. That is: layouts, partials, templates, tag helpers, _ViewStart.cshtml
, _ViewImports.cstml
all work in the same ways they do for conventional Razor views.
Let's declutter this page by taking advantage of some of those features.
First, add a layout page for the HTML skeleton, and set the Layout
property from _ViewStart.cshtml
. This should look pretty familar.
MyApp/Pages/_Layout.chsml
<html>
...
</html>
MyApp/Pages/_ViewStart.chsml
@{ Layout = "_Layout"; }
Notice that we're placing the layout in the MyApp/Pages
folder. Pages look for other views (layouts, templates, partials) hierarchically, starting in the same folder as the current page. This means that a layout in the MyApp/Pages
folder can be used from any page.
View search from a page also will include the MyApp/Views/Shared
folder so that layouts, templates, and partials you're using with MVC controllers and conventional Razor views 'just work'.
Next, let's add a _ViewImports.cshtml
MyApp/Pages/_ViewImports.chsml
@namespace MyApp.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
The @namespace
directive is a new feature that will control the namespace of the generated code - this allows us to get rid of @using
directives from our pages. The @namespace
directive works by computing the different in folders between your view code and the _ViewImports.cshtml
where it appears. So since our Customer.cshtml
is also in the MyApp/Pages
folder it will have the namespace MyApp.Pages
. If the path were instead MyApp/Pages/Store/Customer.cshtml
then the namespace of the generated code would be MyApp.Pages.Store
. This is intended so that C# classes you add and pages generated code just work without having to add extra usings.
@namespace
also works for conventional Razor views.
Here's what the page looks like after simplication.
MyApp/Pages/Contact.chsml
@page
@inject ApplicationDbContext Db
@functions {
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
Db.Contacts.Add(Contact);
await Db.SaveChangesAsync();
return RedirectToPage();
}
return Page();
}
}
<div class="row">
<div class="col-md-3">
<p>Enter your contact info here and we will email you about our fine products!</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Contact.Name" /></div>
<div>Email: <input asp-for="Contact.Email" /></div>
<input type="submit" />
</form>
</div>
</div>
Let's suppose we want to do something more useful than showing the same page again when the visitor submits their contact information. We can use RedirectToPage(...)
to redirect to other pages in a few useful ways.
This example adds a confirmation message and redirects back to the home page:
MyApp/Pages/Contact.chsml
@page
@inject ApplicationDbContext Db
@functions {
[BindProperty]
public Contact Contact { get; set; }
[TempData]
public string Message { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
Db.Contacts.Add(Contact);
await Db.SaveChangesAsync();
Message = "Thanks, we'll be in touch shortly.";
return RedirectToPage("/Index");
}
return Page();
}
}
<div class="row">
<div class="col-md-3">
<p>Enter your contact info here and we will email you about our fine products!</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Contact.Name" /></div>
<div>Email: <input asp-for="Contact.Email" /></div>
<input type="submit" />
</form>
</div>
</div>
MyApp/Pages/Index.chsml
@page
@functions {
[TempData]
public string Message { get; set; }
}
<div class="row">
<div class="col-md-3">
@if (Message != null)
{
<h3>@Message</h3>
}
<p>Hi, welcome to our website!</p>
</div>
</div>
We've now added another page MyApp/Pages/Index.cshtml
, and are redirecting to it using RedirectToPage("/Index")
. The string /Index
is the name of the page we just added, and can be used with Url.Page(...)
, <a asp-page="..." />
or RedirectToPage
.
The page name is just the path to the page from the root MyApp/Pages
folder (including a leading /
). It seems simple, but this is much more feature rich than just hardcoding a URL. This is URL generation using routing, and can generate and encode parameters accoring to how the route is defined in the destination path.
Additionally, URL generation for pages also supports relative names. From MyApp/Pages/Contact.cshtml
you could also redirect to MyApp/Pages/Index.cshtml
using RedirectToPage("Index")
or RedirectToPage("./Index")
. These are both relative names. The provided string is combined with the page name of the current page to compute the name of the destination page. You can also use the directory traversal ..
operator if you like.
Relative name linking is useful when building sites with a complex structure. For instance if you use relative names to link between pages in a folder, and then you rename that folder, all of the links still work (because they didn't include the folder name).
Since we have another page here we're also taking advantage of the [TempData]
attribute to pass data across pages. [TempData]
is a more convenient way to use the existing MVC temp data features. The [TempData]
attribute is new in 2.0.0 and is supported on controlers and pages. Also in 2.0.0 the default storage for temp data is now cookies, so a session provider is no longer required by default.
Let's update this form to support multiple operations. A visitor to the site can either join the mailing list, or ask for a free quote.
If you want one page to handle multiple logical action you can use named handler methods. Any text in the name after On<Verb>
and before Async
(if present) in the method name is considered a handler name. The handler methods in the following example have the handler names JoinMailingList
and RequestQuote
respectively.
MyApp/Pages/Contact.chsml
@page
@inject ApplicationDbContext Db
@functions {
[BindProperty]
public Contact Contact { get; set; }
public async Task<IActionResult> OnPostJoinMailingListAsync()
{
...
}
public async Task<IActionResult> OnPostRequestQuoteAsync()
{
...
}
}
<div class="row">
<div class="col-md-3">
<p>Enter your contact info here we will email you about our fine products! Or get a free quote!</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Contact.Name" /></div>
<div>Email: <input asp-for="Contact.Email" /></div>
<input type="submit" asp-page-handler="JoinMailingList" value="Join our mailing list"/>
<input type="submit" asp-page-handler="RequestQuote" value="Get a free quote"/>
</form>
</div>
</div>
The form in this example has two submit buttons, each using the new FormActionTagHelper
in conjunction to submit to a different URL. The asp-handler
attribute is a companion to asp-page
and will generate URLs that will submit to each of the handler methods defined by our page. We don't need to specify asp-page
here because we're linking to the current page.
In this case the URL path that submits to OnPostJoinMailingListAsync
is /Contact?handler=JoinMailingList
and the URL path that submits to OnPostRequestQuoteAsync
is /Contact?handler=RequestQuote
.
If you don't like seeing ?handler=RequestQuote
in the URL, we can change the route to put the handler name in the path portion of the URL. You can customize the route by adding a route template enclosed in quotes after the @page
directive.
@page "{handler?}"
@inject ApplicationDbContext Db
...
This route will now put the handler name in the URL path instead of the query string.
You can use @page
to add additional segments and parameters to a page's route, whatever's there will be appended to the default route of the page. Using an absolute or virtual path to change the page's route (like "~/Some/Other/Path"
) is not supported.
Use the extension method AddRazorPagesOptions
on the MVC builder to configure advanced options like this:
public class Startup
{
public void ConfigureServices(IServiceCollections services)
{
services.AddMvc().AddRazorPagesOptions(options =>
{
...
});
}
...
}
Currently you can use the RazorPagesOptions
to set the root directory for pages, or a add application model conventions for pages. We hope to enable more extensibility this way in the future.
We think that Razor Pages is a good way to lower the overhead of MVC for simple page-centric scenarios. We're of course interested in your feedback and experiences trying it out.
A few more words about our philosophy for Razor Pages.
We are trying to...
Make dynamic HTML and forms with ASP.NET Core easier, e.g. how many files & concepts required to print Hello World in a page, build a CRUD form, etc.
Reduce the number of files and folder-structure required for page-focused MVC scenarios.
Simplify the code required to implement common page-focused patterns, e.g. dynamic pages, CRUD forms, PRG, etc.
Allow straightforward migration to and from traditional MVC organization.
Enable the ability to return non-HTML responses when necessary, e.g. 404s.
Share the existing MVC features as much as possible:
- MVC's Model Binding and Validation
- Filters
- Action Results
- HTML Helpers and Tag Helpers
- Existing Razor features like
@inherits
,@model
,@inject
- Layouts & partials
- _ViewStart.cshtml and _ViewImports.cshtml
We are not trying to...
Create a scripted page framework to compete with PHP, etc.
Hide C# with a DSL in Razor or otherwise.
Create new Razor Pages primitives that don't work in controllers.
Create undue burdens for the ASP.NET team with regards to forking our user-base, tooling support, etc.