September 2017

Volume 32 Number 9

[ASP.NET Core]

Simpler ASP.NET MVC Apps with Razor Pages

By Steve Smith

Razor Pages are a new feature in ASP.NET Core 2.0. They provide a simpler way to organize code within ASP.NET Core applications, keeping implementation logic and view models closer to the view implementation code. They also offer a simpler way to get started developing ASP.NET Core apps, but that doesn’t mean you should dismiss them if you’re an experienced .NET developer. You can also use Razor Pages to improve the organization of larger and more complex ASP.NET Core apps.

The Model-View-Controller (MVC) pattern is a mature UI pattern that Microsoft has supported for developing ASP.NET applications since 2009. It offers a number of benefits that can help application developers achieve a separation of concerns, resulting in more maintainable software. Unfortunately, the pattern as implemented in the default project templates often results in a lot of files and folders, which can add friction to development, especially as an application grows. In my September 2016 article, I wrote about using Feature Slices as one approach to address this issue (msdn.com/magazine/mt763233). Razor Pages offer a new and different way to tackle this same problem, especially for scenarios that are conceptually page-based. This approach is especially useful when all you have is a nearly static view, or a simple form that just needs to perform a POST-Redirect-GET. These scenarios are the sweet spot for Razor Pages, which avoid a great deal of the convention required by MVC apps.

Getting Started with Razor Pages

To get started using Razor Pages, you can create a new ASP.NET Core Web Application in Visual Studio using ASP.NET Core 2.0, and select the Razor Pages template, as shown in Figure 1.

ASP.NET Core 2.0 Web Application with Razor Pages Template

Figure 1 ASP.NET Core 2.0 Web Application with Razor Pages Template

You can achieve the same thing from the dotnet command-line interface (CLI) using:

dotnet new razor

You’ll need to make sure you’re running at least version 2.0 of the .NET Core SDK; check with:

dotnet --version

In either case, if you examine the project produced, you’ll see it includes a new folder, Pages, as shown in Figure 2.

Razor Pages Project Template Organization

Figure 2 Razor Pages Project Template Organization

Notably absent from this template are two folders that are typically associated with MVC projects: Controllers and Views. Razor Pages use the Pages folder to hold all of the pages for the application. You’re free to use folders within the Pages root folder to organize pages in whatever way makes sense for your application. Razor Pages allow developers to combine the code-quality features of the MVC pattern with the productivity benefits of grouping together things that tend to change together.

Note that Pages is part of ASP.NET Core MVC in version 2. You can add support for Pages to any ASP.NET Core MVC app by simply adding a Pages folder and adding Razor Pages files to this folder.

Razor Pages use the folder structure as a convention for routing requests. While the default page in a typical MVC app can be found at “/,” as well as “/Home/” and “/Home/Index,” the default Index page in an app using Razor Pages will match “/” and “/Index.” Using subfolders, it’s very intuitive to create different sections of your app, with routes that match accordingly. Each folder can have an Index.cshtml file to act as its root page.

Looking at an individual Page, you’ll find there’s a new page directive, @page, that’s required on Razor Pages. This directive must appear on the first line of the page file, which should use the .cshtml extension. Razor Pages look and behave very similarly to Razor-based View files, and a very simple page can include just HTML:

@page
<h1>Hello World</h1>

Where Razor Pages shine is in encapsulating and grouping UI details. Razor Pages support inline or separate class-based page models, which can represent data elements the page will display or manipulate. They also support handlers that eliminate the need for separate controllers and action methods. These features greatly reduce the number of separate folders and files required to work with a given page on a Web app. Figure 3 compares the folders and files required for a typical MVC-based approach with the Razor Pages approach.

MVC Folders and Files vs. Razor Pages

Figure 3 MVC Folders and Files vs. Razor Pages

To demonstrate Razor Pages in the context of an ASP.NET Core MVC app, I’m going to use a simple sample project.

A Sample Project

To simulate a project with a little bit of complexity and some different feature areas, I’m going to return to the sample I used for my Feature Slices article. This sample involves viewing and managing a number of different kinds of entities, including ninjas and ninja swords, as well as pirates, plants, and zombies. Imagine the app is a companion for a casual game and helps you manage in-game constructs. Using the typical MVC organizational approach, you’d most likely have many different folders holding controllers, views, viewmodels, and more for each of these kinds of constructs. With Razor Pages, you can create a simple folder hierarchy that maps to your application’s URL structure.

In this case, the application has a simple homepage and four different sections, each with its own subfolder under Pages. The folder structure is very clean, with just the homepage (Index.cshtml) and some supporting files in the root of the Pages folder, and the other sections in their own folders, as Figure 4 shows.

Folder Organization with Razor Pages

Figure 4 Folder Organization with Razor Pages

Simple pages often don’t need separate page models. For example, the list of ninja swords shown in /Ninjas/Swords/Index.cshtml simply uses inline variables, as Figure 5 shows.

Figure 5 Using Inline Variables

@page
@{ 
  var swords = new List<string>()
  {
    "Katana",
    "Ninjago"
  };
}
<h2>Ninja Swords</h2>
<ul>
  @foreach (var item in swords)
  {
    <li>@item</li>
  }
</ul>
<a asp-page="/Ninjas/Index">Ninja List</a>

Variables declared in Razor blocks are in scope on the page; you’ll see how you can declare functions and even classes via @functions blocks in the next section. Note the use of the new asp-page tag helper in the link at the bottom of the page. These tag helpers reference pages by their routes, and support absolute and relative paths. In this example, “/Ninjas/Index” could also have been written as “../Index” or even just “..” and it would route to the same Index.cshtml Razor Page in the Ninjas folder. You can also use the asp-page tag helper on <form> elements to specify a form’s destination. Because the asp-page tag helpers build on top of the powerful ASP.NET Core routing support, they support many URL generation scenarios beyond simple relative URLs.

Page Models

Razor Pages can support strongly typed page models. You specify the model for a Razor Page with the @model directive (just like a strongly typed MVC View). You can define the model within the Razor Page file, as shown in Figure 6.

Figure 6 Defining the Model

@page
@using WithRazorPages.Core.Interfaces;
@using WithRazorPages.Core.Model;
@model IndexModel
@functions
{
  public class IndexModel : PageModel
  {
    private readonly IRepository<Zombie> _zombieRepository;
       
    public IndexModel(IRepository<Zombie> zombieRepository)
    {
      _zombieRepository = zombieRepository;
    }
    // additional code omitted
  }
}

You can also define the page model in a separate codebehind file named Pagename.cshtml.cs. In Visual Studio, files that follow this convention are linked to their corresponding page file, making it easy to navigate between them. The same code shown in the @functions block in Figure 6 could be placed into a separate file.

There are pros and cons to both approaches for storing page models. Placing page-model logic within the Razor Page itself results in fewer files and allows for the flexibility of runtime compilation, so you can make updates to the page’s logic without the need for a full deployment of the app. On the other hand, compilation errors in page models defined within Razor Pages may not be discovered until runtime. Visual Studio will show errors in open Razor files (without actually compiling them). Running the dotnet build command doesn’t compile Razor Pages or provide any information about potential errors in these files.

Separate page-model classes offer slightly better separation of concerns, because the Razor Page can focus purely on the template for displaying data, leaving the separate page model to handle the structure of the page’s data and the corresponding handlers. Separate codebehind page models also benefit from compile-time error checking and are easier to unit test than inline page models. Ultimately, you can choose whether to use no model, an inline model or separate page models in your Razor Pages.

Routing, Model Binding and Handlers

Two key features of MVC that are typically found within Controller classes are routing and model binding. Most ASP.NET Core MVC apps use attributes to define routes, HTTP verbs and route parameters, using syntax like this:

[HttpGet("{id}")]
public Task<IActionResult> GetById(int id)

As previously noted, the route path for Razor Pages is convention-based, and matches the page’s location within the /Pages folder hierarchy. However, you can support route parameters by adding them to the @page directive. Instead of specifying supported HTTP verbs using attributes, Razor Pages use handlers that follow a naming convention of OnVerb, where Verb is an HTTP verb like Get, Post and so on. Razor Page handlers behave very similarly to MVC Controller actions, and they use model binding to populate any parameters they define. Figure 7 shows a sample Razor Page that uses route parameters, dependency injection and a handler to display the details of a record.

Figure 7 Details.cshtml—Displaying Details for a Given Record Id

public async Task OnGetAsync()
{
  Ninjas = _ninjaRepository.List()
    .Select(n => new NinjaViewModel { Id = n.Id, Name = n.Name }).ToList();
}

public async Task<IActionResult> OnPostAddAsync()
{
  var entity = new Ninja()
  {
    Name = "Random Ninja"
  };
_  ninjaRepository.Add(entity);

  return RedirectToPage();
}

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
  var entityToDelete = _ninjaRepository.GetById(id);
_ ninjaRepository.Delete(entityToDelete);

  return RedirectToPage();
}
@page "{id:int}"
@using WithRazorPages.Core.Interfaces;
@using WithRazorPages.Core.Model;
@inject IRepository<Ninja> _repository

@functions {
  public Ninja Ninja { get; set; }

  public IActionResult OnGet(int id)
  {
    Ninja = _repository.GetById(id);

    // A void handler (no return) is equivalent to return Page()
    return Page();
  }
}
<h2>Ninja: @Ninja.Name</h2>
<div>
    Id: @Ninja.Id
</div>
<div>
    <a asp-page="..">Ninja List</a>
</div>

Pages can support multiple handlers, so you can define OnGet, OnPost and so forth. Razor Pages also introduce a new model-binding attribute, [BindProperty], which is especially useful on forms. You can apply this attribute to a property on a Razor Page (with or without an explicit PageModel) to opt into data binding for non-GET requests to the page. This enables tag helpers like asp-for and asp-validation-for to work with the property you’ve specified, and allows handlers to work with bound properties without having to specify them as method parameters. The [BindProperty] attribute also works on Controllers.

Figure 8 shows a Razor Page that lets users add new records to the application.

Figure 8 New.cshtml—Adds a New Plant

@page
@using WithRazorPages.Core.Interfaces;
@using WithRazorPages.Core.Model;
@inject IRepository<Plant> _repository

@functions {
  [BindProperty]
  public Plant Plant { get; set; }

  public IActionResult OnPost()
  {
    if(!ModelState.IsValid) return Page();

    _repository.Add(Plant);

    return RedirectToPage("./Index");
  }
}
<h1>New Plant</h1>
<form method="post" class="form-horizontal">
  <div asp-validation-summary="All" class="text-danger"></div>
  <div class="form-group">
    <label asp-for="Plant.Name" class="col-md-2 control-label"></label>
    <div class="col-md-10">
      <input asp-for="Plant.Name" class="form-control" />
      <span asp-validation-for="Plant.Name" class="text-danger"></span>
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-offset-2 col-md-10">
      <button type="submit" class="btn btn-primary">Save</button>
    </div>
  </div>
</form>
<div>
  <a asp-page="./Index">Plant List</a>
</div>

It’s pretty common to have a page that supports more than one operation using the same HTTP verb. For example, the main page in the sample supports listing the entities (as the default GET behavior), as well as the ability to delete an entry or add a new entry (both as POST requests). Razor Pages support this scenario using named handlers, shown in Figure 9, which include the name after the verb (but before the “Async” suffix, if present). The PageModel base type is similar to the base Controller type in that it provides a number of helper methods you can use when returning action results. When performing updates like adding a new record, you often want to redirect the user immediately after the operation, if successful. This eliminates the issue of browser refreshes triggering duplicate calls to the server, resulting in duplicate records (or worse). You can use RedirectToPage with no arguments to redirect to the default GET handler of the current Razor Page.

Figure 9 Named Handlers

public async Task OnGetAsync()
{
  Ninjas = _ninjaRepository.List()
    .Select(n => new NinjaViewModel { Id = n.Id, Name = n.Name }).ToList();
}

public async Task<IActionResult> OnPostAddAsync()
{
  var entity = new Ninja()
  {
    Name = "Random Ninja"
  };
_  ninjaRepository.Add(entity);

  return RedirectToPage();
}

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
  var entityToDelete = _ninjaRepository.GetById(id);
_ ninjaRepository.Delete(entityToDelete);

  return RedirectToPage();
}

You can specify a named handler using the asp-page-handler tag helper, applied to a form, link or button:

<a asp-page-handler="Handler">Link Text</a>
<button type="submit" asp-page-handler="delete" asp-route-id="@id">Delete</button>

The asp-page-handler tag uses routing to build the URL. The handler name and any asp-route-parameter attributes are applied as querystring values by default. The Delete button in the previous code generates a URL like this one:

Ninjas?handler=delete&id=1

If you’d rather have the handler as part of the URL, you can specify this behavior with the @page directive:

@page "{handler?}/{id?}"

With this route specified, the generated link for the Delete button would be:

Ninjas/Delete/1

Filters

Filters are another powerful feature of ASP.NET Core MVC (and one I covered in the August 2016 issue: msdn.microsoft.com/mt767699). If you’re using a Page Model in a separate file, you can use attribute-based filters with Razor Pages, including placing filter attributes on the page model class. Otherwise, you can still specify global filters when you configure MVC for your app. One of the most common uses of filters is to specify authorization policies within your app. You can configure folder- and page-based authorization policies globally:

services.AddMvc()
  .AddRazorPagesOptions(options =>
  {
    options.Conventions.AuthorizeFolder("/Account/Manage");
    options.Conventions.AuthorizePage("/Account/Logout");
    options.Conventions.AllowAnonymousToPage("/Account/Login");
  });

You can use all of the existing kinds of filters with Razor Pages except for Action filters, which apply only to action methods within Controllers. Razor Pages also introduce the new Page filter, represented by IPageFilter (or IAsyncPageFilter). This filter lets you add code that will run after a particular page handler has been selected, or before or after a handler method executes. The first method can be used to change which handler is used to handle a request, for example:

public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
  context.HandlerMethod = 
    context.ActionDescriptor.HandlerMethods.First(m => m.Name == "Add");
}

After a handler has been selected, model binding occurs. After model binding, the OnPageHandlerExecuting method of any page filters is called. This method can access and manipulate any model-bound data available to the handler, and can short circuit the call to the handler. The OnPageHandlerExecuted method is then called after the handler has executed, but before the action result executes.

Conceptually, page filters are very similar to action filters, which run before and after actions execute.

Note that one filter, ValidateAntiforgeryToken, isn’t required for Razor Pages at all. This filter is used to protect against Cross-Site Request Forgery (CSRF or XSRF) attacks, but this protection is built into Razor Pages automatically.

Architectural Pattern

Razor Pages ship as part of ASP.NET Core MVC, and take advantage of many built-in ASP.NET Core MVC features like routing, model binding and filters. They share some naming similarity with the Web Pages feature that Microsoft shipped with Web Matrix in 2010. However, while Web Pages primarily targeted novice Web developers (and were of little interest to most experienced developers), Razor Pages combine strong architectural design with approachability.

Architecturally, Razor Pages don’t follow the Model-View-Controller (MVC) pattern, because they lack Controllers. Rather, Razor Pages follow more of a Model-View-ViewModel (MVVM) pattern that should be familiar to many native app developers. You can also consider Razor Pages to be an example of the Page Controller pattern, which Martin Fowler describes as “An object that handles a request for a specific page or action on a Web site. That [object] may be the page itself, or it may be a separate object that corresponds to that page.” Of course, the Page Controller pattern should also be familiar to anyone who has worked with ASP.NET Web Forms, because this was how the original ASP.NET pages worked, as well.

Unlike ASP.NET Web Forms, Razor Pages are built on ASP.NET Core and support loose coupling, separation of concerns and SOLID principles. Razor Pages are easily unit tested (if separate PageModel classes are used) and can provide a foundation for clean, maintainable enterprise applications. Don’t write off Razor Pages as just a “training wheels” feature meant for hobbyist programmers. Give Razor Pages a serious look and consider whether Razor Pages (alone or in combination with traditional Controller and View pages) can improve the design of your ASP.NET Core application by reducing the number of folders you need to jump between when working on a particular feature.

Migrating

Although Razor Pages don’t follow the MVC pattern, they’re so closely compatible with the existing ASP.NET Core MVC Controllers and Views that switching between one and the other is usually very simple. To migrate existing Controller/View-based pages to use Razor Pages, follow these steps:

  1. Copy the Razor View file to the appropriate location in the /Pages folder.
  2. Add the @page directive to the View. If this was a GET-only View, you’re done.
  3. Add a PageModel file named viewname.cshtml.cs and place it in the folder with the Razor Page.
  4. If the View had a ViewModel, copy it to a PageModel file.
  5. Copy any actions associated with the view from its Controller to the PageModel class.
  6. Rename the actions to use the Razor Pages handler syntax (for example, “OnGet”).
  7. Replace references to View helper methods with Page methods.
  8. Copy any constructor dependency injection code from the Controller to the PageModel.
  9. Replace code-passing model to views with a [BindProperty] property on the PageModel.
  10. Replace action method parameters accepting view model objects with a [BindProperty] property, as well.

A well-factored MVC app will often have separate files for views, controllers, viewmodels, and binding models, usually each in separate folders in the project. Razor Pages allow you to consolidate these concepts into a couple of linked files, in a single folder, while still allowing your code to follow logical separation of concerns.

You should be able to reverse these steps to move from a Razor Pages implementation to a Controller/View-based approach, in most cases. Following these steps should work for most simple MVC-based actions and views. More complex applications may require additional steps and troubleshooting.

Next Steps

The sample includes four versions of the NinjaPiratePlantZombie organizer application, with support for adding and viewing each data type. The sample shows how to organize an app with several distinct functional areas using traditional MVC, MVC with Areas, MVC with Feature Slices and Razor Pages. Explore these different approaches and see which ones will work best in your own ASP.NET Core applications. The updated source code for this sample is available at bit.ly/2eJ01cS.


Steve Smith* is an independent trainer, mentor and consultant. He is a 14-time Microsoft MVP award recipient, and works closely with several Microsoft product teams. Contact him at ardalis.com or on Twitter: @ardalis if your team is considering a move to ASP.NET Core or if you’re looking to adopt better coding practices.*

Thanks to the following Microsoft technical expert for reviewing this article: Ryan Nowak


Discuss this article in the MSDN Magazine forum