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

Last Updated: July 31, 2020 | 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.

4.5 4 votes
Article Rating
Subscribe
Notify of
guest
19 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Scotty
Scotty
4 months ago

SRP in razor pages?? Where the actions/handlers are in the same class as the viewmodel (and post args model)? Give me MVC any day over razor pages. Throw everything in a single pot to make it n00b friendly whilst simultaneously removing a lot of the MVC flexibility…. FWIW I’ve done something similar to you by defining models outside the page, but still having to add them as properties to the page.

Arcyom
Arcyom
1 year ago

Tried to copy your approach and started from ordering drowdown. And then i see – ItemsList is not defined exception.
Where to dig for answers? Copied js, bounded – nothing. ItemsList is not defined.

Arcyom
Arcyom
1 year ago
Reply to  Jon P Smith

Stuck with mimicring index.cshtml View from EfCoreInAction.Part1
Where you have booklist in EFCoreInAction part1 (the one from master branch) and bookList.js script I have an itemList reference in my Index.cshtml .I got the page rendered. Dropdown OrderBy works, but doesn’t invoke any action. Did you register somehow the js file?
Browser console upon inspecting dropdown OrderBy says
Uncaught reference error courseList is not defined (identical to bookList except for different sorting options).

Last edited 1 year ago by Arcyom
Lee
Lee
1 year ago

Hi, Jon. There is a broken link in the article.

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.

Lee
Lee
1 year ago
Reply to  Jon P Smith

Thanks, Jon!

Last edited 1 year ago by Jon P Smith
Thomas Mutton
Thomas Mutton
3 years ago

Hi Jon, great article.

I have a question for you regarding routing and the best practise behind it. This is something easily achieved with MVC and not so straight forward with razor pages.

Here’s the scenario:

Say I have a resource (page) /blog and I want a razor page for /blog/123

What is best practise to carry this out? Currently I’ve got a folder in the Pages folder called Blog with an Index.cshtml page. This takes care of /blog/123. I’ve also then got Blog.cshtml in the root of Pages to take care of /blog.

Have you come across this scenario with razor pages and have you come to a solution? I’m not too happy with having those two pages being split up. They should be in the same folder ideally.

Jon P Smith
3 years ago
Reply to  Thomas Mutton

Hi Thomas,

The default Razor Page routing uses folders and the name of the .cshtml page for routing (with special handling of “Home” folder and Index.cshtml). This mean that a .cshtml file in the folder Pages/Authors/Other/NamedPage.cshtml would be accessed by the URL http://localhost:51220/authors/other/namedpage . I tried it and it works. I think that’s pretty neat!

So in your example I would define things a bit differently. I would have used
– Pages/Blog/Index.cshtml – that would have the URL http://localhost:51220/blog
– Pages/Blog/IndexWithId.cshtml – that would have the URL http://localhost:51220/blog/indexwithid/123

Have a look at my example code at https://github.com/JonPSmith/EfCore.GenericServices/tree/master/RazorPageApp . You can clone the whole https://github.com/JonPSmith/EfCore.GenericServices project and run it locally – it uses an in-memory Sqlite database so runs pretty much anywhere.

Thomas Mutton
Thomas Mutton
5 years ago
Reply to  Jon P Smith

Hi Jon,

“- Pages/Blog/IndexWithId.cshtml – that would have the URL http://localhost:51220/blog/indexwithid/123

I was looking for an url that would give me http://localhost:51220/blog/123

To solve this I ended up adding PageRoutes in the startup class.

options.Conventions.AddPageRoute(“/Questions/Ask/AskQuestionStep1”, “/Questions/Ask”);
options.Conventions.AddPageRoute(“/Questions/Ask/AskQuestionStep2”, “/Questions/Ask/{id}”);

With the following folder structure:

– Questions
— Ask
— AskQuestionStep1.cshtml
— AskQuestionStep2.cshtml

My urls are now as follows:

http://localhost:456/Questions/Ask
http://localhost:456/Questions/Ask/123

Mark Phillips
Mark Phillips
3 years ago

I shutter at the suggestion of adding more code to the razor Page class. Didn’t we learn about this disaster in webforms and poorly written controllers.

Jon P Smith
3 years ago
Reply to  Mark Phillips

Hi Mark,

I’m not quite sure what you mean by “… adding more code to the razor Page class.”, but I hear you about “disaster in webforms and poorly written controllers”.

For myself I really like that the Razor Pages follow the Single Responsibility Principle (SRP), i.e. everything about that page goes in one place, while MVC Controllers don’t follow SRP and can get ugly, with lots action methods. On the other hand you can end up with a lot of lines in a Razor Page due to class setup (e.g. constructor for dependency injection etc.), while a MVC Controller is a little bit shorter as the class setup is shared across all the action methods. Length isn’t a good metric though – its about ease of writing, understanding and maintaining the code.

I think Razor Pages are a good step forward, but I still find myself using MVC Controllers because authorization is simpler in MVC Controller UPDATE: Not true – see update below. In the end large projects will use some sort of Single Page Application (React.js etc.) and Web APIs, so Razor Pages will be used in small to medium sized applications and there they may be a good fit.

Jon P Smith
4 years ago
Reply to  Jon P Smith

Update: I found that you can use the Authorize attribute on a PageModel – see https://docs.microsoft.com/en-gb/aspnet/core/razor-pages/filter?view=aspnetcore-2.1#authorize-filter-attribute . That’s a nice thing to know as it makes per-view authorization much easier. And my new approach to authorization, explained in this article, will work.

Thanks to Khalid Abuhakmeh for pointing that out.

Everest Ezebuilo
Everest Ezebuilo
3 years ago

I think it’s called pages not views.

Manit Chanthavong
Manit Chanthavong
3 years ago

I was about to ditch asp.net for Django but Microsoft got me back on board with asp.net core razor pages. The development is so much faster than asp.net MVC especially when prototyping a web app.

jwood
3 years ago

I find myself still needing view models whenever I want to customer the displayed data or validation on a particular page. I’m still trying to find good examples of where to put my ViewModels folder. I did see one suggestion to keep it in the same folder as the page model but I don’t always have a one-to-one correlation between pages and view models. Another example was to put it under the Models folder. Not sure what I think about that.

Jon P Smith
3 years ago
Reply to  jwood

Hi jwood,

There are lots of options, but I can tell you want I do. I use a ServiceLayer (see this section of article for why I do that https://www.thereformedprogrammer.net/six-ways-to-build-better-entity-framework-core-and-ef6-applications/#2-pattern-the-service-layer-separating-data-actions-from-presentation-actions ) and I put my ViewModels (I call them DTO) in the same folder as my code that uses that ViewModel – that provides a good separation of concerns.

This works for me because I don’t put any heavy code inside a Razor Page, but I register a service that is called by the Razor Page. I do this because it makes it easier to test. Have a look at the RazorPageApp and ServiceLayer projects in this repo https://github.com/JonPSmith/EfCore.GenericServices to see what I mean.

But I do see that the Razor Page is pretty contained so putting code inside a Razor Page would work too, but then you have the problem of where to put the ViewModels – most likely in a folder with the Razor Pages, e.g. Pages/Home/Models. But that doesn’t cover ViewModels that uses in multiple places