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.

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!
The Sample Project
For this post, and for others dealing with Razor Pages, we are using this sample project over on GitHub. Check it out!
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 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
directive. 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.


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 our class IndexModel
, which inherits from 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 BoardGame
instances, and currently no such list exists. Let's modify the IndexModel
class to do the following:
- Have the
BoardGamesDbContext
be injected into it. - Store a list of
BoardGame
objects as a model. - 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 BoardGamesDbContext
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.
Razor Pages Automatically Implements Antiforgery Tokens!
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:
- Razor Pages are automatically included in any app which uses MVC.
- MVC groups by function, Razor Pages groups by purpose. These are subtly different, though neither is worse or better than the other.
- 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).
- Razor Pages includes anti-forgery token validation automatically; in MVC it must be enabled.
- MVC's structure allow for more complex routing out-of-the-box; such routing can be done in Razor Pages but requires more work.
- Input validation and routing "just work".
Now we know how to spot the difference between Razor Pages and MVC!

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!
If Razor pages will have to be a real replacement of MVC the architecture needs to be completed. How its described now it looks that its for small applications. But how is the structure for an enterprise application. It looks like a promissing start but it has no end yet.
Thanks for the summary! Since I have been using asp.net mvc razor on .net framework, from your post, I feel there is one feature of this new Razor I really don't like, PageModel. Currently I have all kinds of model classes that are bound to the view, if these models need to change to inherit PageModel, this approach is not working for classes already inherited to other parent classes. Is it supported to have a direct migration from asp.net mvc razor to aspnetcore razor application.
I Created a project using Razor Pages for a client. It was surprisingly
easy to transition to coming from Web-forms. My main issue is understating
where I put logic that I used to put in the MasterPage's code behind.
Layout pages don't have code behind nor partial pages. I stuck the code into the .cshtml file but somehow that feels too Classic ASP... If you could do a future article on those issues it would be nice.
What is the purpose of "context" in https://github.com/exceptio... ?
You forgot to refactor the Generator?
Razor is very nice, but after making a website and going to use UWP or WPF, I wonder when will we have the REAL UWP XAML UI on the Web, oficially supported.
If you are looking for Xaml UI tech on the web that is officially supported, you can try Uno.Platform - very good Xaml UI for the web. Else, you can use Ooui also.
This takes me right back to 20 years ago when I was working in "classic ASP". Each view would handle GET and POST requests for a specific URL, and (for my own sanity) I had all the 'code behind' contained in a code block in a section of the page where all the data access, business logic and formation of a model happened. Markup with embedded insertion points for model members made up the remainder of the page content.
Now we're using the latest C# language features instead of clunky VBA, and HTML5 instead of HTML 1.1, but it feels like coming home :)
I found razor pages to be to limiting for the application I was working on. I forget exactly what those limitations were though, so unfortunately I can't list them here. I think one of them was the nonstandard url paths and another was creating custom end points. So, I guess my application wasn't a really good fit. Unfortunately that is the downside of razor pages, you are stuck with how they say to do it, which can be good or bad.
FYI, you should be using
Display(Name = "My Name")
notDisplayName("My Name")
. I think the former is more standard now for display to users. Not a huge deal just thought I would point it out though.. https://stackoverflow.com/a...From what I'm seeing... Razor is ASP.NET done like ColdFusion 5.0+
Of course then MVC came along, and multiple ColdFusion MVC frameworks were made.
And then the older ASP.NET got MVC...
And now we're back to what ColdFusion 5.0 was... but this time using C#.
And I'm happy we get C# on the client-side in Blazor... I tried using VBScript on the client-side to manipulate the DOM back in the days of HTML 3.2... but then JScript caved and was replaced by JavaScript and MS gave up on the VBScript alternative and just went with JavaScript... nice that we're back with client-side script that matches server-side.
Of course with HTML 5.0+ we're back to testing in every browser just like back with 3.2.
(You know because The Lords of Cobol forbid we should use plug-ins... they STILL haven't got HTML 5 up to where Flash was when they killed it off.)
+++
"All of this has happened before, and all of this will happen again."
BSG
Great post, thanks for sharing!
calling something like "Helpers" is wrong. Its to abstract. It does not say anything about what it does. I never have something like "Helpers".