22 Creating custom MVC and Razor Page filters
This chapter covers
• Creating custom filters to refactor complex action methods
• Using authorization filters to protect your action methods and Razor Pages
• Short-circuiting the filter pipeline to bypass action and page handler execution
• Injecting dependencies into filters
In chapter 21 I introduced the Model-View-Controller (MVC) and Razor Pages filter pipeline and showed where it fits into the life cycle of a request. You learned how to apply filters to your action method, controllers, and Razor Pages, and the effect of scope on the filter execution order.
In this chapter you’ll take that knowledge and apply it to a concrete example. You’ll learn to create custom filters that you can use in your own apps and how to use them to reduce duplicate code in your action methods.
In section 22.1 I take you through the filter types in detail, how they fit into the MVC pipeline, and what to use them for. For each one, I’ll provide example implementations that you might use in your own application and describe the built-in options available.
A key feature of filters is the ability to short-circuit a request by generating a response and halting progression through the filter pipeline. This is similar to the way short-circuiting works in middleware, but there are subtle differences for MVC filters. On top of that, the exact behavior is slightly different for each filter, and I cover that in section 22.2.
You typically add MVC filters to the pipeline by implementing them as attributes added to your controller classes, action methods, and Razor Pages. Unfortunately, you can’t easily use dependency injection (DI) with attributes due to the limitations of C#. In section 22.3 I show you how to use the ServiceFilterAttribute and TypeFilterAttribute base classes to enable DI in your filters.
We covered all the background for filters in chapter 21, so in the next section we jump straight into the code and start creating custom MVC filters.
22.1 Creating custom filters for your application
ASP.NET Core includes several filters that you can use out of the box, but often the most useful filters are the custom ones that are specific to your own apps. In this section we’ll work through each of the six types of filters I covered in chapter 21. I’ll explain in more detail what they’re for and when you should use them. I’ll point out examples of these filters that are part of ASP.NET Core itself, and you’ll see how to create custom filters for an example application.
To give you something realistic to work with, we’ll start with a web API controller for accessing the recipe application from chapter 12. This controller contains two actions: one for fetching a RecipeDetailViewModel and another for updating a Recipe with new values. The following listing shows your starting point for this chapter, including both action methods.
Listing 22.1 Recipe web API controller before refactoring to use filters
[Route("api/recipe")]
public class RecipeApiController : ControllerBase
{
private readonly bool IsEnabled = true; #A
public RecipeService _service;
public RecipeApiController(RecipeService service)
{
_service = service;
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
if (!IsEnabled) { return BadRequest(); } #B
try
{
if (!_service.DoesRecipeExist(id)) #C
{ #C
return NotFound(); #C
} #C
var detail = _service.GetRecipeDetail(id); #D
Response.GetTypedHeaders().LastModified = #E
detail.LastModified; #E
return Ok(detail); #F
}
catch (Exception ex) #G
{ #G
return GetErrorResponse(ex); #G
} #G
}
[HttpPost("{id}")]
public IActionResult Edit(
int id, [FromBody] UpdateRecipeCommand command)
{
if (!IsEnabled) { return BadRequest(); } #H
try
{
if (!ModelState.IsValid) #I
{ #I
return BadRequest(ModelState); #I
} #I
if (!_service.DoesRecipeExist(id)) #J
{ #J
return NotFound(); #J
} #J
_service.UpdateRecipe(command); #K
return Ok(); #K
}
catch (Exception ex) #L
{ #L
return GetErrorResponse(ex); #L
} #L
}
private static IActionResult GetErrorResponse(Exception ex)
{
var error = new ProblemDetails
{
Title = "An error occurred",
Detail = context.Exception.Message,
Status = 500,
Type = "https://httpstatuses.com/500"
};
return new ObjectResult(error)
{
StatusCode = 500
};
}
}
❶ This field would be passed in as configuration and is used to control access to actions.
❷ If the API isn’t enabled, blocks further execution
❸ If the requested Recipe doesn’t exist, returns a 404 response
❹ Fetches RecipeDetailViewModel
❺ Sets the Last-Modified response header to the value in the model
❻ Returns the view model with a 200 response
❼ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
❽ If the API isn’t enabled, blocks further execution
❾ Validates the binding model and returns a 400 response if there are errors
❿ If the requested Recipe doesn’t exist, returns a 404 response
⓫ Updates the Recipe from the command and returns a 200 response
⓬ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
These action methods currently have a lot of code to them, which hides the intent of each action. There’s also quite a lot of duplication between the methods, such as checking that the Recipe entity exists and formatting exceptions.
In this section you’re going to refactor this controller to use filters for all the code in the methods that’s unrelated to the intent of each action. By the end of the chapter you’ll have a much simpler controller controller that’s far easier to understand, as shown here.
Listing 22.2 Recipe web API controller after refactoring to use filters
[Route("api/recipe")]
[ValidateModel] #A
[HandleException] #A
[FeatureEnabled(IsEnabled = true)] #A
public class RecipeApiController : ControllerBase
{
public RecipeService _service;
public RecipeApiController(RecipeService service)
{
_service = service;
}
[HttpGet("{id}")]
[EnsureRecipeExists] #B
[AddLastModifiedHeader] #B
public IActionResult Get(int id)
{
var detail = _service.GetRecipeDetail(id); #C
return Ok(detail); #C
}
[HttpPost("{id}")]
[EnsureRecipeExists] #D
public IActionResult Edit(
int id, [FromBody] UpdateRecipeCommand command)
{
_service.UpdateRecipe(command); #E
return Ok(); #E
}
}
❶ The filters encapsulate the majority of logic common to multiple action
methods.
❷ Placing filters at the action level limits them to a single action.
❸ The intent of the action, return a Recipe view model, is much clearer.
❹ Placing filters at the action level can control the order in which they execute.
❺ The intent of the action, update a Recipe, is much clearer.
I think you’ll have to agree that the controller in listing 22.2 is much easier to read! In this section you’ll refactor the controller bit by bit, removing cross-cutting code to get to something more manageable. All the filters we’ll create in this section will use the sync filter interfaces. I’ll leave it to you, as an exercise, to create their async counterparts. We’ll start by looking at authorization filters and how they relate to security in ASP.NET Core.
22.1.1 Authorization filters: Protecting your APIs
Authentication and authorization are related, fundamental concepts in security that we’ll be looking at in detail in chapters 23 and 24.
DEFINITION Authentication is concerned with determining who made a request. Authorization is concerned with what a user is allowed to access.
Authorization filters run first in the MVC filter pipeline, before any other filters. They control access to the action method by immediately short-circuiting the pipeline when a request doesn’t meet the necessary requirements.
ASP.NET Core has a built-in authorization framework that you should use when you need to protect your MVC application or your web APIs. You can configure this framework with custom policies that let you finely control access to your actions.
Tip It’s possible to write your own authorization filters by implementing IAuthorizationFilter or IAsyncAuthorizationFilter, but I strongly advise against it. The ASP.NET Core authorization framework is highly configurable and should meet all your needs.
At the heart of MVC authorization is an authorization filter, AuthorizeFilter, which you can add to the filter pipeline by decorating your actions or controllers with the [Authorize] attribute. In its simplest form, adding the [Authorize] attribute to an action, as in the following listing, means that the request must be made by an authenticated user to be allowed to continue. If you’re not logged in, it will short-circuit the pipeline, returning a 401 Unauthorized response to the browser.
Listing 22.3 Adding [Authorize] to an action method
public class RecipeApiController : ControllerBase
{
public IActionResult Get(int id) #A
{
// method body
}
[Authorize] #B
public IActionResult Edit( #C
int id, [FromBody] UpdateRecipeCommand command) #C
{
// method body
}
}
❶ The Get method has no [Authorize] attribute, so it can be executed by anyone.
❷ Adds the AuthorizeFilter to the filter pipeline using [Authorize]
❸ The Edit method can be executed only if you’re logged in.
As with all filters, you can apply the [Authorize] attribute at the controller level to protect all the actions on a controller, to a Razor Page to protect all the page handler methods in a page, or even globally to protect every endpoint in your app.
NOTE We’ll explore authorization in detail in chapter 24, including how to add more detailed requirements so that only specific sets of users can execute an action.
The next filters in the pipeline are resource filters. In the next section you’ll extract some of the common code from RecipeApiController and see how easy it is to create a short-circuiting filter.
22.1.2 Resource filters: Short-circuiting your action methods
Resource filters are the first general-purpose filters in the MVC filter pipeline. In chapter 21 you saw minimal examples of both sync and async resource filters, which logged to the console. In your own apps, you can use resource filters for a wide range of purposes, thanks to the fact that they execute so early (and late) in the filter pipeline.
The ASP.NET Core framework includes a few implementations of resource filters you can use in your apps:
• ConsumesAttribute—Can be used to restrict the allowed formats an action method can accept. If your action is decorated with [Consumes("application/json")], but the client sends the request as Extensible Markup Language (XML), the resource filter will short-circuit the pipeline and return a 415 Unsupported Media Type response.
• SkipStatusCodePagesAttribute—This filter prevents the StatusCodePagesMiddleware from running for the response. This can be useful if, for example, you have both web API controllers and Razor Pages in the same application. You can apply this attribute to the controllers to ensure that API error responses are passed untouched, but all error responses from Razor Pages are handled by the middleware.
Resource filters are useful when you want to ensure that the filter runs early in the pipeline, before model binding. They provide an early hook into the pipeline for your logic so you can quickly short-circuit the request if you need to.
Look back at listing 22.1 and see whether you can refactor any of the code into a resource filter. One candidate line appears at the start of both the Get and Edit methods:
if (!IsEnabled) { return BadRequest(); }
This line of code is a feature toggle that you can use to disable the availability of the whole API, based on the IsEnabled field. In practice, you’d probably load the IsEnabled field from a database or configuration file so you could control the availability dynamically at runtime, but for this example I’m using a hardcoded value.
Tip To read more about using feature toggles in your applications, see my series “Adding feature flags to an ASP.NET Core app” at http://mng.bz/2e40.
This piece of code is self-contained cross-cutting logic, which is somewhat orthogonal to the main intent of each action method—a perfect candidate for a filter. You want to execute the feature toggle early in the pipeline, before any other logic, so a resource filter makes sense.
Tip Technically, you could also use an authorization filter for this example, but I’m following my own advice of “Don’t write your own authorization filters!”
The next listing shows an implementation of FeatureEnabledAttribute, which extracts the logic from the action methods and moves it into the filter. I’ve also exposed the IsEnabled field as a property on the filter.
Listing 22.4 The FeatureEnabledAttribute resource filter
public class FeatureEnabledAttribute : Attribute, IResourceFilter
{
public bool IsEnabled { get; set; } #A
public void OnResourceExecuting( #B
ResourceExecutingContext context) #B
{
if (!IsEnabled) #C
{ #C
context.Result = new BadRequestResult(); #C
} #C
}
public void OnResourceExecuted( #D
ResourceExecutedContext context) { } #D
}
❶ Defines whether the feature is enabled
❷ Executes before model binding, early in the filter pipeline
❸ If the feature isn’t enabled, short-circuits the pipeline by setting the context.Result property
❹ Must be implemented to satisfy IResourceFilter, but not needed in this case
This simple resource filter demonstrates a few important concepts, which are applicable to most filter types:
• The filter is an attribute as well as a filter. This lets you decorate your controller, action methods, and Razor Pages with it using [FeatureEnabled(IsEnabled = true)].
• The filter interface consists of two methods: Executing, which runs before model binding, and Executed, which runs after the result has executed. You must implement both, even if you only need one for your use case.
• The filter execution methods provide a context object. This provides access to, among other things, the HttpContext for the request and metadata about the action method that was selected.
• To short-circuit the pipeline, set the context.Result property to an IactionResult instance. The framework will execute this result to generate the response, bypassing any remaining filters in the pipeline and skipping the action method (or page handler) entirely. In this example, if the feature isn’t enabled, you bypass the pipeline by returning BadRequestResult, which returns a 400 error to the client.
By moving this logic into the resource filter, you can remove it from your action methods and instead decorate the whole API controller with a simple attribute:
[FeatureEnabled(IsEnabled = true)]
[Route("api/recipe")]
public class RecipeApiController : ControllerBase
You’ve extracted only two lines of code from your action methods so far, but you’re on the right track. In the next section we’ll move on to action filters and extract two more filters from the action method code.
22.1.3 Action filters: Customizing model binding and action results
Action filters run just after model binding, before the action method executes. Thanks to this positioning, action filters can access all the arguments that will be used to execute the action method, which makes them a powerful way of extracting common logic out of your actions.
On top of this, they run after the action method has executed and can completely change or replace the IActionResult returned by the action if you want. They can even handle exceptions thrown in the action.
NOTE Action filters don’t execute for Razor Pages. Similarly, page filters don’t execute for action methods.
The ASP.NET Core framework includes several action filters out of the box. One of these commonly used filters is ResponseCacheFilter, which sets HTTP caching headers on your action-method responses.
NOTE I have described filters as being attributes, but that’s not always the case. For example, the action filter is called ResponseCacheFilter, but this type is internal to the ASP.NET Core framework. To apply the filter, you use the public [ResponseCache] attribute instead, and the framework automatically configures the ResponseCacheFilter as appropriate. This separation between attribute and filter is largely an artifact of the internal design, but it can be useful, as you’ll see in section 22.3.
Response caching vs. output caching
Caching is a broad topic that aims to improve the performance of an application over the naive approach. But caching can also make debugging issues difficult and may even be undesirable in some situations. Consequently, I often apply ResponseCacheFilter to my action methods to set HTTP caching headers that disable caching! You can read about this and other approaches to caching in Microsoft’s “Response caching in ASP.NET Core” documentation at http://mng.bz/2eGd.Note that the ResponseCacheFilter applies cache control headers only to your outgoing responses; it doesn’t cache the response on the server. These headers tell the client (such as a browser) whether it can skip sending a request and reuse the response. If you have relatively static endpoints, this can massively reduce the load on your app.
This is different from output caching, introduced in .NET 7. Output caching involves storing a generated response on the server and reusing it for subsequent requests. In the simplest case, the response is stored in memory and reused for appropriate requests, but you can configure ASP.NET Core to store the output elsewhere, such as a database.
Output caching is generally more configurable than response caching, as you can choose exactly what to cache and when to invalidate it, but it is also much more resource-heavy. For details on how to enable output caching for an endpoint, see the documentation at http://mng.bz/Bmlv.
The real power of action filters comes when you build filters tailored to your own apps by extracting common code from your action methods. To demonstrate, I’m going to create two custom filters for RecipeApiController:
• ValidateModelAttribute—This will return BadRequestResult if the model state indicates that the binding model is invalid and will short-circuit the action execution. This attribute used to be a staple of my web API applications, but the [ApiController] attribute now handles this (and more) for you. Nevertheless, I think it’s useful to understand what’s going on behind the scenes.
• EnsureRecipeExistsAttribute—This uses each action method’s id argument to validate that the requested Recipe entity exists before the action method runs. If the Recipe doesn’t exist, the filter returns NotFoundResult and short-circuits the pipeline.
As you saw in chapter 16, the MVC framework automatically validates your binding models before executing your actions and Razor Page handlers, but it’s up to you to decide what to do about it. For web API controllers, it’s common to return a 400 Bad Request response containing a list of the errors, as shown in figure 22.1.
Figure 22.1 Posting data to a web API using Postman. The data is bound to the action method’s binding model and validated. If validation fails, it’s common to return a 400 Bad Request response with a list of the validation errors.
You should ordinarily use the [ApiController] attribute on your web API controllers, which gives you this behavior (and uses Problem Details responses) automatically. But if you can’t or don’t want to use that attribute, you can create a custom action filter instead. The following listing shows a basic implementation that is similar to the behavior you get with the [ApiController] attribute.
Listing 22.5 The action filter for validating ModelState
public class ValidateModelAttribute : ActionFilterAttribute #A
{
public override void OnActionExecuting( #B
ActionExecutingContext context) #B
{
if (!context.ModelState.IsValid) #C
{
context.Result = #D
new BadRequestObjectResult(context.ModelState); #D
}
}
}
❶ For convenience, you derive from the ActionFilterAttribute base class.
❷ Overrides the Executing method to run the filter before the Action executes
❸ Model binding and validation have already run at this point, so you can check the state.
❹ If the model isn’t valid, sets the Result property, which short-circuits the action execution
This attribute is self-explanatory and follows a similar pattern to the resource filter in section 22.1.2, but with a few interesting points:
• I have derived from the abstract ActionFilterAttribute. This class implements IActionFilter and IResultFilter, as well as their async counterparts, so you can override the methods you need as appropriate. This prevents needing to add an unused OnActionExecuted() method, but using the base class is entirely optional and a matter of preference.
• Action filters run after model binding has taken place, so context.ModelState contains the validation errors if validation failed.
• Setting the Result property on context short-circuits the pipeline. But due to the position of the action filter stage, only the action method execution and later action filters are bypassed; all the other stages of the pipeline run as though the action executed as normal.
If you apply this action filter to your RecipeApiController, you can remove this code from the start of both the action methods, as it will run automatically in the filter pipeline:
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
You’ll use a similar approach to remove the duplicate code that checks whether the id provided as an argument to the action methods corresponds to an existing Recipe entity.
The following listing shows the EnsureRecipeExistsAttribute action filter. This uses an instance of RecipeService to check whether the Recipe exists and returns a 404 Not Found if it doesn’t.
Listing 22.6 An action filter to check whether a Recipe exists
public class EnsureRecipeExistsAtribute : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext context)
{
var service = context.HttpContext.RequestServices #A
.GetService<RecipeService>(); #A
var recipeId = (int) context.ActionArguments["id"]; #B
if (!service.DoesRecipeExist(recipeId)) #C
{
context.Result = new NotFoundResult(); #D
}
}
}
❶ Fetches an instance of RecipeService from the DI container
❷ Retrieves the id parameter that will be passed to the action method when it executes
❸ Checks whether a Recipe entity with the given RecipeId exists
❹ If it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
As before, you’ve derived from ActionFilterAttribute for simplicity and overridden the OnActionExecuting method. The main functionality of the filter relies on the DoesRecipeExist() method of RecipeService, so the first step is to obtain an instance of RecipeService. The context parameter provides access to the HttpContext for the request, which in turn lets you access the DI container and use RequestServices.GetService() to return an instance of RecipeService.
Warning This technique for obtaining dependencies is known as service location and is generally considered to be an antipattern. In section 22.3 I’ll show you a better way to use the DI container to inject dependencies into your filters.
As well as RecipeService, the other piece of information you need is the id argument of the Get and Edit action methods. In action filters, model binding has already occurred, so the arguments that the framework will use to execute the action method are already known and are exposed on context.ActionArguments.
The action arguments are exposed as Dictionary<string, object>
, so you can obtain the id parameter using the "id" string key. Remember to cast the object to the correct type.
Tip Whenever I see magic strings like this, I always try to replace them by using the nameof operator. Unfortunately, nameof won’t work for method arguments like this, so be careful when refactoring your code. I suggest explicitly applying the action filter to the action method (instead of globally, or to a controller) to remind you about that implicit coupling.
With RecipeService and id in place, it’s a case of checking whether the identifier corresponds to an existing Recipe entity and if not, setting context.Result to NotFoundResult. This short-circuits the pipeline and bypasses the action method altogether.
NOTE Remember that you can have multiple action filters running in a single stage. Short-circuiting the pipeline by setting context.Result prevents later filters in the stage from running and bypasses the action method execution.
Before we move on, it’s worth mentioning a special case for action filters. The ControllerBase base class implements IActionFilter and IAsyncActionFilter itself. If you find yourself creating an action filter for a single controller and want to apply it to every action in that controller, you can override the appropriate methods on your controller instead, as in the following listing.
Listing 22.7 Overriding action filter methods directly on ControllerBase
public class HomeController : ControllerBase #A
{
public override void OnActionExecuting( #B
ActionExecutingContext context) #B
{ } #B
public override void OnActionExecuted( #C
ActionExecutedContext context) #C
{ } #C
}
❶ Derives from the ControllerBase class
❷ Runs before any other action filters for every action in the controller
❸ Runs after all other action filters for every action in the controller
If you override these methods on your controller, they’ll run in the action filter stage of the filter pipeline for every action on the controller. The OnActionExecuting method runs before any other action filters, regardless of ordering or scope, and the OnActionExecuted method runs after all other action filters.
Tip The controller implementation can be useful in some cases, but you can’t control the ordering related to other filters. Personally, I generally prefer to break logic into explicit, declarative filter attributes, but it depends on the situation, and as always, the choice is yours.
With the resource and action filters complete, your controller is looking much tidier, but there’s one aspect in particular that would be nice to remove: the exception handling. In the next section we’ll look at how to create a custom exception filter for your controller and why you might want to do this instead of using exception handling middleware.
22.1.4 Exception filters: Custom exception handling for your action methods
In chapter 4 I went into some depth about types of error-handling middleware you can add to your apps. These let you catch exceptions thrown from any later middleware and handle them appropriately. If you’re using exception handling middleware, you may be wondering why we need exception filters at all.
The answer to this is pretty much the same as I outlined in chapter 21: filters are great for cross-cutting concerns, when you need behavior that’s specific to MVC or that should only apply to certain routes.
Both of these can apply in exception handling. Exception filters are part of the MVC framework, so they have access to the context in which the error occurred, such as the action or Razor Page that was executing. This can be useful for logging additional details when errors occur, such as the action parameters that caused the error.
Warning If you use exception filters to record action method arguments, make sure you’re not storing sensitive data in your logs, such as passwords or credit card details.
You can also use exception filters to handle errors from different routes in different ways. Imagine you have both Razor Pages and web API controllers in your app, as we do in the recipe app. What happens when an exception is thrown by a Razor Page?
As you saw in chapter 4, the exception travels back up the middleware pipeline and is caught by exception handler middleware. The exception handler middleware reexecutes the pipeline and generates an HTML error page.
That’s great for your Razor Pages, but what about exceptions in your web API controllers? If your API throws an exception and consequently returns HTML generated by the exception handler middleware, that’s going to break a client that called the API expecting a JavaScript Object Notation (JSON) response!
Tip The added complexity introduced by having to handle these two very different clients is the reason I prefer to create separate applications for APIs and server-rendered apps.
Instead, exception filters let you handle the exception in the filter pipeline and generate an appropriate response body for API clients. The exception handler middleware intercepts only errors without a body, so it will let the modified web API response pass untouched.
NOTE The [ApiController] attribute converts error StatusCodeResults to a ProblemDetails object, but it doesn’t catch exceptions.
Exception filters can catch exceptions from more than your action methods and page handlers. They’ll run if an exception occurs at these times:
• During model binding or validation
• When the action method or page handler is executing
• When an action filter or page filter is executing
You should note that exception filters won’t catch exceptions thrown in any filters other than action and page filters, so it’s important that your resource and result filters don’t throw exceptions. Similarly, they won’t catch exceptions thrown when executing an IActionResult, such as when rendering a Razor view to HTML.
Now that you know why you might want an exception filter, go ahead and implement one for RecipeApiController, as shown next. This lets you safely remove the try-catch block from your action methods, knowing that your filter will catch any errors.
Listing 22.8 The HandleExceptionAttribute exception filter
public class HandleExceptionAttribute : ExceptionFilterAttribute #A
{
public override void OnException(ExceptionContext context) #B
{
var error = new ProblemDetails #C
{ #C
Title = "An error occurred", #C
Detail = context.Exception.Message, #C
Status = 500, #C
Type = " https://httpwg.org/specs/rfc9110.html#status.500" #C
}; #C
context.Result = new ObjectResult(error) #D
{ #D
StatusCode = 500 #D
}; #D
context.ExceptionHandled = true; #E
}
}
❶ ExceptionFilterAttribute is an abstract base class that implements
IExceptionFilter.
❷ There’s only a single method to override for IExceptionFilter.
❸ Building a problem details object to return in the response
❹ Creates an ObjectResult to serialize the ProblemDetails and to set the response status code
❺ Marks the exception as handled to prevent it propagating into the middleware pipeline
It’s quite common to have an exception filter in your application if you are mixing API controllers and Razor Pages in your application, but they’re not always necessary. If you can handle all the exceptions in your application with a single piece of middleware, ditch the exception filters and go with that instead.
You’re almost done refactoring your RecipeApiController. You have one more filter type to add: result filters. Custom result filters tend to be relatively rare in the apps I’ve written, but they have their uses, as you’ll see.
22.1.5 Result filters: Customizing action results before they execute
If everything runs successfully in the pipeline, and there’s no short-circuiting, the next stage of the pipeline after action filters is result filters. These run before and after the IActionResult returned by the action method (or action filters) is executed.
Warning If the pipeline is short-circuited by setting context.Result, the result filter stage won’t run, but the IActionResult will still be executed to generate the response. The exceptions to this rule are action and page filters, which only short-circuit the action execution, as you saw in chapter 21. Result filters run as normal, as though the action or page handler itself generated the response.
Result filters run immediately after action filters, so many of their use cases are similar, but you typically use result filters to customize the way the IActionResult executes. For example, ASP.NET Core has several result filters built into its framework:
• ProducesAttribute—This forces a web API result to be serialized to a specific output format. For example, decorating your action method with [Produces ("application/xml")] forces the formatters to try to format the response as XML, even if the client doesn’t list XML in its Accept header.
• FormatFilterAttribute—Decorating an action method with this filter tells the formatter to look for a route value or query string parameter called format and to use that to determine the output format. For example, you could call /api/recipe/11?format=json and FormatFilter will format the response as JSON or call api/recipe/11?format=xml and get the response as XML.
NOTE Remember that you need to explicitly configure the XML formatters if you want to serialize to XML, as described in chapter 20. For details on formatting results based on the URL, see my blog entry on the topic: http://mng.bz/1rYV.
As well as controlling the output formatters, you can use result filters to make any last-minute adjustments before IActionResult is executed and the response is generated.
As an example of the kind of flexibility available, in the following listing I demonstrate setting the LastModified header, based on the object returned from the action. This is a somewhat contrived example—it’s specific enough to a single action that it likely doesn’t warrant being moved to a result filter—but I hope you get the idea.
Listing 22.9 Setting a response header in a result filter
public class AddLastModifedHeaderAttribute : ResultFilterAttribute #A
{
public override void OnResultExecuting( #B
ResultExecutingContext context) #B
{
if (context.Result is OkObjectResult result #C
&& result.Value is RecipeDetailViewModel detail) #D
{
var viewModelDate = detail.LastModified; #E
context.HttpContext.Response #E
.GetTypedHeaders().LastModified = viewModelDate; #E
}
}
}
❶ ResultFilterAttribute provides a useful base class you can override.
❷ You could also override the Executed method, but the response would already be sent by then.
❸ Checks whether the action result returned a 200 Ok result with a view model.
❹ Checks whether the view model type is RecipeDetailViewModel . . .
❺ . . . and if it is, fetches the LastModified property and sets the Last-Modified header in the response
I’ve used another helper base class here, ResultFilterAttribute, so you need to override only a single method to implement the filter. Fetch the current IActionResult, exposed on context.Result, and check that it’s an OkObjectResult instance with a RecipeDetailViewModel value. If it is, fetch the LastModified field from the view model and add a Last-Modified header to the response.
Tip GetTypedHeaders() is an extension method that provides strongly typed access to request and response headers. It takes care of parsing and formatting the values for you. You can find it in the Microsoft.AspNetCore.Http namespace.
As with resource and action filters, result filters can implement a method that runs after the result has executed: OnResultExecuted. You can use this method, for example, to inspect exceptions that happened during the execution of IActionResult.
Warning Generally, you can’t modify the response in the OnResultExecuted method, as you may have already started streaming the response to the client.
We’ve finished simplifying the RecipeApiController now. By extracting various pieces of functionality to filters, the original controller in listing 22.1 has been simplified to the version in listing 22.2. This is obviously a somewhat extreme and contrived demonstration, and I’m not advocating that filters should always be your go-to option.
Tip Filters should be a last resort in most cases. Where possible, it is often preferable to use a simple private method in a controller, or to push functionality into the domain instead of using filters. Filters should generally be used to extract repetitive, HTTP-related, or common cross-cutting code from your controllers.
There’s still one more filter we haven’t looked at yet, because it applies only to Razor Pages: page filters.
22.1.6 Page filters: Customizing model binding for Razor Pages
As already discussed, action filters apply only to controllers and actions; they have no effect on Razor Pages. Similarly, page filters have no effect on controllers and actions. Nevertheless, page filters and action filters fulfill similar roles.
As is the case for action filters, the ASP.NET Core framework includes several page filters out of the box. One of these is the Razor Page equivalent of the caching action filter, ResponseCacheFilter, called PageResponseCacheFilter. This works identically to the action-filter equivalent I described in section 22.1.3, setting HTTP caching headers on your Razor Page responses.
Page filters are somewhat unusual, as they implement three methods, as discussed in section 22.1.2. In practice, I’ve rarely seen a page filter that implements all three. It’s unusual to need to run code immediately after page handler selection and before model validation. It’s far more common to perform a role directly analogous to action filters. The following listing shows a page filter equivalent to the EnsureRecipeExistsAttribute action filter.
Listing 22.10 A page filter to check whether a Recipe exists
public class PageEnsureRecipeExistsAttribute : Attribute, IPageFilter #A
{
public void OnPageHandlerSelected( #B
PageHandlerSelectedContext context) #B
{} #B
public void OnPageHandlerExecuting( #C
PageHandlerExecutingContext context) #C
{
var service = context.HttpContext.RequestServices #D
.GetService<RecipeService>(); #D
var recipeId = (int) context.HandlerArguments["id"]; #E
if (!service.DoesRecipeExist(recipeId)) #F
{
context.Result = new NotFoundResult(); #G
}
}
public void OnPageHandlerExecuted( #H
PageHandlerExecutedContext context) #H
{ } #H
}
❶ Implements IPageFilter and as an attribute so you can decorate the Razor Page PageModel
❷ Executed after handler selection and before model binding—not used in this example
❸ Executed after model binding and validation, and before page handler
execution
❹ Fetches an instance of RecipeService from the DI container
❺ Retrieves the id parameter that will be passed to the page handler method when it executes
❻ Checks whether a Recipe entity with the given RecipeId exists . . .
❼ . . . and if it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
❽ Executed after page handler execution (or short-circuiting)—not used in this example
The page filter is similar to the action filter equivalent. The most obvious difference is the need to implement three methods to satisfy the IPageFilter interface. You’ll commonly want to implement the OnPageHandlerExecuting method, which runs after model binding and validation, and before the page handler executes.
A subtle difference between the action filter code and the page filter code is that the action filter accesses the model-bound action arguments using context.ActionArguments. The page filter uses context.HandlerArguments in the example, but there’s also another option.
Remember from chapter 16 that Razor Pages often bind to public properties on the PageModel using the [BindProperty] attribute. You can access those properties directly instead of using magic strings by casting a HandlerInstance property to the correct PageModel type and accessing the property directly, as in this example:
var recipeId = ((ViewRecipePageModel)context.HandlerInstance).Id
This is similar to the way the ControllerBase class implements IActionFilter and PageModel implements IPageFilter and IAsyncPageFilterT. If you want to create an action filter for a single Razor Page, you could save yourself the trouble of creating a separate page filter and override these methods directly in your Razor Page.
Tip I generally find it’s not worth the hassle of using page filters unless you have a common requirement. The extra level of indirection that page filters add, coupled with the typically bespoke nature of individual Razor Pages, means that I normally find they aren’t worth using. Your mileage may vary, of course, but don’t jump to them as a first option.
That brings us to the end of this detailed look at each of the filters in the MVC pipeline. Looking back and comparing listings 22.1 and 22.2, you can see filters allowed us to refactor the controllers and make the intent of each action method much clearer. Writing your code in this way makes it easier to reason about, as each filter and action has a single responsibility.
In the next section we’ll take a slight detour into exactly what happens when you short-circuit a filter. I’ve described how to do this, by setting the context.Result property on a filter, but I haven’t described exactly what happens. For example, what if there are multiple filters in the stage when it’s short-circuited? Do those still run?
22.2 Understanding pipeline short-circuiting
In this short section you’ll learn about the details of filter-pipeline short-circuiting. You’ll see what happens to the other filters in a stage when the pipeline is short-circuited and how to short-circuit each type of filter.
A brief warning: the topic of filter short-circuiting can be a little confusing. Unlike middleware short-circuiting, which is cut-and-dried, the filter pipeline is a bit more nuanced. Luckily, you won’t often need to dig into it, but when you do, you’ll be glad for the detail.
You short-circuit the authorization, resource, action, page, and result filters by setting context.Result to IActionResult. Setting an action result in this way causes some or all of the remaining pipeline to be bypassed. But the filter pipeline isn’t entirely linear, as you saw in chapter 21, so short-circuiting doesn’t always do an about-face back down the pipeline. For example, short-circuited action filters bypass only action method execution; the result filters and result execution stages still run.
The other difficultly is what happens if you have more than one filter in a stage. Let’s say you have three resource filters executing in a pipeline. What happens if the second filter causes a short circuit? Any remaining filters are bypassed, but the first resource filter has already run its Executing command, as shown in figure 22.2. This earlier filter gets to run its Executed command too, with context.Cancelled = true, indicating that a filter in that stage (the resource filter stage) short-circuited the pipeline.
Figure 22.2 The effect of short-circuiting a resource filter on other resource filters in that stage. Later filters in the stage won’t run at all, but earlier filters run their OnResourceExecuted function.
Running result filters after short-circuits with IAlwaysRunResultFilter
Result filters are designed to wrap the execution of an IActionResult returned by an action method or action filter so that you can customize how the action result is executed. However, this customization doesn’t apply to the IActionResult set when you short-circuit the filter pipeline by setting context.Result in an authorization filter, resource filter, or exception filter.That’s often not a problem, as many result filters are designed to handle “happy path” transformations. But sometimes you want to make sure that a transformation is always applied to an IActionResult, regardless of whether it was returned by an action method or a short-circuiting filter.
For those cases, you can implement IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter. These interfaces extend (and are identical) to the standard result filter interfaces, so they run like normal result filters in the filter pipeline. But these interfaces mark the filter to also run after an authorization filter, resource filter, or exception filter short-circuits the pipeline, where standard result filters won’t run.
You can use IAlwaysRunResultFilter to ensure that certain action results are always updated. For example, the documentation shows how to use an IAlwaysRunResultFilter to convert a 415 StatusCodeResult to a 422 StatusCodeResult, regardless of the source of the action result. See the “IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter” section of Microsoft’s “Filters in ASP.NET Core” documentation: http://mng.bz/JDo0.
Understanding which other filters run when you short-circuit a filter can be somewhat of a chore, but I’ve summarized each filter in table 22.1. You’ll also find it useful to refer to the pipeline diagrams in chapter 21 to visualize the shape of the pipeline when thinking about short circuits.
Table 22.1 The effect of short-circuiting filters on filter-pipeline execution
Filter type | How to short-circuit? | What else runs? |
---|---|---|
Authorization filters | Set context.Result. | Runs only IAlwaysRunResultFilters. |
Resource filters | Set context.Result. | Resource-filter *Executed functions from earlier filters run with context.Cancelled = true. Runs IAlwaysRunResultFilters before executing the IActionResult. |
Action filters | Set context.Result. | Bypasses only action method execution. Action filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal. |
Page filters | Set context.Result in OnPageHandlerSelected. | Bypasses only page handler execution. Page filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal. |
Exception filters | Set context.Result and Exception.Handled = true. | All resource-filter *Executed functions run. Runs IAlwaysRunResultFilters before executing the IActionResult. |
Result filters | Set context.Cancelled = true. | Result filters earlier in the pipeline run their Executed functions with context.Cancelled = true. All resource-filter Executed functions run as normal. |
The most interesting point here is that short-circuiting an action filter (or a page filter) doesn’t short-circuit much of the pipeline at all. In fact, it bypasses only later action filters and the action method execution itself. By building primarily action filters, you can ensure that other filters, such as result filters that define the output format, run as usual, even when your action filters short-circuit.
The last thing I’d like to talk about in this chapter is how to use DI with your filters. You saw in chapters 8 and 9 that DI is integral to ASP.NET Core, and in the next section you’ll see how to design your filters so that the framework can inject service dependencies into them for you.
22.3 Using dependency injection with filter attributes
In this section you’ll learn how to inject services into your filters so you can take advantage of the simplicity of DI in your filters. You’ll learn to use two helper filters to achieve this, TypeFilterAttribute and ServiceFilterAttribute, and you’ll see how they can be used to simplify the action filter you defined in section 22.1.3.
The filters we’ve created so far have been created as attributes. This is useful for applying filters to action methods and controllers, but it means you can’t use DI to inject services into the constructor. C# attributes don’t let you pass dependencies into their constructors (other than constant values), and they’re created as singletons, so there’s only a single instance of an attribute for the lifetime of your app. So what happens if you need to access a transient or scoped service from inside the singleton attribute?
Listing 22.6 showed one way of doing this, using a pseudo-service locator pattern to reach into the DI container and pluck out RecipeService at runtime. This works but is generally frowned upon as a pattern in favor of proper DI. So how can you add DI to your filters?
The key is to split the filter in two. Instead of creating a class that’s both an attribute and a filter, create a filter class that contains the functionality and an attribute that tells the framework when and where to use the filter.
Let’s apply this to the action filter from listing 22.6. Previously, I derived from ActionFilterAttribute and obtained an instance of RecipeService from the context passed to the method. In the following listing I show two classes, EnsureRecipeExistsFilter and EnsureRecipeExistsAttribute. The filter class is responsible for the functionality and takes in RecipeService as a constructor dependency.
Listing 22.11 Using DI in a filter by not deriving from Attribute
public class EnsureRecipeExistsFilter : IActionFilter #A
{
private readonly RecipeService _service; #B
public EnsureRecipeExistsFilter(RecipeService service) #B
{ #B
_service = service; #B
} #B
public void OnActionExecuting(ActionExecutingContext context) #C
{ #C
var recipeId = (int) context.ActionArguments["id"]; #C
if (!_service.DoesRecipeExist(recipeId)) #C
{ #C
context.Result = new NotFoundResult(); #C
} #C
} #C
public void OnActionExecuted(ActionExecutedContext context) { } #D
}
public class EnsureRecipeExistsAttribute : TypeFilterAttribute #E
{
public EnsureRecipeExistsAttribute() #F
: base(typeof(EnsureRecipeExistsFilter)) {} #F
}
❶ Doesn’t derive from an Attribute class
❷ RecipeService is injected into the constructor.
❸ The rest of the method remains the same.
❹ You must implement the Executed action to satisfy the interface.
❺ Derives from TypeFilter, which is used to fill dependencies using the DI container
❻ Passes the type EnsureRecipeExistsFilter as an argument to the base TypeFilter constructor
EnsureRecipeExistsFilter is a valid filter; you could use it on its own by adding it as a global filter (as global filters don’t need to be attributes). But you can’t use it directly by decorating controller classes and action methods, as it’s not an attribute. That’s where EnsureRecipeExistsAttribute comes in.
You can decorate your methods with EnsureRecipeExistsAttribute instead. This attribute inherits from TypeFilterAttribute and passes the Type of filter to create as an argument to the base constructor. This attribute acts as a factory for EnsureRecipeExistsFilter by implementing IFilterFactory.
When ASP.NET Core initially loads your app, it scans your actions and controllers, looking for filters and filter factories. It uses these to form a filter pipeline for every action in your app, as shown in figure 22.3.
Figure 22.3 The framework scans your app on startup to find both filters and attributes that implement IFilterFactory. At runtime, the framework calls CreateInstance() to get an instance of the filter
When an action decorated with EnsureRecipeExistsAttribute is called, the framework calls CreateInstance() on the IFilterFactory attribute. This creates a new instance of EnsureRecipeExistsFilter and uses the DI container to populate its dependencies (RecipeService).
By using this IFilterFactory approach, you get the best of both worlds: you can decorate your controllers and actions with attributes, and you can use DI in your filters. Out of the box, two similar classes provide this functionality, which have slightly different behaviors:
• TypeFilterAttribute—Loads all the filter’s dependencies from the DI container and uses them to create a new instance of the filter.
• ServiceFilterAttribute—Loads the filter itself from the DI container. The DI container takes care of the service lifetime and building the dependency graph. Unfortunately, you must also explicitly register your filter with the DI container:
builder.Services.AddTransient<EnsureRecipeExistsFilter>();
Tip You can register your services with any lifetime you choose. If your service is registered as a singleton, you can consider setting the IsReusable flag, as described in the documentation: http://mng.bz/d1JD.
If you choose to use ServiceFilterAttribute instead of TypeFilterAttribute, and register the EnsureRecipeExistsFilter as a service in the DI container, you can apply the ServiceFilterAttribute directly to an action method:
[ServiceFilter(typeof(EnsureRecipeExistsFilter))]
public IActionResult Index() => Ok();
Whether you choose to use TypeFilterAttribute or ServiceFilterAttribute is somewhat a matter of preference, and you can always implement a custom IFilterFactory if you need to. The key takeaway is that you can now use DI in your filters. If you don’t need to use DI for a filter, implement it as an attribute directly, for simplicity.
Tip I like to create my filters as a nested class of the attribute class when using this pattern. This keeps all the code nicely contained in a single file and indicates the relationship between the classes.
That brings us to the end of this chapter on the filter pipeline. Filters are a somewhat advanced topic, in that they aren’t strictly necessary for building basic apps, but I find them extremely useful for ensuring that my controller and action methods are simple and easy to understand.
In the next chapter we’ll take our first look at securing your app. We’ll discuss the difference between authentication and authorization, the concept of identity in ASP.NET Core, and how you can use the ASP.NET Core Identity system to let users register and log in to your app.
22.4 Summary
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.
ASP.NET Core includes many built-in filters, but you can also create custom filters tailored to your application. You can use custom filters to extract common cross-cutting functionality out of your MVC controllers and Razor Pages, reducing duplication and ensuring consistency across your endpoints.
Authorization filters run first in the pipeline and control access to APIs. ASP.NET Core includes an [Authorization] attribute that you can apply to action methods so that only logged-in users can execute the action.
Resource filters run after authorization filters and again after an IActionResult has been executed. They can be used to short-circuit the pipeline so that an action method is never executed. They can also be used to customize the model-binding process for an action method.
Action filters run after model binding has occurred and before an action method executes. They also run after the action method has executed. They can be used to extract common code out of an action method to prevent duplication. They don’t execute for Razor Pages, only for MVC controllers.
The ControllerBase base class also implements IActionFilter and IAsyncActionFilter. They run at the start and end of the action filter pipeline, regardless of the ordering or scope of other action filters. They can be used to create action filters that are specific to one controller.
Page filters run three times: after page handler selection, after model binding, and after the page handler method executes. You can use page filters for similar purposes as action filters. Page filters execute only for Razor Pages; they don’t run for MVC controllers.
Razor Page PageModels implement IPageFilter and IAsyncPageFilter, so they can be used to implement page-specific page filters. These are rarely used, as you can typically achieve similar results with simple private methods.
Exception filters execute after action and page filters, when an action method or page handler has thrown an exception. They can be used to provide custom error handling specific to the action executed.
Generally, you should handle exceptions at the middleware level, but you can use exception filters to customize how you handle exceptions for specific actions, controllers, or Razor Pages.
Result filters run before and after an IActionResult is executed. You can use them to control how the action result is executed or to completely change the action result that will be executed.
All filters can short-circuit the pipeline by setting a response. This generally prevents the request progressing further in the filter pipeline, but the exact behavior varies with the type of filter that is short-circuited.
Result filters aren’t executed when you short-circuit the pipeline using authorization, resource, or exception filters. You can ensure that result filters also run for these short-circuit cases by implementing a result filter as IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter.
You can use ServiceFilterAttribute and TypeFilterAttribute to allow dependency injection in your custom filters. ServiceFilterAttribute requires that you register your filter and all its dependencies with the DI container, whereas TypeFilterAttribute requires only that the filter’s dependencies have been registered.