Understanding ASP.NET Core - Filters

Posted by dmcentire on Tue, 30 Nov 2021 03:24:44 +0100

Note: This article is part of the understanding ASP.NET Core series. Please check the top blog or Click here to view the full-text catalog

Filter overview

If you came all the way from ASP.NET, you must be no stranger to Filter. Of course, the ASP.NET Core still inherits the Filter mechanism.

The filter runs in the filter pipeline. This is an official diagram, which well explains the position of the filter pipeline in the HTTP request pipeline:

It can be seen that the filter pipeline has the opportunity to execute only after MVC Action is selected in the route.

There is more than one filter, but there are many types. In order to make it easier for you to understand the execution sequence of each filter, I have changed the official magic diagram as follows:

  • Authorization Filters: this filter is located at the top of all filters and is executed first. The authorization filter is used to confirm whether the requesting user has been authorized. If not, the pipeline can be short circuited to prohibit the request from continuing to pass.
  • Resource Filters: after authorization is passed, custom logic can be executed before and after other stages of the filter pipeline (such as model binding)
  • Action Filters: execute custom logic before and after calling action. Through the action filter, you can modify the parameters to be passed into the action, or set or modify the return results of the action. In addition, it can first catch and handle the unhandled exception thrown in the action.
  • Exception Filters: when unhandled exceptions are thrown during Controller creation, model binding, Action Filters and Action execution, the exception filter can catch and handle them. Note that the response body has not been written before, which means you can set the return result.
  • Result Filters: the result filter is executed only when the execution of the Action does not throw an exception or the Action Filter handles an exception, allowing you to execute custom logic before and after the execution of the Action result.

There are a lot of things. You have to bear it. After reading the following detailed introduction, it is easy to understand by reviewing the above.

These filters all implement the IFilterMetadata interface, which does not contain any behavior and is only used to mark that it is a filter in the MVC request pipeline.

In addition, such as Resource Filters, Action Filters and Result Filters, they have two behaviors, which are executed before and after the pipeline stage. According to custom, the former is named OnXXXing, such as OnActionExecuting, and the latter is named OnXXXExecuted, such as OnActionExecuted

Scope and registration method of filter

Because there are many kinds of filters, in order to facilitate everyone to learn and test, first introduce the scope and registration method of the filter.

Scope and execution order of the filter

Similarly, before introducing the filter, let's introduce the scope and execution order of the filter.

The scope of filters can be divided into three types, from small to large:

  • On an Action in a Controller (the processing method in Razor Page is not supported)
  • On a Controller or Razor Page
  • Global, applied to all controllers, actions and razor pages

The execution order of different filters, we pass The picture above You can clearly know, but what is the execution order for the same type of filters with different scopes?

Taking IActionFilter as an example, the execution order is:

  • OnActionExecuting for global filter
    • OnActionExecuting for Controller and Razor Page filters
      • OnActionExecuting for Action filter
      • OnActionExecuted for Action filter
    • OnActionExecuted for Controller and Razor Page filters
  • OnActionExecuted for global filter

That is, for the same type of filters with different scopes, the execution order is from the scope range to the small, and then from the small to the large

Registration method of filter

Next, let's see how to register the filter as a different scope:

overall situation

It is easy to register as global. You can directly configure MvcOptions.Filters:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => options.Filters.Add<MyFilter>());
    // or
    services.AddControllers(options => options.Filters.Add<MyFilter>());
    // or
    services.AddControllersWithViews(options => options.Filters.Add<MyFilter>());
}

Controller, Razor Page or Action

The scope is Controller, Razor Page or Action. In fact, the registration methods are similar, and they are marked in the way of characteristics.

In the simplest way, the filter constructor has no parameters or these parameters do not need to be provided by DI. At this time, only the filter inherits the Attribute:

class MyFilterAttribute : Attribute, IActionFilter { }

[MyFilter]
public class HomeController : Controller { }

On the other hand, the constructor parameters of the filter need to be provided by DI. In this case, the ServiceFilterAttribute is required:

class MyFilter :IActionFilter
{
    public MyFilter(IWebHostEnvironment env) { }
}

public void ConfigureServices(IServiceCollection services)
{
    // Add filter to DI container
    services.AddScoped<MyFilter>();
}

[ServiceFilter(typeof(MyFilter))]
public class HomeController : Controller { }

How does ServiceFilterAttribute create instances of this type of filter? Look at its structure and you will understand:

public interface IFilterFactory : IFilterMetadata
{
    // Can filter instances be reused across requests
    bool IsReusable { get; }

    // Create an instance of the specified filter type through IServiceProvider
    IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}

public class ServiceFilterAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    // Type is the type of filter to create
    public ServiceFilterAttribute(Type type) 
    {
        ServiceType = type ?? throw new ArgumentNullException(nameof(type)); 
    }

    public int Order { get; set; }

    // Gets the type of the filter, which is passed in the constructor
    public Type ServiceType { get; }

    // Whether the filter instance can be reused across requests. The default is false
    public bool IsReusable { get; set; }

    // Create an instance of the specified filter type through IServiceProvider.GetRequiredService
    // Therefore, the filter and constructor parameters are required to be registered in the DI container
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 
    {
        var filter = (IFilterMetadata)serviceProvider.GetRequiredService(ServiceType);
        if (filter is IFilterFactory filterFactory)
        {
            // Expand IFilterFactory
            filter = filterFactory.CreateInstance(serviceProvider);
        }

        return filter;
    }
}

If you want the filter instance to be reused outside its scope, you can achieve the goal by specifying IsReusable = true. Note that the service life cycle on which the filter depends must be single instance. In addition, this does not guarantee that the filter instance is a single instance, or multiple instances may occur.

Well, the last and most complex is that the constructor part of the filter does not need to be provided by DI, and the other part needs to be provided by DI. At this time, TypeFilterAttribute is needed:

class MyFilter : IActionFilter
{
    // The first parameter, caller, is not provided through DI
    // The second parameter env is provided through DI
    public MyFilter(string caller, IWebHostEnvironment env) { }
}

// ... note that there is no need to register MyFilter to the DI container. Remember to delete the registration code

// The parameters stored in Arguments are parameters that do not need to be provided by DI
[TypeFilter(typeof(MyFilter), 
    Arguments = new object[] { "HomeController" })]
public class HomeController : Controller { }

Similarly, take a look at the structure of TypeFilterAttribute:

public class TypeFilterAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    private ObjectFactory _factory;

    // Type is the type of filter to create
    public TypeFilterAttribute(Type type) 
    { 
        ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
    }

    // Non DI container supplied parameters to pass to the filter constructor
    public object[] Arguments { get; set; }

    // Gets the type of the filter, which is passed in the constructor
    public Type ImplementationType { get; }

    public int Order { get; set; }

    public bool IsReusable { get; set; }

    // Creates an instance of the specified filter type through ObjectFactory
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 
    { 
        if (_factory == null)
        {
            var argumentTypes = Arguments?.Select(a => a.GetType())?.ToArray();
            _factory = ActivatorUtilities.CreateFactory(ImplementationType, argumentTypes ?? Type.EmptyTypes);
        }

        var filter = (IFilterMetadata)_factory(serviceProvider, Arguments);
        if (filter is IFilterFactory filterFactory)
        {
            // Expand IFilterFactory
            filter = filterFactory.CreateInstance(serviceProvider);
        }

        return filter;
    }
}

Filter context

The behavior in the filter will have a context parameter. These context parameters inherit from the abstract class FilterContext, and the FilterContext inherits from ActionContext (this also explains from the side that the filter serves Action):

public class ActionContext
{
    // Action related information
    public ActionDescriptor ActionDescriptor { get; set; }

    // HTTP context
    public HttpContext HttpContext { get; set; }

    // Model binding and validation
    public ModelStateDictionary ModelState { get; }

    // Routing data
    public RouteData RouteData { get; set; }
}

public abstract class FilterContext : ActionContext
{
    public virtual IList<IFilterMetadata> Filters { get; }

    public bool IsEffectivePolicy<TMetadata>(TMetadata policy) where TMetadata : IFilterMetadata {}

    public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata {}
}

When we customize a filter, we have to interact with the context. Therefore, it is indispensable to understand the structure of the context. Here are two important parameters to explore.

Let's first look at ActionDescriptor, which contains information related to Action:

public class ActionDescriptor
{
    // The unique identifier identifying the Action is actually a Guid
    public string Id { get; }

    // Routing dictionary, including the names of controller and action, etc
    public IDictionary<string, string> RouteValues { get; set; }

    // Information about characteristic routing
    public AttributeRouteInfo? AttributeRouteInfo { get; set; }

    // Constraint list for Action
    public IList<IActionConstraintMetadata>? ActionConstraints { get; set; }

    // We generally don't use endpoint metadata
    public IList<object> EndpointMetadata { get; set; }

    // The parameter list in the route, including parameter name, parameter type, binding information, etc
    public IList<ParameterDescriptor> Parameters { get; set; }

    public IList<ParameterDescriptor> BoundProperties { get; set; }

    // List of filters related to the current Action in the filter pipeline
    public IList<FilterDescriptor> FilterDescriptors { get; set; }

    // Personalized name of Action
    public virtual string? DisplayName { get; set; }

    // Shared metadata
    public IDictionary<object, object> Properties { get; set; }
}

The following HttpContext is too large. But you should know that with it, you can do what you want for requests and responses.

The next step is ModelState, which is used to verify the model binding. Through it, you can know whether the model binding is successful or not, and you can also get the verification information of binding failure. Relevant details will be introduced in the subsequent articles on model binding.

Then there is RouteData. Obviously, it stores routing related information. Let's see what it includes:

public class RouteData
{
    // The data mark generated by the route on the current route path
    public RouteValueDictionary DataTokens { get; }

    // List of instances of Microsoft.AspNetCore.Routing.IRouter
    public IList<IRouter> Routers { get; }

    // Route value, including the data in ActionDescriptor.RouteValues
    public RouteValueDictionary Values { get; }
}

Later, I will come to Filters. I believe you have guessed when you see IFilterMetadata, which represents the filter list related to the current Action in the filter pipeline.

Authorization Filters

Authorization filter is the first executed filter in the filter pipeline for system authorization. In general, you will not write custom authorization filters, but configure authorization policies or write custom authorization policies. The details will be introduced in subsequent articles.

Resource Filters

The resource filter is executed after the authorization filter is executed. The filter includes "before" and "after" behaviors, including model binding, Action filter, Action execution, exception filter, result filter and result execution.

By implementing IResourceFilter or IAsyncResourceFilter interfaces:

public interface IResourceFilter : IFilterMetadata
{
    void OnResourceExecuting(ResourceExecutingContext context);

    void OnResourceExecuted(ResourceExecutedContext context);
}

public interface IAsyncResourceFilter : IFilterMetadata
{
    Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next);
}

When the request is intercepted, you can get the context of resource information:

public class ResourceExecutingContext : FilterContext
{
    // Gets or sets the execution result of the Action
    public virtual IActionResult? Result { get; set; }

    // The Action parameter binds the source provider factory, such as Form, Route, QueryString, JQueryForm, FormFile, etc
    public IList<IValueProviderFactory> ValueProviderFactories { get; }
}

public class ResourceExecutedContext : FilterContext
{
    // Indicates whether the execution of the Action has been cancelled
    public virtual bool Canceled { get; set; }

    // If an unhandled exception is caught, it is stored here
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // Indicates whether the exception has been handled
    public virtual bool ExceptionHandled { get; set; }

    // Gets or sets the execution result of the Action
    public virtual IActionResult? Result { get; set; }
}

Similarly, once Result is set, the filter pipe can be short circuited.

For ResourceExecutedContext, there are two ways to handle exceptions:

  • Set Exception or ExceptionDispatchInfo to null
  • Set ExceptionHandled to true

Simply setting Result is not feasible. Therefore, I suggest that when handling exceptions, in addition to setting Result, you also set ExceptionHandled to true, which also makes it easier for code readers to understand the code logic.

In addition, ResourceExecutedContext.Canceled is used to indicate whether the execution of the Action has been cancelled. When ResourceExecutingContext.Result is manually set in OnResourceExecuting, canceled will be set to true. It should be noted that to test this situation, you must register at least two resource filters and set Result in the second resource filter before you can see the effect in the first filter.

Action Filters

The operation filter is executed after the model is bound. The filter also contains "before" and "after" behaviors, which wrap the execution of Action (excluding the creation of Controller).

If an exception is thrown during the execution of an Action or in a subsequent Action filter, the OnActionExecuted of the Action filter is the first to catch the exception, not the exception filter.

Implement the IActionFilter or IAsyncActionFilter interface:

public interface IActionFilter : IFilterMetadata
{
    void OnActionExecuting(ActionExecutingContext context);

    void OnActionExecuted(ActionExecutedContext context);
}

public interface IAsyncActionFilter : IFilterMetadata
{
    Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
}

Similarly, look at the context structure:

public class ActionExecutingContext : FilterContext
{
    // Gets or sets the execution result of the Action
    public virtual IActionResult? Result { get; set; }

    // The parameter Dictionary of Action. key is the parameter name and value is the parameter value
    public virtual IDictionary<string, object> ActionArguments { get; }

    // Get the Controller to which the Action belongs
    public virtual object Controller { get; }
}

public class ActionExecutedContext : FilterContext
{
    // Indicates whether the execution of the Action has been cancelled
    public virtual bool Canceled { get; set; }

    // Get the Controller to which the Action belongs
    public virtual object Controller { get; }

    // If an unhandled exception is caught, it is stored here
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // Indicates whether the exception has been handled
    public virtual bool ExceptionHandled { get; set; }

    // Gets or sets the execution result of the Action
    public virtual IActionResult Result { get; set; }
}

The ActionExecutedContext.Canceled attribute and the knowledge points related to exception handling are similar to resource filters, which will not be repeated here.

Since the operation filter is often used frequently in applications, its use is described in detail here. The ASP.NET Core framework provides an abstract class ActionFilterAttribute, which implements multiple interfaces and inherits Attribute, allowing us to use it in the way of characteristics. Therefore, it is generally recommended that you define the operation filter by inheriting this abstract class:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class ActionFilterAttribute :
    Attribute, IActionFilter, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
{
    public int Order { get; set; }

    public virtual void OnActionExecuting(ActionExecutingContext context) { }

    public virtual void OnActionExecuted(ActionExecutedContext context) { }

    public virtual async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Deleted some empty check codes

        OnActionExecuting(context);
        if (context.Result == null)
        {
            OnActionExecuted(await next());
        }
    }

    public virtual void OnResultExecuting(ResultExecutingContext context) { }

    public virtual void OnResultExecuted(ResultExecutedContext context) { }

    public virtual async Task OnResultExecutionAsync(
        ResultExecutingContext context,
        ResultExecutionDelegate next)
    {
        // Deleted some empty check codes

        OnResultExecuting(context);
        if (!context.Cancel)
        {
            OnResultExecuted(await next());
        }
    }
}

As you can see, ActionFilterAttribute implements both synchronous and asynchronous interfaces. However, when we use it, we only need to implement synchronous or asynchronous interfaces, not both. This is because the runtime will first check whether the filter implements the asynchronous interface. If so, call the asynchronous interface. Otherwise, the synchronization interface is called. If both asynchronous and synchronous interfaces are implemented in a class, only the asynchronous interface will be called.

When you want to verify the model binding state globally, it is most appropriate to use the action filter!

public class ModelStateValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            if (context.HttpContext.Request.AcceptJson())
            {
                var errorMsg = string.Join(Environment.NewLine, context.ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
                context.Result = new BadRequestObjectResult(AjaxResponse.Failed(errorMsg));
            }
            else
            {
                context.Result = new ViewResult();
            }
        }
    }
}

public static class HttpRequestExtensions
{
    public static bool AcceptJson(this HttpRequest request)
    {
        if (request == null) throw new ArgumentNullException(nameof(request));

        var regex = new Regex(@"^(\*|application)/(\*|json)$");

        return request.Headers[HeaderNames.Accept].ToString()
            .Split(',')
            .Any(type => regex.IsMatch(type));
    }
}

Exception Filters

Exception filter, which can capture unhandled exceptions thrown in Controller creation (that is, only exceptions thrown in constructor), model binding, Action Filter and Action.

Let's emphasize again: if an exception is thrown during the execution of an Action or in a non first Action filter, the OnActionExecuted of the Action filter is the first to catch the exception, not the exception filter. However, if an exception is thrown when the Controller is created, the exception filter will catch the exception first.

I know that when you first use the exception filter, some people will mistakenly think that it can catch any exception in the program, which is wrong!

Exception filter:

  • Customize the exception filter by implementing the interface IExceptionFilter or IAsyncExceptionFilter
  • You can catch unhandled exceptions thrown in Controller creation (that is, only exceptions thrown in constructor), model binding, Action Filter and Action
  • Exceptions thrown elsewhere will not be caught

Let's take a look at the two interfaces:

// It only has the function of marking it as a filter of mvc request pipeline
public interface IFilterMetadata { }

public interface IExceptionFilter : IFilterMetadata
{
    // When an exception is thrown, the method catches
    void OnException(ExceptionContext context);
}

public interface IAsyncExceptionFilter : IFilterMetadata
{
    // When an exception is thrown, the method catches
    Task OnExceptionAsync(ExceptionContext context);
}

Both OnException and OnExceptionAsync methods contain a parameter of type ExceptionContext. Obviously, it is the context related to exceptions, and our exception handling logic is inseparable from it. Then let's take a look at its structure:

public class ExceptionContext : FilterContext
{
    // Unhandled exception caught
    public virtual Exception Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // Indicates whether the exception has been handled
    // true: indicates that the exception has been handled and will not be thrown upward
    // false: indicates that the exception has not been handled, and the exception will continue to be thrown upward
    public virtual bool ExceptionHandled { get; set; }

    // Set the iationresult of the response
    // If the result is set, it also indicates that the exception has been handled and will not be thrown upward
    public virtual IActionResult? Result { get; set; }
}

Next, let's implement a custom exception handler:

public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public MyExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)
    {
        _modelMetadataProvider = modelMetadataProvider;
    }

    public override void OnException(ExceptionContext context)
    {
        if (!context.ExceptionHandled)
        {
            // This is a simple demonstration only
            var exception = context.Exception;
            var result = new ViewResult()
            {
                ViewName = "Error",
                ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState)
                {
                    // Remember to add the Message property to ErrorViewModel
                    Model = new ErrorViewModel
                    {
                        Message = exception.ToString()
                    }
                }
            };

            context.Result = result;

            // Mark exception handled
            context.ExceptionHandled = true;
        }
    }
}

Next, find / Views/Shared/Error.cshtml to display the error message:

@model ErrorViewModel
@{
    ViewData["Title"] = "Error";
}

<p>@Model.Message</p>

Finally, register MyExceptionFilterAttribute:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<MyExceptionFilterAttribute>();

    services.AddControllersWithViews();
}

Now, we add the exception handler to / Home/Index and throw an exception:

public class HomeController : Controller
{
    [ServiceFilter(typeof(MyExceptionFilterAttribute))]
    public IActionResult Index()
    {
        throw new Exception("Home Index Error");

        return View();
    }
}

When requesting / Home/Index, you will get the following page:

Result Filters

The result filter wraps the execution of the operation result. The execution of operation results can be the processing operation of Razor view or the serialization operation of Json results.

By implementing IResultFilter or IAsyncResultFilter interfaces:

public interface IResultFilter : IFilterMetadata
{
    void OnResultExecuting(ResultExecutingContext context);

    void OnResultExecuted(ResultExecutedContext context);
}

public interface IAsyncResultFilter : IFilterMetadata
{
    Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}

When one of these two interfaces is implemented, the Result filter will be executed only when Action or Action Filters generate a Result. The Result filter will not be executed, such as authorization, short circuit of pipeline by resource filter, or exception filter processing exception by generating Result.

If an exception is thrown in OnResultExecuting, it will lead to a short circuit. The Action result and subsequent result filters will not be executed, and the execution result will also be regarded as a failure.

Similarly, look at the context structure:

public class ResultExecutingContext : FilterContext
{
    // Get the Controller to which the Action belongs
    public virtual object Controller { get; }

    // Gets or sets the result of the Action
    public virtual IActionResult Result { get; set; }

    // Indicates whether the result filter should be short circuited. If it is short circuited, the Action result and subsequent result filters will not be executed
    public virtual bool Cancel { get; set; }
}

public class ResultExecutedContext : FilterContext
{
    // Indicates whether the result filter is short circuited. If it is short circuited, the Action result and subsequent result filters will not be executed
    public virtual bool Canceled { get; set; }

    // Get the Controller to which the Action belongs
    public virtual object Controller { get; }

    // Gets or sets the unhandled exception thrown during the execution of the result or result filter
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // Has the exception been handled
    public virtual bool ExceptionHandled { get; set; }

    // Gets or sets the execution result of the Action
    public virtual IActionResult Result { get; }
}

You can implement custom result filters by inheriting the abstract class ResultFilterAttribute:

class MyResultFilter : ResultFilterAttribute
{
    private readonly ILogger<MyResultFilter> _logger;

    public MyResultFilter(ILogger<MyResultFilter> logger)
    {
        _logger = logger;
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        context.HttpContext.Response.Headers.Add("CustomHeaderName", "CustomHeaderValue");
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.HttpContext.Response.HasStarted)
        {
            _logger.LogInformation("Response has started!");
        }
    }
}

As mentioned above, the IResultFilter or IAsyncResultFilter interface has certain limitations. When the authorization and resource filter short circuit the pipeline, or the exception filter handles exceptions by generating results, the Result filter will not be executed. But what if we also want to execute the Result filter in this case? Don't panic, ASP.NET Core has thought of this situation.

That is to implement the ialwaysrun resultfilter or iasyncalwaysrun resultfilter interface. The name is direct enough - always run:

public interface IAlwaysRunResultFilter : IResultFilter, IFilterMetadata { }

public interface IAsyncAlwaysRunResultFilter : IAsyncResultFilter, IFilterMetadata { }

Middleware filter

Middleware filter is actually adding middleware pipeline to the filter pipeline. The execution timing of middleware filter is the same as that of resource filter, that is, before model binding and after the execution of the rest of the pipeline.

To create a middleware filter, you need to meet a condition that the middleware must include a Configure method (generally, it also includes an iaapplicationbuilder parameter to Configure the middleware pipeline, but this is not mandatory).

For example:

class MyPipeline
{
    public void Configure(IApplicationBuilder app)
    {
        System.Console.WriteLine("MyPipeline");
    }
}

[MiddlewareFilter(typeof(MyPipeline))]
public class HomeController : Controller { }

other

IOrderedFilter

For the same type of filter, we can have multiple implementations, which can be registered in different scopes, and the same scope can have multiple implementations of the filter type. If we apply such multiple implementations to the same Action, the execution order of these filter instances is what we need to care about.

By default, if multiple implementations of filters of the same type in the same scope are applied to an Action, the execution order of these filter instances is in the order of registration.

For example, we now have two action filters - MyActionFilter1 and MyActionFilter2:

public class MyActionFilter1 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter1");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter1");
    }
}

public class MyActionFilter2 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter2");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter2");
    }
}

Then apply it to the HomeController.Index method, and register MyActionFilter2 first, and then MyActionFilter1:

public class HomeController : Controller
{
    [MyActionFilter2]
    [MyActionFilter1]
    public IActionResult Index()
    {
        return View();
    }
}

When requesting Home/Index, the console output is as follows:

OnActionExecuting: MyActionFilter2
OnActionExecuting: MyActionFilter1
OnResultExecuted: MyActionFilter1
OnResultExecuted: MyActionFilter2

However, in the development process, it is easy to make a mistake in the registration order by hand. At this time, we need a mechanism to manually specify the execution order, which uses the IOrderedFilter interface.

public interface IOrderedFilter : IFilterMetadata
{
    // Execution sequence
    int Order { get; }
}

The IOrderedFilter interface is very simple. There is only one Order attribute, which indicates the execution Order. The default value is 0. The smaller the Order value, the earlier the Before method of the filter is executed and the later the After method is executed.

Let's transform MyActionFilter1 and MyActionFilter2 and let MyActionFilter1 execute first:

public class MyActionFilter1 : ActionFilterAttribute
{
    public MyActionFilter1()
    {
        Order = -1;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter1");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter1");
    }
}

public class MyActionFilter2 : ActionFilterAttribute
{
    public MyActionFilter2()
    {
        Order = 1;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter2");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter2");
    }
}

At this time, request Home/Index again, and the console output is as follows:

OnActionExecuting: MyActionFilter1
OnActionExecuting: MyActionFilter2
OnResultExecuted: MyActionFilter2
OnResultExecuted: MyActionFilter1

Now, let's take a look at whether Order takes effect under different scopes. Promote the MyActionFilter2 scope to the controller.

[MyActionFilter2]
public class HomeController : Controller
{
    [MyActionFilter1]
    public IActionResult Index()
    {
        return View();
    }
}

At this time, request Home/Index again, and the console output is as follows:

OnActionExecuting: MyActionFilter1
OnActionExecuting: MyActionFilter2
OnResultExecuted: MyActionFilter2
OnResultExecuted: MyActionFilter1

Wow, something magical happened. MyActionFilter1 with the scope of Action takes precedence over MyActionFilter2 with the scope of Controller.

In fact, Order rewrites the scope, that is, first sort the filters by Order, and then eliminate the juxtaposition problem through the scope.

Also, to always execute the global filter first, set Order to int.MinValue:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.Filters.Add<MyActionFilter2>(int.MinValue);
    });
}

Topics: ASP.NET .NET filter