Introducing PageActionInvoker
This commit is contained in:
parent
13b32adeae
commit
a7abdeabcd
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Page, object, Task<IActionResult>> Create(MethodInfo method)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,408 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
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
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
public class PageActionInvoker : IActionInvoker
|
public class PageActionInvoker : ResourceInvoker, IActionInvoker
|
||||||
{
|
{
|
||||||
private readonly ActionContext _actionContext;
|
private readonly IPageHandlerMethodSelector _selector;
|
||||||
private readonly IFilterMetadata[] _filters;
|
private readonly PageContext _pageContext;
|
||||||
|
|
||||||
|
private Page _page;
|
||||||
|
private object _model;
|
||||||
|
private ExceptionContext _exceptionContext;
|
||||||
|
|
||||||
public PageActionInvoker(
|
public PageActionInvoker(
|
||||||
PageActionInvokerCacheEntry cacheEntry,
|
IPageHandlerMethodSelector handlerMethodSelector,
|
||||||
ActionContext actionContext,
|
DiagnosticSource diagnosticSource,
|
||||||
IFilterMetadata[] filters)
|
ILogger logger,
|
||||||
|
PageContext pageContext,
|
||||||
|
IFilterMetadata[] filterMetadata,
|
||||||
|
IList<IValueProviderFactory> valueProviderFactories,
|
||||||
|
PageActionInvokerCacheEntry cacheEntry)
|
||||||
|
: base(
|
||||||
|
diagnosticSource,
|
||||||
|
logger,
|
||||||
|
pageContext,
|
||||||
|
filterMetadata,
|
||||||
|
valueProviderFactories)
|
||||||
{
|
{
|
||||||
|
_selector = handlerMethodSelector;
|
||||||
|
_pageContext = pageContext;
|
||||||
CacheEntry = cacheEntry;
|
CacheEntry = cacheEntry;
|
||||||
_actionContext = actionContext;
|
|
||||||
_filters = filters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageActionInvokerCacheEntry CacheEntry { get; }
|
public PageActionInvokerCacheEntry CacheEntry { get; }
|
||||||
|
|
||||||
public Task InvokeAsync()
|
/// <remarks>
|
||||||
|
/// <see cref="ResourceInvoker"/> for details on what the variables in this method represent.
|
||||||
|
/// </remarks>
|
||||||
|
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<IExceptionFilter, IAsyncExceptionFilter>();
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
CompiledPageActionDescriptor actionDescriptor,
|
CompiledPageActionDescriptor actionDescriptor,
|
||||||
Func<PageContext, object> pageFactory,
|
Func<PageContext, object> pageFactory,
|
||||||
Action<PageContext, object> releasePage,
|
Action<PageContext, object> releasePage,
|
||||||
|
Func<PageContext, object> modelFactory,
|
||||||
|
Action<PageContext, object> releaseModel,
|
||||||
FilterItem[] cacheableFilters)
|
FilterItem[] cacheableFilters)
|
||||||
{
|
{
|
||||||
ActionDescriptor = actionDescriptor;
|
ActionDescriptor = actionDescriptor;
|
||||||
PageFactory = pageFactory;
|
PageFactory = pageFactory;
|
||||||
ReleasePage = releasePage;
|
ReleasePage = releasePage;
|
||||||
|
ModelFactory = modelFactory;
|
||||||
|
ReleaseModel = releaseModel;
|
||||||
CacheableFilters = cacheableFilters;
|
CacheableFilters = cacheableFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,6 +33,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<PageContext, object> ReleasePage { get; }
|
public Action<PageContext, object> ReleasePage { get; }
|
||||||
|
|
||||||
|
public Func<PageContext, object> ModelFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action invoked to release a model. This may be <c>null</c>.
|
||||||
|
/// </summary>
|
||||||
|
public Action<PageContext, object> ReleaseModel { get; }
|
||||||
|
|
||||||
public FilterItem[] CacheableFilters { get; }
|
public FilterItem[] CacheableFilters { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,18 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
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
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -21,18 +26,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
private readonly IPageFactoryProvider _pageFactoryProvider;
|
private readonly IPageFactoryProvider _pageFactoryProvider;
|
||||||
private readonly IActionDescriptorCollectionProvider _collectionProvider;
|
private readonly IActionDescriptorCollectionProvider _collectionProvider;
|
||||||
private readonly IFilterProvider[] _filterProviders;
|
private readonly IFilterProvider[] _filterProviders;
|
||||||
|
private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
|
||||||
|
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||||
|
private readonly ITempDataDictionaryFactory _tempDataFactory;
|
||||||
|
private readonly HtmlHelperOptions _htmlHelperOptions;
|
||||||
|
private readonly IPageHandlerMethodSelector _selector;
|
||||||
|
private readonly DiagnosticSource _diagnosticSource;
|
||||||
|
private readonly ILogger<PageActionInvoker> _logger;
|
||||||
private volatile InnerCache _currentCache;
|
private volatile InnerCache _currentCache;
|
||||||
|
|
||||||
public PageActionInvokerProvider(
|
public PageActionInvokerProvider(
|
||||||
IPageLoader loader,
|
IPageLoader loader,
|
||||||
IPageFactoryProvider pageFactoryProvider,
|
IPageFactoryProvider pageFactoryProvider,
|
||||||
IActionDescriptorCollectionProvider collectionProvider,
|
IActionDescriptorCollectionProvider collectionProvider,
|
||||||
IEnumerable<IFilterProvider> filterProviders)
|
IEnumerable<IFilterProvider> filterProviders,
|
||||||
|
IEnumerable<IValueProviderFactory> valueProviderFactories,
|
||||||
|
IModelMetadataProvider modelMetadataProvider,
|
||||||
|
ITempDataDictionaryFactory tempDataFactory,
|
||||||
|
IOptions<HtmlHelperOptions> htmlHelperOptions,
|
||||||
|
IPageHandlerMethodSelector selector,
|
||||||
|
DiagnosticSource diagnosticSource,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_collectionProvider = collectionProvider;
|
_collectionProvider = collectionProvider;
|
||||||
_pageFactoryProvider = pageFactoryProvider;
|
_pageFactoryProvider = pageFactoryProvider;
|
||||||
_filterProviders = filterProviders.ToArray();
|
_filterProviders = filterProviders.ToArray();
|
||||||
|
_valueProviderFactories = valueProviderFactories.ToArray();
|
||||||
|
_modelMetadataProvider = modelMetadataProvider;
|
||||||
|
_tempDataFactory = tempDataFactory;
|
||||||
|
_htmlHelperOptions = htmlHelperOptions.Value;
|
||||||
|
_selector = selector;
|
||||||
|
_diagnosticSource = diagnosticSource;
|
||||||
|
_logger = loggerFactory.CreateLogger<PageActionInvoker>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order { get; } = -1000;
|
public int Order { get; } = -1000;
|
||||||
|
|
@ -44,7 +70,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
throw new ArgumentNullException(nameof(context));
|
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)
|
if (actionDescriptor == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
@ -56,17 +83,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
IFilterMetadata[] filters;
|
IFilterMetadata[] filters;
|
||||||
if (!cache.Entries.TryGetValue(actionDescriptor, out cacheEntry))
|
if (!cache.Entries.TryGetValue(actionDescriptor, out cacheEntry))
|
||||||
{
|
{
|
||||||
var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, context.ActionContext);
|
var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext);
|
||||||
filters = filterFactoryResult.Filters;
|
filters = filterFactoryResult.Filters;
|
||||||
cacheEntry = CreateCacheEntry(context, filterFactoryResult.CacheableFilters);
|
cacheEntry = CreateCacheEntry(context, filterFactoryResult.CacheableFilters);
|
||||||
cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry);
|
cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry);
|
||||||
}
|
}
|
||||||
else
|
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)
|
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<IValueProviderFactory>(_valueProviderFactories),
|
||||||
|
cacheEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageActionInvokerCacheEntry CreateCacheEntry(
|
||||||
|
ActionInvokerProviderContext context,
|
||||||
|
FilterItem[] cachedFilters)
|
||||||
{
|
{
|
||||||
var actionDescriptor = (PageActionDescriptor)context.ActionContext.ActionDescriptor;
|
var actionDescriptor = (PageActionDescriptor)context.ActionContext.ActionDescriptor;
|
||||||
var compiledType = _loader.Load(actionDescriptor).GetTypeInfo();
|
var compiledType = _loader.Load(actionDescriptor).GetTypeInfo();
|
||||||
|
|
@ -107,7 +161,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
compiledActionDescriptor,
|
compiledActionDescriptor,
|
||||||
_pageFactoryProvider.CreatePageFactory(compiledActionDescriptor),
|
_pageFactoryProvider.CreatePageFactory(compiledActionDescriptor),
|
||||||
_pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor),
|
_pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor),
|
||||||
filters);
|
c => { throw new NotImplementedException(); },
|
||||||
|
(_, __) => { throw new NotImplementedException(); },
|
||||||
|
cachedFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InnerCache
|
private class InnerCache
|
||||||
|
|
|
||||||
|
|
@ -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<ILogger, string, Exception> _pageExecuting;
|
||||||
|
private static readonly Action<ILogger, string, double, Exception> _pageExecuted;
|
||||||
|
private static readonly Action<ILogger, object, Exception> _exceptionFilterShortCircuit;
|
||||||
|
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
||||||
|
|
||||||
|
static PageLoggerExtensions()
|
||||||
|
{
|
||||||
|
_pageExecuting = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
1,
|
||||||
|
"Executing page {ActionName}");
|
||||||
|
|
||||||
|
_pageExecuted = LoggerMessage.Define<string, double>(
|
||||||
|
LogLevel.Information,
|
||||||
|
2,
|
||||||
|
"Executed page {ActionName} in {ElapsedMilliseconds}ms");
|
||||||
|
|
||||||
|
_exceptionFilterShortCircuit = LoggerMessage.Define<object>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
4,
|
||||||
|
"Request was short circuited at exception filter '{ExceptionFilter}'.");
|
||||||
|
|
||||||
|
_pageFilterShortCircuit = LoggerMessage.Define<object>(
|
||||||
|
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<KeyValuePair<string, object>>
|
||||||
|
{
|
||||||
|
private readonly ActionDescriptor _action;
|
||||||
|
|
||||||
|
public PageLogScope(ActionDescriptor action)
|
||||||
|
{
|
||||||
|
_action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<string, object> this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
return new KeyValuePair<string, object>("ActionId", _action.Id);
|
||||||
|
}
|
||||||
|
else if (index == 1)
|
||||||
|
{
|
||||||
|
return new KeyValuePair<string, object>("PageName", _action.DisplayName);
|
||||||
|
}
|
||||||
|
throw new IndexOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => 2;
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<string, object>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
|
@ -14,9 +15,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
public class PageContext : ViewContext
|
public class PageContext : ViewContext
|
||||||
{
|
{
|
||||||
private CompiledPageActionDescriptor _actionDescriptor;
|
private CompiledPageActionDescriptor _actionDescriptor;
|
||||||
|
private Page _page;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an empty <see cref="ViewContext"/>.
|
/// Creates an empty <see cref="PageContext"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The default constructor is provided for unit test purposes only.
|
/// The default constructor is provided for unit test purposes only.
|
||||||
|
|
@ -25,6 +27,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="PageContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
|
||||||
|
/// <param name="viewData">The <see cref="ViewDataDictionary"/>.</param>
|
||||||
|
/// <param name="tempDataDictionary">The <see cref="ITempDataDictionary"/>.</param>
|
||||||
|
/// <param name="htmlHelperOptions">The <see cref="HtmlHelperOptions"/> to apply to this instance.</param>
|
||||||
public PageContext(
|
public PageContext(
|
||||||
ActionContext actionContext,
|
ActionContext actionContext,
|
||||||
ViewDataDictionary viewData,
|
ViewDataDictionary viewData,
|
||||||
|
|
@ -49,5 +58,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
base.ActionDescriptor = value;
|
base.ActionDescriptor = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Page Page
|
||||||
|
{
|
||||||
|
get { return _page; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_page = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="ActionResult"/> that renders a Razor Page.
|
||||||
|
/// </summary>
|
||||||
|
public class PageViewResult : ActionResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="PageViewResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">The <see cref="RazorPages.Page"/> to render.</param>
|
||||||
|
public PageViewResult(Page page)
|
||||||
|
{
|
||||||
|
Page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="PageViewResult"/> with the specified <paramref name="model"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">The <see cref="RazorPages.Page"/> to render.</param>
|
||||||
|
/// <param name="model">The page model.</param>
|
||||||
|
public PageViewResult(Page page, object model)
|
||||||
|
{
|
||||||
|
Page = page;
|
||||||
|
Model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Content-Type header for the response.
|
||||||
|
/// </summary>
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the page model.
|
||||||
|
/// </summary>
|
||||||
|
public object Model { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="RazorPages.Page"/> to execute.
|
||||||
|
/// </summary>
|
||||||
|
public Page Page { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HTTP status code.
|
||||||
|
/// </summary>
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<PageResultExecutor>();
|
||||||
|
return executor.ExecuteAsync(Page.PageContext, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("ActivatedInstance_MustBeAnInstanceOf"), p0, p1);
|
return string.Format(CultureInfo.CurrentCulture, GetString("ActivatedInstance_MustBeAnInstanceOf"), p0, p1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Argument '{0}' is not the same instance used to create '{1}'.
|
||||||
|
/// </summary>
|
||||||
|
internal static string PageViewResult_ContextIsInvalid
|
||||||
|
{
|
||||||
|
get { return GetString("PageViewResult_ContextIsInvalid"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Argument '{0}' is not the same instance used to create '{1}'.
|
||||||
|
/// </summary>
|
||||||
|
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)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -129,4 +129,7 @@
|
||||||
<data name="ActivatedInstance_MustBeAnInstanceOf" xml:space="preserve">
|
<data name="ActivatedInstance_MustBeAnInstanceOf" xml:space="preserve">
|
||||||
<value>Page created by '{0}' must be an instance of '{1}'.</value>
|
<value>Page created by '{0}' must be an instance of '{1}'.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PageViewResult_ContextIsInvalid" xml:space="preserve">
|
||||||
|
<value>Argument '{0}' is not the same instance used to create '{1}'.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -2,12 +2,18 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -36,15 +42,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny<CompiledPageActionDescriptor>()))
|
factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny<CompiledPageActionDescriptor>()))
|
||||||
.Returns(releaser);
|
.Returns(releaser);
|
||||||
|
|
||||||
var invokerProvider = new PageActionInvokerProvider(
|
var invokerProvider = CreateInvokerProvider(
|
||||||
loader.Object,
|
loader.Object,
|
||||||
factoryProvider.Object,
|
|
||||||
actionDescriptorProvider.Object,
|
actionDescriptorProvider.Object,
|
||||||
new IFilterProvider[0]);
|
factoryProvider.Object);
|
||||||
var context = new ActionInvokerProviderContext(new ActionContext
|
var context = new ActionInvokerProviderContext(
|
||||||
{
|
new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor));
|
||||||
ActionDescriptor = descriptor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
invokerProvider.OnProvidersExecuting(context);
|
invokerProvider.OnProvidersExecuting(context);
|
||||||
|
|
@ -75,15 +78,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
|
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
|
||||||
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
|
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
|
||||||
|
|
||||||
var invokerProvider = new PageActionInvokerProvider(
|
var invokerProvider = CreateInvokerProvider(
|
||||||
loader.Object,
|
loader.Object,
|
||||||
Mock.Of<IPageFactoryProvider>(),
|
|
||||||
actionDescriptorProvider.Object,
|
actionDescriptorProvider.Object,
|
||||||
new IFilterProvider[0]);
|
Mock.Of<IPageFactoryProvider>());
|
||||||
var context = new ActionInvokerProviderContext(new ActionContext
|
var context = new ActionInvokerProviderContext(
|
||||||
{
|
new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor));
|
||||||
ActionDescriptor = descriptor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act - 1
|
// Act - 1
|
||||||
invokerProvider.OnProvidersExecuting(context);
|
invokerProvider.OnProvidersExecuting(context);
|
||||||
|
|
@ -122,15 +122,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
var loader = new Mock<IPageLoader>();
|
var loader = new Mock<IPageLoader>();
|
||||||
loader.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
|
loader.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
|
||||||
.Returns(typeof(object));
|
.Returns(typeof(object));
|
||||||
var invokerProvider = new PageActionInvokerProvider(
|
var invokerProvider = CreateInvokerProvider(
|
||||||
loader.Object,
|
loader.Object,
|
||||||
Mock.Of<IPageFactoryProvider>(),
|
actionDescriptorProvider.Object,
|
||||||
actionDescriptorProvider.Object,
|
Mock.Of<IPageFactoryProvider>());
|
||||||
new IFilterProvider[0]);
|
var context = new ActionInvokerProviderContext(
|
||||||
var context = new ActionInvokerProviderContext(new ActionContext
|
new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor));
|
||||||
{
|
|
||||||
ActionDescriptor = descriptor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act - 1
|
// Act - 1
|
||||||
invokerProvider.OnProvidersExecuting(context);
|
invokerProvider.OnProvidersExecuting(context);
|
||||||
|
|
@ -149,5 +146,28 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
var entry2 = actionInvoker.CacheEntry;
|
var entry2 = actionInvoker.CacheEntry;
|
||||||
Assert.NotSame(entry1, entry2);
|
Assert.NotSame(entry1, entry2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PageActionInvokerProvider CreateInvokerProvider(
|
||||||
|
IPageLoader loader,
|
||||||
|
IActionDescriptorCollectionProvider actionDescriptorProvider,
|
||||||
|
IPageFactoryProvider factoryProvider)
|
||||||
|
{
|
||||||
|
var tempDataFactory = new Mock<ITempDataDictionaryFactory>();
|
||||||
|
tempDataFactory.Setup(t => t.GetTempData(It.IsAny<HttpContext>()))
|
||||||
|
.Returns((HttpContext context) => new TempDataDictionary(context, Mock.Of<ITempDataProvider>()));
|
||||||
|
|
||||||
|
return new PageActionInvokerProvider(
|
||||||
|
loader,
|
||||||
|
factoryProvider,
|
||||||
|
actionDescriptorProvider,
|
||||||
|
new IFilterProvider[0],
|
||||||
|
new IValueProviderFactory[0],
|
||||||
|
new EmptyModelMetadataProvider(),
|
||||||
|
tempDataFactory.Object,
|
||||||
|
new TestOptionsManager<HtmlHelperOptions>(),
|
||||||
|
Mock.Of<IPageHandlerMethodSelector>(),
|
||||||
|
new DiagnosticListener("Microsoft.AspNetCore"),
|
||||||
|
NullLoggerFactory.Instance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(new[] { filter.Object }, pageThrows: false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
filter.Verify(f => f.OnException(It.IsAny<ExceptionContext>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_DoesNotAsyncInvokeExceptionFilter_WhenPageDoesNotThrow()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter = new Mock<IAsyncExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter
|
||||||
|
.Setup(f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()))
|
||||||
|
.Returns<ExceptionContext>((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<ExceptionContext>()),
|
||||||
|
Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesExceptionFilter_WhenPageThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Exception exception = null;
|
||||||
|
IActionResult pageAction = null;
|
||||||
|
var expected = new Mock<IActionResult>(MockBehavior.Strict);
|
||||||
|
expected
|
||||||
|
.Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns(Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter1 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter1
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Verifiable();
|
||||||
|
var filter2 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(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<ActionContext>()), Times.Once());
|
||||||
|
filter2.Verify(f => f.OnException(It.IsAny<ExceptionContext>()), 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<IActionResult>(MockBehavior.Strict);
|
||||||
|
expected
|
||||||
|
.Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns(Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter1 = new Mock<IAsyncExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter1
|
||||||
|
.Setup(f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()))
|
||||||
|
.Returns<ExceptionContext>((context) => Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
var filter2 = new Mock<IAsyncExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(context =>
|
||||||
|
{
|
||||||
|
exception = context.Exception;
|
||||||
|
pageAction = context.Result;
|
||||||
|
|
||||||
|
// Handle the exception
|
||||||
|
context.Result = expected.Object;
|
||||||
|
})
|
||||||
|
.Returns<ExceptionContext>((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<ActionContext>()), Times.Once());
|
||||||
|
filter2.Verify(
|
||||||
|
f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()),
|
||||||
|
Times.Once());
|
||||||
|
|
||||||
|
Assert.Same(_pageException, exception);
|
||||||
|
Assert.Null(pageAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter1 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var filter2 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(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<ExceptionContext>()),
|
||||||
|
Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesExceptionFilter_ShortCircuit_ExceptionHandled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter1 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var filter2 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(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<ExceptionContext>()),
|
||||||
|
Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter1 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
var filter2 = new Mock<IAsyncExceptionFilter>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(context =>
|
||||||
|
{
|
||||||
|
filter2.ToString();
|
||||||
|
context.Exception = null;
|
||||||
|
})
|
||||||
|
.Returns<ExceptionContext>((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<ExceptionContext>()),
|
||||||
|
Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAsyncExceptionFilter_ShortCircuit_ExceptionHandled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter1 = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var filter2 = new Mock<IAsyncExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnExceptionAsync(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(context =>
|
||||||
|
{
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
})
|
||||||
|
.Returns<ExceptionContext>((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<ExceptionContext>()),
|
||||||
|
Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesExceptionFilter_UnhandledExceptionIsThrown()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
filter
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.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<ExceptionContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAuthorizationFilter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter.Setup(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>())).Verifiable();
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(new[] { filter.Object });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
filter.Verify(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAsyncAuthorizationFilter()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filter = new Mock<IAsyncAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter
|
||||||
|
.Setup(f => f.OnAuthorizationAsync(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Returns<AuthorizationFilterContext>(context => Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(new[] { filter.Object });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
filter.Verify(
|
||||||
|
f => f.OnAuthorizationAsync(It.IsAny<AuthorizationFilterContext>()),
|
||||||
|
Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAuthorizationFilter_ShortCircuit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var createCalled = false;
|
||||||
|
var challenge = new Mock<IActionResult>(MockBehavior.Strict);
|
||||||
|
challenge
|
||||||
|
.Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns(Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter1 = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter1
|
||||||
|
.Setup(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Callback<AuthorizationFilterContext>(c => Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter2 = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Callback<AuthorizationFilterContext>(c => c.Result = challenge.Object)
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter3 = new Mock<IAuthorizationFilter>(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<ActionContext>()), Times.Once());
|
||||||
|
filter1.Verify(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()), Times.Once());
|
||||||
|
Assert.False(createCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAsyncAuthorizationFilter_ShortCircuit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var createCalled = false;
|
||||||
|
var challenge = new Mock<IActionResult>(MockBehavior.Strict);
|
||||||
|
challenge
|
||||||
|
.Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns(Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter1 = new Mock<IAsyncAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter1
|
||||||
|
.Setup(f => f.OnAuthorizationAsync(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Returns<AuthorizationFilterContext>((context) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult(true);
|
||||||
|
})
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var filter2 = new Mock<IAsyncAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
filter2
|
||||||
|
.Setup(f => f.OnAuthorizationAsync(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Returns<AuthorizationFilterContext>((context) =>
|
||||||
|
{
|
||||||
|
context.Result = challenge.Object;
|
||||||
|
return Task.FromResult(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
var filter3 = new Mock<IAuthorizationFilter>(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<ActionContext>()), Times.Once());
|
||||||
|
filter1.Verify(
|
||||||
|
f => f.OnAuthorizationAsync(It.IsAny<AuthorizationFilterContext>()),
|
||||||
|
Times.Once());
|
||||||
|
|
||||||
|
Assert.False(createCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_ExceptionInAuthorizationFilter_CannotBeHandledByOtherFilters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = new InvalidCastException();
|
||||||
|
|
||||||
|
var exceptionFilter = new Mock<IExceptionFilter>(MockBehavior.Strict);
|
||||||
|
exceptionFilter
|
||||||
|
.Setup(f => f.OnException(It.IsAny<ExceptionContext>()))
|
||||||
|
.Callback<ExceptionContext>(context =>
|
||||||
|
{
|
||||||
|
// Mark as handled
|
||||||
|
context.Result = new EmptyResult();
|
||||||
|
})
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var authorizationFilter1 = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
authorizationFilter1
|
||||||
|
.Setup(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Callback<AuthorizationFilterContext>(c => { throw expected; })
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
// None of these filters should run
|
||||||
|
var authorizationFilter2 = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
var resourceFilter = new Mock<IResourceFilter>(MockBehavior.Strict);
|
||||||
|
var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict);
|
||||||
|
var resultFilter = new Mock<IResultFilter>(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<InvalidCastException>(invoker.InvokeAsync);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(expected, thrown);
|
||||||
|
exceptionFilter.Verify(f => f.OnException(It.IsAny<ExceptionContext>()), Times.Never());
|
||||||
|
authorizationFilter1.Verify(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvokeAsync_InvokesAuthorizationFilter_ChallengeNotSeenByResultFilters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var challenge = new Mock<IActionResult>(MockBehavior.Strict);
|
||||||
|
challenge
|
||||||
|
.Setup(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns<ActionContext>((context) => Task.FromResult(true))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var authorizationFilter = new Mock<IAuthorizationFilter>(MockBehavior.Strict);
|
||||||
|
authorizationFilter
|
||||||
|
.Setup(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()))
|
||||||
|
.Callback<AuthorizationFilterContext>(c => c.Result = challenge.Object)
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
var invoker = CreateInvoker(new IFilterMetadata[] { authorizationFilter.Object, resultFilter.Object });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await invoker.InvokeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
authorizationFilter.Verify(f => f.OnAuthorization(It.IsAny<AuthorizationFilterContext>()), Times.Once());
|
||||||
|
challenge.Verify(c => c.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageActionInvoker CreateInvoker(
|
||||||
|
IFilterMetadata[] filters,
|
||||||
|
bool pageThrows = false,
|
||||||
|
int maxAllowedErrorsInModelState = 200,
|
||||||
|
List<IValueProviderFactory> valueProviderFactories = null)
|
||||||
|
{
|
||||||
|
Func<PageContext, Task> 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<IValueProviderFactory> 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<ITempDataDictionary>(),
|
||||||
|
new HtmlHelperOptions())
|
||||||
|
{
|
||||||
|
ActionDescriptor = actionDescriptor
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selector == null)
|
||||||
|
{
|
||||||
|
selector = Mock.Of<IPageHandlerMethodSelector>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueProviderFactories == null)
|
||||||
|
{
|
||||||
|
valueProviderFactories = new List<IValueProviderFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
{
|
||||||
|
logger = new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
Func<PageContext, object> 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<PageContext, Task> _executeAction;
|
||||||
|
|
||||||
|
public TestPageResultExecutor(Func<PageContext, Task> executeAction)
|
||||||
|
{
|
||||||
|
_executeAction = executeAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ExecuteAsync(PageContext pageContext, PageViewResult result)
|
||||||
|
=> _executeAction(pageContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPage : Page
|
||||||
|
{
|
||||||
|
public override Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"target": "project"
|
"target": "project"
|
||||||
},
|
},
|
||||||
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
||||||
|
"Microsoft.Extensions.Logging.Testing": "1.2.0-*",
|
||||||
"Microsoft.Extensions.DependencyInjection": "1.2.0-*",
|
"Microsoft.Extensions.DependencyInjection": "1.2.0-*",
|
||||||
"Moq": "4.6.36-*",
|
"Moq": "4.6.36-*",
|
||||||
"xunit": "2.2.0-*"
|
"xunit": "2.2.0-*"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue