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.
This commit is contained in:
parent
93c9b3419e
commit
f19fe0cbef
|
|
@ -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<Task> next)
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
object age = null;
|
||||
|
||||
|
|
@ -28,8 +26,6 @@ namespace MvcSample.Web.Filters
|
|||
context.ActionArguments["age"] = intAge;
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Task> next)
|
||||
public override void OnAuthorization(AuthorizationContext context)
|
||||
{
|
||||
if (!context.HasAllowAnonymous())
|
||||
if (!HasAllowAnonymous(context))
|
||||
{
|
||||
context.Fail();
|
||||
context.Result = new HttpStatusCodeResult(401);
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<Task> next)
|
||||
#pragma warning disable 1998
|
||||
public override async Task OnAuthorizationAsync(AuthorizationContext context)
|
||||
{
|
||||
await next();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Task> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,8 @@
|
|||
<Compile Include="FiltersController.cs" />
|
||||
<Compile Include="Filters\AgeEnhancerFilterAttribute.cs" />
|
||||
<Compile Include="Filters\BlockAnonymous.cs" />
|
||||
<Compile Include="Filters\DelayAttribute.cs" />
|
||||
<Compile Include="Filters\ErrorMessagesAttribute.cs" />
|
||||
<Compile Include="Filters\InspectResultPageAttribute.cs" />
|
||||
<Compile Include="Filters\PassThroughAttribute.cs" />
|
||||
<Compile Include="Filters\UserNameProvider.cs" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IFilter> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class ActionExecutingContext : FilterContext
|
||||
{
|
||||
public ActionExecutingContext(
|
||||
[NotNull] ActionContext actionContext,
|
||||
[NotNull] IList<IFilter> filters,
|
||||
[NotNull] IDictionary<string, object> actionArguments)
|
||||
: base(actionContext, filters)
|
||||
{
|
||||
ActionArguments = actionArguments;
|
||||
}
|
||||
|
||||
public virtual IActionResult Result { get; set; }
|
||||
|
||||
public virtual IDictionary<string, object> ActionArguments { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public delegate Task<ActionExecutedContext> ActionExecutionDelegate();
|
||||
}
|
||||
|
|
@ -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<Task> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FilterItem> filterItems,
|
||||
[NotNull] IDictionary<string, object> actionArguments)
|
||||
: base(actionContext, filterItems)
|
||||
{
|
||||
ActionArguments = actionArguments;
|
||||
}
|
||||
|
||||
public virtual IDictionary<string, object> ActionArguments { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> next);
|
||||
|
||||
public int Order { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FilterItem> filterItems, IActionResult initialResult)
|
||||
: base(actionContext, filterItems)
|
||||
{
|
||||
ActionResult = initialResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<IFilter> filters)
|
||||
: base(actionContext, filters)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual IActionResult Result { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FilterItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> next)
|
||||
{
|
||||
WasEndPointCalled = true;
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IFilter> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FilterItem> filterItems)
|
||||
public FilterContext(
|
||||
[NotNull] ActionContext actionContext,
|
||||
[NotNull] IList<IFilter> filters)
|
||||
: base(actionContext)
|
||||
{
|
||||
ActionContext = actionContext;
|
||||
FilterItems = filterItems;
|
||||
Filters = filters;
|
||||
}
|
||||
|
||||
public ActionContext ActionContext { get; private set; }
|
||||
public IReadOnlyList<FilterItem> FilterItems { get; private set; }
|
||||
|
||||
// Result
|
||||
public virtual IActionResult ActionResult { get; set; }
|
||||
public virtual IList<IFilter> Filters { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<TContext>
|
||||
{
|
||||
private readonly IFilter<TContext>[] _filters;
|
||||
private readonly TContext _context;
|
||||
|
||||
// FilterDescriptors are already ordered externally.
|
||||
public FilterPipelineBuilder(IEnumerable<IFilter<TContext>> 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<TContext>[] _filters;
|
||||
private readonly Func<Task> _next;
|
||||
|
||||
private int _index;
|
||||
|
||||
public CallNextAsync(TContext context, IFilter<TContext>[] filters)
|
||||
{
|
||||
_context = context;
|
||||
_next = CallNextProvider;
|
||||
_filters = filters;
|
||||
}
|
||||
|
||||
public async Task CallNextProvider()
|
||||
{
|
||||
if (_filters.Length > _index)
|
||||
{
|
||||
await _filters[_index++].Invoke(_context, _next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IActionFilter : IFilter<ActionFilterContext>
|
||||
public interface IActionFilter : IFilter
|
||||
{
|
||||
void OnActionExecuting([NotNull] ActionExecutingContext context);
|
||||
|
||||
void OnActionExecuted([NotNull] ActionExecutedContext context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Microsoft.AspNet.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IActionResultFilter : IFilter<ActionResultFilterContext>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Microsoft.AspNet.Mvc.Filters
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAllowAnonymous : IFilter
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAsyncActionFilter : IFilter
|
||||
{
|
||||
Task OnActionExecutionAsync([NotNull] ActionExecutingContext context, [NotNull] ActionExecutionDelegate next);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAsyncAuthorizationFilter : IFilter
|
||||
{
|
||||
Task OnAuthorizationAsync([NotNull] AuthorizationContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAsyncExceptionFilter : IFilter
|
||||
{
|
||||
Task OnActionExecutedAsync([NotNull] ExceptionContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAsyncResultFilter : IFilter
|
||||
{
|
||||
Task OnResultExecutionAsync([NotNull] ResultExecutingContext context, [NotNull] ResultExecutionDelegate next);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IAuthorizationFilter : IFilter<AuthorizationFilterContext>
|
||||
public interface IAuthorizationFilter : IFilter
|
||||
{
|
||||
void OnAuthorization([NotNull] AuthorizationContext context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using Microsoft.AspNet.Mvc.Filters;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IExceptionFilter : IFilter<ExceptionFilterContext>
|
||||
public interface IExceptionFilter : IFilter
|
||||
{
|
||||
void OnActionExecuted([NotNull] ExceptionContext context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Filters
|
||||
{
|
||||
public interface IFilter<TContext>
|
||||
{
|
||||
Task Invoke(TContext context, Func<Task> next);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public interface IResultFilter : IFilter
|
||||
{
|
||||
void OnResultExecuting([NotNull] ResultExecutingContext context);
|
||||
|
||||
void OnResultExecuted([NotNull] ResultExecutedContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IFilter> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class ResultExecutingContext : FilterContext
|
||||
{
|
||||
public ResultExecutingContext(
|
||||
[NotNull] ActionContext actionContext,
|
||||
[NotNull] IList<IFilter> filters,
|
||||
[NotNull] IActionResult result)
|
||||
: base(actionContext, filters)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public virtual IActionResult Result { get; set; }
|
||||
|
||||
public virtual bool Cancel { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public delegate Task<ResultExecutedContext> ResultExecutionDelegate();
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,39 +53,41 @@
|
|||
<Compile Include="DefaultControllerDescriptorFactory.cs" />
|
||||
<Compile Include="DefaultControllerFactory.cs" />
|
||||
<Compile Include="DefaultParameterDescriptorFactory.cs" />
|
||||
<Compile Include="Extensions\FilterContextExtensions.cs" />
|
||||
<Compile Include="Extensions\IEnumerableExtensions.cs" />
|
||||
<Compile Include="Filters\ActionExecutedContext.cs" />
|
||||
<Compile Include="Filters\ActionExecutingContext.cs" />
|
||||
<Compile Include="Filters\ActionExecutionDelegate.cs" />
|
||||
<Compile Include="Filters\ActionFilterAttribute.cs" />
|
||||
<Compile Include="Filters\ActionFilterContext.cs" />
|
||||
<Compile Include="Filters\ActionFilterEndPoint.cs" />
|
||||
<Compile Include="Filters\ActionResultFilterAttribute.cs" />
|
||||
<Compile Include="Filters\ActionResultFilterContext.cs" />
|
||||
<Compile Include="Filters\ActionResultFilterEndPoint.cs" />
|
||||
<Compile Include="Filters\AllowAnonymousAttribute.cs" />
|
||||
<Compile Include="Filters\AuthorizationContext.cs" />
|
||||
<Compile Include="Filters\AuthorizationFilterAttribute.cs" />
|
||||
<Compile Include="Filters\AuthorizationFilterContext.cs" />
|
||||
<Compile Include="Filters\AuthorizationFilterEndPoint.cs" />
|
||||
<Compile Include="Filters\DefaultFilterProvider.cs" />
|
||||
<Compile Include="Filters\ExceptionContext.cs" />
|
||||
<Compile Include="Filters\ExceptionFilterAttribute.cs" />
|
||||
<Compile Include="Filters\ExceptionFilterContext.cs" />
|
||||
<Compile Include="Filters\FilterContext.cs" />
|
||||
<Compile Include="Filters\FilterDescriptor.cs" />
|
||||
<Compile Include="Filters\FilterDescriptorOrderComparer.cs" />
|
||||
<Compile Include="Filters\FilterItem.cs" />
|
||||
<Compile Include="Filters\FilterPipelineBuilder.cs" />
|
||||
<Compile Include="Filters\FilterProviderContext.cs" />
|
||||
<Compile Include="Filters\FilterScope.cs" />
|
||||
<Compile Include="Filters\IActionFilter.cs" />
|
||||
<Compile Include="Filters\IActionResultFilter.cs" />
|
||||
<Compile Include="Filters\IAllowAnonymous.cs" />
|
||||
<Compile Include="Filters\IAsyncActionFilter.cs" />
|
||||
<Compile Include="Filters\IAsyncAuthorizationFilter.cs" />
|
||||
<Compile Include="Filters\IAsyncExceptionFilter.cs" />
|
||||
<Compile Include="Filters\IAsyncResultFilter.cs" />
|
||||
<Compile Include="Filters\IAuthorizationFilter.cs" />
|
||||
<Compile Include="Filters\IExceptionFilter.cs" />
|
||||
<Compile Include="Filters\IFilter.cs" />
|
||||
<Compile Include="Filters\IFilterContainer.cs" />
|
||||
<Compile Include="Filters\IFilterOfTContext.cs" />
|
||||
<Compile Include="Filters\IOrderedFilter.cs" />
|
||||
<Compile Include="Filters\IResultFilter.cs" />
|
||||
<Compile Include="Filters\IServiceFilter.cs" />
|
||||
<Compile Include="Filters\ITypeFilter.cs" />
|
||||
<Compile Include="Filters\ResultExecutedContext.cs" />
|
||||
<Compile Include="Filters\ResultExecutingContext.cs" />
|
||||
<Compile Include="Filters\ResultExecutionDelegate.cs" />
|
||||
<Compile Include="Filters\ResultFilterAttribute.cs" />
|
||||
<Compile Include="Filters\ServiceFilterAttribute.cs" />
|
||||
<Compile Include="FormContext.cs" />
|
||||
<Compile Include="HttpDeleteAttribute.cs" />
|
||||
|
|
|
|||
|
|
@ -554,12 +554,44 @@ namespace Microsoft.AspNet.Mvc.Core
|
|||
return GetString("NoRoutesMatched");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
internal static string AsyncActionFilter_InvalidShortCircuit
|
||||
{
|
||||
get { return GetString("AsyncActionFilter_InvalidShortCircuit"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
internal static string AsyncResultFilter_InvalidShortCircuit
|
||||
{
|
||||
get { return GetString("AsyncResultFilter_InvalidShortCircuit"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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}.
|
||||
/// </summary>
|
||||
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++)
|
||||
|
|
|
|||
|
|
@ -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<FilterProviderContext> _filterProvider;
|
||||
|
||||
private readonly List<IAuthorizationFilter> _authorizationFilters = new List<IAuthorizationFilter>();
|
||||
private readonly List<IActionFilter> _actionFilters = new List<IActionFilter>();
|
||||
private readonly List<IActionResultFilter> _actionResultFilters = new List<IActionResultFilter>();
|
||||
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<IActionResult> RunAuthorizationFilters(FilterItem[] filterMetaItems)
|
||||
{
|
||||
if (_authorizationFilters.Count > 0)
|
||||
{
|
||||
var authZEndPoint = new AuthorizationFilterEndPoint();
|
||||
_authorizationFilters.Add(authZEndPoint);
|
||||
|
||||
var authZContext = new AuthorizationFilterContext(_actionContext, filterMetaItems);
|
||||
var authZPipeline = new FilterPipelineBuilder<AuthorizationFilterContext>(_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<IDictionary<string, object>> 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<IExceptionFilter, IAsyncExceptionFilter>();
|
||||
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<IAuthorizationFilter, IAsyncAuthorizationFilter>();
|
||||
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<IDictionary<string, object>> 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<IActionResult> RunActionFiltersAndActions(FilterItem[] filterMetaItems, object controller)
|
||||
private async Task<ActionExecutedContext> 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<ActionFilterContext>(_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<ActionResultFilterContext>(_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<IActionFilter, IAsyncActionFilter>();
|
||||
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<IActionResult> 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<ResultExecutedContext> 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<IResultFilter, IAsyncResultFilter>();
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A one-way cursor for filters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
///
|
||||
/// </remarks>
|
||||
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<TFilter, TFilterAsync> GetNextFilter<TFilter, TFilterAsync>()
|
||||
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<TFilter, TFilterAsync>(Stage, Index, filter, filterAsync);
|
||||
}
|
||||
}
|
||||
|
||||
return default(FilterCursorItem<TFilter, TFilterAsync>);
|
||||
}
|
||||
|
||||
public bool StillAt<TFilter, TFilterAsync>(FilterCursorItem<TFilter, TFilterAsync> current)
|
||||
{
|
||||
return current.Stage == Stage && current.Index == Index;
|
||||
}
|
||||
}
|
||||
|
||||
private struct FilterCursorItem<TFilter, TFilterAsync>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,4 +219,10 @@
|
|||
<data name="NoRoutesMatched" xml:space="preserve">
|
||||
<value>No route matches the supplied values.</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="AsyncActionFilter_InvalidShortCircuit" xml:space="preserve">
|
||||
<value>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}.</value>
|
||||
</data>
|
||||
<data name="AsyncResultFilter_InvalidShortCircuit" xml:space="preserve">
|
||||
<value>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}.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
<Compile Include="DefaultControllerAssemblyProviderTests.cs" />
|
||||
<Compile Include="DefaultControllerFactoryTest.cs" />
|
||||
<Compile Include="PropertyHelperTest.cs" />
|
||||
<Compile Include="ReflectedActionInvokerTest.cs" />
|
||||
<Compile Include="Rendering\HtmlAttributePropertyHelperTest.cs" />
|
||||
<Compile Include="Rendering\ViewContextTests.cs" />
|
||||
<Compile Include="Rendering\ViewDataOfTTest.cs" />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue