In my previous post, we discussed the ASP.NET Core Model-View-Controller (MVC) web application framework and what better way to start our journey through ASP.NET Core. This framework is battle-hardened and has been around since early 2009. To touch on the essential components, we reviewed everything that gets scaffolded in a new project. While some of this code is considered to be “boilerplate” code, we must understand it. Remember, we are responsible for all the code we generate, whether made from a new project template, created by scaffolding or hand-coded.
Our next stop is with Microsoft’s new web application framework, ASP.NET Core Razor Pages. While Razor Pages uses many of the same underlying ASP.NET Core components, it takes on a different routing paradigm and a more compacted programming model. Let us get started.
Contents
Getting Started
Like any new .NET Core application, we can create a new project using one of the following methods.
Something interesting can be noticed when comparing the Razor Pages template to MVC. In Visual Studio, you have the option to create a “Web Application” project for Razor Pages or a “Web Application (Model-View-Controller)” project for MVC. Razor Pages seems like the default choice.
While it is not a focal point of this article, the ASP.NET Core Identity views have also been rewritten with Razor Pages. So even if your web application is MVC, there might be a little bit of Razor Pages in your application. Perhaps Microsoft is trying to tell us something.
A New Project Template
After looking at the ASP.NET Core MVC template, you will find many similarities with the Razor Pages template. For instance, the wwwroot folder and Program class are virtually unchanged. There are slight differences in the Startup class, and the Pages folder is entirely new. Before we dig into the Razor Pages themselves, let’s look at the startup class variations.
Startup
As a recap, all ASP.NET Core applications use a startup class to register services and configure the HTTP request middleware. This process takes place in the ConfigureServices and Configure methods. I’ve discussed these methods previously, so I won’t go into the details here (for reference, I’ve included the links below).
- ConfigureServices – Service Registration
- Configure – HTTP Request Pipeline
Now, let’s look at how the startup class differs in an ASP.NET Core Razor Pages application. I’ve highlighted two key lines below.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class Startup
{
// Removed for brevity…
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Removed for brevity…
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
public class Startup { // Removed for brevity… public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Removed for brevity… app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); } }
public class Startup { // Removed for brevity... public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Removed for brevity... app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); } }
If we understand the thought process behind the ConfigureServices and Configure methods, the changes we see in the startup class make perfect sense. Instead of configuring MVC controllers and views for dependency injection, Razor Pages need to be registered. We can see these changes reflected in the ConfigureServices method. Furthermore, instead of using the MVC controller routing, Razor Pages are mapped with endpoint routing.
Next, let’s look at the most fundamental part of our new application, the Razor Pages.
The ‘Pages’ Folder
As you’d imagine, the Pages folder is where the Razor Pages reside. Expanding this folder, we can see that we start with a few default pages. If these pages look familiar, they should. Running the Razor Pages template gives us the same web application that our MVC template did.
Right off the bat, you will notice that Razor Pages follows a much more compact programming model than MVC does. In Razor Pages, each request gets routed directly to a page. This routing paradigm is different in MVC, where requests get routed to a controller who generates a model for a view to data bind.
The Razor View Engine
Before we go any further, let’s talk about a fundamental concept. Just like MVC, Razor Pages uses Razor as it’s templating engine. What is Razor? Razor is a server-side templating engine that allows developers to use C# to generate data-driven markup. As we unpack the previous sentence, there are two things to keep in mind. First, Razor is not unique to Razor Pages. MVC also uses Razor to render its views. Second, markup gets generated server-side so, we don’t want to get Razor confused with Blazor.
What does all this mean? The Razor templating syntax has been around for a long time. While Razor is front and center in the name of the new “Razor Pages” framework, it is just a piece of the puzzle. Razor is shared by several .NET Core web application frameworks and focuses solely on generating HTML markup and rendering data objects.
If you would like to read more about the Razor Syntax, Tag Helpers, or any other features provided by Razor View Engine, I recommend reading through the Microsoft documentation.
Razor Pages Architecture
Unlike MVC, which breaks into three separate components, a Razor page is made up of two pieces, a Razor markup file and a C# code file. The Razor markup looks similar to an MVC view; however, there is a unique @page
directive placed at the top of the file to give it the features of a Razor Page.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@page
@model IndexModel
@{
ViewData[“Title”] = “Home page”;
}
<div class=”text-center”>
<h1 class=”display-4″>Welcome</h1>
<p>Learn about <a href=”https://docs.microsoft.com/aspnet/core”>building Web apps with ASP.NET Core</a>.</p>
</div>
@page @model IndexModel @{ ViewData[“Title”] = “Home page”; } <div class=”text-center”> <h1 class=”display-4″>Welcome</h1> <p>Learn about <a href=”https://docs.microsoft.com/aspnet/core”>building Web apps with ASP.NET Core</a>.</p> </div>
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> </div>
The Razor markup file also contains a @model
directive that binds the Razor Page to a specific page model. If we expand the Razor Page in Visual Studio, we find a page model class with the same name as the Razor Page with a .cs
suffix. By default, this is the page model referenced by the Razor markup.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger) { _logger = logger; } public void OnGet() { } }
public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger) { _logger = logger; } public void OnGet() { } }
Handler Methods
When an HTTP request is routed to a Razor Page, a naming convention is used to find the appropriate handler method to execute. Handler methods are prefixed with the word “On” followed by the HTTP Verb. For example, the OnGet
method shown above is invoked when an HTTP GET request is routed to the Index
page. To create an asynchronous handler method, the Async suffix can be added to the end. Below is a list of the most frequently used handler methods.
- OnGet or OnGetAsync
- OnPost or OnPostAsync
- OnPut or OnPutAsync
- OnDelete or OnDeleteAsync
Model Binding
One key difference between Razor Pages and MVC is how data gets bound to the Razor markup. With Razor Pages, the page model not only handles requests, but it is also bound directly to the page markup. You can almost think of it like a model and controller combined. Properties exposed in the page model can be accessed directly in the page markup using the @Model syntax.
This condensed strategy works excellent for GET requests. OnGet
handler methods have to populate data into any available public properties, and away we go! For requests where data is being sent from the client, such as POST or PUT, a special [BindProperty]
attribute is required. Similar to a parameter in an MVC controller action, this attribute makes properties available for model binding.
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class CreateModel : PageModel
{
// Removed for brevity
[BindProperty]
public Donut Donut { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Donuts.Add(Donut);
await _context.SaveChangesAsync();
return RedirectToPage(“./Index”);
}
}
public class CreateModel : PageModel { // Removed for brevity [BindProperty] public Donut Donut { get; set; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } _context.Donuts.Add(Donut); await _context.SaveChangesAsync(); return RedirectToPage(“./Index”); } }
public class CreateModel : PageModel { // Removed for brevity [BindProperty] public Donut Donut { get; set; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } _context.Donuts.Add(Donut); await _context.SaveChangesAsync(); return RedirectToPage("./Index"); } }
When Should I use Razor Pages?
Razor Pages have several benefits over the traditional ASP.NET Core Model-View-Controller (MVC) framework. MVC is entity and action-focused while Razor Pages are more page-focused. This, in itself, has an interesting side effect for MVC. Entities in most MVC applications start with simple CRUD operations; however, this is typically short-lived. As more “actions” are required, controllers quickly become bloated. This is not a concern with Razor Pages. Each page stays focused on a single activity, which allows them to be smaller and less bloated.
Razor Pages are also simple to wrap your head around. The condensed, page-focused architecture is very intuitive. This simplicity reminds many of the legacy ASP.NET Web Forms framework. While there are certainly parallels to draw between the two, Razor Pages maintains a strict separation between the markup and page model. This lack of separation in Web Forms made unit testing difficult and, in many ways, violated the separation of concerns principle.
So when should you use Razor Pages instead of MVC? It depends. In my opinion, Razor Pages shines in smaller, “action” based web applications. For larger CRUD applications, MVC could still a great fit.
What About The Client Side?
As mentioned previously, both MVC and Razor Pages are server-side web applications. This means every time a user navigates to a page, a request is sent to the server where data is retrieved and embedded into an HTML template. This can lead to a little extra overhead, which is evident on slower networks. Over the years, the desire to do more in the browser has lead to an explosion of JavaScript frameworks such as Angular, React, and Vue. Microsoft also has an answer for this in the new ASP.NET Core Blazor framework.
This blog post was written originally on espressocoder.com Jason Robert.