Follow me: Jack Histon's Twitter Share on LinkedIn Share on Google+ RSS Feed

Author avatar

Welcome. I am Jack Histon. My career would not be what it is today without dedication and hard work from software bloggers. My purpose is to give back to that online community.

ASP.NET Core 2.0 - Repository Overview: Razor Pages

Sunday, 24 September 2017

Tweet about this on Twitter Share on Facebook Share on LinkedIn Share on Google+ Pin on Pinterest Share on Reddit Share on StumbleUpon

Previously in this Series

  1. ASP.NET Core MVC - Repository Overview: Model Binding
  2. ASP.NET MVC Core - Repository Overview: Value Providers
  3. ASP.NET Core 2.0 - Repository Overview: Action Discovery
  4. ASP.NET Core 2.0 - Repository Overview: Action Selection

Contents

Introduction

This article is the fifth in a series I'm dedicating to reviewing the code and design of the ASP.NET Core GitHub repository. The series tries to explain the underlying mechanisms of ASP.NET Core, and how all the code fits together to create the framework as we know it at the published date.

This article will discuss Razor Pages within ASP.NET Core 2.0. The previous article discussed how a specific action is selected, given a set of action descriptors found on start up. This article explains how to use razor pages, and how it fits into this existing ecosystem.

What is Razor Pages?

Many features have been given to us in the latest ASP.NET Core 2.0 release. Part of that release is the brand new feature Razor Pages. Razor Pages is advertised as being useful for page-focused scenarios, where little to no real logic is needed. For example, an about page where the page content is neither dynamic, or based on conditional logic. Or a contact page, where details of a simple form is to be filled out by a user.

Razor Pages consists of a back-end code file, paired with a view for C# razor code. If you're familiar with the classic ASP.NET Web Forms, then Razor Pages will give you a feeling of déjà vu. Razor Pages feels like ASP.NET Web Forms, but with a modern approach.

Razor Pages has access to many existing mechanisms of the ASP.NET Core repository. Access to existing features is possible due to Razor Pages and MVC sharing code implementation. Mechanisms such as the razor engine itself, that is, writing C# code within the view, is identical to the original methods used in MVC. This allows the use of Tag Helpers, View Components, and more that the razor engine has to offer.

Razor Pages and MVC can be used together. Routing uses the same process of action discovery in MVC, as described in my previous article in the series, as it does in Razor Pages.

Razor pages uses the notion of "handlers". A handler is similar to an action on an MVC controller. Handlers on razor pages, and actions on controllers, are inserted into the same collection. And so selecting a handler to run is performed with all routes considered.

How to setup Razor Pages

Setting up an application to use MVC is a two step process. The ASP.NET Core framework has two startup methods that are called by reflection. These are the ConfigureServices and Configure methods. The methods are used to setup the application dependency injection system, and the middleware pipeline, respectively:


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

public void Configure(IApplicationBuilder app)
{
    app.UseMvc();
}

It was a conscious effort by the ASP.NET Core team to make it easy to use Razor Pages and MVC together. Therefore, to start using Razor Pages, you have to add nothing more to the startup process than what should already be there if using classic MVC.

So please, stand down if you thought this section was going to be riddled with complication.

If you would like to know more about how these startup methods are executed, refer to the ASP.NET Hosting GitHub Repository, specifically the WebHostBuilderExtensions file where you call UseStartup.

Creating your first Razor Page

Razor Pages is designed to be simple. Razor Pages is there to be used by simple pages within your (mostly) static website.

An example of a simple page is an "about me" page. An "about me" page is the iconic content that is used by companies and invididuals to describe themselves to their audience. It can have many details, such as where the entity comes from, their motivations, and details that might be interesting to the intended audience. However, all of this is static content. This is prime real-estate for a razor page.

To create your first page, you need to provide a place to store them. Razor pages are by default stored under "/Pages", at the root of your application directory. This can be changed, by providing a startup configuration:


services
    .AddMvc()
    .WithRazorPagesRoot("/MyRazorPages");

With this change, the razor pages engine will search for all razor pages under the "MyRazorPages" folder. This can enable the use of custom root directories, where your application may already have a use for the default "/Pages" folder. Also, allowing customisation of the root directory allows two or more applications to share razor pages. For example, two websites may exist under the same umbrella of a corporation, and the about me sections could have identical content.

The extension method "WithRazorPagesRoot" is declared in the MvcRazorPagesMvcBuilderExtensions class. This class houses common extensions that can customise how razor pages works. Reading the code for this class, you can see that customising the root directory can be achieved in a different way:


services
    .Configure(options => options.RootDirectory = "/MyRazorPages");

This code skips the middle-man, and shows how razor pages works by directly modifying the razor pages' options. This sheds light on where your root directory value is used. It is stored on a static, and global configuration class: RazorPagesOptions. This means that these options are available to the entire dependency injection system, that is, any class within the ASP.NET Core framework (and your own code).

The RazorProjectPageRouteModelProvider Class

A good example of the use case for the root directory setting is within the RazorProjectPageRouteModelProvider class. The classes primary purpose is to provide route models for the razor pages found in your root directory.

Reading the classes code, it becomes apparent what we need to provide for our "about me" page.

The codes first directive is to skip any file that starts with an underscore:


if (item.FileName.StartsWith("_"))
{
    // Pages like _ViewImports should not be routable.
    continue;
}

This means for our razor page, it should not be prefixed with an "_". So something like "AboutMe" would be a good name.

The second directive in the code shows that we also need the file to be a .cshtml file, and be marked with @page at the top of it:


if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
{
    // .cshtml pages without @page are not RazorPages.
    continue;
}

With these rules in mind, we end up with a razor pages file similar to:


@page

<h1>About me</h1>
<p>This is my simple razor page</p>

This content would then need to be placed in a file called "AboutMe.cshtml" under the root directory (by default "/Pages").

Once the AboutMe.cshtml file is in place, there is nothing else we need to do. The ASP.NET Core framework will explore your pages root directory, find the file, and route to the page using the pages name. Therefore, to access our brand new AboutMe.cshtml file as a url, navigate to the "/AboutMe" path in your favourite web browser.

As I have said previously, razor pages is designed to be simple and straight-forward. In this example, there is no need even for a code-behind file, as there is no logic. With just one file, we have been able to create routable content that provides a purpose.

Razor Pages and Forms

Sometimes, a page needs more static content than a typical about me page. A "contact me" page is a good example of interaction with the end user. Generally, you need to collect the users details, and a message that the user would like to give.

Here is the view of a typical contact us razor page:


@page
@model MyApplication.Pages.ContactUsModel

<form method="post">
    <div asp-validation-summary="All"></div>
    <div>
        <label asp-for="Name"></label>
        <div>
            <input asp-for="Name" />
            <span asp-validation-for="Name"></span>
        </div>
    </div>

    <div>
        <label asp-for="Message"></label>
        <div>
            <input asp-for="Message" />
            <span asp-validation-for="Message"></span>
        </div>
    </div>

    <div>
        <button type="submit">Save</button>
    </div>
</form>

In this contact us page, we can see that we have declared the necessary @page directive, named the file without an underscore prefix, and with an .cshtml extension; all of these are prerequisites for a file to be classed as a razor page. This file is also placed under the root directory for razor pages (again, by default "/Pages").

The code-behind file

To provide value to the contact us page, there needs to be code that handles the posting of the form data:


public class ContactUsModel : PageModel
{
    [BindProperty]
    public string Name { get; set; }

    [BindProperty]
    public string Message { get; set; }

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

        ...

        return RedirectToPage("/Index");
    }
}

This is shown as a code-behind file. Razor Pages is flexible enough to allow this to be declared in-line with the razor view itself. My preference is a separate file, for clarity. You can define an @functions area like so:


@page
@model ContactUsModel
@functions
{
    public class ContactUsModel : PageModel
    {
        ...
    }
}

<h1>About Me</h1>
...

But generally, I prefere a separate file, as it makes it more clear the intended purpose of each file.

The code behind file inherits from the PageModel class. This class provides many helper methods and properties that can help in the handler methods declared on your page's model. The inheritance is not mandatory, and draws a similar concept to inheriting the Controller class when coding in classic MVC.

There are two properties defined in the file: Name, and Message. Each have an attribute declared called BindPropertyAttribute.

What is this attribute for?

The attribute is an implementation of the IModelNameProvider interface. This allows us to specify the name of the metadata (for request binding) other than the property name.

The BindPropertyAttribute also implements IRequestPredicateProvider interface. This allows the implementation of custom code to narrow down when a property is actually bound to. In this case, the BindPropertyAttribute allows binding only when it is not a GET request:


private static bool IsNonGetRequest(ActionContext context)
{
    return !string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase);
}

This is overridable:


[BindProperty(SupportsGet = true)]
public string Name { get; set; }

The Razor Page Application Model

A general theme throughout this article, is that Razor Pages fits snugly into the same framework implementation as classic MVC. As discussed previously in this series, an application model is built up in order to provide routable data.

The DefaultPageApplicationModelProvider class will take the model of your razor page, and with reflection, extract binding metadata. Binding metadata that is being provided by the handy bind property attribute.

Controller metadata is extracted using the BindingInfo class; Razor pages is no different. Using the BindingInfo class, the application model provider extracts property metadata defined on the razor page model.

Controller action metadata is extracted through MVC application model providers. Handlers of specific razor page requests are done in a similar fashion.

The DefaultPageApplicationModelProvider class will populate "handler" methods for the application model. But what is a handler method?

The page application model provider will retrieve all the methods from the page model:


var methods = pageModel.HandlerType.GetMethods();

for (var i = 0; i < methods.Length; i++)
{
    var handler = CreateHandlerModel(methods[i]);
    if (handler != null)
    {
        pageModel.HandlerMethods.Add(handler);
    }
}

Using this code, we can understand how a method is deemed a "handler" in regards to razor pages (remember, a handler is similar to an action, as can be seen in the example for the OnPostAsync method above in our page model). The methods on the handler type are looped over, and a handler model is created for each. If this is successful, a handler method is added to the page model.

This means that we can create a number of methods on our page model, and all could be deemed a "handler" if a handler model is successfully created for it. So what does the CreateHandlerModel method do?

The CreateHandlerModel method first checks whether your method is a candidate for being a handler:


if (!IsHandler(method))
{
    return null;
}

To become a handler method, you need to pass the following criteria:

  • It can not be static
  • It can not be abstract
  • It can not be a constructor
  • It can not be generic
  • It has to be a public method
  • It can not have the "NonHandlerAttribute" attribute.

All other method signatures are up for grabs. Our method "OnPostAsync" has the following declaration:


public async Task<IActionResult> OnPostAsync()
{
    ...
}

Let us go through the rules and make sure it is a candidate for becoming a handler. The method is not static, it is not abstract, it is not a constructor, is is not generic (It has not supplied it's own generic parameters), it is a public method, and we have not declared a "NonHandlerAttribute" on it. Therefore, we pass the first hurdle of the CreateHandlerModel method.

The next step in the CreateHandlerModel method, is to try and parse the handler name and http method that it will be used for:


if (!TryParseHandlerMethod(method.Name, out var httpMethod, out var handlerName))
{
    return null;
}

If the system can not parse the handler method name, then it will not be added as a handler.

A handler's name needs to be in a specific format to be accepted as a handler. The first rule is that the method name needs to start with "On":


// Handler method names always start with "On"
if (!methodName.StartsWith("On") || methodName.Length <= "On".Length)
{
    return false;
}

Our method "OnPostAsync" passes this test.

The code following this, parses the rest of the method name, to find the http method that should be chosen, and the specific handler name to use. Without going into the code too much, valid names can be of the form OnGet, OnPost, OnPostAsync, OnGetTestAsync, etc.

What is key here, is that the http method and the handler name are optional, and the Async suffix is completely irrelevant (and is ignored). If a http method is excluded, then the handler method will be considered for any http request. If a handler name is specified, then the route data needs to match against this value. You can not exclude both the name and http method from a handler. If you do so, then this will not be considered a valid handler method.

So if we take our example of the "OnPostAsync" method, then this is a successful handler candidate, as it's name starts with On, defines the http method of POST, and has an ignored -Async suffix. Perfect for our view's html form.

Dealing with Multiple Handlers

We have defined how handlers are added to the page application model. However, there is nothing stopping us from having multiple handlers per razor page. What if we wanted to have two forms on the same page? what if we wanted to provide some sort of AJAX handler for a specific razor page?

Defining multiple handlers is easy: just create two different methods following the name conventions outlined above. However, how do we define which form submit button corresponds to which handler?

You can use the "asp-page-handler" tag helper to achieve this:


<form method="post">
    <input type="submit" asp-page-handler="Test" value="Go to index" />
</form>

this will match up with a handler named "OnPostTest":


public IActionResult OnPostTest()
{
    return RedirectToPage("/Index");
}

All this does is redirect to an index page. What the "asp-page-handler" tag helper does, is evaluate to the following input element:


<input type="submit" value="Go to index" formaction="/About?handler=Test">

The key here, is the query string added to the formaction attribute.

When selecting a handler to invoke, the PageActionInvoker class will use the DefaultPageHandlerMethodSelector class to choose an appropriate handler. It selects potential candidates by taking the action descriptor for the current context, and checking to see if it has any handler methods. If it does, then there are important checks that happen to select the best candidate.

Firstly, if the handler specified a http method name, then it needs to match for the current request:


if (handler.HttpMethod != null &&
    !string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
{
    continue;
}

If this passes, then next it checks the handler name. If no handler name was specified - e.g., "OnPostAsync" - then the handler will be a candidate regardless of the current value of the handler name in the http request. Otherwise, the name needs to match:


else if (handler.Name != null &&
    !handler.Name.Equals(handlerName, StringComparison.OrdinalIgnoreCase))
{
    continue;
}

So given these checks, let's have an example. A http web request arrives and with the handler name "DoStuff", and the http method "Post". Possible handler names that can be considered are:

  • OnPost - For a post request, but no handler name specified.
  • OnPostAsync - the async alternative to the previous name.
  • OnDoStuff - For any http method, but handles anything where the handler route value is equal to "DoStuff".
  • OnDoStuffAsync - the async alternative to the previous name.
  • OnPostDoStuff - For a http method of post, and handles anything where the handler route value is equal to "DoStuff".
  • OnPostDoStuffAsync - the async alternative to the previous name.

If we had a page model that defined all of these, then we would get an ambiguous handler exception. That is why we have to perform an extra step, and assign a score to each of these handlers.

The more specific a handler, the higher the score it is assigned:


private static int GetScore(HandlerMethodDescriptor descriptor)
{
    if (descriptor.Name != null)
    {
        return 2;
    }
    else if (descriptor.HttpMethod != null)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

If an action has a name, then it is automatically seen as the best candidate. If it doesn't specify a name, but specifies an http method, then it is less specific, but still defines something for the current http request context. Finally, if neither a name or method is given, then it is the least specific type of handler name. N.B., getting into this scenario should be impossible, as handler's need either a name, or a http method. If neither are provided, for example "OnAsync", then it would not be classed as a handler at all.

Once we have measured each handler with a score, the highest score wins. So taking all the previous possible handlers and their scores:

  • OnPost - 1
  • OnPostAsync - 1
  • OnDoStuff - 2
  • OnDoStuffAsync - 2
  • OnPostDoStuff - 2
  • OnPostDoStuffAsync - 2

As the framework does not know which handler out of OnDoStuff, OnDoStuffAsync, OnPostDoStuff, OnPostDoStuffAsync to choose from, as they are considered equal and the handler route value is "DoStuff", an ambiguous exception would be thrown. The solution here, is to reduce the number of handlers for this specific razor page (which is the directive for this new feature anyway: keep it simple stupid (KISS)).

Getting rid of the handler query parameter

Razor pages allows you to define your own route parameters within the @page directive of the razor page:


@page "{handler?}"

It is the job of the PageDirectiveFeature static class to extract this value.

The "handler" route parameter is a preserved name for the handler route value. For example, the contact us page is located under the root directory: "/Pages/ContactUs.cshtml". If we placed the handler name in the routing, then we can create a handler that can be routed to without any query string. So given an "OnDoStuff" handler on the contact us page model, the corresponding url path would become "/ContactUs/OnDoStuff", rather than "/ContactUs?handler=OnDoStuff".

This all works because of the DefaultPageHandlerMethodSelector class checking for a handler name both in the query string, and the route data:


var handlerName = Convert.ToString(context.RouteData.Values[Handler]);

if (string.IsNullOrEmpty(handlerName) &&
    context.HttpContext.Request.Query.TryGetValue(Handler, out StringValues queryValues))
{
    handlerName = queryValues[0];
}

Without this, we couldn't create nice, SEO-friendly urls when it came to different handlers.

Now we have multiple handlers per razor page, it resembles a controller and view system. Albeit, a simpler version (and that's the point).

Summary

Razor Pages should be seen as MVC's little brother. A younger brother who learns from it's older brother, but ultimately has a different goal in life. A goal that is far simpler (for now), and one that closely resembles the structure of the web application itself.

A razor page can be stretched to the point where it looks like a controller/view relationship. But that is not it's intended purpose. You should strive for your razor pages to be simple, and to serve one purpose.

MVC and Razor Pages share the same ecosystem, so using both at the same time is possible, even encouraged. A popular methodology that is taking traction, is to start using razor pages for your actual pages - e.g., about me, contact us, terms & conditions etc. - and controllers for API level control, such as AJAX, single-page application back-ends, etc.

It is up to you to define the line for when to use Razor Pages, like anything in software, it requires a gut feeling. A rule of thumb I have been following, is if I can define a web page as a simple http GET request, then I could probably define this as a simple Razor Page.

Thank you for reading, and I hope this helps on your journey with Razor Pages.

Share with a friend

Please share this blog post so others can learn from it as well.

Tweet about this on Twitter Share on Facebook Share on LinkedIn Share on Google+ Pin on Pinterest Share on Reddit Share on StumbleUpon

Recent Posts

Archives



© 2017 - Jack Histon - Blog