Filters in Razor Pages

Filters are centralised units of code that can be registered to execute at various points during Razor pages processing. The Razor Pages framework includes a range of built in filters to ensure that requests are served over HTTPS, that requests are authenticated and to managed client-side caching of responses. You can also write your own filters to execute custom logic.

Razor Page Lifecycle

Once a URL has been matched to a page, the correct handler method is selected. Bindable PageModel properties and parameters to the handler method are bound by the model binder. Then the handler method is executed returning a response. Razor Pages filters implement IPageFilter or its async equivalent, IAsyncPageFilter. These interfaces expose methods that enable you to execute code before the handler is selected, after it has been selected but before it has been executed, and after it has been executed:

IPageFilter

Method Description
OnPageHandlerSelected(PageHandlerSelectedContext) Handler method has been selected. Model binding has not taken place
OnPageHandlerExecuting(PageHandlerExecutingContext) Model binding completed. Handler has not been executed
OnPageHandlerExecuted(PageHandlerExecutedContext) Handler method has been executed.

IAsyncPageFilter

Method Description
OnPageHandlerSelectionAsync(PageHandlerSelectedContext) Handler method has been selected. Model binding has not taken place
OnPageHandlerExecutionAsync(PageHandlerExecutingContext, PageHandlerExecutionDelegate) Model binding completed. Handler has not been executed

The various *Context parameters passed in to these methods provide access to the current HttpContext. This makes filters particularly suitable for cross-cutting logic that requires access to the HTTP Request or Response, such as code that needs to work with cookies, headers etc.

Using Filter Methods in a PageModel

The Razor Pages PageModel class implements both IPageFilter and IAsyncPageFilter, meaning that you can override all the methods detailed above in the PageModel class:

Filter methods

If you also implement the async methods, they will execute instead of their non-async equivalents.

The context parameter provides information about the current handler method enabling you execute code conditionally based on the method being selected or executed:

public override void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
    if(context.HandlerMethod.MethodInfo.Name == nameof(OnGet))
    {
        // code placed here will only execute if the OnGet() method has been selected
    }
}

Creating A Custom Filter

Overriding IPageFilter methods in a PageModel is not particularly re-usable especially if you want to have your filter methods executed globally i.e. on every request. What you can do instead is to create a class that derives from PageModel and then have your actual PageModel classes inherit from that instead of from PageModel:

public class BasePageModel : PageModel
{
    
    public override void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
        //...
    }

    public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        //...
    }
}


public class IndexModel : BasePageModel
{
    public void OnGet()
    {
        //...
    }
}

This approach is valid enough, but an alternative approach is to create a class that implements IPageFilter (or IAsyncPageFilter if asynchronous processing is required) and then register this globally.

The following example features an asynchronous filter that obtains the IP Address of the visitor and uses it to perform a geographical lookup. The first element is a service class that utilises the free ipstack API to obtain geographical information about an IP address:

public interface IGeoService
{
    Task<string> GetCountry(string ipAddress);
}
public class GeoService : IGeoService
{
    public async Task<string> GetCountry(string ipAddress)
    {
        using (var client = new HttpClient())
        {
            var json = await client.GetStringAsync($"http://api.ipstack.com/${ipAddress}?access_key=<your api key>");
            dynamic data = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
            return data.country_code;
        }
    }
}

The filter class will make use of this service. The only operation exposed by the service class is async so the filter class will need to implement IAsyncPageFilter:

public class CustomPageFilter : IAsyncPageFilter
{
    private readonly IGeoService _geoService;

    public CustomPageFilter(IGeoService geoService)
    {
        _geoService = geoService;
    }


    public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        await Task.CompletedTask;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
    {
        var ipAddress = context.HttpContext.Request.Host.ToString();
        var countryCode = await _geoService.GetCountry(ipAddress);
        
        // do something with countryCode
        
        await next.Invoke();
    }
}

The filter is registered globally in the ConfigureServices method in Startup.cs along with the service by adding to MvcOptions.Filters:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.RazorPages(options =>
    {
        options.Filters.Add(new CustomPageFilter(new GeoService()));
    });

    services.AddTransient<IGeoService, GeoService>();
    ...
            
}
public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddMvc(options =>
    {
        options.Filters.Add(new CustomPageFilter(new GeoService()));
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddTransient<IGeoService, GeoService>();
    ...
            
}

This filter will execute on every page request. If you want to apply the filter to a subset of pages, you have a couple of options. if all the affected pages are located in the same folder, you can apply the filter to the contents of the folder by using the AddFolderApplicationModelConvention:

services.AddRazorPages()
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.AddFolderApplicationModelConvention("/folder_name", model => model.Filters.Add(new CustomPageFilter(new GeoService())));
        });
services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.AddFolderApplicationModelConvention("/folder_name", model => model.Filters.Add(new CustomPageFilter(new GeoService())));
        }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

The other option is to create a filter attribute and apply it to PageModel classes selectively.

Creating A Filter Attribute

Attributes enable you to declaratively apply behaviour to PageModels. In this example, the code that forms the body of the OnPageHandlerExecutionAsync method in the global filter will be moved to an attribute that derives from ResultFilterAttribute:

public class CustomFilterAttribute : ResultFilterAttribute
{

    public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        IGeoService geoService = context.HttpContext.RequestServices.GetService<IGeoService>();
        var ipAddress = context.HttpContext.Request.Host.ToString();
        var countryCode = await geoService.GetCountry(ipAddress);
        var result = (PageResult)context.Result;
        result.ViewData["CountryCode"] = countryCode;
        await next.Invoke();
    }
}

This example has been enhanced to illustrate how to access the ViewData property and set a value from within an attribute. The ResultFilterAttribute is an MVC artefact which when used in an MVC application can be applied to action methods or controller classes. In Razor Pages, it can only be applied to PageModel classes. It cannot be applied to handler methods:

[CustomFilter]
public class ContactModel : PageModel
{
    ...
}

This example also illustrates the use of the Service Locator pattern to access an instance of IGeoService from within the filter (the RequestServices.GetServices method). This is mainly used in this example to keep things simple, but the Service Locator approach is considered an anti-pattern and there are alternative, more robust approaches to references dependencies from within filters.

The ResultFilterAttribute enables access to the Model via the context.Result.Model property. The Result property must be cast to a PageResult in order for that to work in the same way as the ViewData property is accessed above.

Last updated: 14/02/2020 07:29:06

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com