diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs new file mode 100644 index 0000000000..1a0f47ce8d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerMethodDescriptor.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class HandlerMethodDescriptor + { + public MethodInfo Method { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageHandlerMethodSelector.cs new file mode 100644 index 0000000000..06ae8f1f73 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageHandlerMethodSelector.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public interface IPageHandlerMethodSelector + { + HandlerMethodDescriptor Select(PageContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs new file mode 100644 index 0000000000..25c07afe09 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public static class ExecutorFactory + { + public static Func> Create(MethodInfo method) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 0c99fd1dca..c74a37a247 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -1,33 +1,408 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Core.Internal; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class PageActionInvoker : IActionInvoker + public class PageActionInvoker : ResourceInvoker, IActionInvoker { - private readonly ActionContext _actionContext; - private readonly IFilterMetadata[] _filters; + private readonly IPageHandlerMethodSelector _selector; + private readonly PageContext _pageContext; + + private Page _page; + private object _model; + private ExceptionContext _exceptionContext; public PageActionInvoker( - PageActionInvokerCacheEntry cacheEntry, - ActionContext actionContext, - IFilterMetadata[] filters) + IPageHandlerMethodSelector handlerMethodSelector, + DiagnosticSource diagnosticSource, + ILogger logger, + PageContext pageContext, + IFilterMetadata[] filterMetadata, + IList valueProviderFactories, + PageActionInvokerCacheEntry cacheEntry) + : base( + diagnosticSource, + logger, + pageContext, + filterMetadata, + valueProviderFactories) { + _selector = handlerMethodSelector; + _pageContext = pageContext; CacheEntry = cacheEntry; - _actionContext = actionContext; - _filters = filters; } public PageActionInvokerCacheEntry CacheEntry { get; } - public Task InvokeAsync() + /// + /// for details on what the variables in this method represent. + /// + protected override async Task InvokeInnerFilterAsync() { - return TaskCache.CompletedTask; + var next = State.ResourceInnerBegin; + var scope = Scope.Resource; + var state = (object)null; + var isCompleted = false; + + while (!isCompleted) + { + await Next(ref next, ref scope, ref state, ref isCompleted); + } + } + + protected override void ReleaseResources() + { + if (_model != null && CacheEntry.ReleaseModel != null) + { + CacheEntry.ReleaseModel(_pageContext, _model); + } + + if (_page != null && CacheEntry.ReleasePage != null) + { + CacheEntry.ReleasePage(_pageContext, _page); + } + } + + private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) + { + var diagnosticSource = _diagnosticSource; + var logger = _logger; + + switch (next) + { + case State.ResourceInnerBegin: + { + goto case State.ExceptionBegin; + } + + case State.ExceptionBegin: + { + _cursor.Reset(); + goto case State.ExceptionNext; + } + + case State.ExceptionNext: + { + var current = _cursor.GetNextFilter(); + if (current.FilterAsync != null) + { + state = current.FilterAsync; + goto case State.ExceptionAsyncBegin; + } + else if (current.Filter != null) + { + state = current.Filter; + goto case State.ExceptionSyncBegin; + } + else if (scope == Scope.Exception) + { + // All exception filters are on the stack already - so execute the 'inside'. + goto case State.ExceptionInside; + } + else + { + // There are no exception filters - so jump right to 'inside'. + Debug.Assert(scope == Scope.Resource); + goto case State.PageBegin; + } + } + + case State.ExceptionAsyncBegin: + { + var task = InvokeNextExceptionFilterAsync(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ExceptionAsyncResume; + return task; + } + + goto case State.ExceptionAsyncResume; + } + + case State.ExceptionAsyncResume: + { + Debug.Assert(state != null); + + var filter = (IAsyncExceptionFilter)state; + var exceptionContext = _exceptionContext; + + // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, + // we'll call the filter. Otherwise there's nothing to do. + if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) + { + _diagnosticSource.BeforeOnExceptionAsync(exceptionContext, filter); + + var task = filter.OnExceptionAsync(exceptionContext); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ExceptionAsyncEnd; + return task; + } + + goto case State.ExceptionAsyncEnd; + } + + goto case State.ExceptionEnd; + } + + case State.ExceptionAsyncEnd: + { + Debug.Assert(state != null); + Debug.Assert(_exceptionContext != null); + + var filter = (IAsyncExceptionFilter)state; + var exceptionContext = _exceptionContext; + + _diagnosticSource.AfterOnExceptionAsync(exceptionContext, filter); + + if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) + { + _logger.ExceptionFilterShortCircuited(filter); + } + + goto case State.ExceptionEnd; + } + + case State.ExceptionSyncBegin: + { + var task = InvokeNextExceptionFilterAsync(); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ExceptionSyncEnd; + return task; + } + + goto case State.ExceptionSyncEnd; + } + + case State.ExceptionSyncEnd: + { + Debug.Assert(state != null); + + var filter = (IExceptionFilter)state; + var exceptionContext = _exceptionContext; + + // When we get here we're 'unwinding' the stack of exception filters. If we have an unhandled exception, + // we'll call the filter. Otherwise there's nothing to do. + if (exceptionContext?.Exception != null && !exceptionContext.ExceptionHandled) + { + _diagnosticSource.BeforeOnException(exceptionContext, filter); + + filter.OnException(exceptionContext); + + _diagnosticSource.AfterOnException(exceptionContext, filter); + + if (exceptionContext.Exception == null || exceptionContext.ExceptionHandled) + { + _logger.ExceptionFilterShortCircuited(filter); + } + } + + goto case State.ExceptionEnd; + } + + case State.ExceptionInside: + { + goto case State.PageBegin; + } + + case State.ExceptionShortCircuit: + { + Debug.Assert(state != null); + Debug.Assert(_exceptionContext != null); + + if (scope == Scope.Resource) + { + Debug.Assert(_exceptionContext.Result != null); + _result = _exceptionContext.Result; + } + + var task = InvokeResultAsync(_exceptionContext.Result); + if (task.Status != TaskStatus.RanToCompletion) + { + next = State.ResourceInnerEnd; + return task; + } + + goto case State.ResourceInnerEnd; + } + + case State.ExceptionEnd: + { + var exceptionContext = _exceptionContext; + + if (scope == Scope.Exception) + { + isCompleted = true; + return TaskCache.CompletedTask; + } + + if (exceptionContext != null) + { + if (exceptionContext.Result != null && !exceptionContext.ExceptionHandled) + { + goto case State.ExceptionShortCircuit; + } + + Rethrow(exceptionContext); + } + + goto case State.ResourceInnerEnd; + } + + case State.PageBegin: + { + var pageContext = _pageContext; + + _cursor.Reset(); + + next = State.PageEnd; + return ExecutePageAsync(); + } + + case State.PageEnd: + { + if (scope == Scope.Exception) + { + // If we're inside an exception filter, let's allow those filters to 'unwind' before + // the result. + isCompleted = true; + return TaskCache.CompletedTask; + } + + Debug.Assert(scope == Scope.Resource); + goto case State.ResourceInnerEnd; + } + + case State.ResourceInnerEnd: + { + isCompleted = true; + return TaskCache.CompletedTask; + } + + default: + throw new InvalidOperationException(); + } + } + + private async Task ExecutePageAsync() + { + var actionDescriptor = _pageContext.ActionDescriptor; + _page = (Page)CacheEntry.PageFactory(_pageContext); + _pageContext.Page = _page; + + if (actionDescriptor.ModelTypeInfo == null) + { + _model = _page; + } + else + { + _model = CacheEntry.ModelFactory(_pageContext); + } + + if (_model != null) + { + _pageContext.ViewData.Model = _model; + } + + IActionResult result = null; + + var handler = _selector.Select(_pageContext); + if (handler != null) + { + var executor = ExecutorFactory.Create(handler.Method); + result = await executor(_page, _model); + } + + if (result == null) + { + result = new PageViewResult(_page); + } + + await result.ExecuteResultAsync(_pageContext); + } + + private async Task InvokeNextExceptionFilterAsync() + { + try + { + var next = State.ExceptionNext; + var state = (object)null; + var scope = Scope.Exception; + var isCompleted = false; + while (!isCompleted) + { + await Next(ref next, ref scope, ref state, ref isCompleted); + } + } + catch (Exception exception) + { + _exceptionContext = new ExceptionContext(_actionContext, _filters) + { + ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception), + }; + } + } + + private static void Rethrow(ExceptionContext context) + { + if (context == null) + { + return; + } + + if (context.ExceptionHandled) + { + return; + } + + if (context.ExceptionDispatchInfo != null) + { + context.ExceptionDispatchInfo.Throw(); + } + + if (context.Exception != null) + { + throw context.Exception; + } + } + + private enum Scope + { + Resource, + Exception, + Page, + } + + private enum State + { + ResourceInnerBegin, + ExceptionBegin, + ExceptionNext, + ExceptionAsyncBegin, + ExceptionAsyncResume, + ExceptionAsyncEnd, + ExceptionSyncBegin, + ExceptionSyncEnd, + ExceptionInside, + ExceptionShortCircuit, + ExceptionEnd, + PageBegin, + PageEnd, + ResourceInnerEnd, } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs index b9a2c09fa0..1386b2bc16 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs @@ -12,11 +12,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal CompiledPageActionDescriptor actionDescriptor, Func pageFactory, Action releasePage, + Func modelFactory, + Action releaseModel, FilterItem[] cacheableFilters) { ActionDescriptor = actionDescriptor; PageFactory = pageFactory; ReleasePage = releasePage; + ModelFactory = modelFactory; + ReleaseModel = releaseModel; CacheableFilters = cacheableFilters; } @@ -29,6 +33,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal /// public Action ReleasePage { get; } + public Func ModelFactory { get; } + + /// + /// The action invoked to release a model. This may be null. + /// + public Action ReleaseModel { get; } + public FilterItem[] CacheableFilters { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index db4e5f89ce..728c38da5b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -4,13 +4,18 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { @@ -21,18 +26,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private readonly IPageFactoryProvider _pageFactoryProvider; private readonly IActionDescriptorCollectionProvider _collectionProvider; private readonly IFilterProvider[] _filterProviders; + private readonly IReadOnlyList _valueProviderFactories; + private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly ITempDataDictionaryFactory _tempDataFactory; + private readonly HtmlHelperOptions _htmlHelperOptions; + private readonly IPageHandlerMethodSelector _selector; + private readonly DiagnosticSource _diagnosticSource; + private readonly ILogger _logger; private volatile InnerCache _currentCache; public PageActionInvokerProvider( IPageLoader loader, IPageFactoryProvider pageFactoryProvider, IActionDescriptorCollectionProvider collectionProvider, - IEnumerable filterProviders) + IEnumerable filterProviders, + IEnumerable valueProviderFactories, + IModelMetadataProvider modelMetadataProvider, + ITempDataDictionaryFactory tempDataFactory, + IOptions htmlHelperOptions, + IPageHandlerMethodSelector selector, + DiagnosticSource diagnosticSource, + ILoggerFactory loggerFactory) { _loader = loader; _collectionProvider = collectionProvider; _pageFactoryProvider = pageFactoryProvider; _filterProviders = filterProviders.ToArray(); + _valueProviderFactories = valueProviderFactories.ToArray(); + _modelMetadataProvider = modelMetadataProvider; + _tempDataFactory = tempDataFactory; + _htmlHelperOptions = htmlHelperOptions.Value; + _selector = selector; + _diagnosticSource = diagnosticSource; + _logger = loggerFactory.CreateLogger(); } public int Order { get; } = -1000; @@ -44,7 +70,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal throw new ArgumentNullException(nameof(context)); } - var actionDescriptor = context.ActionContext.ActionDescriptor as PageActionDescriptor; + var actionContext = context.ActionContext; + var actionDescriptor = actionContext.ActionDescriptor as PageActionDescriptor; if (actionDescriptor == null) { return; @@ -56,17 +83,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal IFilterMetadata[] filters; if (!cache.Entries.TryGetValue(actionDescriptor, out cacheEntry)) { - var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, context.ActionContext); + var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext); filters = filterFactoryResult.Filters; cacheEntry = CreateCacheEntry(context, filterFactoryResult.CacheableFilters); cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry); } else { - filters = FilterFactory.CreateUncachedFilters(_filterProviders, context.ActionContext, cacheEntry.CacheableFilters); + filters = FilterFactory.CreateUncachedFilters( + _filterProviders, + actionContext, + cacheEntry.CacheableFilters); } - context.Result = new PageActionInvoker(cacheEntry, context.ActionContext, filters); + context.Result = CreateActionInvoker(actionContext, cacheEntry, filters); } public void OnProvidersExecuted(ActionInvokerProviderContext context) @@ -91,7 +121,31 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } } - private PageActionInvokerCacheEntry CreateCacheEntry(ActionInvokerProviderContext context, FilterItem[] filters) + private PageActionInvoker CreateActionInvoker( + ActionContext actionContext, + PageActionInvokerCacheEntry cacheEntry, + IFilterMetadata[] filters) + { + var tempData = _tempDataFactory.GetTempData(actionContext.HttpContext); + var pageContext = new PageContext( + actionContext, + new ViewDataDictionary(_modelMetadataProvider, actionContext.ModelState), + tempData, + _htmlHelperOptions); + + return new PageActionInvoker( + _selector, + _diagnosticSource, + _logger, + pageContext, + filters, + new CopyOnWriteList(_valueProviderFactories), + cacheEntry); + } + + private PageActionInvokerCacheEntry CreateCacheEntry( + ActionInvokerProviderContext context, + FilterItem[] cachedFilters) { var actionDescriptor = (PageActionDescriptor)context.ActionContext.ActionDescriptor; var compiledType = _loader.Load(actionDescriptor).GetTypeInfo(); @@ -107,7 +161,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal compiledActionDescriptor, _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor), _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor), - filters); + c => { throw new NotImplementedException(); }, + (_, __) => { throw new NotImplementedException(); }, + cachedFilters); } private class InnerCache diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs new file mode 100644 index 0000000000..63e4f4f0a5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageLoggerExtensions.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + internal static class PageLoggerExtensions + { + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private static readonly Action _pageExecuting; + private static readonly Action _pageExecuted; + private static readonly Action _exceptionFilterShortCircuit; + private static readonly Action _pageFilterShortCircuit; + + static PageLoggerExtensions() + { + _pageExecuting = LoggerMessage.Define( + LogLevel.Debug, + 1, + "Executing page {ActionName}"); + + _pageExecuted = LoggerMessage.Define( + LogLevel.Information, + 2, + "Executed page {ActionName} in {ElapsedMilliseconds}ms"); + + _exceptionFilterShortCircuit = LoggerMessage.Define( + LogLevel.Debug, + 4, + "Request was short circuited at exception filter '{ExceptionFilter}'."); + + _pageFilterShortCircuit = LoggerMessage.Define( + LogLevel.Debug, + 3, + "Request was short circuited at page filter '{PageFilter}'."); + + } + + public static IDisposable PageScope(this ILogger logger, ActionDescriptor actionDescriptor) + { + Debug.Assert(logger != null); + Debug.Assert(actionDescriptor != null); + + return logger.BeginScope(new PageLogScope(actionDescriptor)); + } + + public static void ExecutingPage(this ILogger logger, ActionDescriptor action) + { + _pageExecuting(logger, action.DisplayName, null); + } + + public static void ExecutedAction(this ILogger logger, ActionDescriptor action, long startTimestamp) + { + // Don't log if logging wasn't enabled at start of request as time will be wildly wrong. + if (logger.IsEnabled(LogLevel.Information) && startTimestamp != 0) + { + var currentTimestamp = Stopwatch.GetTimestamp(); + var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp))); + + _pageExecuted(logger, action.DisplayName, elapsed.TotalMilliseconds, null); + } + } + + public static void ExceptionFilterShortCircuited( + this ILogger logger, + IFilterMetadata filter) + { + _exceptionFilterShortCircuit(logger, filter, null); + } + + public static void PageFilterShortCircuited( + this ILogger logger, + IFilterMetadata filter) + { + _pageFilterShortCircuit(logger, filter, null); + } + + private class PageLogScope : IReadOnlyList> + { + private readonly ActionDescriptor _action; + + public PageLogScope(ActionDescriptor action) + { + _action = action; + } + + public KeyValuePair this[int index] + { + get + { + if (index == 0) + { + return new KeyValuePair("ActionId", _action.Id); + } + else if (index == 1) + { + return new KeyValuePair("PageName", _action.DisplayName); + } + throw new IndexOutOfRangeException(nameof(index)); + } + } + + public int Count => 2; + + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + + public override string ToString() + { + // We don't include the _action.Id here because it's just an opaque guid, and if + // you have text logging, you can already use the requestId for correlation. + return _action.DisplayName; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageResultExecutor.cs new file mode 100644 index 0000000000..575265b331 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageResultExecutor.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageResultExecutor + { + public virtual Task ExecuteAsync(PageContext pageContext, PageViewResult result) + { + if (result.Model != null) + { + result.Page.PageContext.ViewData.Model = result.Model; + } + + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs index 965c33189b..7e5bae2352 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.IO; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -14,9 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public class PageContext : ViewContext { private CompiledPageActionDescriptor _actionDescriptor; + private Page _page; /// - /// Creates an empty . + /// Creates an empty . /// /// /// The default constructor is provided for unit test purposes only. @@ -25,6 +27,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages { } + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . + /// The to apply to this instance. public PageContext( ActionContext actionContext, ViewDataDictionary viewData, @@ -49,5 +58,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages base.ActionDescriptor = value; } } + + public Page Page + { + get { return _page; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _page = value; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageViewResult.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageViewResult.cs new file mode 100644 index 0000000000..cadeef803b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageViewResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// An that renders a Razor Page. + /// + public class PageViewResult : ActionResult + { + /// + /// Initializes a new instance of . + /// + /// The to render. + public PageViewResult(Page page) + { + Page = page; + } + + /// + /// Initializes a new instance of with the specified . + /// + /// The to render. + /// The page model. + public PageViewResult(Page page, object model) + { + Page = page; + Model = model; + } + + /// + /// Gets or sets the Content-Type header for the response. + /// + public string ContentType { get; set; } + + /// + /// Gets the page model. + /// + public object Model { get; } + + /// + /// Gets the to execute. + /// + public Page Page { get; } + + /// + /// Gets or sets the HTTP status code. + /// + public int? StatusCode { get; set; } + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + if (!object.ReferenceEquals(context, Page.PageContext)) + { + throw new ArgumentException( + Resources.FormatPageViewResult_ContextIsInvalid(nameof(context), nameof(Page))); + } + + var executor = context.HttpContext.RequestServices.GetRequiredService(); + return executor.ExecuteAsync(Page.PageContext, this); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 4241a5c649..1741a5572c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -74,6 +74,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return string.Format(CultureInfo.CurrentCulture, GetString("ActivatedInstance_MustBeAnInstanceOf"), p0, p1); } + /// + /// Argument '{0}' is not the same instance used to create '{1}'. + /// + internal static string PageViewResult_ContextIsInvalid + { + get { return GetString("PageViewResult_ContextIsInvalid"); } + } + + /// + /// Argument '{0}' is not the same instance used to create '{1}'. + /// + internal static string FormatPageViewResult_ContextIsInvalid(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PageViewResult_ContextIsInvalid"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index ee0b86b563..26db628257 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -129,4 +129,7 @@ Page created by '{0}' must be an instance of '{1}'. + + Argument '{0}' is not the same instance used to create '{1}'. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 27cd2c1e36..38c5876b99 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -2,12 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { @@ -36,15 +42,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) .Returns(releaser); - var invokerProvider = new PageActionInvokerProvider( + var invokerProvider = CreateInvokerProvider( loader.Object, - factoryProvider.Object, actionDescriptorProvider.Object, - new IFilterProvider[0]); - var context = new ActionInvokerProviderContext(new ActionContext - { - ActionDescriptor = descriptor, - }); + factoryProvider.Object); + var context = new ActionInvokerProviderContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); // Act invokerProvider.OnProvidersExecuting(context); @@ -75,15 +78,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var actionDescriptorProvider = new Mock(); actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); - var invokerProvider = new PageActionInvokerProvider( + var invokerProvider = CreateInvokerProvider( loader.Object, - Mock.Of(), actionDescriptorProvider.Object, - new IFilterProvider[0]); - var context = new ActionInvokerProviderContext(new ActionContext - { - ActionDescriptor = descriptor, - }); + Mock.Of()); + var context = new ActionInvokerProviderContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); // Act - 1 invokerProvider.OnProvidersExecuting(context); @@ -122,15 +122,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var loader = new Mock(); loader.Setup(l => l.Load(It.IsAny())) .Returns(typeof(object)); - var invokerProvider = new PageActionInvokerProvider( - loader.Object, - Mock.Of(), - actionDescriptorProvider.Object, - new IFilterProvider[0]); - var context = new ActionInvokerProviderContext(new ActionContext - { - ActionDescriptor = descriptor, - }); + var invokerProvider = CreateInvokerProvider( + loader.Object, + actionDescriptorProvider.Object, + Mock.Of()); + var context = new ActionInvokerProviderContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); // Act - 1 invokerProvider.OnProvidersExecuting(context); @@ -149,5 +146,28 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var entry2 = actionInvoker.CacheEntry; Assert.NotSame(entry1, entry2); } + + private static PageActionInvokerProvider CreateInvokerProvider( + IPageLoader loader, + IActionDescriptorCollectionProvider actionDescriptorProvider, + IPageFactoryProvider factoryProvider) + { + var tempDataFactory = new Mock(); + tempDataFactory.Setup(t => t.GetTempData(It.IsAny())) + .Returns((HttpContext context) => new TempDataDictionary(context, Mock.Of())); + + return new PageActionInvokerProvider( + loader, + factoryProvider, + actionDescriptorProvider, + new IFilterProvider[0], + new IValueProviderFactory[0], + new EmptyModelMetadataProvider(), + tempDataFactory.Object, + new TestOptionsManager(), + Mock.Of(), + new DiagnosticListener("Microsoft.AspNetCore"), + NullLoggerFactory.Instance); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs new file mode 100644 index 0000000000..6484c78217 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerTest.cs @@ -0,0 +1,622 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageActionInvokerTest + { + private readonly DivideByZeroException _pageException = new DivideByZeroException(); + + [Fact] + public async Task InvokeAsync_DoesNotInvokeExceptionFilter_WhenPageDoesNotThrow() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnException(It.IsAny())) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: false); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify(f => f.OnException(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task InvokeAsync_DoesNotAsyncInvokeExceptionFilter_WhenPageDoesNotThrow() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnExceptionAsync(It.IsAny())) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: false); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify( + f => f.OnExceptionAsync(It.IsAny()), + Times.Never()); + } + + [Fact] + public async Task InvokeAsync_InvokesExceptionFilter_WhenPageThrows() + { + // Arrange + Exception exception = null; + IActionResult pageAction = null; + var expected = new Mock(MockBehavior.Strict); + expected + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnException(It.IsAny())) + .Verifiable(); + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnException(It.IsAny())) + .Callback(context => + { + exception = context.Exception; + pageAction = context.Result; + + // Handle the exception + context.Result = expected.Object; + }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + expected.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + filter2.Verify(f => f.OnException(It.IsAny()), Times.Once()); + + Assert.Same(_pageException, exception); + Assert.Null(pageAction); + } + + [Fact] + public async Task InvokeAsync_InvokesAsyncExceptionFilter_WhenPageThrows() + { + // Arrange + Exception exception = null; + IActionResult pageAction = null; + var expected = new Mock(MockBehavior.Strict); + expected + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnExceptionAsync(It.IsAny())) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnExceptionAsync(It.IsAny())) + .Callback(context => + { + exception = context.Exception; + pageAction = context.Result; + + // Handle the exception + context.Result = expected.Object; + }) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + expected.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + filter2.Verify( + f => f.OnExceptionAsync(It.IsAny()), + Times.Once()); + + Assert.Same(_pageException, exception); + Assert.Null(pageAction); + } + + [Fact] + public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionNull() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnException(It.IsAny())) + .Callback(context => + { + filter2.ToString(); + context.Exception = null; + }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter2.Verify( + f => f.OnException(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionHandled() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnException(It.IsAny())) + .Callback(context => + { + context.ExceptionHandled = true; + }) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter1.Object, filter2.Object }, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter2.Verify( + f => f.OnException(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionNull() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + var filter2 = new Mock(MockBehavior.Strict); + + filter2 + .Setup(f => f.OnExceptionAsync(It.IsAny())) + .Callback(context => + { + filter2.ToString(); + context.Exception = null; + }) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + + var filterMetadata = new IFilterMetadata[] { filter1.Object, filter2.Object }; + var invoker = CreateInvoker(filterMetadata, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter2.Verify( + f => f.OnExceptionAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionHandled() + { + // Arrange + var filter1 = new Mock(MockBehavior.Strict); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnExceptionAsync(It.IsAny())) + .Callback(context => + { + context.ExceptionHandled = true; + }) + .Returns((context) => Task.FromResult(true)) + .Verifiable(); + + var invoker = CreateInvoker(new IFilterMetadata[] { filter1.Object, filter2.Object }, pageThrows: true); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter2.Verify( + f => f.OnExceptionAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesExceptionFilter_UnhandledExceptionIsThrown() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnException(It.IsAny())) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: true); + + // Act + await Assert.ThrowsAsync(_pageException.GetType(), invoker.InvokeAsync); + + // Assert + filter.Verify(f => f.OnException(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAuthorizationFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter.Setup(f => f.OnAuthorization(It.IsAny())).Verifiable(); + + var invoker = CreateInvoker(new[] { filter.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAsyncAuthorizationFilter() + { + // Arrange + var filter = new Mock(MockBehavior.Strict); + filter + .Setup(f => f.OnAuthorizationAsync(It.IsAny())) + .Returns(context => Task.FromResult(true)) + .Verifiable(); + + var invoker = CreateInvoker(new[] { filter.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + filter.Verify( + f => f.OnAuthorizationAsync(It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAuthorizationFilter_ShortCircuit() + { + // Arrange + var createCalled = false; + var challenge = new Mock(MockBehavior.Strict); + challenge + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnAuthorization(It.IsAny())) + .Callback(c => Task.FromResult(true)) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnAuthorization(It.IsAny())) + .Callback(c => c.Result = challenge.Object) + .Verifiable(); + + var filter3 = new Mock(MockBehavior.Strict); + var actionDescriptor = new CompiledPageActionDescriptor(); + var cacheEntry = new PageActionInvokerCacheEntry( + actionDescriptor, + (context) => createCalled = true, + null, + (context) => null, + null, + new FilterItem[0]); + var invoker = CreateInvoker( + new[] { filter1.Object, filter2.Object, filter3.Object }, + actionDescriptor, + cacheEntry: cacheEntry); + + // Act + await invoker.InvokeAsync(); + + // Assert + challenge.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + filter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + Assert.False(createCalled); + } + + [Fact] + public async Task InvokeAsync_InvokesAsyncAuthorizationFilter_ShortCircuit() + { + // Arrange + var createCalled = false; + var challenge = new Mock(MockBehavior.Strict); + challenge + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns(Task.FromResult(true)) + .Verifiable(); + + var filter1 = new Mock(MockBehavior.Strict); + filter1 + .Setup(f => f.OnAuthorizationAsync(It.IsAny())) + .Returns((context) => + { + return Task.FromResult(true); + }) + .Verifiable(); + + var filter2 = new Mock(MockBehavior.Strict); + filter2 + .Setup(f => f.OnAuthorizationAsync(It.IsAny())) + .Returns((context) => + { + context.Result = challenge.Object; + return Task.FromResult(true); + }); + + var filter3 = new Mock(MockBehavior.Strict); + + var actionDescriptor = new CompiledPageActionDescriptor(); + var cacheEntry = new PageActionInvokerCacheEntry( + actionDescriptor, + (context) => createCalled = true, + null, + (context) => null, + null, + new FilterItem[0]); + var invoker = CreateInvoker( + new IFilterMetadata[] { filter1.Object, filter2.Object, filter3.Object }, + actionDescriptor, + cacheEntry: cacheEntry); + + // Act + await invoker.InvokeAsync(); + + // Assert + challenge.Verify(r => r.ExecuteResultAsync(It.IsAny()), Times.Once()); + filter1.Verify( + f => f.OnAuthorizationAsync(It.IsAny()), + Times.Once()); + + Assert.False(createCalled); + } + + [Fact] + public async Task InvokeAsync_ExceptionInAuthorizationFilter_CannotBeHandledByOtherFilters() + { + // Arrange + var expected = new InvalidCastException(); + + var exceptionFilter = new Mock(MockBehavior.Strict); + exceptionFilter + .Setup(f => f.OnException(It.IsAny())) + .Callback(context => + { + // 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 resourceFilter = new Mock(MockBehavior.Strict); + var actionFilter = new Mock(MockBehavior.Strict); + var resultFilter = new Mock(MockBehavior.Strict); + + var invoker = CreateInvoker(new IFilterMetadata[] + { + exceptionFilter.Object, + authorizationFilter1.Object, + authorizationFilter2.Object, + resourceFilter.Object, + actionFilter.Object, + resultFilter.Object, + }); + + // Act + var thrown = await Assert.ThrowsAsync(invoker.InvokeAsync); + + // Assert + Assert.Same(expected, thrown); + exceptionFilter.Verify(f => f.OnException(It.IsAny()), Times.Never()); + authorizationFilter1.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task InvokeAsync_InvokesAuthorizationFilter_ChallengeNotSeenByResultFilters() + { + // Arrange + var challenge = new Mock(MockBehavior.Strict); + challenge + .Setup(r => r.ExecuteResultAsync(It.IsAny())) + .Returns((context) => Task.FromResult(true)) + .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 IFilterMetadata[] { authorizationFilter.Object, resultFilter.Object }); + + // Act + await invoker.InvokeAsync(); + + // Assert + authorizationFilter.Verify(f => f.OnAuthorization(It.IsAny()), Times.Once()); + challenge.Verify(c => c.ExecuteResultAsync(It.IsAny()), Times.Once()); + } + + private PageActionInvoker CreateInvoker( + IFilterMetadata[] filters, + bool pageThrows = false, + int maxAllowedErrorsInModelState = 200, + List valueProviderFactories = null) + { + Func executeAction; + if (pageThrows) + { + executeAction = _ => { throw _pageException; }; + } + else + { + executeAction = context => context.HttpContext.Response.WriteAsync("Hello"); + } + var executor = new TestPageResultExecutor(executeAction); + var actionDescriptor = new CompiledPageActionDescriptor + { + ViewEnginePath = "/Index.cshtml", + RelativePath = "/Index.cshtml", + PageTypeInfo = typeof(TestPage).GetTypeInfo(), + }; + + return CreateInvoker( + filters, + actionDescriptor, + executor); + } + + private PageActionInvoker CreateInvoker( + IFilterMetadata[] filters, + CompiledPageActionDescriptor actionDescriptor, + PageResultExecutor executor = null, + IPageHandlerMethodSelector selector = null, + PageActionInvokerCacheEntry cacheEntry = null, + int maxAllowedErrorsInModelState = 200, + List valueProviderFactories = null, + RouteData routeData = null, + ILogger logger = null, + object diagnosticListener = null) + { + var httpContext = new DefaultHttpContext(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(executor ?? new PageResultExecutor()); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + if (routeData == null) + { + routeData = new RouteData(); + } + + var actionContext = new ActionContext( + httpContext: httpContext, + routeData: routeData, + actionDescriptor: actionDescriptor); + var pageContext = new PageContext( + actionContext, + new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), + Mock.Of(), + new HtmlHelperOptions()) + { + ActionDescriptor = actionDescriptor + }; + + if (selector == null) + { + selector = Mock.Of(); + } + + if (valueProviderFactories == null) + { + valueProviderFactories = new List(); + } + + if (logger == null) + { + logger = new NullLogger(); + } + + Func pageFactory = (context) => + { + var instance = (Page)Activator.CreateInstance(actionDescriptor.PageTypeInfo.AsType()); + instance.PageContext = context; + return instance; + }; + + cacheEntry = new PageActionInvokerCacheEntry( + actionDescriptor, + pageFactory, + (c, page) => { (page as IDisposable)?.Dispose(); }, + _ => Activator.CreateInstance(actionDescriptor.ModelTypeInfo.AsType()), + (c, model) => { (model as IDisposable)?.Dispose(); }, + new FilterItem[0]); + var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); + + var invoker = new PageActionInvoker( + selector, + diagnosticSource, + logger, + pageContext, + filters, + valueProviderFactories.AsReadOnly(), + cacheEntry); + return invoker; + } + + private class TestPageResultExecutor : PageResultExecutor + { + private readonly Func _executeAction; + + public TestPageResultExecutor(Func executeAction) + { + _executeAction = executeAction; + } + + public override Task ExecuteAsync(PageContext pageContext, PageViewResult result) + => _executeAction(pageContext); + } + + private class TestPage : Page + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json index 3f143e5b96..937bd4bfa8 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json @@ -10,6 +10,7 @@ "target": "project" }, "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Microsoft.Extensions.Logging.Testing": "1.2.0-*", "Microsoft.Extensions.DependencyInjection": "1.2.0-*", "Moq": "4.6.36-*", "xunit": "2.2.0-*"