Six things I learnt about using ASP.NET Core’s Razor Pages

Last Updated: April 13, 2018 | Created: April 9, 2018

ASP.NET Core 2.0 introduced a new way to build a web site, called Razor Pages. I was interested in the new Razor Pages approach, as I hoped Razor Pages would allow me to code better by following the SOLID principals – and they do. But first I had to work out how to use Razor Pages. This article my journey in learning how to use Razor Pages.

TL;DR – summary

ASP.NET Core Razor Pages make building web applications simpler, but they work differently to the existing controller->actions->views approach. This article about the things that have changed and what is the best way to use the new approach provided by Razor Pages.

My view of Razor Pages are they are a very good improvement! They produce better structured code, with a pattern that follows the Single Responsibility Principle (SRP), and you get better Separation of Concerns (SoC).

The aims of this article

This article will be useful to people who want to use ASP.NET Core razor pages. It looks at areas that I found that were different from the existing controller->actions->views approach that has been around for many years. The six areas I cover are:

  1. Replacing the action: How should I construct my Razor Page code part?
  2. Routing: Using folders and filenames, with a different handling to invalid URLs
  3. It’s getting messy – how do I segregate the “Home” razor views?
  4. Model Binding: What is the best way to get data in/out to a razor action method?
  5. Changed: The validation name in the View is a bit more complex
  6. How can I implement AJAX requests in razor pages?

This article assumes you know C# and have some idea of what the ASP.NET Core framework does. I do talk about the existing controller->actions->views approach, as a contrast, so its useful (but not essential) you know that. The code is focused on ASP.NET Core 2.0 or above, as that is the point where Razor Pages was introduced.

NOTE: I should have read the ASP.NET Core documentation page “Introduction to Razor Pages in ASP.NET Core” before I started. It has a great overview of what Razor Pages are and how to use them. Do have a look if you have never come across Razor Pages before.

Setting the scene – how razor pages changes the design of your web application

I have used ASP.NET Core and ASP.NET MVC for many years, using its controller->actions->views approach. I found that the controller class could get very messy inside, with a whole host of action methods jumbled together. I learnt to put the absolute minimum of code in each action method, both to keep the controller as simple and to allow the code to be tested outside the controller. Even so my controllers contains a mash-up of different actions handling different URLs – definitely not a nice structure (in my opinion).

Razor pages changes all this. The controller has gone, and the action methods move into their own, per-URL class that deals with all the HTTP requests (GET, PUT, POST etc) to one specific URL. The code is associated to the View file of the same name: the code provides the dynamic data and he view generates the HTML to show to the user.

That means Razor Pages applications are much cleaner and simpler to work with. The only problem I found was working out how to do all the things I used to do with controller and actions, but now with Razor pages. That took a bit of head scratching and experimenting, which is why I wrote this article.

Replacing the action: How should I construct my Razor Page code part?

If you create a Razor Pages application in Visual Studio or via the command line (dotnet new razor) then you get an ASP.NET Core application with three views, Index.cshtml, About.cshtml and Contact.cshtml. Each view has a file with the same name and extension, but with .cs on the end, e.g. Index.cshtml.cs, which contains the code. If we look at the About.cshtml.cs file its content looks like this.

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

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

This threw me to start with, as action methods inside a controller must return a result – the same About action in a ASP.NET Core controller application would return a IActionResult result. Let’s understand what is happening here, and then we can see how this is helpful.

What Razor Pages does is return the class in the …cshtml.cs file, known as the PageModel. This contains various public properties and methods. In the About page case it returns the AboutModel – this class inherits from the PageModel class, and adds one public property called Message and an OnGet method. When a HTTP GET is sent to the URL /About then

  1. The OnGet method is called, which sets up the property Message.
  2. Then the associated About.cshtml razor view is executed to create the HTML. This has access to the public property Message in the AboutModel class via the @Model.Message.

The About case is very simple, but many requests can be handled with no return from the method. But there are other options. The first is you don’t have to have a PageModel file at all if the page contains static data, or its simple enough to handle using razor (enhanced) features. But I quite like the PageModel for the typical code you would have put in the controller’s action method.

The other case is where the OnGet (or other On… methods) needs to return a result. One case is if you want to redirect, or return a HTTP response such as 404 NotFound. Here is an example of a redirect.

public IActionResult OnGet(int id)
{
    Data = _context.Find<Book>(id);
    if (Data == null)
    {
        return RedirectToPage("MyError", 
              new { message = “I could not find that book” });
    }
}

The other case is if you are responding to a AJAX request and need to return data. I cover this in my AJAX article – use this link to go directly to the code that returns a JsonResult.

Routing: Using folders and filenames, with a different handling to invalid URLs

The first thing to say is Razor Pages route HTTP requests in a different way to the controller->actions->views approach. It uses the folder that the razor page is in as the first part of the URL, and the name of the file for the second part. So, a razor page called “PlaceOrder” in the folder “Pages/Checkout” would give a URL of Checkout/PlaceOrder.

There are a couple of rules that are similar to the controller->actions approach.

  • Any razor page in the top level of the “Pages” folder are accesses with no URL prefix. Bit like the HomeController.
  • Any razor page called “Index” is access without needing a URL prefix, same as with the controller->actions->views approach.

NOTE: Microsoft’s full documentation on routing and URL construction can be found here.

Now we need to talk about the different HTTP requests, like GET, POST, etc, and Async handling of requests. These are all done by the name of the methods in the PageModel file: methods starting with “On” then have the HTTP request type added, e.g. OnGet, OnPost, OnPut etc. Then, if you want async you need to add “Async” to the end of the name, e.g. OnGetAsync, and make the method async. These methods are known as handler methods.

NOTE: There is a way to have named handler methods – see Microsoft’s documentation on this. I also cover this in my article on AJAX and Razor Pages.

Warning: If you leave out the On… method, or misname it then the HTTP request won’t fail, but will just show the same view again. I mistyped the OnPost method as OnPut (I’m dyslexic) and chased around for an hour before I spotted it.

It’s getting messy – how do I segregate the “Home” razor views?

As I built my application I found I had more and more razor pages at the top level of the “Pages” directory (the “Home” directory in the controller->actions->views approach). The problem was that my “Home” pages were mixed in with the “Shared” pages like _Layout.cshtml, _ViewStart.cshtml etc., which made finding things more complex.

Thanks to the article “Setting a Custom Default Page in ASP.NET Core Razor Pages” by Matthew Jones I could move my razor pages into a subdirectory, which I called “Home” and then added this code you the Startup.cs file in my ASP.NET Core application

services.AddMvc().AddRazorPagesOptions(options =>
{
    options.Conventions.AddPageRoute("/Home/Index", "");
});

That means that on startup the top URL will go to my Home/Index page. Now my “Home” razor pages are easy to find in my application.

Model Binding: What is the best way to get data in/out to a razor action method?

Model binding is what happens when you receive a HTTP request with information in the url, header, form etc. Model binding is a big topic and I’m not going to cover it all, but with Razor Pages applications you get an interesting alternative to binding in the handler method parameters.

I’m going to show you the default binding approach to use with forms, which adds the [BindProperty] attribute to the properties in the PageModel. But first let me explain the model binding that we are all used to from the controller->actions->views approach. Here is very simple example of method parameter binding taking in the a ViewModel which has the data from the form

public MyFormViewModel Data { get; set; }

public IActionResult OnPost(MyFormViewModel formInput)
{
    if (!ModelState.IsValid)
    {
        return Page();
    } 
        //… do something with the formInput
}

That would work with razor pages, but what happens if the ModelState isn’t valid – it redisplays the form, but the data provided by the user is lost! That is because the “Data” property is empty. So, the correct way to do it is like this

[BindProperty]
public MyFormViewModel Data { get; set; }

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    } 
        //… do something with the Data
}

The [BindProperty] attribute (line 1 in the code above) says that when a POST HTTP request comes in then it will try and bind the data in the property(s) that have the [BindProperty] attribute. So, if the form data doesn’t pass the validation, i.e. ModelState.IsValid is false, then you can return the page and the data will still be there. That is very nice!

For a more in-depth look at form input to razor pages I recommend this page on model binding on the excellent www.learnrazorpages.com site.

Changed: The validation name in the View is a bit more complex

I haven’t shown you a .cshtml page yet, so let’s look at one with a form to update the title of a book.

@page
@using Home
@model Home.ChangeTitleModel
@{
    ViewData["Title"] = "ChangeTitle";
}


<h2>Change book title</h2>


<div class="form-horizontal">
    
<form method="post">
        @Html.ValidationSummary(false, null, new { @class = "text-danger h4" })
        <input name="Data.BookId" type="hidden" value="@Model.Data?.BookId" />
      
<div class="form-group">           
<div class="col-sm-2">
                <label class="control-label">Book Title</label>
            </div>
          
<div class="col-sm-10">
                <input asp-for="Data.Title" class="form-control col-sm-10" />
                <span asp-validation-for="Data.Title" class="text-danger"></span>
            </div>
        </div>
       
<div class="form-group">
            
<div class="col-md-offset-2 col-md-10">
                <button type="submit">Update</button>
            </div>

        </div>
    </form>
</div>

Now, if you look at the that says asp-validation-for, then you will see that its set to “Data.Title”. When using ViewModels (which I do quite often) the name has a prefix of whatever you call your property – in my case “Data”.

The problem comes if you do your own validation tests somewhere else in your application, as you need to add the name of your property as a prefix. I do validation in my business logic and I therefore must prefix the property name with “Data.” (or the name of your property) when transferring the error to the ASP.NET ModelState. If you don’t you get a “hidden error”, where the page is redisplayed, but no error message is shown. This is very annoying to users!

Here is a very simple (naïve) approach for copying ValidationResult errors to the ModelState and adding the prefix.

var prefix = “Data.”;
foreach (var error in service.Errors)
{
    var properties = error.MemberNames.ToList();
    ModelState.AddModelError(properties.Any() 
        ? prefix + properties.First() 
        : "", 
        error.ErrorMessage);
}

NOTE: This naïve approach works, but you need to careful that the property names that your business logic correspond to actual properties in the ViewModel you are using, otherwise your error will still not be seen. I have a more sophisticated version here that checks that the MemberNames match a property in my ViewModel, otherwise I provide no name, which means the error is shown in the ValidationSummary part.

How can I implement AJAX requests in razor pages?

My application uses AJAX to fill a user-selectable filter, and I wasn’t sure how to do that in a Razor Pages application. There were some suggestions out there, but none of them were quite as optimal as I wanted.

I wrote a long article called “ASP.NET Core Razor Pages: How to implement AJAX requests” where I describe the whole process, but in summary I added another razor page where the handler method returns a JsonResult. It works quite well, and continues the use of a SOLID software approach. If you need AJAX then have a look at the article.

Further room for thought

In the controller->actions->views design I made the action methods in my controller as simple I possibly could, and moved all the real code into a project I call a “Service Layer” (see the point 2 in my “Six ways to build better Entity Framework (Core and EF6) applications” article to see what I mean by the Service Layer).

With Razor Pages, which isolate the code of each view, I find myself want to put more code in the PageModel class. The only problem then is – can I unit test it? Unit testing of ASP.NET Core is supposed to be easier, but the current approach isn’t simple. But Scott Hanselman has highlighted a new package coming out with ASP.NET Core 2.1 release called Microsoft.AspNetCore.Mvc.Testing, which looks very promising.

If that turns out to make unit testing of razor pages easier, then I think I will migrate much of the code that lives in my service layer into the razor pages handler methods. That would mean I could put more complex code in my razor pages and still be able to unit test the code. I look forward to trying that out.

NOTE: I am also working on a library to make building ASP.NET Core pages that access the database quicker to implement. Follow me on twitter @thereformedprog, or follow this technical blog site to be informed when something is available.

Conclusion

I am delighted by the cleaner approach that Razor Pages provides over the other approach, i.e. controller->actions->views. For human-focused applications I think Razor Pages is a good solution, but ASP.NET Core with swagger is most likely a better solution for a full-on WebAPI application.

Sure, there is bit of learning to do to find the right ways to use Razor Pages, but the effort is rewarded with a cleaner and more refactorable web/mobile application. I hope these points help you in understanding and adopting Razor Pages.

Happy coding.