21 The MVC and Razor Pages filter pipeline
This chapter covers
• The filter pipeline and how it differs from middleware
• The different types of filters
• Filter ordering
Part 3 of this book has covered the Model-View-Controller (MVC) and Razor Pages frameworks of ASP.NET Core in some detail. You learned how routing is used to select a Razor Page or action to execute. You also saw model binding, validation, and how to generate a response by returning an IActionResult from your actions and page handlers. In this chapter I’m going to head deeper into the MVC/Razor Pages frameworks and look at the filter pipeline, sometimes called the action invocation pipeline, which is analogous to the minimal API endpoint filter pipeline you learned about in chapter 5.
MVC and Razor Pages use several built-in filters to handle cross-cutting concerns, such as authorization (controlling which users can access which action methods and pages in your application). Any application that has the concept of users will use authorization filters as a minimum, but filters are much more powerful than this single use case. In sections 21.1 and 21.2 you’ll learn about all the different types of filters and how they combine to create the MVC filter pipeline for a request that reaches the MVC or Razor Pages framework.
Think of the MVC filter pipeline as a mini middleware pipeline running inside the MVC and Razor Pages frameworks, like the minimal API endpoint filter pipeline. Like the middleware pipeline in ASP.NET Core, the MVC filter pipeline consists of a series of components connected as a pipe, so the output of one filter feeds into the input of the next. In section 21.3 we’ll look at the similarities and differences between these two pipelines, and when you should choose one over the other.
In section 21.4 you’ll see how to create a simple custom filter. Rather than focus on the functionality of the filter itself, you’ll learn how to apply it to multiple endpoints in section 21.5. In section 21.6 you’ll see how the choice of where you apply your attributes affects the order in which your filters execute.
The filter pipeline is a complex topic, but it can enable some advanced behaviors in your app and potentially reduce overall complexity. In this chapter you’ll learn the basics of the pipeline and how it works. In chapter 22 we dig into practical examples of filters, looking at the filters that come out of the box in ASP.NET Core, as well as building custom filters to extract common code from your controllers and Razor Pages.
Before we can start writing code, we should get to grips with the basics of the filter pipeline. The first section of this chapter explains what the pipeline is, why you might want to use it, and how it differs from the middleware pipeline.
21.1 Understanding the MVC filter pipeline
In this section you’ll learn all about the MVC filter pipeline. You’ll see where it fits in the life cycle of a typical request and the roles of the six types of filters available.
The filter pipeline is a relatively simple concept in that it provides hooks into the normal MVC request, as shown in figure 21.1. For example, say you wanted to ensure that users can create or edit products on an e-commerce app only if they’re logged in. The app would redirect anonymous users to a login page instead of executing the action.
Figure 21.1 Filters run at multiple points in the EndpointMiddleware as part of the normal handling of an MVC request. A similar pipeline exists for Razor Page requests.
Without filters, you’d need to include the same code to check for a logged-in user at the start of each specific action method. With this approach, the MVC framework would still execute the model binding and validation, even if the user were not logged in.
With filters, you can use the hooks in the MVC request to run common code across all requests or a subset of requests. This way you can do a wide range of things, such as
• Ensure that a user is logged in before an action method, model binding, or validation runs.
• Customize the output format of particular action methods.
• Handle model validation failures before an action method is invoked.
Catch exceptions from an action method and handle them in a special way.
In many ways, the MVC filter pipeline is like an extra middleware pipeline, restricted to MVC and Razor Pages requests only. Like middleware, filters are good for handling cross-cutting concerns for your application and are useful tools for reducing code duplication in many cases.
The linear1 view of an MVC request and the filter pipeline that I’ve used so far doesn’t quite match up with how these filters execute. There are five types of filters that apply to MVC requests, each of which runs at a different stage in the MVC framework, as shown in figure 21.2.
Figure 21.2 The MVC filter pipeline, including the five filter stages. Some filter stages (resource, action, and result) run twice, before and after the remainder of the pipeline.
Each filter stage lends itself to a particular use case, thanks to its specific location in the pipeline, with respect to model binding, action execution, and result execution:
• Authorization filters—These run first in the pipeline, so they’re useful for protecting your APIs and action methods. If an authorization filter deems the request unauthorized, it short-circuits the request, preventing the rest of the filter pipeline (or action) from running.
• Resource filters—After authorization, resource filters are the next filters to run in the pipeline. They can also execute at the end of the pipeline, in much the same way that middleware components can handle both the incoming request and the outgoing response. Alternatively, resource filters can completely short-circuit the request pipeline and return a response directly.
Thanks to their early position in the pipeline, resource filters can have a variety of uses. You could add metrics to an action method; prevent an action method from executing if an unsupported content type is requested; or, as they run before model binding, control the way model binding works for that request.
• Action filters—Action filters run immediately before and after an action method is executed. As model binding has already happened, action filters let you manipulate the arguments to the method—before it executes—or they can short-circuit the action completely and return a different IActionResult. Because they also run after the action executes, they can optionally customize an IActionResult returned by the action before the action result is executed.
• Exception filters—Exception filters catch exceptions that occur in the filter pipeline and handle them appropriately. You can use exception filters to write custom, MVC-specific error-handling code, which can be useful in some situations. For example, you could catch exceptions in API actions and format them differently from exceptions in your Razor Pages.
• Result filters—Result filters run before and after an action method’s IActionResult is executed. You can use result filters to control the execution of the result or even to short-circuit the execution of the result.
Exactly which filter you pick to implement will depend on the functionality you’re trying to introduce. Want to short-circuit a request as early as possible? Resource filters are a good fit. Need access to the action method parameters? Use an action filter.
Think of the filter pipeline as a small middleware pipeline that lives by itself in the MVC framework. Alternatively, you could think of filters as hooks into the MVC action invocation process that let you run code at a particular point in a request’s life cycle.
NOTE The design of the MVC filter pipeline is quite different from the minimal API endpoint filter pipeline you saw in chapter 5. The endpoint filter pipeline is linear and doesn’t have multiple types of filters.
This section described how the filter pipeline works for MVC and Web API controllers; Razor Pages use an almost-identical filter pipeline.
21.2 The Razor Pages filter pipeline
The Razor Pages framework uses the same underlying architecture as MVC and Web API controllers, so it’s perhaps not surprising that the filter pipeline is virtually identical. The only difference between the pipelines is that Razor Pages do not use action filters. Instead, they use page filters, as shown in figure 21.3.
Figure 21.3 The Razor Pages filter pipeline, including the five filter stages. Authorization, resource, exception, and result filters execute in exactly the same way as for the MVC pipeline. Page filters are specific to Razor Pages and execute in three places: after page hander selection, after model binding and validation, and after page handler execution.
The authorization, resource, exception, and result filters are exactly the same filters you saw for the MVC pipeline. They execute in the same way, serve the same purposes, and can be short-circuited in the same way.
NOTE These filters are literally the same classes shared between the Razor Pages and MVC frameworks.
The difference with the Razor Pages filter pipeline is that it uses page filters instead of action filters. By contrast with other filter types, page filters run three times in the filter pipeline:
• After page handler selection—After the resource filters have executed, a page handler is selected, based on the request’s HTTP verb and the {handler} route value, as you learned in chapter 15. After page handler selection, a page filter method executes for the first time. You can’t short-circuit the pipeline at this stage, and model binding and validation have not yet executed.
• After model binding—After the first page filter execution, the request is model-bound to the Razor Page’s binding models and is validated. This execution is highly analogous to the action filter execution for API controllers. At this point you could manipulate the model-bound data or short-circuit the page handler execution completely by returning a different IActionResult.
• After page handler execution—If you don’t short-circuit the page handler execution, the page filter runs a third and final time after the page handler has executed. At this point you could customize the IActionResult returned by the page handler before the result is executed.
The triple execution of page filters makes it a bit harder to visualize the pipeline, but you can generally think of them as beefed-up action filters. Everything you can do with an action filter, you can do with a page filter, and you can hook in after page handler selection if necessary.
Tip Each execution of a filter executes a different method of the appropriate interface, so it’s easy to know where you are in the pipeline and to execute a filter in only one of its possible locations if you wish.
One of the main questions I hear when people learn about filters in ASP.NET Core is “Why do we need them?” If the filter pipeline is like a mini middleware pipeline, why not use a middleware component directly, instead of introducing the filter concept? That’s an excellent point, which I’ll tackle in the next section.
21.3 Filters or middleware: Which should you choose?
The filter pipeline is similar to the middleware pipeline in many ways, but there are several subtle differences that you should consider when deciding which approach to use. The considerations are essentially the same as those for the minimal API endpoint filter I discussed in chapter 5. MVC filters and middleware are similar in three ways:
• Requests pass through a middleware component on the way “in,” and responses pass through again on the way “out.” Resource, action, and result filters are also two-way, though authorization and exception filters run only once for a request, and page filters run three times.
• Middleware can short-circuit a request by returning a response instead of passing it on to later middleware. MVC and page filters can also short-circuit the filter pipeline by returning a response.
• Middleware is often used for cross-cutting application concerns, such as logging, performance profiling, and exception handling. Filters also lend themselves to cross-cutting concerns.
Filters and middleware also differ primarily in three ways:
• Middleware can run for all requests; filters run only for requests that reach the EndpointMiddleware and execute a controller action or Razor Page handler.
• Filters have access to MVC constructs such as ModelState and IActionResults. Middleware in general is independent from MVC and Razor Pages and works at a lower level, so it can’t use these concepts.
• Filters can be easily applied to a subset of requests, such as all actions on a single controller or a single Razor Page. Middleware generally applies to all requests that reach a given point in the middleware pipeline.
As for the endpoint filter pipeline, I like to think of middleware versus MVC filters as a question of specificity. Middleware is the more general concept, so it has the wider reach. But if you need to access to MVC constructs or want to behave differently for some MVC actions or Razor Pages, you should consider using a filter.
The middleware-versus-filters argument is a subtle one, and it doesn’t matter which you choose as long as it works for you. You can even use middleware components inside the MVC filter pipeline, effectively turning a middleware component into a filter!
Tip The middleware-as-filters feature was introduced in ASP.NET Core 1.1 and is also available in later versions. The canonical use case is for localizing requests to multiple languages. I have a blog series on how to use the feature here: http://mng.bz/RXa0.
Filters can be a little abstract in isolation, so in the next section we’ll look at some code and learn how to write a custom MVC filter in ASP.NET Core.
21.4 Creating a simple filter
In this section, I show you how to create your first filters; in section 21.5 you’ll see how to apply them to MVC controllers and actions. We’ll start small, creating filters that only write to the console, but in chapter 22 we look at some more practical examples and discuss some of their nuances.
You implement a filter for a given stage by implementing one of a pair of interfaces, one synchronous (sync) and one asynchronous (async):
• Authorization filters—IAuthorizationFilter or IAsyncAuthorizationFilter
• Resource filters—IResourceFilter or IAsyncResourceFilter
• Action filters—IActionFilter or IAsyncActionFilter
• Page filters—IPageFilter or IAsyncPageFilter
• Exception filters—IExceptionFilter or IAsyncExceptionFilter
• Result filters—IResultFilter or IAsyncResultFilter
You can use any plain old CLR object (POCO) class to implement a filter, but you’ll typically implement them as C# attributes, which you can use to decorate your controllers, actions, and Razor Pages, as you’ll see in section 21.5. You can achieve the same results with either the sync or async interface, so which you choose should depend on whether any services you call in the filter require async support.
NOTE You should implement either the sync interface or the async interface, not both. If you implement both, only the async interface will be used.
Listing 21.1 shows a resource filter that implements IResourceFilter and writes to the console when it executes. The OnResourceExecuting method is called when a request first reaches the resource filter stage of the filter pipeline. By contrast, the OnResourceExecuted method is called after the rest of the pipeline has executed: after model binding, action execution, result execution, and all intermediate filters have run.
Listing 21.1 Example resource filter implementing IResourceFilter
public class LogResourceFilter : Attribute, IResourceFilter
{
public void OnResourceExecuting( #A
ResourceExecutingContext context) #B
{
Console.WriteLine("Executing!");
}
public void OnResourceExecuted( #C
ResourceExecutedContext context) #D
{
Console.WriteLine("Executed");
}
}
❶ Executed at the start of the pipeline, after authorization filters
❷ The context contains the HttpContext, routing details, and information about the current action.
❸ Executed after model binding, action execution, and result execution
❹ Contains additional context information, such as the IActionResult returned by the action
The interface methods are simple and are similar for each stage in the filter pipeline, passing a context object as a method parameter. Each of the two-method sync filters has an Executing and an Executed method. The type of the argument is different for each filter, but it contains all the details for the filter pipeline.
For example, the ResourceExecutingContext passed to the resource filter contains the HttpContext object itself, details about the route that selected this action, details about the action itself, and so on. Contexts for later filters contain additional details, such as the action method arguments for an action filter and the ModelState.
The context object for the ResourceExecutedContext method is similar, but it also contains details about how the rest of the pipeline executed. You can check whether an unhandled exception occurred, you can see if another filter from the same stage short-circuited the pipeline, or you can see the IActionResult used to generate the response.
These context objects are powerful and are the key to advanced filter behaviors like short-circuiting the pipeline and handling exceptions. We’ll make use of them in chapter 22 when we create more complex filter examples.
The async version of the resource filter requires implementing a single method, as shown in listing 21.2. As for the sync version, you’re passed a ResourceExecutingContext object as an argument, and you’re passed a delegate representing the remainder of the filter pipeline. You must call this delegate (asynchronously) to execute the remainder of the pipeline, which returns an instance of ResourceExecutedContext.
Listing 21.2 Example resource filter implementing IAsyncResourceFilter
public class LogAsyncResourceFilter : Attribute, IAsyncResourceFilter
{
public async Task OnResourceExecutionAsync( ❶
ResourceExecutingContext context,
ResourceExecutionDelegate next) ❷
{
Console.WriteLine("Executing async!"); ❸
ResourceExecutedContext executedContext = await next(); ❹
Console.WriteLine("Executed async!"); ❺
}
}
❶ Executed at the start of the pipeline, after authorization filters
❷ You’re provided a delegate, which encapsulates the remainder of the filter pipeline.
❸ Called before the rest of the pipeline executes
❹ Executes the rest of the pipeline and obtains a ResourceExecutedContext instance
❺ Called after the rest of the pipeline executes
The sync and async filter implementations have subtle differences, but for most purposes they’re identical. I recommend implementing the sync version for simplicity, falling back to the async version only if you need to.
You’ve created a couple of filters now, so we should look at how to use them in the application. In the next section we’ll tackle two specific issues: how to control which requests execute your new filters and how to control the order in which they execute.
21.5 Adding filters to your actions and Razor Pages
In section 21.3 I discussed the similarities and differences between middleware and filters. One of those differences is that filters can be scoped to specific actions or controllers so that they run only for certain requests. Alternatively, you can apply a filter globally so that it runs for every MVC action and Razor Page.
By adding filters in different ways, you can achieve several different results. Imagine you have a filter that forces you to log in to execute an action. How you add the filter to your app will significantly change your app’s behavior:
• Apply the filter to a single action or Razor Page. Anonymous users could browse the app as normal, but if they tried to access the protected action or Razor Page, they would be forced to log in.
• Apply the filter to a controller. Anonymous users could access actions from other controllers, but accessing any action on the protected controller would force them to log in.
• Apply the filter globally. Users couldn’t use the app without logging in. Any attempt to access an action or Razor Page would redirect the user to the login page.
NOTE ASP.NET Core comes with such a filter out of the box: AuthorizeFilter. I discuss this filter in chapter 22, and you’ll be seeing a lot more of it in chapter 24.
As I described in the previous section, you normally create filters as attributes, and for good reason: it makes it easy for you to apply them to MVC controllers, actions, and Razor Pages. In this section you’ll see how to apply LogResourceFilter from listing 21.1 to an action, a controller, a Razor Page, and globally. The level at which the filter applies is called its scope.
DEFINITION The scope of a filter refers to how many different actions it applies to. A filter can be scoped to the action method, to the controller, to a Razor Page, or globally.
You’ll start at the most specific scope: applying filters to a single action. The following listing shows an example of an MVC controller that has two action methods, one with LogResourceFilter and one without.
Listing 21.3 Applying filters to an action method
public class RecipeController : ControllerBase
{
[LogResourceFilter] #A
public IActionResult Index() #A
{ #A
return Ok(); #A
} #A
public IActionResult View() #B
{ #B
return OK(); #B
} #B
}
❶ LogResourceFilter runs as part of the pipeline when executing this action.
❷ This action method has no filters at the action level.
Alternatively, if you want to apply the same filter to every action method, you could add the attribute at the controller scope, as in the next listing. Every action method in the controller uses LogResourceFilter without having to specifically decorate each method.
Listing 21.4 Applying filters to a controller
[LogResourceFilter] #A
public class RecipeController : ControllerBase
{
public IActionResult Index () #B
{ #B
return Ok(); #B
} #B
public IActionResult View() #B
{ #B
return Ok(); #B
} #B
}
❶ The LogResourceFilter is added to every action on the controller.
❷ Every action in the controller is decorated with the filter.
For Razor Pages, you can apply attributes to your PageModel, as shown in the following listing. The filter applies to all page handlers in the Razor Page. It’s not possible to apply filters to a single page handler; you must apply them at the page level.
Listing 21.5 Applying filters to a Razor Page
[LogResourceFilter] #A
public class IndexModel : PageModel
{
public void OnGet() #B
{ #B
} #B
public void OnPost() #B
{ #B
} #B
}
❶ The LogResourceFilter is added to the Razor Page’s PageModel.
❷ The filter applies to every page handler in the page.
Filters you apply as attributes to controllers, actions, and Razor Pages are automatically discovered by the framework when your application starts up. For common attributes, you can go one step further and apply filters globally without having to decorate individual classes.
You add global filters in a different way from controller- or action-scoped filters—by adding a filter directly to the MVC services when configuring your controllers and Razor Pages. The next listing shows three equivalent ways to add a globally scoped filter.
Listing 21.6 Applying filters globally to an application
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options => #A
{
options.Filters.Add(new LogResourceFilter()); #B
options.Filters.Add(typeof(LogResourceFilter)); #C
options.Filters.Add<LogResourceFilter>(); #D
});
❶ Adds filters using the MvcOptions object
❷ You can pass an instance of the filter directly. . .
❸ . . . or pass in the Type of the filter and let the framework create it.
❹ Alternatively, the framework can create a global filter using a generic type parameter.
You can configure the MvcOptions by using the AddControllers() overload. When you configure filters globally, they apply both to controllers and to any Razor Pages in your application. If you wish to configure a global filter for a Razor Pages application, there isn’t an overload for configuring the MvcOptions. Instead, you need to use the AddMvcOptions() extension method to configure the filters, as shown in the following listing.
Listing 21.7 Applying filters globally to a Razor Pages application
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.RazorPages() #A
.AddMvcOptions(options => #B
{
options.Filters.Add(new LogResourceFilter()); #C
options.Filters.Add(typeof(LogResourceFilter)); #C
options.Filters.Add<LogResourceFilter>(); #C
});
❶ This method doesn’t let you pass a lambda to configure the MvcOptions.
❷ You must use an extension method to add the filters to the MvcOptions object.
❸ You can configure the filters in any of the ways shown previously.
With potentially three different scopes in play, you’ll often find action methods that have multiple filters applied to them, some applied directly to the action method and others inherited from the controller or globally. Then the question becomes which filter runs first.
21.6 Understanding the order of filter execution
You’ve seen that the filter pipeline contains five stages, one for each type of filter. These stages always run in the fixed order I described in sections 21.1 and 21.2. But within each stage, you can also have multiple filters of the same type (for example, multiple resource filters) that are part of a single action method’s pipeline. These could all have multiple scopes, depending on how you added them, as you saw in the preceding section.
In this section we’re thinking about the order of filters within a given stage and how scope affects this. We’ll start by looking at the default order and then move on to ways to customize the order to your own requirements.
21.6.1 The default scope execution order
When thinking about filter ordering, it’s important to remember that resource, action, and result filters implement two methods: an Executing before method and an Executed after method. On top of that, page filters implement three methods! The order in which each method executes depends on the scope of the filter, as shown in figure 21.4 for the resource filter stage.
Figure 21.4 The default filter ordering within a given stage, based on the scope of the filters. For the Executing method, globally scoped filters run first, followed by controller-scoped, and finally action-scoped filters. For the Executed method, the filters run in reverse order.
By default, filters execute from the broadest scope (global) to the narrowest (action) when running the Executing method for each stage. The filters’ Executed methods run in reverse order, from the narrowest scope (action) to the broadest (global).
The ordering for Razor Pages is somewhat simpler, given that you have only two scopes: global scope filters and Razor Page scope filters. For Razor Pages, global scope filters run the Executing and PageHandlerSelected methods first, followed by the page scope filters. For the Executed methods, the filters run in reverse order.
You’ll sometimes find you need a bit more control over this order, especially if you have, for example, multiple action filters applied at the same scope. The filter pipeline caters to this requirement by way of the IOrderedFilter interface.
21.6.2 Overriding the default order of filter execution with IOrderedFilter
Filters are great for extracting cross-cutting concerns from your controller actions and Razor Page, but if you have multiple filters applied to an action, you’ll often need to control the precise order in which they execute.
Scope can get you some of the way, but for those other cases, you can implement IOrderedFilter. This interface consists of a single property, Order:
public interface IOrderedFilter
{
int Order { get; }
}
You can implement this property in your filters to set the order in which they execute. The filter pipeline orders the filters in each stage based on the Order property first, from lowest to highest, and uses the default scope order to handle ties, as shown in figure 21.5.
Figure 21.5 Controlling the filter order for a stage using the IOrderedFilter interface. Filters are ordered by the Order property first, and then by scope.
The filters for Order = -1 execute first, as they have the lowest Order value. The controller filter executes first because it has a broader scope than the action-scope filter. The filters with Order = 0 execute next, in the default scope order, as shown in figure 21.5. Finally, the filter with Order = 1 executes.
By default, if a filter doesn’t implement IOrderedFilter, it’s assumed to have Order = 0. All the filters that ship as part of ASP.NET Core have Order = 0, so you can implement your own filters relative to these.
NOTE You can completely customize how the filter pipeline is built by customizing the MVC frameworks application model conventions. These control everything about how controllers and Razor Pages are discovered, how they’re added to the pipeline, and how filters are discovered. This is an advanced concept, that you won’t often need, but it may occasionally come in handy. You can read about the MVC application model in the documentation at http://mng.bz/nWNa.
This chapter has provided a lot of background on the MVC filter pipeline, and we covered most of the technical details you need to use filters and create custom implementations for your own application. In chapter 22 you’ll see some of the built-in filters provided by ASP.NET Core, as well as some practical examples of filters you might want to use in your own applications.
21.7 Summary
The filter pipeline provides hooks into an MVC request so you can run functions at various points within an MVC request. With filters you can run code at specific points in the MVC process across all requests or a subset of requests. This is particularly useful for handling cross-cutting concerns that are specific to MVC.
The filter pipeline executes as part of the MVC or Razor Pages execution. It consists of authorization filters, resource filters, action filters, page filters, exception filters, and result filters. Each filter type is grouped in a stage and can be used to achieve effects specific to that stage.
Resource, action, and result filters run twice in the pipeline: an Executing method on the way in and an Executed method on the way out. Page filters run three times: after page handler selection, and before and after page handler execution.
Authorization and exception filters run only once as part of the pipeline; they don’t run after a response has been generated.
Each type of filter has both a sync and an async version. For example, resource filters can implement either the IResourceFilter interface or the IAsync-ResourceFilter interface. You should use the synchronous interface unless your filter needs to use asynchronous method calls.
You can add filters globally, at the controller level, at the Razor Page level, or at the action level. This is called the scope of the filter. Which scope you should choose depends on how broadly you want to apply the filter.
Within a given stage, global-scoped filters run first, then controller-scoped, and finally action-scoped. You can also override the default order by implementing the IOrderedFilter interface. Filters run from lowest to highest Order and use scope to break ties.