Skip to content

Instantly share code, notes, and snippets.

@jdaigle
Last active March 13, 2017 15:40
Show Gist options
  • Save jdaigle/8c0cc3d6ee507369d74717282f9a667c to your computer and use it in GitHub Desktop.
Save jdaigle/8c0cc3d6ee507369d74717282f9a667c to your computer and use it in GitHub Desktop.
Async Action Filters in MVC

API:

interface IFilterMetadata

interface IOrderedFilter : IFilterMetadata {
    int Order { get; }
}

interface IExceptionFilter : IFilterMetadata {
    void OnException(ExceptionContext context);
}

interface IAsyncExceptionFilter : IFilterMetadata {
    Task OnExceptionAsync(ExceptionContext context);
}

abstract class ExceptionFilterAttribute : Attribute, IAsyncExceptionFilter, IExceptionFilter, IOrderedFilter

interface IAuthorizationFilter : IFilterMetadata {
    void OnAuthorization(AuthorizationFilterContext context);
}

interface IAsyncAuthorizationFilter : IFilterMetadata {
    Task OnAuthorizationAsync(AuthorizationFilterContext context);
}

interface IResourceFilter : IFilterMetadata {
    void OnResourceExecuting(ResourceExecutingContext context);
    void OnResourceExecuted(ResourceExecutedContext context);
}

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

interface IActionFilter : IFilterMetadata {
    void OnActionExecuting(ActionExecutingContext context);
    void OnActionExecuted(ActionExecutedContext context);
}

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

interface IResultFilter : IFilterMetadata {
    void OnResultExecuting(ResultExecutingContext context);
    void OnResultExecuted(ResultExecutedContext context);
}

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

abstract class ActionFilterAttribute :
    Attribute, IActionFilter, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
    
abstract class ResultFilterAttribute : Attribute, IResultFilter, IAsyncResultFilter, IOrderedFilter

ASP.NET MVC Core uses this awesome state machine construct (aspnet/Mvc#4687) which seems to keep the logic for calling the async versus sync filters a lot simpler. For the async filters (Resource/Action/Result) it follows the same sort of chained pipeline approach that WebApi and others use.

API:

interface IMvcFilter {
    bool AllowMultiple { get; }
    int Order { get; }
}

abstract class MvcFilter : IMvcFilter // not even used?!

abstract class FilterAttribute : Attribute, IMvcFilter

interface IExceptionFilter {
    void OnException(ExceptionContext filterContext);
}

interface IAuthenticationFilter {
    void OnAuthentication(AuthenticationContext filterContext);
    void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext);
}

interface IAuthorizationFilter {
    void OnAuthorization(AuthorizationContext filterContext);
}

class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter

interface IActionFilter {
    void OnActionExecuting(ActionExecutingContext filterContext);
    void OnActionExecuted(ActionExecutedContext filterContext);
}

interface IResultFilter {
    void OnResultExecuting(ResultExecutingContext filterContext);
    void OnResultExecuted(ResultExecutedContext filterContext);
}

abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter

interface IOverrideFilter : IFilter {
     Type FiltersToOverride { get; }
}

Since the API is not async, the execution of these filters happen in a loop rather than recursively as in WebAPI or ASP.NET Core.

What can we replace in System.Web.Mvc at runtime?

  1. We can replace the IActionInvoker or IAsyncActionInvoker be either a) registering with the service or b) implementing IAsyncActionInvokerFactory or IActionInvokerFactory.
  2. Could also override Controller.CreateActionInvoker().
  3. Could replace ActionDescriptor by overriding ControllerActionInvoker.FindAction().
  4. Want to replace TaskAsyncActionDescriptor. Problematic because it's created by AsyncActionMethodSelector (subclass of ActionMethodSelectorBase) which is internal.
  5. In theory we could replace that entire thing with our own ControllerDescriptor as the ControllerDescriptorCache is owned by the action invokers.
  6. Problematic because AttributeRoutingMapper uses all of the built-in types directly :(

If we create an custom async filter Attributes, then we must inherit from FilterAttribute since the framework expects this. Also, annoyingly, the Async versions of IActionFilter/IResultFilter/etc probably need to implement the non-async version as well so that framework code works correctly (i.e. FilterInfo and GlobalFilterCollection, etc.).

API:

interface IFilter {
    bool AllowMultiple { get; }
}

interface IActionFilter : IFilter {
    Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation);
}

interface IAuthenticationFilter : IFilter {
    Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken);
    Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken);
}

interface IAuthorizationFilter : IFilter {
    Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation);
}

interface IExceptionFilter : IFilter {
    Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken);
}

abstract class FilterAttribute : Attribute, IFilter

abstract class ActionFilterAttribute : FilterAttribute, IActionFilter {
    virtual void OnActionExecuting(HttpActionContext actionContext)
    virtual void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    virtual Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    virtual Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
}

// AuthorizationFilterAttribute and ExceptionFilterAttribute follow similar pattern

interface IOverrideFilter : IFilter {
     Type FiltersToOverride { get; }
}

The way dispatch works in WebApi is that the ApiController's ExecuteAsync() method determines which filters (Action, Authn, AuthX, Exception) need to execute. Each is wrapped in an IHttpActionResult which encapsulates the others. So it goes in order:

  • IExceptionFilters await an inner IHttpActionResult in a try/catch block and await the the filter's logic if there was an exception.
  • IAuthenticationFilters handle authn which may short-circuit with a new IHttpActionResult, then await the inner IHttpActionResult. (There is the "Challenge" aspect of this too).
  • IAuthorizationFilters create a chained pipeline surrounding the inner IHttpActionResult (may be short-circuited).
  • IActionFilters create a chained pipeline surrounding an ActionInvoker. May be short-circuited. The ActionInvoker calls to ApiControllerActionInvoker which executes the Action returning an IHttpActionResult which is then await.

Eventually this ApiController returns the HttpResponseMessage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment