Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive. One of new features of ASP.NET Core 2.0 is support for Razor Pages. Yes, those same pages that came times ago with WebMatrix. Today Razor Pages is subset of MVC on ASP.NET Core. Yes, support for Razor Pages comes with ASP.NET Core MVC meaning that Razor Pages application is technically MVC application. Also Razor Pages have same features as MVC views.
Why Razor Pages?
MVC developers want probably ask why we need one more way to build web sites on ASP.NET Core? Isn’t MVC enough? From information I have found from public space I found the following reasoning:
- It’s easier to get to web development for beginners as Razor pages are more lightweight than MVC. Besides beginners there are people who are coming from other scripting languages be it old ASP or PHP or something else.
- Razor Pages fit well to smaller scenarios where building controllers and models as separate classes is overkill.
I don’t fully agree with these points as MVC on ASP.NET Core is lightweight and flexible enough. I cover also smaller scenarios with it and it goes way faster as I’m using things I already know very well. The amount of code that MVC introduces is not so big that it makes a lot of difference for small applications.
ASP.NET Core 2.0 prerequisites
Install .NET Core 2.0.0 or later.
If you're using Visual Studio, install Visual Studio 2017 version 15.3 or later with the following workloads:
- ASP.NET and web developmen
- .NET Core cross-platform development
Creating a Razor Pages project
See Getting started with Razor Pages for detailed instructions on how to create a Razor Pages project using Visual Studio.
Razor Pages
Razor Pages is enabled in Startup.cs:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
Consider a basic page:
@page
Hello, world!
The time on the server is @DateTime.Now
The preceding code looks a lot like a Razor view file. What makes it different is the @page
directive. @page
makes the file into an MVC action - which means that it handles requests directly, without going through a controller. @page
must be the first Razor directive on a page. @page
affects the behavior of other Razor constructs.
A similar page, using a PageModel
class, is shown in the following two files. The Pages/Index2.cshtml file:
@page
@using RazorPages
@model IndexModel2
Separate page model
@Model.Message
The Pages/Index2.cshtml.cs "code-behind" file:
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace RazorPages
{
public class IndexModel2 : PageModel
{
public string Message { get; private set; } = "PageModel in C#";
public void OnGet()
{
Message += $" Server time is { DateTime.Now }";
}
}
}
By convention, the PageModel
class file has the same name as the Razor Page file with .cs appended. For example, the previous Razor Page is Pages/Index2.cshtml. The file containing the PageModel
class is named Pages/Index2.cshtml.cs.
The associations of URL paths to pages are determined by the page's location in the file system. The following table shows a Razor Page path and the matching URL:
File name and path |
matching URL |
/Pages/Index.cshtml |
/ or /Index |
/Pages/Contact.cshtml |
/Contact |
/Pages/Store/Contact.cshtml |
/Store/Contact |
/Pages/Store/Index.cshtml |
/Store or /Store/Index |
Notes:
- The runtime looks for Razor Pages files in the Pages folder by default.
Index
is the default page when a URL doesn't include a page.
Writing a basic form
Razor Pages features are designed to make common patterns used with web browsers easy. Model binding, Tag Helpers, and HTML helpers all just work with the properties defined in a Razor Page class. Consider a page that implements a basic "contact us" form for the Contact
model:+
For the samples in this document, the DbContext
is initialized in the Startup.cs file.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;
namespace RazorPagesContacts
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("name"));
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
}
The data model:
using System.ComponentModel.DataAnnotations;
namespace RazorPagesContacts.Data
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
}
}
The db context:
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Customer> Customers { get; set; }
}
}
The Pages/Create.cshtml view file:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
Enter your name.
Name:
</body>
</html>
The Pages/Create.cshtml.cs code-behind file for the view:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}
By convention, the PageModel
class is called <PageName>Model
and is in the same namespace as the page
Using a PageModel
code-behind file supports unit testing, but requires you to write an explicit constructor and class. Pages without PageModel
code-behind files support runtime compilation, which can be an advantage in development. The page has an OnPostAsync
handler method, which runs on POST
requests (when a user posts the form). You can add handler methods for any HTTP verb. The most common handlers are:
OnGet
to initialize state needed for the page
OnPost
to handle form submissions.
The Async
naming suffix is optional but is often used by convention for asynchronous functions. The OnPostAsync
code in the preceding example looks similar to what you would normally write in a controller. The preceding code is typical for Razor Pages. Most of the MVC primitives like model binding, validation, and action results are shared.
The previous OnPostAsync
method:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The basic flow of OnPostAsync
:
Check for validation errors.
- If there are no errors, save the data and redirect.
- If there are errors, show the page again with validation messages. Client-side validation is identical to traditional ASP.NET Core MVC applications. In many cases, validation errors would be detected on the client, and never submitted to the server.
When the data is entered successfully, the OnPostAsync
handler method calls the RedirectToPage
helper method to return an instance of RedirectToPageResult
. RedirectToPage
is a new action result, similar to RedirectToAction
or RedirectToRoute
, but customized for pages. In the preceding sample, it redirects to the root Index page (/Index).
When the submitted form has validation errors (that are passed to the server), the OnPostAsync
handler method calls the Page
helper method. Page
returns an instance of PageResult
. Returning Page
is similar to how actions in controllers return View
. PageResult
is the default return type for a handler method. A handler method that returns void renders the page
.
The Customer
property uses [BindProperty]
attribute to opt in to model binding.
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
Razor Pages, by default, bind properties only with non-GET verbs. Binding to properties can reduce the amount of code you have to write. Binding reduces code by using the same property to render form fields (<input asp-for="Customer.Name" />
) and accept the input.
The home page (Index.cshtml):
@page
@model RazorPagesContacts.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Contacts
@foreach (var contact in Model.Customers)
{
}
ID |
Name |
@contact.Id |
@contact.Name |
edit
|
Create
The code behind Index.cshtml.cs file:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace RazorPagesContacts.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
public IndexModel(AppDbContext db)
{
_db = db;
}
public IList<Customer> Customers { get; private set; }
public async Task OnGetAsync()
{
Customers = await _db.Customers.AsNoTracking().ToListAsync();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
}
}
The Index.cshtml file contains the following markup to create an edit link for each contact:
edit
The Anchor Tag Helper used the asp-route-{value} attribute to generate a link to the Edit page. The link contains route data with the contact ID. For example, http://localhost:5000/Edit/1
.
The Pages/Edit.cshtml file:
@page "{id:int}"
@model RazorPagesContacts.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Edit Customer";
}
Edit Customer - @Model.Customer.Id
The first line contains the @page "{id:int}"
directive. The routing constraint "{id:int}"
tells the page to accept requests to the page that contain int
route data. If a request to the page doesn't contain route data that can be converted to an int
, the runtime returns an HTTP 404 (not found) error.
The Pages/Edit.cshtml.cs file:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
public EditModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Customer = await _db.Customers.FindAsync(id);
if (Customer == null)
{
return RedirectToPage("/Index");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Attach(Customer).State = EntityState.Modified;
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}
return RedirectToPage("/Index");
}
}
}
The Index.cshtml file also contains markup to create a delete button for each customer contact:
When the delete button is rendered in HTML, its formaction
includes parameters for:
- The customer contact ID specified by the
asp-route-id
attribute.
- The
handler
specified by the asp-page-handler
attribute.
When the button is selected, a form POST
request is sent to the server. By convention, the name of the handler method is selected based the value of the handler
parameter according to the scheme OnPost[handler]Async
.
Because the handler is delete in this example, the OnPostDeleteAsync
handler method is used to process the POST
request. If the asp-page-handler
is set to a different value, such as remove
, a page handler method with the name OnPostRemoveAsync
is selected.
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
The OnPostDeleteAsync
method:
- Accepts the
id
from the query string.
- Queries the database for the customer contact with
FindAsync
.
- If the customer contact is found, they're removed from the list of customer contacts. The database is updated.
- Calls
RedirectToPage
to redirect to the root Index page (/Index
).
XSRF/CSRF and Razor Pages
You don't have to write any code for antiforgery validation. Antiforgery token generation and validation are automatically included in Razor Pages.
Using Layouts, partials, templates, and Tag Helpers with Razor Pages
Pages work with all the features of the Razor view engine. Layouts, partials, templates, Tag Helpers, _ViewStart.cshtml, _ViewImports.cshtml work in the same way they do for conventional Razor views. Let's declutter this page by taking advantage of some of those features.
Add a layout page to Pages/_Layout.cshtml:
<!DOCTYPE html>
<html>
<head>
<title>Razor Pages Sample</title>
</head>
<body>
Home
@RenderBody()
Create
</body>
</html>
The Layout:
The Layout property is set in Pages/_ViewStart.cshtml:
@{
Layout = "_Layout";
}
- Controls the layout of each page (unless the page opts out of layout).
- Imports HTML structures such as JavaScript and stylesheets.
Note: The layout is in the Pages folder. Pages look for other views (layouts, templates, partials) hierarchically, starting in the same folder as the current page. A layout in the Pages folder can be used from any Razor page under the Pages folder.
We recommend you not put the layout file in the Views/Shared folder. Views/Shared is an MVC views pattern. Razor Pages are meant to rely on folder hierarchy, not path conventions.
View search from a Razor Page includes the Pages folder. The layouts, templates, and partials you're using with MVC controllers and conventional Razor views just work.
Add a Pages/_ViewImports.cshtml file:
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@namespace
is explained later in the tutorial. The @addTagHelper
directive brings in the built-in Tag Helpers to all the pages in the Pages folder. When the @namespace directive is used explicitly on a page:
@page
@namespace RazorPagesIntro.Pages.Customers
@model NameSpaceModel
Name space
@Model.Message
The directive sets the namespace for the page. The @model
directive doesn't need to include the namespace.
When the @namespace
directive is contained in _ViewImports.cshtml, the specified namespace supplies the prefix for the generated namespace in the Page that imports the @namespace
directive. The rest of the generated namespace (the suffix portion) is the dot-separated relative path between the folder containing _ViewImports.cshtml and the folder containing the page. For example, the code behind file Pages/Customers/Edit.cshtml.cs explicitly sets the namespace:
namespace RazorPagesContacts.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
public EditModel(AppDbContext db)
{
_db = db;
}
// Code removed for brevity.
The Pages/_ViewImports.cshtml file sets the following namespace:
@namespace RazorPagesContacts.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The generated namespace for the Pages/Customers/Edit.cshtml Razor Page is the same as the code behind file. The @namespace
directive was designed so the C# classes added to a project and pages-generated code just work without having to add an @using
directive for the code behind file.
Note: @namespace
also works with conventional Razor views.
The original Pages/Create.cshtml view file:
@page
@model RazorPagesContacts.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
Enter your name.
Name:
</body>
</html>
The updated Pages/Create.cshtml view file:
@page
@model CreateModel
<html>
<body>
Enter your name.
Name:
</body>
</html>
The Razor Pages starter project contains the Pages/_ValidationScriptsPartial.cshtml, which hooks up client-side validation.
URL generation for Pages
The Create
page, shown previously, uses RedirectToPage
:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
The app has the following file/folder structure:
- /Pages
- Index.cshtml
- /Customer
- Create.cshtml
- Edit.cshtml
- Index.cshtml
The Pages/Customers/Create.cshtml and Pages/Customers/Edit.cshtml pages redirect to Pages/Index.cshtml after success. The string /Index is part of the URI to access the preceding page. The string /Index can be used to generate URIs to the Pages/Index.cshtml page. For example:
Url.Page("/Index", ...)
<a asp-page="/Index">My Index Page</a>
RedirectToPage("/Index")
The page name is the path to the page from the root /Pages folder (including a leading /, for example /Index). The preceding URL generation samples are much more feature rich than just hardcoding a URL. URL generation uses routing and can generate and encode parameters according to how the route is defined in the destination path.
URL generation for pages supports relative names. The following table shows which Index page is selected with different RedirectToPage
parameters from Pages/Customers/Create.cshtml:
RedirectToPage(x) |
Page |
RedirectToPage("/Index") |
Pages/Index |
RedirectToPage("./Index") |
Pages/Customers/Index |
RedirectToPage("../Index") |
Pages/Index |
RedirectToPage("Index") |
Pages/Customers/Index |
RedirectToPage("Index")
, RedirectToPage("./Index")
, and RedirectToPage("../Index")
are relative names. The RedirectToPage
parameter is combined with the path of the current page to compute the name of the destination page.
Relative name linking is useful when building sites with a complex structure. If you use relative names to link between pages in a folder, you can rename that folder. All the links still work (because they didn't include the folder name).
TempData in ASP.NET Core
ASP.NET Core exposes the TempData property on a controller. This property stores data until it is read. The Keep
and Peek
methods can be used to examine the data without deletion. TempData
is useful for redirection, when data is needed for more than a single request.
The [TempData]
attribute is new in ASP.NET Core 2.0 and is supported on controllers and pages. The following code sets the value of Message
using TempData
:
public class CreateDotModel : PageModel
{
private readonly AppDbContext _db;
public CreateDotModel(AppDbContext db)
{
_db = db;
}
[TempData]
public string Message { get; set; }
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
Message = $"Customer {Customer.Name} added";
return RedirectToPage("./Index");
}
}
The following markup in the Pages/Customers/Index.cshtml file displays the value of Message
using TempData
.
Msg: @Model.Message
The Pages/Customers/Index.cshtml.cs code-behind file applies the [TempData]
attribute to the Message
property.
[TempData]
public string Message { get; set; }
See TempData for more information.
Multiple handlers per page
The following page generates markup for two page handlers using the asp-page-handler
Tag Helper:
@page
@model CreateFATHModel
<html>
<body>
Enter your name.
Name:
</body>
</html>
The form in the preceding example has two submit buttons, each using the FormActionTagHelper
to submit to a different URL. The asp-page-handler
attribute is a companion to asp-page
. asp-page-handler
generates URLs that submit to each of the handler methods defined by a page. asp-page
is not specified because the sample is linking to the current page. The code-behind file:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace RazorPagesContacts.Pages.Customers
{
public class CreateFATHModel : PageModel
{
private readonly AppDbContext _db;
public CreateFATHModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostJoinListAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
public async Task<IActionResult> OnPostJoinListUCAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Customer.Name = Customer.Name?.ToUpper();
return await OnPostJoinListAsync();
}
}
}
The preceding code uses named handler methods. Named handler methods are created by taking the text in the name after On<HTTP Verb>
and before Async (if present). In the preceding example, the page methods are OnPostJoinListAsync
and OnPostJoinListUCAsync
. With OnPost
and Async
removed, the handler names are JoinList
and JoinListUC
.
Using the preceding code, the URL path that submits to OnPostJoinListAsync
is http://localhost:5000/Customers/CreateFATH?handler=JoinList
. The URL path that submits to OnPostJoinListUCAsync
is http://localhost:5000/Customers/CreateFATH?handler=JoinListUC
.
Customizing Routing in ASP.NET Core
If you don't like the query string ?handler=JoinList
in the URL, you 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 double quotes after the @page
directive.
@page "{handler?}"
@model CreateRouteModel
<html>
<body>
Enter your name.
Name:
</body>
</html>
The preceding route puts the handler name in the URL path instead of the query string. The ?
following handler
means the route parameter is optional.
You can use @page
to add additional segments and parameters to a page's route. Whatever's there is 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.
Configuration and settings
To configure advanced options, use the extension method AddRazorPagesOptions
on the MVC builder:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.RootDirectory = "/MyPages";
options.Conventions.AuthorizeFolder("/MyPages/Admin");
});
}
Currently you can use the RazorPagesOptions
to set the root directory for pages, or add application model conventions for pages. We hope to enable more extensibility this way in the future.
To precompile views, see Razor view compilation. Download or view sample code. See Getting started with Razor Pages in ASP.NET Core, which builds on this introduction.
reference: https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=visual-studio