How Does Razor Pages Differ From MVC In ASP.NET Core?

Sponsored by #native_company#
#native_desc# #native_cta#

When Razor Pages were first introduced with ASP.NET Core 2.0, I was excited.  I asserted at the time that Razor Pages were "WebForms done right," and I still think they have great potential.  What I haven't seen anyone do yet is point out where Razor Pages is different from a standard MVC app.

How many differences are there? [1]

That's what I'm going to try to do here: provide some insight into how Razor Pages and MVC differ and how they're the same, and in the process hopefully give you, my readers, some assistance in figuring out which one works better for you.  I've already written a ton of posts about MVC, so I won't rehash much of those here.

In this post, we're going to build a new ASP.NET Core Razor Pages app (the sample version of which is over on GitHub) and attempt to point out where and why Razor Pages might do a few things better, different, or the same as MVC.  

Let's get coding!

Function vs Purpose

There is a fundamental difference in the way Razor Pages and MVC group their files.

In MVC, functionality is grouped by function, by what a thing does.  Controllers contain actions, models contain data, and views contain display information.  Each of these are put in their own dedicated folders, and rules exist that govern how cross-function features (e.g. routing, authentication, filters, etc.) interact with these primary groupings.

Razor Pages, conversely, groups files by purpose, by what problem a thing solves.  A Razor Page is both function and form, purpose and design.  A single page not only has a Razor view but also a tightly-integrated "code-behind" class which defines the functionality for that page.  This fundamental difference in the way files are grouped together represents a subtle but significant shift in architectural thinking for Razor Pages vs MVC.

Neither of these are better or worse than the other.  They are simply different.  Use whichever suits you and your project the best.  However, it may turn out to be significant that Microsoft has seemingly designated Razor Pages as the "default" web application type.  

Including Razor Pages In Your ASP.NET Core App

Razor Pages are automatically included in any ASP.NET Core web app which uses MVC.  You include MVC in your Core web apps by calling AddMvc() in the ConfigureServices() method of Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
}

InMemory "Database" Setup

For our sample app, we are going to store a "database" in memory and use Entity Framework Core to access said data.  I've already written a post about how to do this, so I won't rehash that here.  The primary thing you need to know is that there is a BoardGamesDbContext item in the Services layer, which we will inject into our pages as necessary.

public class BoardGame
{
    public int ID { get; set; }

    public string Title { get; set; }

    [DisplayName("Publishing Company")]
    public string PublishingCompany { get; set; }

    [DisplayName("Minimum Players")]
    public int MinPlayers { get; set; }

    [DisplayName("Maximum Players")]
    public int MaxPlayers { get; set; }
}

public class BoardGamesDbContext : DbContext
{
    public BoardGamesDbContext(DbContextOptions<BoardGamesDbContext> options)
        : base(options)
    {
    }

    public DbSet<BoardGame> BoardGames { get; set; }
}

Basic Layout of a Razor Pages App

Let's now take a look at a brand-new Razor Pages app generated by Visual Studio and see how the structure is different from a standard MVC app. Here's a quick screencap of the project as it existed when I wrote this section.

Note that the /DataContext, /Helpers, and /Pages/BoardGames folders are custom for my project; they do not appear in a newly-generated Razor Pages project.

If you're coming from MVC-world, you'll immediately notice one major thing: there's no controllers!  As alluded to earlier, Razor Pages doesn't use controllers; rather it uses Pages which consist of a CSHTML Razor file and a .cshtml.cs code-behind file (which is what the About, Contact, Error, Index, and Privacy pages are above).  

Structure of a Razor Page

Let's grab one of the aforementioned pages to see an example of the structure of a Razor Page.

Here's the markup for the About.cshtml page:

@page
@model AboutModel
@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"]</h2>
<h3>@Model.Message</h3>

<p>Use this area to provide additional information.</p>

This looks remarkably like an MVC view, with two exceptions.

The first exception is the @page directive.  That directive is unique to Razor Pages and tells ASP.NET that this page is to treated as such, and in fact makes the page itself handle actions that would otherwise be handled by controllers.  This directive must be the first line of any Razor Page, because it changes how other Razor constructs work.

The second, less obvious, difference from an MVC view is the @model declaration. Not that it exists; rather, that the model for the page is something called AboutModel.  Here's the code for that model:

public class AboutModel : PageModel
{
    public string Message { get; set; }

    public void OnGet()
    {
        Message = "Your application description page.";
    }
}

In Razor Pages, the model for the page "code behind" files needs to inherit from the PageModel class.  (Pardon me, I just had unpleasant memories of WebForms). The PageModel wraps several things that are otherwise separate in MVC applications, things like the HttpContext, ModelState, Request and Response values, and TempData.  In this way, we are given the building blocks to create "actions" within a Razor Page.

All we need to do now is take our "database" in memory, which stores information about board games, and come up with a set of pages to manage that data.  In other words, we are now ready to build our first Razor Pages app!

Building the Index Page

NOTE: You can do this entire section via the "scaffolding" feature in Visual Studio. Right-click the /Pages/BoardGames folder, select Add -> Razor Page, and fill out the form for a List page with the BoardGame class as a model and the BoardGamesDbContext as the data context. This won't give you the exact code we're using here, but it will be close enough.

The first thing we need is an Index page which lists all of our board games.  Right-click on the /Pages/BoardGames folder and select Add -> Razor Page.  Here's a couple of screencaps that show what I did on the following dialogs.

First, I selected the basic type Razor Page...
...then I set the name to Index and clicked Add.

This will generate an extremely basic Razor Page that we're going to modify.  Here's the Razor markup:

@page
@model RazorPagesWalkthrough.Web.Pages.BoardGames.IndexModel
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

...and here's the PageModel:

public class IndexModel : PageModel
{
    public void OnGet()
    {

    }
}

You might immediately have a question: what in the world is that OnGet method? Recall that RazorPages have their "actions" within their PageModel code-behind classes, so the OnGet() method is exactly what it sounds like: what happens when the page needs to execute a GET request.  In short, while GET and POST requests in MVC happen in Controllers, in Razor Pages they happen in classes which inherit from PageModel.

This page is supposed to be a list of BoardGames, and currently no such list exists. Let's modify the IndexModel to do the following:

  1. Have the BoardGamesDbContext be injected into it.
  2. Store a list of BoardGame objects as a model.
  3. Return that list of BoardGame objects to the page on the GET action.

Here's that code:

public class IndexModel : PageModel
{
    private BoardGamesDbContext _context;

    public IndexModel(BoardGamesDbContext context)
    {
        _context = context;
    }

    public List<BoardGame> BoardGames { get; set; }

    public void OnGet()
    {
        BoardGames = _context.BoardGames.ToList();
    }
}

That's all the OnGet() method has to do: load the values from the DbContext into the model property.  

Here's the markup for the corresponding Razor Page:

@page
@model RazorPagesWalkthrough.Web.Pages.BoardGames.IndexModel
@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<table class="table">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(x => x.BoardGames[0].Title)</th>
            <th>@Html.DisplayNameFor(x => x.BoardGames[0].PublishingCompany)</th>
            <th>@Html.DisplayNameFor(x => x.BoardGames[0].MinPlayers)</th>
            <th>@Html.DisplayNameFor(x => x.BoardGames[0].MaxPlayers)</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach(var game in Model.BoardGames)
        {
            <tr>
                <td>@game.Title</td>
                <td>@game.PublishingCompany</td>
                <td>@game.MinPlayers</td>
                <td>@game.MaxPlayers</td>
                <td>
                    Links will go here
                </td>
            </tr>
        }
    </tbody>
</table>

If you run the app locally and go to localhost:(portnumber)/BoardGames (replacing "portnumber" with your port number for your instance of this app), you will see our brand new page!  It looks like this:

All right!  We've finished the Index page!  But this one was easy; there's no POST action to worry about.  Let's turn our attention to the Add page, where we will need to implement a POST action.

Building the Add Page

As with the Index page, let's build a new page and page model.  This time, we're going to use the scaffolded page that Visual Studio generates and break down what that's doing.  Here's the code-behind for the Add page:

public class AddModel : PageModel
{
    private readonly BoardGamesDbContext _context;

    public AddModel(BoardGamesDbContext context)
    {
        _context = context;
    }

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

    [BindProperty]
    public BoardGame BoardGame { get; set; }

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

        _context.BoardGames.Add(BoardGame);
        await _context.SaveChangesAsync();

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

Just like the Index page, the Add page has the BoardGamesDbContext injected to it. Further, note that by default our POST action is asynchronous, though this can easily be changed.  The AddModel also has a property BoardGame, which we will use to bind the view.  Speaking of the view, here's the autogenerated markup for it:

@model RazorPagesWalkthrough.Web.Pages.BoardGames.AddModel

@{
    ViewData["Title"] = "Add";
}

<h2>Add</h2>

<h4>BoardGame</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="BoardGame.Title" class="control-label"></label>
                <input asp-for="BoardGame.Title" class="form-control" />
                <span asp-validation-for="BoardGame.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="BoardGame.PublishingCompany" class="control-label"></label>
                <input asp-for="BoardGame.PublishingCompany" class="form-control" />
                <span asp-validation-for="BoardGame.PublishingCompany" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="BoardGame.MinPlayers" class="control-label"></label>
                <input asp-for="BoardGame.MinPlayers" class="form-control" />
                <span asp-validation-for="BoardGame.MinPlayers" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="BoardGame.MaxPlayers" class="control-label"></label>
                <input asp-for="BoardGame.MaxPlayers" class="form-control" />
                <span asp-validation-for="BoardGame.MaxPlayers" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Notice the label and input elements with the "asp-for" attribute.  This is an example of Tag Helpers, which allow Razor to bind elements to the model object without needing to use the @Html. helpers.  The resulting elements look much more like HTML than C#, and that may make it easier to write and read.

Finally, note the single Form element.  By default in Razor Pages, each Page may only handle one GET and one POST, though this can be changed.

At this point, try running the app!  You should be able to add a new Board Game to our "database" in memory.  If you are having difficulty, feel free to reach out in the comments below or on Twitter.

A Hidden Benefit

The primary benefit of using Razor Pages over basic MVC, as has already been discussed, is that it makes it simple and quick to develop page-based applications. But there is also a hidden benefit included with this architecture: Razor Pages automatically implement antiforgery validation, which protects against cross-site request forgery (XSRF/CSRF) attacks.  All that without using the [ValidateAntiForgeryToken] attribute!

Summary

Here's the primary differences between a Razor Pages app and an MVC app:

  1. Razor Pages are automatically included in any app which uses MVC.
  2. MVC groups by function, Razor Pages groups by purpose. These are subtly different, though neither is worse or better than the other.
  3. Following from 2, Razor Pages are designed for page-focused scenarios; each page can handle its own model and actions.  MVC divides these responsibilities among other classes (e.g. controllers).
  4. Razor Pages includes anti-forgery token validation automatically; in MVC it must be enabled.
  5. MVC's structure allow for more complex routing out-of-the-box; such routing can be done in Razor Pages but requires more work.
  6. Input validation and routing "just work".

Now we know how to spot the difference between Razor Pages and MVC!

Maaaan, I dunno about that cat. He looks shifty. [2]

I'll be continuing my exploration of Razor Pages for a while, with new posts forthcoming.  Lots of them will use the same sample project that I used for this one. Feel free to suggest topics in the comments!

Happy Coding!

[1]: Image from Wikimedia, used under license.
[2]: Image from Wikimedia, used under license.

How Does Razor Pages Differ From MVC In ASP.NET Core?
Share this