From f19fe0cbefd45d18b8c2a6e418fccdaef1f8a89b Mon Sep 17 00:00:00 2001 From: Louis DeJardin Date: Tue, 1 Apr 2014 16:16:17 -0700 Subject: [PATCH] Filters version 2.0 This is functionally much more similar to legacy MVC. Rather than a pure single pipeline, filter execution takes place in more stages. --- .../Filters/AgeEnhancerFilterAttribute.cs | 8 +- .../MvcSample.Web/Filters/BlockAnonymous.cs | 12 +- .../MvcSample.Web/Filters/DelayAttribute.cs | 33 + .../Filters/ErrorMessagesAttribute.cs | 22 + .../Filters/InspectResultPageAttribute.cs | 7 +- .../Filters/PassThroughAttribute.cs | 6 +- .../MvcSample.Web/Filters/UserNameProvider.cs | 8 +- samples/MvcSample.Web/FiltersController.cs | 10 +- samples/MvcSample.Web/MvcSample.Web.kproj | 2 + .../Extensions/FilterContextExtensions.cs | 13 - .../Filters/ActionExecutedContext.cs | 60 + .../Filters/ActionExecutingContext.cs | 20 + .../Filters/ActionExecutionDelegate.cs | 6 + .../Filters/ActionFilterAttribute.cs | 41 +- .../Filters/ActionFilterContext.cs | 18 - .../Filters/ActionFilterEndPoint.cs | 40 - .../Filters/ActionResultFilterAttribute.cs | 13 - .../Filters/ActionResultFilterContext.cs | 14 - .../Filters/ActionResultFilterEndPoint.cs | 20 - .../Filters/AllowAnonymousAttribute.cs | 1 - .../Filters/AuthorizationContext.cs | 17 + .../Filters/AuthorizationFilterAttribute.cs | 21 +- .../Filters/AuthorizationFilterContext.cs | 37 - .../Filters/AuthorizationFilterEndPoint.cs | 18 - .../Filters/ExceptionContext.cs | 54 + .../Filters/ExceptionFilterAttribute.cs | 15 +- .../Filters/ExceptionFilterContext.cs | 22 - .../Filters/FilterContext.cs | 18 +- .../Filters/FilterDescriptor.cs | 2 +- .../Filters/FilterPipelineBuilder.cs | 51 - .../Filters/IActionFilter.cs | 11 +- .../Filters/IActionResultFilter.cs | 8 - .../Filters/IAllowAnonymous.cs | 2 +- .../Filters/IAsyncActionFilter.cs | 9 + .../Filters/IAsyncAuthorizationFilter.cs | 10 + .../Filters/IAsyncExceptionFilter.cs | 10 + .../Filters/IAsyncResultFilter.cs | 9 + .../Filters/IAuthorizationFilter.cs | 6 +- .../Filters/IExceptionFilter.cs | 6 +- .../Filters/IFilterOfTContext.cs | 10 - .../Filters/IResultFilter.cs | 9 + .../Filters/ResultExecutedContext.cs | 62 + .../Filters/ResultExecutingContext.cs | 20 + .../Filters/ResultExecutionDelegate.cs | 6 + .../Filters/ResultFilterAttribute.cs | 28 + .../Microsoft.AspNet.Mvc.Core.kproj | 26 +- .../Properties/Resources.Designer.cs | 34 +- .../ReflectedActionInvoker.cs | 565 ++++++-- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 8 +- .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.Designer.cs | 2 +- .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + .../ReflectedActionInvokerTest.cs | 1265 +++++++++++++++++ 54 files changed, 2276 insertions(+), 444 deletions(-) create mode 100644 samples/MvcSample.Web/Filters/DelayAttribute.cs create mode 100644 samples/MvcSample.Web/Filters/ErrorMessagesAttribute.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Extensions/FilterContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutedContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutingContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutionDelegate.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterAttribute.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterEndPoint.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterEndPoint.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterContext.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/FilterPipelineBuilder.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IActionResultFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncActionFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncAuthorizationFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncExceptionFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncResultFilter.cs delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IFilterOfTContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/IResultFilter.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutedContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutingContext.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutionDelegate.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Filters/ResultFilterAttribute.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs diff --git a/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs b/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs index 4d4345277f..dc251bbd03 100644 --- a/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs +++ b/samples/MvcSample.Web/Filters/AgeEnhancerFilterAttribute.cs @@ -1,12 +1,10 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; namespace MvcSample.Web.Filters { public class AgeEnhancerAttribute : ActionFilterAttribute { - public async override Task Invoke(ActionFilterContext context, Func next) + public override void OnActionExecuting(ActionExecutingContext context) { object age = null; @@ -28,8 +26,6 @@ namespace MvcSample.Web.Filters context.ActionArguments["age"] = intAge; } } - - await next(); } } } diff --git a/samples/MvcSample.Web/Filters/BlockAnonymous.cs b/samples/MvcSample.Web/Filters/BlockAnonymous.cs index 64b364da36..f05cfe9f8c 100644 --- a/samples/MvcSample.Web/Filters/BlockAnonymous.cs +++ b/samples/MvcSample.Web/Filters/BlockAnonymous.cs @@ -1,19 +1,15 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; namespace MvcSample.Web.Filters { public class BlockAnonymous : AuthorizationFilterAttribute { - public override async Task Invoke(AuthorizationFilterContext context, Func next) + public override void OnAuthorization(AuthorizationContext context) { - if (!context.HasAllowAnonymous()) + if (!HasAllowAnonymous(context)) { - context.Fail(); + context.Result = new HttpStatusCodeResult(401); } - - await next(); } } } \ No newline at end of file diff --git a/samples/MvcSample.Web/Filters/DelayAttribute.cs b/samples/MvcSample.Web/Filters/DelayAttribute.cs new file mode 100644 index 0000000000..85e01f23d3 --- /dev/null +++ b/samples/MvcSample.Web/Filters/DelayAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace MvcSample.Web.Filters +{ + public class DelayAttribute : ActionFilterAttribute + { + public DelayAttribute(int milliseconds) + { + Delay = TimeSpan.FromMilliseconds(milliseconds); + } + + public TimeSpan Delay { get; private set; } + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context.HttpContext.Request.Method == "GET") + { + // slow down incoming GET requests + await Task.Delay(Delay); + } + + var executedContext = await next(); + + if (executedContext.Result is ViewResult) + { + // slow down outgoing view results + await Task.Delay(Delay); + } + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Filters/ErrorMessagesAttribute.cs b/samples/MvcSample.Web/Filters/ErrorMessagesAttribute.cs new file mode 100644 index 0000000000..5fbb473d00 --- /dev/null +++ b/samples/MvcSample.Web/Filters/ErrorMessagesAttribute.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Filters; + +namespace MvcSample.Web +{ + public class ErrorMessagesAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + if (context.Exception != null && !context.ExceptionHandled) + { + context.ExceptionHandled = true; + + context.Result = new ContentResult + { + ContentType = "text/plain", + Content = "Boom " + context.Exception.Message + }; + } + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Filters/InspectResultPageAttribute.cs b/samples/MvcSample.Web/Filters/InspectResultPageAttribute.cs index a5154a5e3b..6d91c843d0 100644 --- a/samples/MvcSample.Web/Filters/InspectResultPageAttribute.cs +++ b/samples/MvcSample.Web/Filters/InspectResultPageAttribute.cs @@ -1,15 +1,16 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Filters; using MvcSample.Web.Models; namespace MvcSample.Web.Filters { - public class InspectResultPageAttribute : ActionResultFilterAttribute + public class InspectResultPageAttribute : ActionFilterAttribute { - public async override Task Invoke(ActionResultFilterContext context, Func next) + public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { - var viewResult = context.ActionResult as ViewResult; + var viewResult = context.Result as ViewResult; if (viewResult != null) { diff --git a/samples/MvcSample.Web/Filters/PassThroughAttribute.cs b/samples/MvcSample.Web/Filters/PassThroughAttribute.cs index f13024ee3a..003ee6e727 100644 --- a/samples/MvcSample.Web/Filters/PassThroughAttribute.cs +++ b/samples/MvcSample.Web/Filters/PassThroughAttribute.cs @@ -1,14 +1,16 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Filters; namespace MvcSample.Web { public class PassThroughAttribute : AuthorizationFilterAttribute { - public async override Task Invoke(AuthorizationFilterContext context, Func next) + #pragma warning disable 1998 + public override async Task OnAuthorizationAsync(AuthorizationContext context) { - await next(); } + #pragma warning restore 1998 } } diff --git a/samples/MvcSample.Web/Filters/UserNameProvider.cs b/samples/MvcSample.Web/Filters/UserNameProvider.cs index 1536258604..918ff6987c 100644 --- a/samples/MvcSample.Web/Filters/UserNameProvider.cs +++ b/samples/MvcSample.Web/Filters/UserNameProvider.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; namespace MvcSample.Web.Filters { @@ -9,7 +7,7 @@ namespace MvcSample.Web.Filters private static readonly string[] _userNames = new[] { "Jon", "David", "Goliath" }; private static int _index; - public override async Task Invoke(ActionFilterContext context, Func next) + public override void OnActionExecuting(ActionExecutingContext context) { object originalUserName = null; @@ -21,8 +19,6 @@ namespace MvcSample.Web.Filters { context.ActionArguments["userName"] = _userNames[(_index++)%3]; } - - await next(); } } } diff --git a/samples/MvcSample.Web/FiltersController.cs b/samples/MvcSample.Web/FiltersController.cs index fcfba57584..9700648001 100644 --- a/samples/MvcSample.Web/FiltersController.cs +++ b/samples/MvcSample.Web/FiltersController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Mvc; +using System; +using Microsoft.AspNet.Mvc; using MvcSample.Web.Filters; using MvcSample.Web.Models; @@ -19,6 +20,7 @@ namespace MvcSample.Web [ServiceFilter(typeof(PassThroughAttribute))] [AllowAnonymous] [AgeEnhancer] + [Delay(500)] public IActionResult Index(int age, string userName) { if (!string.IsNullOrEmpty(userName)) @@ -35,5 +37,11 @@ namespace MvcSample.Web { return Index(age, userName); } + + [ErrorMessages, AllowAnonymous] + public IActionResult Crash(string message) + { + throw new Exception(message); + } } } \ No newline at end of file diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj index 1d5d97fbeb..8581f2626d 100644 --- a/samples/MvcSample.Web/MvcSample.Web.kproj +++ b/samples/MvcSample.Web/MvcSample.Web.kproj @@ -47,6 +47,8 @@ + + diff --git a/src/Microsoft.AspNet.Mvc.Core/Extensions/FilterContextExtensions.cs b/src/Microsoft.AspNet.Mvc.Core/Extensions/FilterContextExtensions.cs deleted file mode 100644 index d2fc5dd08a..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Extensions/FilterContextExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Linq; -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc -{ - public static class FilterContextExtensions - { - public static bool HasAllowAnonymous([NotNull] this FilterContext context) - { - return context.FilterItems.Any(item => item.Filter is IAllowAnonymous); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutedContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutedContext.cs new file mode 100644 index 0000000000..55aec0d9f1 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutedContext.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; + +namespace Microsoft.AspNet.Mvc +{ + public class ActionExecutedContext : FilterContext + { + private Exception _exception; + private ExceptionDispatchInfo _exceptionDispatchInfo; + + public ActionExecutedContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters) + : base(actionContext, filters) + { + } + + public virtual bool Canceled { get; set; } + + public virtual Exception Exception + { + get + { + if (_exception == null && _exceptionDispatchInfo != null) + { + return _exceptionDispatchInfo.SourceException; + } + else + { + return _exception; + } + } + + set + { + _exceptionDispatchInfo = null; + _exception = value; + } + } + + public virtual ExceptionDispatchInfo ExceptionDispatchInfo + { + get + { + return _exceptionDispatchInfo; + } + + set + { + _exception = null; + _exceptionDispatchInfo = value; + } + } + + public virtual bool ExceptionHandled { get; set; } + + public virtual IActionResult Result { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutingContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutingContext.cs new file mode 100644 index 0000000000..f4afb4fa3e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutingContext.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc +{ + public class ActionExecutingContext : FilterContext + { + public ActionExecutingContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters, + [NotNull] IDictionary actionArguments) + : base(actionContext, filters) + { + ActionArguments = actionArguments; + } + + public virtual IActionResult Result { get; set; } + + public virtual IDictionary ActionArguments { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutionDelegate.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutionDelegate.cs new file mode 100644 index 0000000000..3b39d00506 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionExecutionDelegate.cs @@ -0,0 +1,6 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public delegate Task ActionExecutionDelegate(); +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterAttribute.cs index f486d99da3..5f011f1d32 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterAttribute.cs @@ -3,12 +3,43 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Mvc { - // TODO: Consider making this user a before and after pattern instead of just Invoke, same for all other filter attributes. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class ActionFilterAttribute : Attribute, IActionFilter, IOrderedFilter + public abstract class ActionFilterAttribute : Attribute, IActionFilter, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter { - public abstract Task Invoke(ActionFilterContext context, Func next); - public int Order { get; set; } + + public virtual void OnActionExecuting([NotNull] ActionExecutingContext context) + { + } + + public virtual void OnActionExecuted([NotNull] ActionExecutedContext context) + { + } + + public virtual async Task OnActionExecutionAsync([NotNull] ActionExecutingContext context, [NotNull] ActionExecutionDelegate next) + { + OnActionExecuting(context); + if (context.Result == null) + { + OnActionExecuted(await next()); + } + } + + public virtual void OnResultExecuting([NotNull] ResultExecutingContext context) + { + } + + public virtual void OnResultExecuted([NotNull] ResultExecutedContext context) + { + } + + public virtual async Task OnResultExecutionAsync([NotNull] ResultExecutingContext context, [NotNull] ResultExecutionDelegate next) + { + OnResultExecuting(context); + if (!context.Cancel) + { + OnResultExecuted(await next()); + } + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs deleted file mode 100644 index 15023c06a3..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterContext.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc -{ - public class ActionFilterContext : FilterContext - { - public ActionFilterContext([NotNull] ActionContext actionContext, - [NotNull] IReadOnlyList filterItems, - [NotNull] IDictionary actionArguments) - : base(actionContext, filterItems) - { - ActionArguments = actionArguments; - } - - public virtual IDictionary ActionArguments { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs deleted file mode 100644 index 7e9d7689cf..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionFilterEndPoint.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc.Core; - -namespace Microsoft.AspNet.Mvc.Filters -{ - // This one lives in the Filters namespace, and only intended to be consumed by folks that rewrite the action invoker. - public class ReflectedActionFilterEndPoint : IActionFilter - { - private readonly IActionResultFactory _actionResultFactory; - private readonly object _controllerInstance; - - public ReflectedActionFilterEndPoint(IActionResultFactory actionResultFactory, object controllerInstance) - { - _actionResultFactory = actionResultFactory; - _controllerInstance = controllerInstance; - } - - public async Task Invoke(ActionFilterContext context, Func next) - { - var reflectedActionDescriptor = context.ActionContext.ActionDescriptor as ReflectedActionDescriptor; - if (reflectedActionDescriptor == null) - { - throw new ArgumentException(Resources.ReflectedActionFilterEndPoint_UnexpectedActionDescriptor); - } - - var actionMethodInfo = reflectedActionDescriptor.MethodInfo; - var actionReturnValue = await ReflectedActionExecutor.ExecuteAsync( - actionMethodInfo, - _controllerInstance, - context.ActionArguments); - - var underlyingReturnType = TypeHelper.GetTaskInnerTypeOrNull(actionMethodInfo.ReturnType) ?? actionMethodInfo.ReturnType; - context.ActionResult = _actionResultFactory.CreateActionResult( - underlyingReturnType, - actionReturnValue, - context.ActionContext); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterAttribute.cs deleted file mode 100644 index cec8cb231d..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Mvc -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class ActionResultFilterAttribute : Attribute, IActionResultFilter, IOrderedFilter - { - public abstract Task Invoke(ActionResultFilterContext context, Func next); - - public int Order { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterContext.cs deleted file mode 100644 index 640a555aa7..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc -{ - public class ActionResultFilterContext : FilterContext - { - public ActionResultFilterContext(ActionContext actionContext, IReadOnlyList filterItems, IActionResult initialResult) - : base(actionContext, filterItems) - { - ActionResult = initialResult; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterEndPoint.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterEndPoint.cs deleted file mode 100644 index 79fa0d6b23..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ActionResultFilterEndPoint.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Mvc.Filters -{ - // This one lives in the Filters namespace, and only intended to be consumed by folks that rewrite the action invoker. - public class ActionResultFilterEndPoint : IActionResultFilter - { - public async Task Invoke(ActionResultFilterContext context, Func next) - { - // result can get cleared at any point in the pipeline - if (context.ActionResult == null) - { - context.ActionResult = new EmptyResult(); - } - - await context.ActionResult.ExecuteResultAsync(context.ActionContext); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AllowAnonymousAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AllowAnonymousAttribute.cs index 113819d498..09c0dc2f14 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/AllowAnonymousAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AllowAnonymousAttribute.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.AspNet.Mvc.Filters; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationContext.cs new file mode 100644 index 0000000000..d66b7ee8b8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationContext.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Filters; + +namespace Microsoft.AspNet.Mvc +{ + public class AuthorizationContext : FilterContext + { + public AuthorizationContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters) + : base(actionContext, filters) + { + } + + public virtual IActionResult Result { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs index 26f15271ce..22a41a7f55 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterAttribute.cs @@ -1,13 +1,28 @@ using System; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.AspNet.Mvc { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter, IOrderedFilter + public abstract class AuthorizationFilterAttribute : Attribute, IAsyncAuthorizationFilter, IAuthorizationFilter, IOrderedFilter { - public abstract Task Invoke(AuthorizationFilterContext context, Func next); - public int Order { get; set; } + + #pragma warning disable 1998 + public virtual async Task OnAuthorizationAsync([NotNull] AuthorizationContext context) + { + OnAuthorization(context); + } + #pragma warning restore 1998 + + public virtual void OnAuthorization([NotNull] AuthorizationContext context) + { + } + + protected virtual bool HasAllowAnonymous([NotNull] AuthorizationContext context) + { + return context.Filters.Any(item => item is IAllowAnonymous); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterContext.cs deleted file mode 100644 index 11c5e01230..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterContext.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc -{ - public class AuthorizationFilterContext : FilterContext - { - private IActionResult _actionResult; - - public AuthorizationFilterContext([NotNull] ActionContext actionContext, [NotNull] IReadOnlyList filterItems) - : base(actionContext, filterItems) - { - } - - public bool HasFailed { get; private set; } - - // Result - public override IActionResult ActionResult - { - get { return _actionResult; } - set - { - if (value != null) - { - Fail(); - } - - _actionResult = value; - } - } - - public void Fail() - { - HasFailed = true; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterEndPoint.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterEndPoint.cs deleted file mode 100644 index 8d42516404..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/AuthorizationFilterEndPoint.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Mvc.Filters -{ - // This one lives in the Filters namespace, and only intended to be consumed by folks that rewrite the action invoker. - public class AuthorizationFilterEndPoint : IAuthorizationFilter - { - public bool WasEndPointCalled { get; private set; } - - public Task Invoke(AuthorizationFilterContext context, Func next) - { - WasEndPointCalled = true; - - return Task.FromResult(true); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionContext.cs new file mode 100644 index 0000000000..d77f91152a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionContext.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; + +namespace Microsoft.AspNet.Mvc +{ + public class ExceptionContext : FilterContext + { + private Exception _exception; + private ExceptionDispatchInfo _exceptionDispatchInfo; + + public ExceptionContext([NotNull] ActionContext actionContext, [NotNull] IList filters) + : base(actionContext, filters) + { + } + + public virtual Exception Exception + { + get + { + if (_exception == null && _exceptionDispatchInfo != null) + { + return _exceptionDispatchInfo.SourceException; + } + else + { + return _exception; + } + } + + set + { + _exceptionDispatchInfo = null; + _exception = value; + } + } + + public virtual ExceptionDispatchInfo ExceptionDispatchInfo + { + get + { + return _exceptionDispatchInfo; + } + + set + { + _exception = null; + _exceptionDispatchInfo = value; + } + } + + public virtual IActionResult Result { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterAttribute.cs index 20348788ce..766341bab2 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterAttribute.cs @@ -4,10 +4,19 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Mvc { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public abstract class ExceptionFilterAttribute : Attribute, IExceptionFilter, IOrderedFilter + public abstract class ExceptionFilterAttribute : Attribute, IAsyncExceptionFilter, IExceptionFilter, IOrderedFilter { - public abstract Task Invoke(ExceptionFilterContext context, Func next); - public int Order { get; set; } + + #pragma warning disable 1998 + public async Task OnActionExecutedAsync([NotNull] ExceptionContext context) + { + OnActionExecuted(context); + } + #pragma warning restore 1998 + + public void OnActionExecuted([NotNull] ExceptionContext context) + { + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterContext.cs deleted file mode 100644 index ebdebbd22f..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/ExceptionFilterContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Mvc -{ - // TODO: For now we have not implemented the ExceptionFilter pipeline, leaving this in until we decide if we are going - // down this path or implementing an ExceptionFilterAttribute being all three filter types with a higher scope. - public class ExceptionFilterContext - { - public ExceptionFilterContext(ActionContext actionContext, Exception exception) - { - ActionContext = actionContext; - Exception = exception; - } - - // TODO: Should we let the exception mutate in the pipeline. MVC lets you do that. - public virtual Exception Exception { get; set; } - - public virtual ActionContext ActionContext { get; private set; } - - public virtual IActionResult Result { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterContext.cs index 4e6ba27236..ba59c109e5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterContext.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterContext.cs @@ -1,19 +1,17 @@ using System.Collections.Generic; -namespace Microsoft.AspNet.Mvc.Filters +namespace Microsoft.AspNet.Mvc { - public class FilterContext + public abstract class FilterContext : ActionContext { - public FilterContext([NotNull] ActionContext actionContext, [NotNull] IReadOnlyList filterItems) + public FilterContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters) + : base(actionContext) { - ActionContext = actionContext; - FilterItems = filterItems; + Filters = filters; } - public ActionContext ActionContext { get; private set; } - public IReadOnlyList FilterItems { get; private set; } - - // Result - public virtual IActionResult ActionResult { get; set; } + public virtual IList Filters { get; private set; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs index edc6d518e4..1df3bea46c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterDescriptor.cs @@ -2,7 +2,7 @@ { public class FilterDescriptor { - public FilterDescriptor([NotNull]IFilter filter, int filterScope) + public FilterDescriptor([NotNull] IFilter filter, int filterScope) { Filter = filter; Scope = filterScope; diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterPipelineBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/FilterPipelineBuilder.cs deleted file mode 100644 index e54f2a1032..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/FilterPipelineBuilder.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Mvc.Filters -{ - public class FilterPipelineBuilder - { - private readonly IFilter[] _filters; - private readonly TContext _context; - - // FilterDescriptors are already ordered externally. - public FilterPipelineBuilder(IEnumerable> filters, TContext context) - { - _filters = filters.ToArray(); - _context = context; - } - - public async Task InvokeAsync() - { - var caller = new CallNextAsync(_context, _filters); - - await caller.CallNextProvider(); - } - - private class CallNextAsync - { - private readonly TContext _context; - private readonly IFilter[] _filters; - private readonly Func _next; - - private int _index; - - public CallNextAsync(TContext context, IFilter[] filters) - { - _context = context; - _next = CallNextProvider; - _filters = filters; - } - - public async Task CallNextProvider() - { - if (_filters.Length > _index) - { - await _filters[_index++].Invoke(_context, _next); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IActionFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IActionFilter.cs index dee1e6d057..a72acd9784 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IActionFilter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IActionFilter.cs @@ -1,8 +1,9 @@ -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc { - public interface IActionFilter : IFilter + public interface IActionFilter : IFilter { + void OnActionExecuting([NotNull] ActionExecutingContext context); + + void OnActionExecuted([NotNull] ActionExecutedContext context); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IActionResultFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IActionResultFilter.cs deleted file mode 100644 index 0ed3d78c4c..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IActionResultFilter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.AspNet.Mvc.Filters; - -namespace Microsoft.AspNet.Mvc -{ - public interface IActionResultFilter : IFilter - { - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAllowAnonymous.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAllowAnonymous.cs index 36f4bcf461..f5d1605e8b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IAllowAnonymous.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAllowAnonymous.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNet.Mvc.Filters +namespace Microsoft.AspNet.Mvc { public interface IAllowAnonymous : IFilter { diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncActionFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncActionFilter.cs new file mode 100644 index 0000000000..7a4bdf0f60 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncActionFilter.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public interface IAsyncActionFilter : IFilter + { + Task OnActionExecutionAsync([NotNull] ActionExecutingContext context, [NotNull] ActionExecutionDelegate next); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncAuthorizationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncAuthorizationFilter.cs new file mode 100644 index 0000000000..563fbc15a6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncAuthorizationFilter.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public interface IAsyncAuthorizationFilter : IFilter + { + Task OnAuthorizationAsync([NotNull] AuthorizationContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncExceptionFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncExceptionFilter.cs new file mode 100644 index 0000000000..8f87a69c1d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncExceptionFilter.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public interface IAsyncExceptionFilter : IFilter + { + Task OnActionExecutedAsync([NotNull] ExceptionContext context); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncResultFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncResultFilter.cs new file mode 100644 index 0000000000..03e54bd86d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAsyncResultFilter.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public interface IAsyncResultFilter : IFilter + { + Task OnResultExecutionAsync([NotNull] ResultExecutingContext context, [NotNull] ResultExecutionDelegate next); + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IAuthorizationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IAuthorizationFilter.cs index d5d44b4b8e..0608801cf6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IAuthorizationFilter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IAuthorizationFilter.cs @@ -1,8 +1,10 @@ -using Microsoft.AspNet.Mvc.Filters; +using System; +using System.Threading.Tasks; namespace Microsoft.AspNet.Mvc { - public interface IAuthorizationFilter : IFilter + public interface IAuthorizationFilter : IFilter { + void OnAuthorization([NotNull] AuthorizationContext context); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IExceptionFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IExceptionFilter.cs index 9259deccd9..e5b9226c2e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IExceptionFilter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IExceptionFilter.cs @@ -1,8 +1,10 @@ -using Microsoft.AspNet.Mvc.Filters; +using System; +using System.Threading.Tasks; namespace Microsoft.AspNet.Mvc { - public interface IExceptionFilter : IFilter + public interface IExceptionFilter : IFilter { + void OnActionExecuted([NotNull] ExceptionContext context); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IFilterOfTContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IFilterOfTContext.cs deleted file mode 100644 index e78c680e0d..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Filters/IFilterOfTContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Mvc.Filters -{ - public interface IFilter - { - Task Invoke(TContext context, Func next); - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/IResultFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/IResultFilter.cs new file mode 100644 index 0000000000..a19c6a1059 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/IResultFilter.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNet.Mvc +{ + public interface IResultFilter : IFilter + { + void OnResultExecuting([NotNull] ResultExecutingContext context); + + void OnResultExecuted([NotNull] ResultExecutedContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutedContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutedContext.cs new file mode 100644 index 0000000000..fe3c6e9960 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutedContext.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; + +namespace Microsoft.AspNet.Mvc +{ + public class ResultExecutedContext : FilterContext + { + private Exception _exception; + private ExceptionDispatchInfo _exceptionDispatchInfo; + + public ResultExecutedContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters, + [NotNull] IActionResult result) + : base(actionContext, filters) + { + Result = result; + } + + public virtual bool Canceled { get; set; } + + public virtual Exception Exception + { + get + { + if (_exception == null && _exceptionDispatchInfo != null) + { + return _exceptionDispatchInfo.SourceException; + } + else + { + return _exception; + } + } + + set + { + _exceptionDispatchInfo = null; + _exception = value; + } + } + + public virtual ExceptionDispatchInfo ExceptionDispatchInfo + { + get + { + return _exceptionDispatchInfo; + } + + set + { + _exception = null; + _exceptionDispatchInfo = value; + } + } + + public virtual bool ExceptionHandled { get; set; } + + public virtual IActionResult Result { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutingContext.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutingContext.cs new file mode 100644 index 0000000000..ed66916758 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutingContext.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc +{ + public class ResultExecutingContext : FilterContext + { + public ResultExecutingContext( + [NotNull] ActionContext actionContext, + [NotNull] IList filters, + [NotNull] IActionResult result) + : base(actionContext, filters) + { + Result = result; + } + + public virtual IActionResult Result { get; set; } + + public virtual bool Cancel { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutionDelegate.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutionDelegate.cs new file mode 100644 index 0000000000..e7caf0b9d3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultExecutionDelegate.cs @@ -0,0 +1,6 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + public delegate Task ResultExecutionDelegate(); +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/ResultFilterAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultFilterAttribute.cs new file mode 100644 index 0000000000..4f97725525 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/ResultFilterAttribute.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public abstract class ResultFilterAttribute : Attribute, IResultFilter, IAsyncResultFilter, IOrderedFilter + { + public int Order { get; set; } + + public virtual void OnResultExecuting([NotNull] ResultExecutingContext context) + { + } + + public virtual void OnResultExecuted([NotNull] ResultExecutedContext context) + { + } + + public virtual async Task OnResultExecutionAsync([NotNull] ResultExecutingContext context, [NotNull] ResultExecutionDelegate next) + { + OnResultExecuting(context); + if (context.Result == null) + { + OnResultExecuted(await next()); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 097cfb1f33..0488dfdae9 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -53,39 +53,41 @@ - + + + - - - - - + - - + - - - + + + + - + + + + + diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index f6177376f7..4ec1b53b2d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -554,12 +554,44 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("NoRoutesMatched"); } + /// + /// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then the it should not call the next filter by invoking {3}. + /// + internal static string AsyncActionFilter_InvalidShortCircuit + { + get { return GetString("AsyncActionFilter_InvalidShortCircuit"); } + } + + /// + /// If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then the it should not call the next filter by invoking {3}. + /// + internal static string FormatAsyncActionFilter_InvalidShortCircuit(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AsyncActionFilter_InvalidShortCircuit"), p0, p1, p2, p3); + } + + /// + /// If an {0} cancels execution by setting the {1} property of {2} to 'true', then the it should not call the next filter by invoking {3}. + /// + internal static string AsyncResultFilter_InvalidShortCircuit + { + get { return GetString("AsyncResultFilter_InvalidShortCircuit"); } + } + + /// + /// If an {0} cancels execution by setting the {1} property of {2} to 'true', then the it should not call the next filter by invoking {3}. + /// + internal static string FormatAsyncResultFilter_InvalidShortCircuit(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AsyncResultFilter_InvalidShortCircuit"), p0, p1, p2, p3); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs index 242583262a..98ed5318af 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionInvoker.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; -using System.Reflection; +using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.Mvc.Core; -using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc @@ -19,9 +19,18 @@ namespace Microsoft.AspNet.Mvc private readonly IActionBindingContextProvider _bindingProvider; private readonly INestedProviderManager _filterProvider; - private readonly List _authorizationFilters = new List(); - private readonly List _actionFilters = new List(); - private readonly List _actionResultFilters = new List(); + private IFilter[] _filters; + private FilterCursor _cursor; + + private ExceptionContext _exceptionContext; + + private AuthorizationContext _authorizationContext; + + private ActionExecutingContext _actionExecutingContext; + private ActionExecutedContext _actionExecutedContext; + + private ResultExecutingContext _resultExecutingContext; + private ResultExecutedContext _resultExecutedContext; public ReflectedActionInvoker([NotNull] ActionContext actionContext, [NotNull] ReflectedActionDescriptor descriptor, @@ -48,71 +57,184 @@ namespace Microsoft.AspNet.Mvc public async Task InvokeActionAsync() { - var filterMetaItems = GetAndArrangeFilters(); + _filters = GetFilters(); + _cursor = new FilterCursor(_filters); - var controller = _controllerFactory.CreateController(_actionContext); + // >> ExceptionFilters >> AuthorizationFilters >> ActionFilters >> Action + await InvokeActionExceptionFilters(); - if (controller == null) + // If Exception Filters or Authorization Filters provide a result, it's a short-circuit, we don't execute + // result filters around it. + if (_authorizationContext.Result != null) { - throw new InvalidOperationException( - Resources.FormatMethodMustReturnNotNullValue(typeof(IControllerFactory), - "controller")); + await _authorizationContext.Result.ExecuteResultAsync(_actionContext); } - - try + else if (_exceptionContext.Result != null) { - var actionResult = await RunAuthorizationFilters(filterMetaItems) ?? - await RunActionFiltersAndActions(filterMetaItems, controller); - - await RunActionResultFilters(actionResult, filterMetaItems); + await _exceptionContext.Result.ExecuteResultAsync(_actionContext); } - finally + else if (_exceptionContext.Exception != null) { - _controllerFactory.ReleaseController(controller); - } - } - - private FilterItem[] GetAndArrangeFilters() - { - var filterProviderContext = - new FilterProviderContext(_descriptor, - _descriptor. - FilterDescriptors. - Select(fd => new FilterItem(fd)).ToList()); - - _filterProvider.Invoke(filterProviderContext); - var filterMetaItems = filterProviderContext.Result.ToArray(); - - PreArrangeFiltersInPipeline(filterProviderContext); - - return filterMetaItems; - } - - private async Task RunAuthorizationFilters(FilterItem[] filterMetaItems) - { - if (_authorizationFilters.Count > 0) - { - var authZEndPoint = new AuthorizationFilterEndPoint(); - _authorizationFilters.Add(authZEndPoint); - - var authZContext = new AuthorizationFilterContext(_actionContext, filterMetaItems); - var authZPipeline = new FilterPipelineBuilder(_authorizationFilters, authZContext); - - await authZPipeline.InvokeAsync(); - - if (authZContext.ActionResult != null || - authZContext.HasFailed || - !authZEndPoint.WasEndPointCalled) + // If we get here, this means that we have an unhandled exception + if (_exceptionContext.ExceptionDispatchInfo != null) { - // User cleaned out the result but we failed or short circuited the end point. - return authZContext.ActionResult ?? new HttpStatusCodeResult(401); + _exceptionContext.ExceptionDispatchInfo.Throw(); + } + else + { + throw _exceptionContext.Exception; } } + else + { + var result = _actionExecutedContext.Result; - return null; + // >> ResultFilters >> (Result) + await InvokeActionResultWithFilters(result); + } } - private async Task> GetParameterValues(ModelStateDictionary modelState) + private IFilter[] GetFilters() + { + var filterProviderContext = new FilterProviderContext( + _descriptor, + _descriptor.FilterDescriptors.Select(fd => new FilterItem(fd)).ToList()); + + _filterProvider.Invoke(filterProviderContext); + + return filterProviderContext.Result.Select(item => item.Filter).Where(filter => filter != null).ToArray(); + } + + private async Task InvokeActionExceptionFilters() + { + _cursor.SetStage(FilterStage.ExceptionFilters); + + await InvokeExceptionFilter(); + } + + private async Task InvokeExceptionFilter() + { + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + // Exception filters run "on the way out" - so the filter is run after the rest of the + // pipeline. + await InvokeExceptionFilter(); + + Contract.Assert(_exceptionContext != null); + if (_exceptionContext.Exception != null) + { + // Exception filters only run when there's an exception - unsetting it will short-circuit + // other exception filters. + await current.FilterAsync.OnActionExecutedAsync(_exceptionContext); + } + } + else if (current.Filter != null) + { + // Exception filters run "on the way out" - so the filter is run after the rest of the + // pipeline. + await InvokeExceptionFilter(); + + Contract.Assert(_exceptionContext != null); + if (_exceptionContext.Exception != null) + { + // Exception filters only run when there's an exception - unsetting it will short-circuit + // other exception filters. + current.Filter.OnActionExecuted(_exceptionContext); + } + } + else + { + // We've reached the 'end' of the exception filter pipeline - this means that one stack frame has + // been built for each exception. When we return from here, these frames will either: + // + // 1) Call the filter (if we have an exception) + // 2) No-op (if we don't have an exception) + Contract.Assert(_exceptionContext == null); + _exceptionContext = new ExceptionContext(_actionContext, _filters); + + try + { + await InvokeActionAuthorizationFilters(); + + Contract.Assert(_authorizationContext != null); + if (_authorizationContext.Result == null) + { + // Authorization passed, run authorization filters and the action + await InvokeActionMethodWithFilters(); + + // Action filters might 'return' an unahndled exception instead of throwing + Contract.Assert(_actionExecutedContext != null); + if (_actionExecutedContext.Exception != null && !_actionExecutedContext.ExceptionHandled) + { + _exceptionContext.Exception = _actionExecutedContext.Exception; + if (_actionExecutedContext.ExceptionDispatchInfo != null) + { + _exceptionContext.ExceptionDispatchInfo = _actionExecutedContext.ExceptionDispatchInfo; + } + } + } + } + catch (Exception exception) + { + _exceptionContext.ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception); + } + } + } + + private async Task InvokeActionAuthorizationFilters() + { + _cursor.SetStage(FilterStage.AuthorizationFilters); + + _authorizationContext = new AuthorizationContext(_actionContext, _filters); + await InvokeAuthorizationFilter(); + } + + private async Task InvokeAuthorizationFilter() + { + // We should never get here if we already have a result. + Contract.Assert(_authorizationContext != null); + Contract.Assert(_authorizationContext.Result == null); + + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + await current.FilterAsync.OnAuthorizationAsync(_authorizationContext); + + if (_authorizationContext.Result == null) + { + // Only keep going if we don't have a result + await InvokeAuthorizationFilter(); + } + } + else if (current.Filter != null) + { + current.Filter.OnAuthorization(_authorizationContext); + + if (_authorizationContext.Result == null) + { + // Only keep going if we don't have a result + await InvokeAuthorizationFilter(); + } + } + else + { + // We've run out of Authorization Filters - if we haven't short circuited by now then this + // request is authorized. + } + } + + private async Task InvokeActionMethodWithFilters() + { + _cursor.SetStage(FilterStage.ActionFilters); + + var arguments = await GetActionArguments(_actionContext.ModelState); + _actionExecutingContext = new ActionExecutingContext(_actionContext, _filters, arguments); + + await InvokeActionMethodFilter(); + } + + private async Task> GetActionArguments(ModelStateDictionary modelState) { var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(_actionContext); var parameters = _descriptor.Parameters; @@ -164,69 +286,304 @@ namespace Microsoft.AspNet.Mvc return parameterValues; } - private async Task RunActionFiltersAndActions(FilterItem[] filterMetaItems, object controller) + private async Task InvokeActionMethodFilter() { - var parameterValues = await GetParameterValues(_actionContext.ModelState); - - var actionFilterContext = new ActionFilterContext(_actionContext, - filterMetaItems, - parameterValues); - - var actionEndPoint = new ReflectedActionFilterEndPoint(_actionResultFactory, controller); - - _actionFilters.Add(actionEndPoint); - var actionFilterPipeline = new FilterPipelineBuilder(_actionFilters, - actionFilterContext); - - await actionFilterPipeline.InvokeAsync(); - - return actionFilterContext.ActionResult; - } - - private async Task RunActionResultFilters(IActionResult actionResult, FilterItem[] filterMetaItems) - { - var actionResultFilterContext = new ActionResultFilterContext(_actionContext, filterMetaItems, actionResult); - var actionResultFilterEndPoint = new ActionResultFilterEndPoint(); - _actionResultFilters.Add(actionResultFilterEndPoint); - - var actionResultPipeline = new FilterPipelineBuilder(_actionResultFilters, - actionResultFilterContext); - - await actionResultPipeline.InvokeAsync(); - } - - private void PreArrangeFiltersInPipeline(FilterProviderContext context) - { - if (context.Result == null || context.Result.Count == 0) + Contract.Assert(_actionExecutingContext != null); + if (_actionExecutingContext.Result != null) { - return; + // If we get here, it means that an async filter set a result AND called next(). This is forbidden. + var message = Resources.FormatAsyncActionFilter_InvalidShortCircuit( + typeof(IAsyncActionFilter).Name, + "Result", + typeof(ActionExecutingContext).Name, + typeof(ActionExecutionDelegate).Name); + + throw new InvalidOperationException(message); } - foreach (var filter in context.Result) + var item = _cursor.GetNextFilter(); + try { - PlaceFilter(filter.Filter); + if (item.FilterAsync != null) + { + await item.FilterAsync.OnActionExecutionAsync(_actionExecutingContext, InvokeActionMethodFilter); + + if (_actionExecutedContext == null) + { + // If we get here then the filter didn't call 'next' indicating a short circuit + _actionExecutedContext = new ActionExecutedContext(_actionExecutingContext, _filters) + { + Canceled = true, + Result = _actionExecutingContext.Result, + }; + } + } + else if (item.Filter != null) + { + item.Filter.OnActionExecuting(_actionExecutingContext); + + if (_actionExecutingContext.Result != null) + { + // Short-circuited by setting a result. + _actionExecutedContext = new ActionExecutedContext(_actionExecutingContext, _filters) + { + Canceled = true, + Result = _actionExecutingContext.Result, + }; + } + else + { + item.Filter.OnActionExecuted(await InvokeActionMethodFilter()); + } + } + else + { + // All action filters have run, execute the action method. + _actionExecutedContext = new ActionExecutedContext(_actionExecutingContext, _filters) + { + Result = await InvokeActionMethod() + }; + } + } + catch (Exception exception) + { + // Exceptions thrown by the action method OR filters bubble back up through ActionExcecutedContext. + _actionExecutedContext = new ActionExecutedContext(_actionExecutingContext, _filters) + { + Exception = exception, + ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception) + }; + } + return _actionExecutedContext; + } + + private async Task InvokeActionMethod() + { + _cursor.SetStage(FilterStage.ActionMethod); + + var controller = _controllerFactory.CreateController(_actionContext); + + var actionMethodInfo = _descriptor.MethodInfo; + var actionReturnValue = await ReflectedActionExecutor.ExecuteAsync( + actionMethodInfo, + controller, + _actionExecutingContext.ActionArguments); + + var underlyingReturnType = TypeHelper.GetTaskInnerTypeOrNull(actionMethodInfo.ReturnType) ?? actionMethodInfo.ReturnType; + var actionResult = _actionResultFactory.CreateActionResult( + underlyingReturnType, + actionReturnValue, + _actionContext); + return actionResult; + } + + private async Task InvokeActionResultWithFilters(IActionResult result) + { + _cursor.SetStage(FilterStage.ResultFilters); + + _resultExecutingContext = new ResultExecutingContext(_actionContext, _filters, result); + await InvokeActionResultFilter(); + + Contract.Assert(_resultExecutingContext != null); + if (_resultExecutedContext.Exception != null && !_resultExecutedContext.ExceptionHandled) + { + // There's an unhandled exception in filters + if (_resultExecutedContext.ExceptionDispatchInfo != null) + { + _resultExecutedContext.ExceptionDispatchInfo.Throw(); + } + else + { + throw _resultExecutedContext.Exception; + } } } - private void PlaceFilter(object filter) + private async Task InvokeActionResultFilter() { - var authFilter = filter as IAuthorizationFilter; - var actionFilter = filter as IActionFilter; - var actionResultFilter = filter as IActionResultFilter; - - if (authFilter != null) + Contract.Assert(_resultExecutingContext != null); + if (_resultExecutingContext.Cancel == true) { - _authorizationFilters.Add(authFilter); + // If we get here, it means that an async filter set cancel == true AND called next(). This is forbidden. + var message = Resources.FormatAsyncResultFilter_InvalidShortCircuit( + typeof(IAsyncResultFilter).Name, + "Cancel", + typeof(ResultExecutingContext).Name, + typeof(ResultExecutionDelegate).Name); + + throw new InvalidOperationException(message); } - if (actionFilter != null) + try { - _actionFilters.Add(actionFilter); + var item = _cursor.GetNextFilter(); + if (item.FilterAsync != null) + { + await item.FilterAsync.OnResultExecutionAsync(_resultExecutingContext, InvokeActionResultFilter); + + if (_resultExecutedContext == null) + { + // Short-circuited by not calling next + _resultExecutedContext = new ResultExecutedContext(_resultExecutingContext, _filters, _resultExecutingContext.Result) + { + Canceled = true, + }; + } + else if (_resultExecutingContext.Cancel == true) + { + // Short-circuited by setting Cancel == true + _resultExecutedContext = new ResultExecutedContext(_resultExecutingContext, _filters, _resultExecutingContext.Result) + { + Canceled = true, + }; + } + } + else if (item.Filter != null) + { + item.Filter.OnResultExecuting(_resultExecutingContext); + + if (_resultExecutingContext.Cancel == true) + { + // Short-circuited by setting Cancel == true + _resultExecutedContext = new ResultExecutedContext(_resultExecutingContext, _filters, _resultExecutingContext.Result) + { + Canceled = true, + }; + } + else + { + item.Filter.OnResultExecuted(await InvokeActionResultFilter()); + } + } + else + { + await InvokeActionResult(); + + Contract.Assert(_resultExecutedContext == null); + _resultExecutedContext = new ResultExecutedContext(_resultExecutingContext, _filters, _resultExecutingContext.Result); + } + } + catch (Exception exception) + { + _resultExecutedContext = new ResultExecutedContext(_resultExecutingContext, _filters, _resultExecutingContext.Result) + { + Exception = exception, + ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception) + }; } - if (actionResultFilter != null) + return _resultExecutedContext; + } + + private async Task InvokeActionResult() + { + _cursor.SetStage(FilterStage.ActionResult); + + // The empty result is always flowed back as the 'executed' result + if (_resultExecutingContext.Result == null) { - _actionResultFilters.Add(actionResultFilter); + _resultExecutingContext.Result = new EmptyResult(); + } + + await _resultExecutingContext.Result.ExecuteResultAsync(_resultExecutingContext); + } + + private enum FilterStage + { + Undefined, + ExceptionFilters, + AuthorizationFilters, + ActionFilters, + ActionMethod, + ResultFilters, + ActionResult + }; + + /// + /// A one-way cursor for filters. + /// + /// + /// This will iterate the filter collection once per-stage, and skip any filters that don't have + /// the one of interfaces that applies to the current stage. + /// + /// Filters are always executed in the following order, but short circuiting plays a role. + /// + /// Indentation reflects nesting. + /// + /// 1. Exception Filters + /// 2. Authorization Filters + /// 3. Action Filters + /// Action + /// + /// 4. Result Filters + /// Result + /// + /// + private struct FilterCursor + { + private FilterStage Stage; + private int Index; + private readonly IFilter[] Filters; + + public FilterCursor(FilterStage stage, int index, IFilter[] filters) + { + Stage = stage; + Index = index; + Filters = filters; + } + + public FilterCursor(IFilter[] filters) + { + Stage = FilterStage.Undefined; + Index = 0; + Filters = filters; + } + + public void SetStage(FilterStage stage) + { + Stage = stage; + Index = 0; + } + + public FilterCursorItem GetNextFilter() + where TFilter : class + where TFilterAsync : class + { + while (Index < Filters.Length) + { + var filter = Filters[Index] as TFilter; + var filterAsync = Filters[Index] as TFilterAsync; + + Index += 1; + + if (filter != null || filterAsync != null) + { + return new FilterCursorItem(Stage, Index, filter, filterAsync); + } + } + + return default(FilterCursorItem); + } + + public bool StillAt(FilterCursorItem current) + { + return current.Stage == Stage && current.Index == Index; + } + } + + private struct FilterCursorItem + { + public readonly FilterStage Stage; + public readonly int Index; + public readonly TFilter Filter; + public readonly TFilterAsync FilterAsync; + + public FilterCursorItem(FilterStage stage, int index, TFilter filter, TFilterAsync filterAsync) + { + Stage = stage; + Index = index; + Filter = filter; + FilterAsync = filterAsync; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 1ba191cef9..e96f8c6796 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -219,4 +219,10 @@ No route matches the supplied values. - + + If an {0} provides a result value by setting the {1} property of {2} to a non-null value, then the it should not call the next filter by invoking {3}. + + + If an {0} cancels execution by setting the {1} property of {2} to 'true', then the it should not call the next filter by invoking {3}. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs index 4e17ae012a..73930c3294 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Properties/Resources.Designer.cs @@ -383,7 +383,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index 97d038e5a3..7d4618123b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -63,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Host var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 9487c23071..7468917dbd 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -239,7 +239,7 @@ namespace Microsoft.AspNet.Mvc.Razor var value = _resourceManager.GetString(name); System.Diagnostics.Debug.Assert(value != null); - + if (formatterNames != null) { for (var i = 0; i < formatterNames.Length; i++) diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 1628f612a4..e38a508a74 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -31,6 +31,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs new file mode 100644 index 0000000000..05ab0ccb12 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs @@ -0,0 +1,1265 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.Mvc.Filters; +using Microsoft.AspNet.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ReflectedActionInvokerTest + { + // Intentionally choosing an uncommon exception type. + private readonly Exception _actionException = new TimeZoneNotFoundException(); + + private readonly JsonResult _result = new JsonResult(new { message = "Hello, world!" }); + + [Fact] + public async Task InvokeAction_DoesNotInvokeExceptionFilter_WhenActionDoesNotThrow() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: false); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task InvokeAction_DoesNotAsyncInvokeExceptionFilter_WhenActionDoesNotThrow() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecutedAsync(It.IsAny())) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: false); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify( + f => f.OnActionExecutedAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task InvokeAction_InvokesExceptionFilter_WhenActionThrows() + { + // Arrange + Exception exception = null; + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(context => + { + exception = context.Exception; + result = context.Result; + + // Handle the exception + context.Result = new EmptyResult(); + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + Assert.Same(_actionException, exception); + Assert.Null(result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncExceptionFilter_WhenActionThrows() + { + // Arrange + Exception exception = null; + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecutedAsync(It.IsAny())) + .Callback(context => + { + exception = context.Exception; + result = context.Result; + + // Handle the exception + context.Result = new EmptyResult(); + }) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify( + f => f.OnActionExecutedAsync(It.IsAny()), + Times.Once()); + + Assert.Same(_actionException, exception); + Assert.Null(result); + } + + [Fact] + public async Task InvokeAction_InvokesExceptionFilter_ShortCircuit() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(context => + { + context.Exception = null; + }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter2.Verify( + f => f.OnActionExecuted(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncExceptionFilter_ShortCircuit() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnActionExecutedAsync(It.IsAny())) + .Callback(context => + { + context.Exception = null; + }) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object }, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter2.Verify( + f => f.OnActionExecutedAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesExceptionFilter_UnhandledExceptionIsThrown() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: true); + + // Act + await Assert.ThrowsAsync(_actionException.GetType(), async () => await invoker.InvokeActionAsync()); + + // Assert + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesExceptionFilter_ResultIsExecuted_WithoutResultFilters() + { + // Arrange + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => c.Result = result.Object) + .Verifiable(); + + var resultFilter = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { filter.Object, resultFilter.Object }, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAuthorizationFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter.Setup(f => f.OnAuthorization(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncAuthorizationFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnAuthorizationAsync(It.IsAny())) + .Returns(context => Task.FromResult(null)) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify( + f => f.OnAuthorizationAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAuthorizationFilter_ShortCircuit() + { + // Arrange + var challenge = new Mock(MockBehavior.Loose).Object; + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnAuthorization(It.IsAny())) + .Callback(c => c.Result = challenge) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncAuthorizationFilter_ShortCircuit() + { + // Arrange + var challenge = new Mock(MockBehavior.Loose).Object; + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnAuthorizationAsync(It.IsAny())) + .Returns((context) => + { + context.Result = challenge; + return Task.FromResult(null); + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify( + f => f.OnAuthorizationAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_ExceptionInAuthorizationFilterHandledByExceptionFilters() + { + // Arrange + Exception exception = null; + var expected = new InvalidCastException(); + + var exceptionFilter = new Mock(MockBehavior.Strict); + exceptionFilter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(context => + { + exception = context.Exception; + + // Mark as handled + context.Result = new EmptyResult(); + }) + .Verifiable(); + + var authorizationFilter1 = new Mock(MockBehavior.Strict); + authorizationFilter1 + .Setup(f => f.OnAuthorization(It.IsAny())) + .Callback(c => { throw expected; }) + .Verifiable(); + + // None of these filters should run + var authorizationFilter2 = new Mock(MockBehavior.Strict); + var actionFilter = new Mock(MockBehavior.Strict); + var resultFilter = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] + { + exceptionFilter.Object, + authorizationFilter1.Object, + authorizationFilter2.Object, + actionFilter.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + exceptionFilter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + authorizationFilter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAuthorizationFilter_ChallengeNotSeenByResultFilters() + { + // Arrange + var challenge = new Mock(MockBehavior.Strict); + challenge + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var authorizationFilter = new Mock(MockBehavior.Strict); + authorizationFilter + .Setup(f => f.OnAuthorization(It.IsAny())) + .Callback(c => c.Result = challenge.Object) + .Verifiable(); + + var resultFilter = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { authorizationFilter.Object, resultFilter.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + authorizationFilter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + challenge.Verify(c => c.ExecuteResultAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesActionFilter() + { + // Arrange + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => result = c.Result) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + Assert.Same(_result, result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncActionFilter() + { + // Arrange + IActionResult result = null; + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (context, next) => + { + var resultContext = await next(); + result = resultContext.Result; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify( + f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + Assert.Same(_result, result); + } + + [Fact] + public async Task InvokeAction_InvokesActionFilter_ShortCircuit() + { + // Arrange + var result = new EmptyResult(); + + ActionExecutedContext context = null; + + var actionFilter1 = new Mock(MockBehavior.Strict); + actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + actionFilter1 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var actionFilter2 = new Mock(MockBehavior.Strict); + actionFilter2 + .Setup(f => f.OnActionExecuting(It.IsAny())) + .Callback(c => c.Result = result) + .Verifiable(); + + var actionFilter3 = new Mock(MockBehavior.Strict); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilter[] + { + actionFilter1.Object, + actionFilter2.Object, + actionFilter3.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + actionFilter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Same(context.Result, result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult() + { + // Arrange + var result = new EmptyResult(); + + ActionExecutedContext context = null; + + var actionFilter1 = new Mock(MockBehavior.Strict); + actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + actionFilter1 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var actionFilter2 = new Mock(MockBehavior.Strict); + actionFilter2 + .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Notice we're not calling next + c.Result = result; + return Task.FromResult(null); + }) + .Verifiable(); + + var actionFilter3 = new Mock(MockBehavior.Strict); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilter[] + { + actionFilter1.Object, + actionFilter2.Object, + actionFilter3.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + actionFilter2.Verify( + f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Same(context.Result, result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithoutResult() + { + // Arrange + ActionExecutedContext context = null; + + var actionFilter1 = new Mock(MockBehavior.Strict); + actionFilter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + actionFilter1 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var actionFilter2 = new Mock(MockBehavior.Strict); + actionFilter2 + .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Notice we're not calling next + return Task.FromResult(null); + }) + .Verifiable(); + + var actionFilter3 = new Mock(MockBehavior.Strict); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilter[] + { + actionFilter1.Object, + actionFilter2.Object, + actionFilter3.Object, + resultFilter.Object, + }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + actionFilter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + actionFilter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + actionFilter2.Verify( + f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncActionFilter_ShortCircuit_WithResult_CallNext() + { + // Arrange + var actionFilter = new Mock(MockBehavior.Strict); + actionFilter + .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + c.Result = new EmptyResult(); + await next(); + }) + .Verifiable(); + + var message = + "If an IAsyncActionFilter provides a result value by setting the Result property of " + + "ActionExecutingContext to a non-null value, then the it should not call the next filter by invoking " + + "ActionExecutionDelegate."; + + var invoker = CreateInvoker(actionFilter.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + async () => await invoker.InvokeActionAsync(), + message); + } + + [Fact] + public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByAction() + { + // Arrange + ActionExecutedContext context = null; + + var filter = new Mock(MockBehavior.Strict); + filter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + filter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception so the test doesn't throw. + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + Assert.Same(_actionException, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesActionFilter_WithExceptionThrownByActionFilter() + { + // Arrange + var exception = new DataMisalignedException(); + ActionExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + filter1 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception so the test doesn't throw. + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + filter2 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => { throw exception; }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + filter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + + Assert.Same(exception, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncActionFilter_WithExceptionThrownByActionFilter() + { + // Arrange + var exception = new DataMisalignedException(); + ActionExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + + // Handle the exception so the test doesn't throw. + Assert.False(context.ExceptionHandled); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + filter2 + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => { throw exception; }) + .Verifiable(); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify( + f => f.OnActionExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + filter2.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + + Assert.Same(exception, context.Exception); + Assert.Null(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesActionFilter_HandleException() + { + // Arrange + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns((context) => Task.FromResult(null)) + .Verifiable(); + + var actionFilter = new Mock(MockBehavior.Strict); + actionFilter.Setup(f => f.OnActionExecuting(It.IsAny())).Verifiable(); + actionFilter + .Setup(f => f.OnActionExecuted(It.IsAny())) + .Callback(c => + { + // Handle the exception so the test doesn't throw. + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + + c.Result = result.Object; + }) + .Verifiable(); + + var resultFilter = new Mock(MockBehavior.Strict); + resultFilter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new IFilter[] { actionFilter.Object, resultFilter.Object }, actionThrows: true); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + actionFilter.Verify(f => f.OnActionExecuting(It.IsAny()), Times.Once()); + actionFilter.Verify(f => f.OnActionExecuted(It.IsAny()), Times.Once()); + + resultFilter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesResultFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + filter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (context, next) => await next()) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter.Verify( + f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesResultFilter_ShortCircuit() + { + // Arrange + ResultExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + filter1 + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => c.Cancel = true) + .Verifiable(); + + var filter3 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object, filter3.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + filter2.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + + Assert.True(context.Canceled); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter_ShortCircuit_WithCancel() + { + // Arrange + ResultExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + filter1 + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Not calling next here + c.Cancel = true; + return Task.FromResult(null); + }) + .Verifiable(); + + var filter3 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object, filter3.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + filter2.Verify( + f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + Assert.True(context.Canceled); + Assert.IsType(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter_ShortCircuit_WithoutCancel() + { + // Arrange + ResultExecutedContext context = null; + + var filter1 = new Mock(MockBehavior.Strict); + filter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + filter1 + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => context = c) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns((c, next) => + { + // Not calling next here + return Task.FromResult(null); + }) + .Verifiable(); + + var filter3 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { filter1.Object, filter2.Object, filter3.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + filter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + filter2.Verify( + f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + Assert.True(context.Canceled); + Assert.IsType(context.Result); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter_ShortCircuit_WithoutCancel_CallNext() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + // Not calling next here + c.Cancel = true; + await next(); + }) + .Verifiable(); + + var message = + "If an IAsyncResultFilter cancels execution by setting the Cancel property of " + + "ResultExecutingContext to 'true', then the it should not call the next filter by invoking " + + "ResultExecutionDelegate."; + + var invoker = CreateInvoker(filter.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + async () => await invoker.InvokeActionAsync(), + message); + } + + [Fact] + public async Task InvokeAction_InvokesResultFilter_ExceptionGoesUnhandled() + { + // Arrange + var exception = new DataMisalignedException(); + + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Throws(exception) + .Verifiable(); + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => c.Result = result.Object) + .Verifiable(); + + filter.Setup(f => f.OnResultExecuted(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await Assert.ThrowsAsync(exception.GetType(), async () => await invoker.InvokeActionAsync()); + + // Assert + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + + filter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesResultFilter_WithExceptionThrownByResult() + { + // Arrange + ResultExecutedContext context = null; + var exception = new DataMisalignedException(); + + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Throws(exception) + .Verifiable(); + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Callback(c => c.Result = result.Object) + .Verifiable(); + + filter + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + Assert.Equal(exception, context.Exception); + + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + + filter.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + filter.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter_WithExceptionThrownByResult() + { + // Arrange + ResultExecutedContext context = null; + var exception = new DataMisalignedException(); + + var result = new Mock(MockBehavior.Strict); + result + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Throws(exception) + .Verifiable(); + + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + c.Result = result.Object; + + context = await next(); + + // Handle the exception + Assert.False(context.ExceptionHandled); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var invoker = CreateInvoker(filter.Object); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + Assert.Equal(exception, context.Exception); + + result.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + + filter.Verify( + f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesResultFilter_WithExceptionThrownByResultFilter() + { + // Arrange + ResultExecutedContext context = null; + var exception = new DataMisalignedException(); + + var resultFilter1 = new Mock(MockBehavior.Strict); + resultFilter1.Setup(f => f.OnResultExecuting(It.IsAny())).Verifiable(); + resultFilter1 + .Setup(f => f.OnResultExecuted(It.IsAny())) + .Callback(c => + { + context = c; + + // Handle the exception + Assert.False(c.ExceptionHandled); + c.ExceptionHandled = true; + }) + .Verifiable(); + + var resultFilter2 = new Mock(MockBehavior.Strict); + resultFilter2 + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Throws(exception) + .Verifiable(); + + var resultFilter3 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { resultFilter1.Object, resultFilter2.Object, resultFilter3.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + Assert.Equal(exception, context.Exception); + + resultFilter1.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + resultFilter1.Verify(f => f.OnResultExecuted(It.IsAny()), Times.Once()); + + resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAction_InvokesAsyncResultFilter_WithExceptionThrownByResultFilter() + { + // Arrange + ResultExecutedContext context = null; + var exception = new DataMisalignedException(); + + var resultFilter1 = new Mock(MockBehavior.Strict); + resultFilter1 + .Setup(f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny())) + .Returns(async (c, next) => + { + context = await next(); + + // Handle the exception + Assert.False(context.ExceptionHandled); + context.ExceptionHandled = true; + }) + .Verifiable(); + + var resultFilter2 = new Mock(MockBehavior.Strict); + resultFilter2 + .Setup(f => f.OnResultExecuting(It.IsAny())) + .Throws(exception) + .Verifiable(); + + var resultFilter3 = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilter[] { resultFilter1.Object, resultFilter2.Object, resultFilter3.Object }); + + // Act + await invoker.InvokeActionAsync(); + + // Assert + Assert.Equal(exception, context.Exception); + + resultFilter1.Verify( + f => f.OnResultExecutionAsync(It.IsAny(), It.IsAny()), + Times.Once()); + + resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny()), Times.Once()); + } + + private ReflectedActionInvoker CreateInvoker(IFilter filter, bool actionThrows = false) + { + return CreateInvoker(new[] { filter }, actionThrows); + } + + private ReflectedActionInvoker CreateInvoker(IFilter[] filters, bool actionThrows = false) + { + var actionDescriptor = new ReflectedActionDescriptor() + { + FilterDescriptors = new List(), + Parameters = new List(), + }; + + if (actionThrows) + { + actionDescriptor.MethodInfo = typeof(ReflectedActionInvokerTest).GetMethod("ThrowingActionMethod"); + } + else + { + actionDescriptor.MethodInfo = typeof(ReflectedActionInvokerTest).GetMethod("ActionMethod"); + } + + var httpContext = new Mock(MockBehavior.Loose); + var httpResponse = new Mock(MockBehavior.Loose); + httpContext.SetupGet(c => c.Response).Returns(httpResponse.Object); + httpResponse.SetupGet(r => r.Body).Returns(new MemoryStream()); + + var actionContext = new ActionContext( + httpContext: httpContext.Object, + router: null, + routeValues: null, + actionDescriptor: actionDescriptor); + + var actionResultFactory = new Mock(MockBehavior.Strict); + actionResultFactory + .Setup(arf => arf.CreateActionResult(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((t, o, ac) => (IActionResult)o); + + var controllerFactory = new Mock(MockBehavior.Strict); + controllerFactory.Setup(c => c.CreateController(It.IsAny())).Returns(this); + + var actionBindingContextProvider = new Mock(MockBehavior.Strict); + actionBindingContextProvider + .Setup(abcp => abcp.GetActionBindingContextAsync(It.IsAny())) + .Returns(Task.FromResult(new ActionBindingContext(null, null, null, null, null, null))); + + var filterProvider = new Mock>(MockBehavior.Strict); + filterProvider + .Setup(fp => fp.Invoke(It.IsAny())) + .Callback( + context => context.Result.AddRange(filters.Select(f => new FilterItem(null, f)))); + + var invoker = new ReflectedActionInvoker( + actionContext, + actionDescriptor, + actionResultFactory.Object, + controllerFactory.Object, + actionBindingContextProvider.Object, + filterProvider.Object); + + return invoker; + } + + public JsonResult ActionMethod() + { + return _result; + } + + public JsonResult ThrowingActionMethod() + { + throw _actionException; + } + } +}