Edit

Share via


Razor Pages architecture and concepts in ASP.NET Core

By Rick Anderson, Dave Brock, and Kirk Larkin

Note

This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.

Warning

This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.

Razor Pages can make coding page-focused scenarios easier and more productive than using controllers and views.

If you're looking for a tutorial that uses the Model-View-Controller approach, see Get started with ASP.NET Core MVC.

This article covers the architecture, concepts, and patterns that make Razor Pages effective for building page-focused web applications. It explains how Razor Pages work, their key components, and best practices for implementation. If you prefer hands-on learning with step-by-step instructions, see Tutorial: Create a Razor Pages web app with ASP.NET Core. For an overview of ASP.NET Core, see the Introduction to ASP.NET Core.

Prerequisites

Create a Razor Pages project

See Get started with Razor Pages for detailed instructions on how to create a Razor Pages project.

Razor Pages

Razor Pages is enabled in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

In the preceding code:

Consider a basic page:

@page

<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

The preceding code looks a lot like a Razor view file used in an ASP.NET Core app with controllers and views. 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. Razor Pages file names have a .cshtml suffix.

A similar page, using a PageModel class, is shown in the following two files. The Pages/Index2.cshtml file:

@page
@using RazorPagesIntro.Pages
@model Index2Model

<h2>Separate page model</h2>
<p>
    @Model.Message
</p>

The Pages/Index2.cshtml.cs page model:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using System;

namespace RazorPagesIntro.Pages
{
    public class Index2Model : 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.

Write a basic form

Razor Pages is designed to make common patterns used with web browsers easy to implement when building an app. Model binding, Tag Helpers, and HTML helpers 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 Program.cs file.

The in memory database requires the Microsoft.EntityFrameworkCore.InMemory NuGet package.

using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<CustomerDbContext>(options =>
    options.UseInMemoryDatabase("name"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The data model:

using System.ComponentModel.DataAnnotations;

namespace RazorPagesContacts.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required, StringLength(10)]
        public string? Name { get; set; }
    }
}

The db context:

using Microsoft.EntityFrameworkCore;

namespace RazorPagesContacts.Data
{
    public class CustomerDbContext : DbContext
    {
        public CustomerDbContext (DbContextOptions<CustomerDbContext> options)
            : base(options)
        {
        }

        public DbSet<RazorPagesContacts.Models.Customer> Customer => Set<RazorPagesContacts.Models.Customer>();
    }
}

The Pages/Customers/Create.cshtml view file:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

The Pages/Customers/Create.cshtml.cs page model:

public class CreateModel : PageModel
{
    private readonly Data.CustomerDbContext _context;

    public CreateModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Customer? Customer { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

By convention, the PageModel class is called <PageName>Model and is in the same namespace as the page.

The PageModel class allows separation of the logic of a page from its presentation. It defines page handlers for requests sent to the page and the data used to render the page. This separation allows:

The page has an OnPostAsync handler method, which runs on POST requests (when a user posts the form). Handler methods for any HTTP verb can be added. The most common handlers are:

  • OnGet to initialize state needed for the page. In the preceding code, the OnGet method displays the Create.cshtml Razor Page.
  • OnPost to handle form submissions.

The Async naming suffix is optional but is often used by convention for asynchronous functions. The preceding code is typical for Razor Pages.

If you're familiar with ASP.NET apps using controllers and views:

  • The OnPostAsync code in the preceding example looks similar to typical controller code.
  • Most of the MVC primitives like model binding, validation, and action results work the same with Controllers and Razor Pages.

The previous OnPostAsync method:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.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. In many cases, validation errors would be detected on the client, and never submitted to the server.

The Pages/Customers/Create.cshtml view file:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>

The rendered HTML from Pages/Customers/Create.cshtml:

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input type="text" data-val="true"
           data-val-length="The field Name must be a string with a maximum length of 10."
           data-val-length-max="10" data-val-required="The Name field is required."
           id="Customer_Name" maxlength="10" name="Customer.Name" value="" />
    <input type="submit" />
    <input name="__RequestVerificationToken" type="hidden"
           value="<Antiforgery token here>" />
</form>

In the previous code, posting the form:

  • With valid data:

    • The OnPostAsync handler method calls the RedirectToPage helper method. RedirectToPage returns an instance of RedirectToPageResult. RedirectToPage:

      • Is an action result.
      • Is similar to RedirectToAction or RedirectToRoute (used in controllers and views).
      • Is customized for pages. In the preceding sample, it redirects to the root Index page (/Index). RedirectToPage is detailed in the URL generation for Pages section.
  • With 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.
    • In the preceding example, posting the form with no value results in ModelState.IsValid returning false. In this sample, no validation errors are displayed on the client. Validation error handling is covered later in this document.
    [BindProperty]
    public Customer? Customer { get; set; }
    
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
    
        if (Customer != null) _context.Customer.Add(Customer);
        await _context.SaveChangesAsync();
    
        return RedirectToPage("./Index");
    }
    
  • With validation errors detected by client side validation:

    • Data is not posted to the server.
    • Client-side validation is explained later in this document.

The Customer property uses [BindProperty] attribute to opt in to model binding:

[BindProperty]
public Customer? Customer { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    if (Customer != null) _context.Customer.Add(Customer);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

[BindProperty] should not be used on models containing properties that should not be changed by the client. For more information, see Overposting.

Razor Pages, by default, bind properties only with non-GET verbs. Binding to properties removes the need to writing code to convert HTTP data to the model type. Binding reduces code by using the same property to render form fields (<input asp-for="Customer.Name">) and accept the input.

Warning

For security reasons, you must opt in to binding GET request data to page model properties. Verify user input before mapping it to properties. Opting into GET binding is useful when addressing scenarios that rely on query string or route values.

To bind a property on GET requests, set the [BindProperty] attribute's SupportsGet property to true:

[BindProperty(SupportsGet = true)]

For more information, see ASP.NET Core Community Standup: Bind on GET discussion (YouTube).

Reviewing the Pages/Customers/Create.cshtml view file:

@page
@model RazorPagesContacts.Pages.Customers.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<p>Enter a customer name:</p>

<form method="post">
    Name:
    <input asp-for="Customer!.Name" />
    <input type="submit" />
</form>
  • In the preceding code, the input tag helper <input asp-for="Customer.Name" /> binds the HTML <input> element to the Customer.Name model expression.
  • @addTagHelper makes Tag Helpers available.

The home page

Index.cshtml is the home page:

@page
@model RazorPagesContacts.Pages.Customers.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>Contacts home page</h1>
<form method="post">
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
        @if (Model.Customers != null)
        {
            foreach (var contact in Model.Customers)
            {
                <tr>
                    <td> @contact.Id </td>
                    <td>@contact.Name</td>
                    <td>
                        <!-- <snippet_Edit> -->
                        <a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |
                        <!-- </snippet_Edit> -->
                        <!-- <snippet_Delete> -->
                        <button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>
                        <!-- </snippet_Delete> -->
                    </td>
                </tr>
            }
        }
        </tbody>
    </table>
    <a asp-page="Create">Create New</a>
</form>

The associated PageModel class (Index.cshtml.cs):

public class IndexModel : PageModel
{
    private readonly Data.CustomerDbContext _context;
    public IndexModel(Data.CustomerDbContext context)
    {
        _context = context;
    }

    public IList<Customer>? Customers { get; set; }

    public async Task OnGetAsync()
    {
        Customers = await _context.Customer.ToListAsync();
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var contact = await _context.Customer.FindAsync(id);

        if (contact != null)
        {
            _context.Customer.Remove(contact);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage();
    }
}

The Index.cshtml file contains the following markup:

<a asp-page="./Edit" asp-route-id="@contact.Id">Edit</a> |

The <a /a> 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, https://localhost:5001/Edit/1. Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.

The Index.cshtml file contains markup to create a delete button for each customer contact:

<button type="submit" asp-page-handler="delete" asp-route-id="@contact.Id">delete</button>

The rendered HTML:

<button type="submit" formaction="/Customers?id=1&amp;handler=delete">delete</button>

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 on 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 handler method with the name OnPostRemoveAsync is selected.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
    var contact = await _context.Customer.FindAsync(id);

    if (contact != null)
    {
        _context.Customer.Remove(contact);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage();
}

The OnPostDeleteAsync method: