Razor Pages route and app convention features in ASP.NET Core

By Luke Latham

Learn how to use page route and app model provider convention features to control page routing, discovery, and processing in Razor Pages apps. When you need to configure custom page routes for individual pages, configure routing to pages with the AddPageRoute convention described later in this topic.

Use the sample app (how to download) to explore the features described in this topic.

Features The sample demonstrates ...
Route and app model conventions

Conventions.Add
  • IPageRouteModelConvention
  • IPageApplicationModelConvention
Adding a route template and header to an app's pages.
Page route action conventions
  • AddFolderRouteModelConvention
  • AddPageRouteModelConvention
  • AddPageRoute
Adding a route template to pages in a folder and to a single page.
Page model action conventions
  • AddFolderApplicationModelConvention
  • AddPageApplicationModelConvention
  • ConfigureFilter (filter class, lambda expression, or filter factory)
Adding a header to pages in a folder, adding a header to a single page, and configuring a filter factory to add a header to an app's pages.
Default page app model provider Replacing the default page model provider to change the conventions for handler naming.

Add route and app model conventions

Add a delegate for IPageConvention to add route and app model conventions that apply to Razor Pages.

Add a route model convention to all pages

Use Conventions to create and add an IPageRouteModelConvention to the collection of IPageConvention instances that are applied during route and page model construction.

The sample app adds a {globalTemplate?} route template to all of the pages in the app:

public class GlobalTemplatePageRouteModelConvention 
    : IPageRouteModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        for (var i = 0; i < selectorCount; i++)
        {
            var selector = model.Selectors[i];
            model.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Order = 0,
                    Template = AttributeRouteModel.CombineTemplates(
                        selector.AttributeRouteModel.Template, 
                        "{globalTemplate?}"),
                }
            });
        }
    }
}

Note

The Order property for the AttributeRouteModel is set to 0 (zero). This ensures that this template is given priority for the first route data value position when a single route value is provided. For example, the sample adds an {aboutTemplate?} route template later in the topic. The {aboutTemplate?} template is given an Order of 1. When the About page is requested at /About/RouteDataValue, "RouteDataValue" is loaded into RouteData.Values["globalTemplate"] (Order = 0) and not RouteData.Values["aboutTemplate"] (Order = 1) due to setting the Order property.

Startup.cs:

options.Conventions.Add(new GlobalTemplatePageRouteModelConvention());

Request the sample's About page at localhost:5000/About/GlobalRouteValue and inspect the result:

The About page is requested with a route segment of GlobalRouteValue. The rendered page shows that the route data value is captured in the OnGet method of the page.

Add an app model convention to all pages

Use Conventions to create and add an IPageApplicationModelConvention to the collection of IPageConvention instances that are applied during route and page model construction.

To demonstrate this and other conventions later in the topic, the sample app includes an AddHeaderAttribute class. The class constructor accepts a name string and a values string array. These values are used in its OnResultExecuting method to set a response header. The full class is shown in the Page model action conventions section later in the topic.

The sample app uses the AddHeaderAttribute class to add a header, GlobalHeader, to all of the pages in the app:

public class GlobalHeaderPageApplicationModelConvention 
    : IPageApplicationModelConvention
{
    public void Apply(PageApplicationModel model)
    {
        model.Filters.Add(new AddHeaderAttribute(
            "GlobalHeader", new string[] { "Global Header Value" }));
    }
}

Startup.cs:

options.Conventions.Add(new GlobalHeaderPageApplicationModelConvention());

Request the sample's About page at localhost:5000/About and inspect the headers to view the result:

Response headers of the About page show that the GlobalHeader has been added.

Page route action conventions

The default route model provider that derives from IPageRouteModelProvider invokes conventions which are designed to provide extensibility points for configuring page routes.

Folder route model convention

Use AddFolderRouteModelConvention to create and add an IPageRouteModelConvention that invokes an action on the PageRouteModel for all of the pages under the specified folder.

The sample app uses AddFolderRouteModelConvention to add an {otherPagesTemplate?} route template to the pages in the OtherPages folder:

options.Conventions.AddFolderRouteModelConvention("/OtherPages", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 1,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{otherPagesTemplate?}"),
            }
        });
    }
});

Note

The Order property for the AttributeRouteModel is set to 1. This ensures that the template for {globalTemplate?} (set earlier in the topic) is given priority for the first route data value position when a single route value is provided. If the Page1 page is requested at /OtherPages/Page1/RouteDataValue, "RouteDataValue" is loaded into RouteData.Values["globalTemplate"] (Order = 0) and not RouteData.Values["otherPagesTemplate"] (Order = 1) due to setting the Order property.

Request the sample's Page1 page at localhost:5000/OtherPages/Page1/GlobalRouteValue/OtherPagesRouteValue and inspect the result:

Page1 in the OtherPages folder is requested with a route segment of GlobalRouteValue and OtherPagesRouteValue. The rendered page shows that the route data values are captured in the OnGet method of the page.

Page route model convention

Use AddPageRouteModelConvention to create and add an IPageRouteModelConvention that invokes an action on the PageRouteModel for the page with the specified name.

The sample app uses AddPageRouteModelConvention to add an {aboutTemplate?} route template to the About page:

options.Conventions.AddPageRouteModelConvention("/About", model =>
{
    var selectorCount = model.Selectors.Count;
    for (var i = 0; i < selectorCount; i++)
    {
        var selector = model.Selectors[i];
        model.Selectors.Add(new SelectorModel
        {
            AttributeRouteModel = new AttributeRouteModel
            {
                Order = 1,
                Template = AttributeRouteModel.CombineTemplates(
                    selector.AttributeRouteModel.Template, 
                    "{aboutTemplate?}"),
            }
        });
    }
});

Note

The Order property for the AttributeRouteModel is set to 1. This ensures that the template for {globalTemplate?} (set earlier in the topic) is given priority for the first route data value position when a single route value is provided. If the About page is requested at /About/RouteDataValue, "RouteDataValue" is loaded into RouteData.Values["globalTemplate"] (Order = 0) and not RouteData.Values["aboutTemplate"] (Order = 1) due to setting the Order property.

Request the sample's About page at localhost:5000/About/GlobalRouteValue/AboutRouteValue and inspect the result:

About page is requested with route segments for GlobalRouteValue and AboutRouteValue. The rendered page shows that the route data values are captured in the OnGet method of the page.

Configure a page route

Use AddPageRoute to configure a route to a page at the specified page path. Generated links to the page use your specified route. AddPageRoute uses AddPageRouteModelConvention to establish the route.

The sample app creates a route to /TheContactPage for Contact.cshtml:

options.Conventions.AddPageRoute("/Contact", "TheContactPage/{text?}");

The Contact page can also be reached at /Contact via its default route.

The sample app's custom route to the Contact page allows for an optional text route segment ({text?}). The page also includes this optional segment in its @page directive in case the visitor accesses the page at its /Contact route:

@page "{text?}"
@model ContactModel
@{
    ViewData["Title"] = "Contact";
}

<h1>@ViewData["Title"]</h1>
<h2>@Model.Message</h2>

<address>
    One Microsoft Way<br>
    Redmond, WA 98052-6399<br>
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

<address>
    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br>
    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
</address>

<p>@Model.RouteDataTextTemplateValue</p>

Note that the URL generated for the Contact link in the rendered page reflects the updated route:

Sample app Contact link in the navigation bar

Inspecting the Contact link in the rendered HTML indicates that the href is set to '/TheContactPage'

Visit the Contact page at either its ordinary route, /Contact, or the custom route, /TheContactPage. If you supply an additional text route segment, the page shows the HTML-encoded segment that you provide:

Edge browser example of supplying an optional 'text' route segment of 'TextValue' in the URL. The rendered page shows the 'text' segment value.

Page model action conventions

The default page model provider that implements IPageApplicationModelProvider invokes conventions which are designed to provide extensibility points for configuring page models. These conventions are useful when building and modifying page discovery and processing features.

For the examples in this section, the sample app uses an AddHeaderAttribute class, which is a ResultFilterAttribute, that applies a response header:

public class AddHeaderAttribute : ResultFilterAttribute
{
    private readonly string _name;
    private readonly string[] _values;

    public AddHeaderAttribute(string name, string[] values)
    {
        _name = name;
        _values = values;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers.Add(_name, _values);
        base.OnResultExecuting(context);
    }
}

Using conventions, the sample demonstrates how to apply the attribute to all of the pages in a folder and to a single page.

Folder app model convention

Use AddFolderApplicationModelConvention to create and add an IPageApplicationModelConvention that invokes an action on PageApplicationModel instances for all pages under the specified folder.

The sample demonstrates the use of AddFolderApplicationModelConvention by adding a header, OtherPagesHeader, to the pages inside the OtherPages folder of the app:

options.Conventions.AddFolderApplicationModelConvention("/OtherPages", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "OtherPagesHeader", new string[] { "OtherPages Header Value" }));
});

Request the sample's Page1 page at localhost:5000/OtherPages/Page1 and inspect the headers to view the result:

Response headers of the OtherPages/Page1 page show that the OtherPagesHeader has been added.

Page app model convention

Use AddPageApplicationModelConvention to create and add an IPageApplicationModelConvention that invokes an action on the PageApplicationModel for the page with the speciifed name.

The sample demonstrates the use of AddPageApplicationModelConvention by adding a header, AboutHeader, to the About page:

options.Conventions.AddPageApplicationModelConvention("/About", model =>
{
    model.Filters.Add(new AddHeaderAttribute(
        "AboutHeader", new string[] { "About Header Value" }));
});

Request the sample's About page at localhost:5000/About and inspect the headers to view the result:

Response headers of the About page show that the AboutHeader has been added.

Configure a filter

ConfigureFilter configures the specified filter to apply. You can implement a filter class, but the sample app shows how to implement a filter in a lambda expression, which is implemented behind-the-scenes as a factory that returns a filter:

options.Conventions.ConfigureFilter(model =>
{
    if (model.RelativePath.Contains("OtherPages/Page2"))
    {
        return new AddHeaderAttribute(
            "OtherPagesPage2Header", 
            new string[] { "OtherPages/Page2 Header Value" });
    }
    return new EmptyFilter();
});

The page app model is used to check the relative path for segments that lead to the Page2 page in the OtherPages folder. If the condition passes, a header is added. If not, the EmptyFilter is applied.

EmptyFilter is an Action filter. Since Action filters are ignored by Razor Pages, the EmptyFilter no-ops as intended if the path doesn't contain OtherPages/Page2.

Request the sample's Page2 page at localhost:5000/OtherPages/Page2 and inspect the headers to view the result:

The OtherPagesPage2Header is added to the response for Page2.

Configure a filter factory

ConfigureFilter configures the specified factory to apply filters to all Razor Pages.

The sample app provides an example of using a filter factory by adding a header, FilterFactoryHeader, with two values to the app's pages:

options.Conventions.ConfigureFilter(new AddHeaderWithFactory());

AddHeaderWithFactory.cs:

public class AddHeaderWithFactory : IFilterFactory
{
    // Implement IFilterFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return new AddHeaderFilter();
    }

    private class AddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "FilterFactoryHeader", 
                new string[] 
                { 
                    "Filter Factory Header Value 1",
                    "Filter Factory Header Value 2"
                });
        }

        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Request the sample's About page at localhost:5000/About and inspect the headers to view the result:

Response headers of the About page show that two FilterFactoryHeader headers have been added.

Replace the default page app model provider

Razor Pages uses the IPageApplicationModelProvider interface to create a DefaultPageApplicationModelProvider. You can inherit from the default model provider to provide your own implementation logic for handler discovery and processing. The default implementation (reference source) establishes conventions for unnamed and named handler naming, which is described below.

Default unnamed handler methods

Handler methods for HTTP verbs ("unnamed" handler methods) follow a convention: On<HTTP verb>[Async] (appending Async is optional but recommended for async methods).

Unnamed handler method Operation
OnGet/OnGetAsync Initialize the page state.
OnPost/OnPostAsync Handle POST requests.
OnDelete/OnDeleteAsync Handle DELETE requests†.
OnPut/OnPutAsync Handle PUT requests†.
OnPatch/OnPatchAsync Handle PATCH requests†.

†Used for making API calls to the page.

Default named handler methods

Handler methods provided by the developer ("named" handler methods) follow a similar convention. The handler name appears after the HTTP verb or between the HTTP verb and Async: On<HTTP verb><handler name>[Async] (appending Async is optional but recommended for async methods). For example, methods that process messages might take the naming shown in the table below.

Example named handler method Example operation
OnGetMessage/OnGetMessageAsync Obtain a message.
OnPostMessage/OnPostMessageAsync POST a message.
OnDeleteMessage/OnDeleteMessageAsync DELETE a message†.
OnPutMessage/OnPutMessageAsync PUT a message†.
OnPatchMessage/OnPatchMessageAsync PATCH a message†.

†Used for making API calls to the page.

Customize handler method names

Assume that you prefer to change the way unnamed and named handler methods are named. An alternative naming scheme is to avoid starting the method names with "On" and use the first word segment to determine the HTTP verb. You can make other changes, such as converting the verbs for DELETE, PUT, and PATCH to POST. Such a scheme provides the method names shown in the following table.

Handler method Operation
Get Initialize the page state.
Post/PostAsync Handle POST requests.
Delete/DeleteAsync Handle DELETE requests†.
Put/PutAsync Handle PUT requests†.
Patch/PatchAsync Handle PATCH requests†.
GetMessage Obtain a message.
PostMessage/PostMessageAsync POST a message.
DeleteMessage/DeleteMessageAsync POST a message to delete.
PutMessage/PutMessageAsync POST a message to put.
PatchMessage/PatchMessageAsync POST a message to patch.

†Used for making API calls to the page.

To establish this scheme, inherit from the DefaultPageApplicationModelProvider class and override the CreateHandlerModel method to supply custom logic for resolving PageModel handler names. The sample app shows you how this is done in its CustomPageApplicationModelProvider class:

public class CustomPageApplicationModelProvider : 
    DefaultPageApplicationModelProvider
{
    protected override PageHandlerModel CreateHandlerModel(MethodInfo method)
    {
        if (method == null)
        {
            throw new ArgumentNullException(nameof(method));
        }

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

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

        var handlerModel = new PageHandlerModel(
            method,
            method.GetCustomAttributes(inherit: true))
        {
            Name = method.Name,
            HandlerName = handlerName,
            HttpMethod = httpMethod,
        };

        var methodParameters = handlerModel.MethodInfo.GetParameters();

        for (var i = 0; i < methodParameters.Length; i++)
        {
            var parameter = methodParameters[i];
            var parameterModel = CreateParameterModel(parameter);
            parameterModel.Handler = handlerModel;

            handlerModel.Parameters.Add(parameterModel);
        }

        return handlerModel;
    }

    private static bool TryParseHandlerMethod(
        string methodName, out string httpMethod, out string handler)
    {
        httpMethod = null;
        handler = null;

        // Parse the method name according to our conventions to 
        // determine the required HTTP verb and optional 
        // handler name.
        //
        // Valid names look like:
        //  - Get
        //  - Post
        //  - PostAsync
        //  - GetMessage
        //  - PostMessage
        //  - DeleteMessage
        //  - DeleteMessageAsync

        var length = methodName.Length;
        if (methodName.EndsWith("Async", StringComparison.Ordinal))
        {
            length -= "Async".Length;
        }

        if (length == 0)
        {
            // The method is named "Async". Exit processing.
            return false;
        }

        // The HTTP verb is at the start of the method name. Use 
        // casing to determine where it ends.
        var handlerNameStart = 1;
        for (; handlerNameStart < length; handlerNameStart++)
        {
            if (char.IsUpper(methodName[handlerNameStart]))
            {
                break;
            }
        }

        httpMethod = methodName.Substring(0, handlerNameStart);

        // The handler name follows the HTTP verb and is optional. 
        // It includes everything up to the end excluding the 
        // "Async" suffix, if present.
        handler = handlerNameStart == length ? null : methodName.Substring(0, length);

        if (string.Equals(httpMethod, "GET", StringComparison.OrdinalIgnoreCase) || 
            string.Equals(httpMethod, "POST", StringComparison.OrdinalIgnoreCase))
        {
            // Do nothing. The httpMethod is correct for GET and POST.
            return true;
        }
        if (string.Equals(httpMethod, "DELETE", StringComparison.OrdinalIgnoreCase) || 
            string.Equals(httpMethod, "PUT", StringComparison.OrdinalIgnoreCase) || 
            string.Equals(httpMethod, "PATCH", StringComparison.OrdinalIgnoreCase))
        {
            // Convert HTTP verbs for DELETE, PUT, and PATCH to POST
            // For example: DeleteMessage, PutMessage, PatchMessage -> POST
            httpMethod = "POST";
            return true;
        }
        else
        {
            return false;
        }
    }
}

Highlights of the class include:

  • The class inherits from DefaultPageApplicationModelProvider.
  • The TryParseHandlerMethod processes a handler to determine the HTTP verb (httpMethod) and named handler name (handlerName) when creating the PageHandlerModel.
    • An Async postfix is ignored, if present.
    • Casing is used to parse the HTTP verb from the method name.
    • When the method name (without Async) is equal to the HTTP verb name, there's no named handler. The handlerName is set to null, and the method name is Get, Post, Delete, Put, or Patch.
    • When the method name (without Async) is longer than the HTTP verb name, there's a named handler. The handlerName is set to <method name (less 'Async', if present)>. For example, both "GetMessage" and "GetMessageAsync" yield a handler name of "GetMessage".
    • DELETE, PUT, and PATCH HTTP verbs are converted to POST.

Register the CustomPageApplicationModelProvider in the Startup class:

services.AddSingleton<IPageApplicationModelProvider, 
    CustomPageApplicationModelProvider>();

The code-behind file Index.cshtml.cs shows how the ordinary handler method naming conventions are changed for pages in the app. The ordinary "On" prefix naming used with Razor Pages is removed. The method that initializes the page state is now named Get. You can see this convention used throughout the app if you open any code-behind file for any of the pages.

Each of the other methods start with the HTTP verb that describes its processing. The two methods that start with Delete would normally be treated as DELETE HTTP verbs, but the logic in TryParseHandlerMethod explicitly sets the verb to POST for both handlers.

Note that Async is optional between DeleteAllMessages and DeleteMessageAsync. They're both asynchronous methods, but you can choose to use the Async postfix or not; we recommend that you do. DeleteAllMessages is used here for demonstration purposes, but we recommend that you name such a method DeleteAllMessagesAsync. It doesn't affect the processing of the sample's implementation, but using the Async postfix calls out the fact that it's an asynchronous method.

public async Task Get()
{
    Messages = await _db.Messages.AsNoTracking().ToListAsync();
}

public async Task<IActionResult> PostMessageAsync()
{
    _db.Messages.Add(Message);
    await _db.SaveChangesAsync();

    Result = $"{nameof(PostMessageAsync)} handler: Message '{Message.Text}' added.";

    return RedirectToPage();
}

public async Task<IActionResult> DeleteAllMessages()
{
    foreach (Message message in _db.Messages)
    {
        _db.Messages.Remove(message);
    }
    await _db.SaveChangesAsync();

    Result = $"{nameof(DeleteAllMessages)} handler: All messages deleted.";

    return RedirectToPage();
}

public async Task<IActionResult> DeleteMessageAsync(int id)
{
    var message = await _db.Messages.FindAsync(id);

    if (message != null)
    {
        _db.Messages.Remove(message);
        await _db.SaveChangesAsync();
    }

    Result = $"{nameof(DeleteMessageAsync)} handler: Message with Id: {id} deleted.";

    return RedirectToPage();
}

Note the handler names provided in Index.cshtml match the DeleteAllMessages and DeleteMessageAsync handler methods:

<div class="row">
    <div class="col-md-3">
        <form method="post">
            <h2>Clear all messages</h2>
            <hr>
            <div class="form-group">
                <button type="submit" asp-page-handler="DeleteAllMessages" 
                        class="btn btn-danger">Clear All</button>
            </div>
        </form>
    </div>
</div>

<div class="row">
    <div class="col-md-12">
        <form method="post">
            <h2>Messages</h2>
            <hr>
            <ol>
                @foreach (var message in Model.Messages)
                {
                    <li>
                        @message.Text
                        <button type="submit" asp-page-handler="DeleteMessage" 
                            asp-route-id="@message.Id">delete</button>
                    </li>
                }
            </ol>
        </form>
    </div>
</div>

Async in the handler method name DeleteMessageAsync is factored out by the TryParseHandlerMethod for handler matching of POST request to method. The asp-page-handler name of DeleteMessage is matched to the handler method DeleteMessageAsync.

MVC Filters and the Page filter (IPageFilter)

MVC Action filters are ignored by Razor Pages, since Razor Pages use handler methods. Other types of MVC filters are available for you to use: Authorization, Exception, Resource, and Result. For more information, see the Filters topic.

The Page filter (IPageFilter) is a filter that applies to Razor Pages. It surrounds the execution of a page handler method. It allows you to process custom code at stages of page handler method execution. Here's an example from the sample app:

[AttributeUsage(AttributeTargets.Class)]
public class ReplaceRouteValueFilterAttribute : Attribute, IPageFilter
{
    public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
    {
        // Called after the handler method executes before the result.
    }

    public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        // Called before the handler method executes after model binding is complete.
    }

    public void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
        // Called after a handler method is selected but before model binding occurs.
        context.RouteData.Values.TryGetValue("globalTemplate", 
            out var globalTemplateValue);
        if (string.Equals((string)globalTemplateValue, "TriggerValue", 
            StringComparison.Ordinal))
        {
            context.RouteData.Values["globalTemplate"] = "ReplacementValue";
        }
    }
}

This filter checks for a globalTemplate route value of "TriggerValue" and swaps in "ReplacementValue".

The ReplaceRouteValueFilter attribute can be applied directly to a PageModel in code-behind:

[ReplaceRouteValueFilter]
public class Page3Model : PageModel
{

Request the Page3 page from the sample app with at localhost:5000/OtherPages/Page3/TriggerValue. Notice how the filter replaces the route value:

Request to OtherPages/Page3 with a TriggerValue route segment results in the filter replacing the route value with ReplacementValue.

See also