Add IPageFactory and IPageActivator

This commit is contained in:
Pranav K 2016-11-15 11:49:35 -08:00
parent 9146fce4ec
commit 2b8233932a
25 changed files with 1897 additions and 174 deletions

View File

@ -0,0 +1,186 @@
// 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.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public class RazorPagePropertyActivator
{
private delegate ViewDataDictionary CreateViewDataNestedDelegate(ViewDataDictionary source);
private delegate ViewDataDictionary CreateViewDataRootDelegate(ModelStateDictionary modelState);
public RazorPagePropertyActivator(
Type pageType,
Type modelType,
IModelMetadataProvider metadataProvider,
PropertyValueAccessors propertyValueAccessors)
{
var viewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType);
ViewDataDictionaryType = viewDataType;
CreateViewDataNested = GetCreateViewDataNested(viewDataType);
CreateViewDataRoot = GetCreateViewDataRoot(viewDataType, metadataProvider);
PropertyActivators = PropertyActivator<ViewContext>.GetPropertiesToActivate(
pageType,
typeof(RazorInjectAttribute),
propertyInfo => CreateActivateInfo(propertyInfo, propertyValueAccessors),
includeNonPublic: true);
}
private PropertyActivator<ViewContext>[] PropertyActivators { get; }
private Type ViewDataDictionaryType { get; }
private CreateViewDataNestedDelegate CreateViewDataNested { get; }
private CreateViewDataRootDelegate CreateViewDataRoot { get; }
public void Activate(object page, ViewContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.ViewData = CreateViewDataDictionary(context);
for (var i = 0; i < PropertyActivators.Length; i++)
{
var activateInfo = PropertyActivators[i];
activateInfo.Activate(page, context);
}
}
private ViewDataDictionary CreateViewDataDictionary(ViewContext context)
{
// Create a ViewDataDictionary<TModel> if the ViewContext.ViewData is not set or the type of
// ViewContext.ViewData is an incompatible type.
if (context.ViewData == null)
{
// Create ViewDataDictionary<TModel>(IModelMetadataProvider, ModelStateDictionary).
return CreateViewDataRoot(context.ModelState);
}
else if (context.ViewData.GetType() != ViewDataDictionaryType)
{
// Create ViewDataDictionary<TModel>(ViewDataDictionary).
return CreateViewDataNested(context.ViewData);
}
return context.ViewData;
}
private static CreateViewDataNestedDelegate GetCreateViewDataNested(Type viewDataDictionaryType)
{
var parameterTypes = new Type[] { typeof(ViewDataDictionary) };
var matchingConstructor = viewDataDictionaryType.GetConstructor(parameterTypes);
Debug.Assert(matchingConstructor != null);
var parameters = new ParameterExpression[] { Expression.Parameter(parameterTypes[0]) };
var newExpression = Expression.New(matchingConstructor, parameters);
var castNewCall = Expression.Convert(
newExpression,
typeof(ViewDataDictionary));
var lambda = Expression.Lambda<CreateViewDataNestedDelegate>(castNewCall, parameters);
return lambda.Compile();
}
private static CreateViewDataRootDelegate GetCreateViewDataRoot(
Type viewDataDictionaryType,
IModelMetadataProvider provider)
{
var parameterTypes = new[]
{
typeof(IModelMetadataProvider),
typeof(ModelStateDictionary)
};
var matchingConstructor = viewDataDictionaryType.GetConstructor(parameterTypes);
Debug.Assert(matchingConstructor != null);
var parameterExpression = Expression.Parameter(parameterTypes[1]);
var parameters = new Expression[]
{
Expression.Constant(provider),
parameterExpression
};
var newExpression = Expression.New(matchingConstructor, parameters);
var castNewCall = Expression.Convert(
newExpression,
typeof(ViewDataDictionary));
var lambda = Expression.Lambda<CreateViewDataRootDelegate>(castNewCall, parameterExpression);
return lambda.Compile();
}
private static PropertyActivator<ViewContext> CreateActivateInfo(
PropertyInfo property,
PropertyValueAccessors valueAccessors)
{
Func<ViewContext, object> valueAccessor;
if (typeof(ViewDataDictionary).IsAssignableFrom(property.PropertyType))
{
// Logic looks reversed in condition above but is OK. Support only properties of base
// ViewDataDictionary type and activationInfo.ViewDataDictionaryType. VDD<AnotherType> will fail when
// assigning to the property (InvalidCastException) and that's fine.
valueAccessor = context => context.ViewData;
}
else if (property.PropertyType == typeof(IUrlHelper))
{
// W.r.t. specificity of above condition: Users are much more likely to inject their own
// IUrlHelperFactory than to create a class implementing IUrlHelper (or a sub-interface) and inject
// that. But the second scenario is supported. (Note the class must implement ICanHasViewContext.)
valueAccessor = valueAccessors.UrlHelperAccessor;
}
else if (property.PropertyType == typeof(IJsonHelper))
{
valueAccessor = valueAccessors.JsonHelperAccessor;
}
else if (property.PropertyType == typeof(DiagnosticSource))
{
valueAccessor = valueAccessors.DiagnosticSourceAccessor;
}
else if (property.PropertyType == typeof(HtmlEncoder))
{
valueAccessor = valueAccessors.HtmlEncoderAccessor;
}
else if (property.PropertyType == typeof(IModelExpressionProvider))
{
valueAccessor = valueAccessors.ModelExpressionProviderAccessor;
}
else
{
valueAccessor = context =>
{
var serviceProvider = context.HttpContext.RequestServices;
var value = serviceProvider.GetRequiredService(property.PropertyType);
(value as IViewContextAware)?.Contextualize(context);
return value;
};
}
return new PropertyActivator<ViewContext>(property, valueAccessor);
}
public class PropertyValueAccessors
{
public Func<ViewContext, object> UrlHelperAccessor { get; set; }
public Func<ViewContext, object> JsonHelperAccessor { get; set; }
public Func<ViewContext, object> DiagnosticSourceAccessor { get; set; }
public Func<ViewContext, object> HtmlEncoderAccessor { get; set; }
public Func<ViewContext, object> ModelExpressionProviderAccessor { get; set; }
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -12,30 +11,19 @@ using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <inheritdoc />
public class RazorPageActivator : IRazorPageActivator
{
private delegate ViewDataDictionary CreateViewDataNested(ViewDataDictionary source);
private delegate ViewDataDictionary CreateViewDataRoot(
IModelMetadataProvider metadataProvider,
ModelStateDictionary modelState);
// Name of the "public TModel Model" property on RazorPage<TModel>
private const string ModelPropertyName = "Model";
private readonly ConcurrentDictionary<Type, PageActivationInfo> _activationInfo;
private readonly ConcurrentDictionary<Type, RazorPagePropertyActivator> _activationInfo;
private readonly IModelMetadataProvider _metadataProvider;
// Value accessors for common singleton properties activated in a RazorPage.
private Func<ViewContext, object> _urlHelperAccessor;
private Func<ViewContext, object> _jsonHelperAccessor;
private Func<ViewContext, object> _diagnosticSourceAccessor;
private Func<ViewContext, object> _htmlEncoderAccessor;
private Func<ViewContext, object> _modelExpressionProviderAccessor;
private readonly RazorPagePropertyActivator.PropertyValueAccessors _propertyAccessors;
/// <summary>
/// Initializes a new instance of the <see cref="RazorPageActivator"/> class.
@ -48,13 +36,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor
HtmlEncoder htmlEncoder,
IModelExpressionProvider modelExpressionProvider)
{
_activationInfo = new ConcurrentDictionary<Type, PageActivationInfo>();
_activationInfo = new ConcurrentDictionary<Type, RazorPagePropertyActivator>();
_metadataProvider = metadataProvider;
_urlHelperAccessor = context => urlHelperFactory.GetUrlHelper(context);
_jsonHelperAccessor = context => jsonHelper;
_diagnosticSourceAccessor = context => diagnosticSource;
_htmlEncoderAccessor = context => htmlEncoder;
_modelExpressionProviderAccessor = context => modelExpressionProvider;
_propertyAccessors = new RazorPagePropertyActivator.PropertyValueAccessors
{
UrlHelperAccessor = context => urlHelperFactory.GetUrlHelper(context),
JsonHelperAccessor = context => jsonHelper,
DiagnosticSourceAccessor = context => diagnosticSource,
HtmlEncoderAccessor = context => htmlEncoder,
ModelExpressionProviderAccessor = context => modelExpressionProvider,
};
}
/// <inheritdoc />
@ -70,159 +62,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor
throw new ArgumentNullException(nameof(context));
}
var activationInfo = _activationInfo.GetOrAdd(page.GetType(),
CreateViewActivationInfo);
context.ViewData = CreateViewDataDictionary(context, activationInfo);
for (var i = 0; i < activationInfo.PropertyActivators.Length; i++)
var pageType = page.GetType();
RazorPagePropertyActivator propertyActivator;
if (!_activationInfo.TryGetValue(pageType, out propertyActivator))
{
var activateInfo = activationInfo.PropertyActivators[i];
activateInfo.Activate(page, context);
}
}
private ViewDataDictionary CreateViewDataDictionary(ViewContext context, PageActivationInfo activationInfo)
{
// Create a ViewDataDictionary<TModel> if the ViewContext.ViewData is not set or the type of
// ViewContext.ViewData is an incompatible type.
if (context.ViewData == null)
{
// Create ViewDataDictionary<TModel>(IModelMetadataProvider, ModelStateDictionary).
return activationInfo.CreateViewDataRoot(
_metadataProvider,
context.ModelState);
}
else if (context.ViewData.GetType() != activationInfo.ViewDataDictionaryType)
{
// Create ViewDataDictionary<TModel>(ViewDataDictionary).
return activationInfo.CreateViewDataNested(context.ViewData);
}
return context.ViewData;
}
private PageActivationInfo CreateViewActivationInfo(Type type)
{
// Look for a property named "Model". If it is non-null, we'll assume this is
// the equivalent of TModel Model property on RazorPage<TModel>
var modelProperty = type.GetRuntimeProperty(ModelPropertyName);
if (modelProperty == null)
{
var message = Resources.FormatViewCannotBeActivated(type.FullName, GetType().FullName);
throw new InvalidOperationException(message);
}
var modelType = modelProperty.PropertyType;
var viewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType);
return new PageActivationInfo
{
ViewDataDictionaryType = viewDataType,
CreateViewDataNested = GetCreateViewDataNested(viewDataType),
CreateViewDataRoot = GetCreateViewDataRoot(viewDataType),
PropertyActivators = PropertyActivator<ViewContext>.GetPropertiesToActivate(
type,
typeof(RazorInjectAttribute),
CreateActivateInfo,
includeNonPublic: true)
};
}
private CreateViewDataNested GetCreateViewDataNested(Type viewDataDictionaryType)
{
var parameterTypes = new Type[] { typeof(ViewDataDictionary) };
var matchingConstructor = viewDataDictionaryType.GetConstructor(parameterTypes);
Debug.Assert(matchingConstructor != null);
var parameters = new ParameterExpression[] { Expression.Parameter(parameterTypes[0]) };
var newExpression = Expression.New(matchingConstructor, parameters);
var castNewCall = Expression.Convert(
newExpression,
typeof(ViewDataDictionary));
var lambda = Expression.Lambda<CreateViewDataNested>(castNewCall, parameters);
return lambda.Compile();
}
private CreateViewDataRoot GetCreateViewDataRoot(Type viewDataDictionaryType)
{
var parameterTypes = new Type[] {
typeof(IModelMetadataProvider),
typeof(ModelStateDictionary) };
var matchingConstructor = viewDataDictionaryType.GetConstructor(parameterTypes);
Debug.Assert(matchingConstructor != null);
var parameters = new ParameterExpression[] {
Expression.Parameter(parameterTypes[0]),
Expression.Parameter(parameterTypes[1]) };
var newExpression = Expression.New(matchingConstructor, parameters);
var castNewCall = Expression.Convert(
newExpression,
typeof(ViewDataDictionary));
var lambda = Expression.Lambda<CreateViewDataRoot>(castNewCall, parameters);
return lambda.Compile();
}
private PropertyActivator<ViewContext> CreateActivateInfo(PropertyInfo property)
{
Func<ViewContext, object> valueAccessor;
if (typeof(ViewDataDictionary).IsAssignableFrom(property.PropertyType))
{
// Logic looks reversed in condition above but is OK. Support only properties of base
// ViewDataDictionary type and activationInfo.ViewDataDictionaryType. VDD<AnotherType> will fail when
// assigning to the property (InvalidCastException) and that's fine.
valueAccessor = context => context.ViewData;
}
else if (property.PropertyType == typeof(IUrlHelper))
{
// W.r.t. specificity of above condition: Users are much more likely to inject their own
// IUrlHelperFactory than to create a class implementing IUrlHelper (or a sub-interface) and inject
// that. But the second scenario is supported. (Note the class must implement ICanHasViewContext.)
valueAccessor = _urlHelperAccessor;
}
else if (property.PropertyType == typeof(IJsonHelper))
{
valueAccessor = _jsonHelperAccessor;
}
else if (property.PropertyType == typeof(DiagnosticSource))
{
valueAccessor = _diagnosticSourceAccessor;
}
else if (property.PropertyType == typeof(HtmlEncoder))
{
valueAccessor = _htmlEncoderAccessor;
}
else if (property.PropertyType == typeof(IModelExpressionProvider))
{
valueAccessor = _modelExpressionProviderAccessor;
}
else
{
valueAccessor = context =>
// Look for a property named "Model". If it is non-null, we'll assume this is
// the equivalent of TModel Model property on RazorPage<TModel>
var modelProperty = pageType.GetRuntimeProperty(ModelPropertyName);
if (modelProperty == null)
{
var serviceProvider = context.HttpContext.RequestServices;
var value = serviceProvider.GetRequiredService(property.PropertyType);
(value as IViewContextAware)?.Contextualize(context);
var message = Resources.FormatViewCannotBeActivated(pageType.FullName, GetType().FullName);
throw new InvalidOperationException(message);
}
return value;
};
var modelType = modelProperty.PropertyType;
propertyActivator = new RazorPagePropertyActivator(
pageType,
modelType,
_metadataProvider,
_propertyAccessors);
propertyActivator = _activationInfo.GetOrAdd(pageType, propertyActivator);
}
return new PropertyActivator<ViewContext>(property, valueAccessor);
}
private class PageActivationInfo
{
public PropertyActivator<ViewContext>[] PropertyActivators { get; set; }
public Type ViewDataDictionaryType { get; set; }
public CreateViewDataNested CreateViewDataNested { get; set; }
public CreateViewDataRoot CreateViewDataRoot { get; set; }
public Action<object, object> ViewDataDictionarySetter { get; set; }
propertyActivator.Activate(page, context);
}
}
}

View File

@ -0,0 +1,40 @@
// 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
{
/// <summary>
/// A <see cref="PageActionDescriptor"/> for a compiled Razor page.
/// </summary>
public class CompiledPageActionDescriptor : PageActionDescriptor
{
/// <summary>
/// Initializes an empty <see cref="CompiledPageActionDescriptor"/>.
/// </summary>
public CompiledPageActionDescriptor()
{
}
/// <summary>
/// Initializes a new instance of <see cref="CompiledPageActionDescriptor"/>
/// from the specified <paramref name="actionDescriptor"/> instance.
/// </summary>
/// <param name="actionDescriptor">The <see cref="PageActionDescriptor"/>.</param>
public CompiledPageActionDescriptor(PageActionDescriptor actionDescriptor)
: base(actionDescriptor)
{
}
/// <summary>
/// Gets or sets the <see cref="TypeInfo"/> of the page.
/// </summary>
public TypeInfo PageTypeInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="TypeInfo"/> of the model.
/// </summary>
public TypeInfo ModelTypeInfo { get; set; }
}
}

View File

@ -0,0 +1,27 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
/// <summary>
/// Provides methods to create a Razor page.
/// </summary>
public interface IPageActivatorProvider
{
/// <summary>
/// Creates a Razor page activator.
/// </summary>
/// <param name="descriptor">The <see cref="CompiledPageActionDescriptor"/>.</param>
/// <returns>The delegate used to activate the page.</returns>
Func<PageContext, object> CreateActivator(CompiledPageActionDescriptor descriptor);
/// <summary>
/// Releases a Razor page.
/// </summary>
/// <param name="descriptor">The <see cref="CompiledPageActionDescriptor"/>.</param>
/// <returns>The delegate used to dispose the activated page.</returns>
Action<PageContext, object> CreateReleaser(CompiledPageActionDescriptor descriptor);
}
}

View File

@ -0,0 +1,27 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
/// <summary>
/// Provides methods for creation and disposal of Razor pages.
/// </summary>
public interface IPageFactoryProvider
{
/// <summary>
/// Creates a factory for producing Razor pages for the specified <see cref="PageContext"/>.
/// </summary>
/// <param name="descriptor">The <see cref="CompiledPageActionDescriptor"/>.</param>
/// <returns>The Razor page factory.</returns>
Func<PageContext, object> CreatePageFactory(CompiledPageActionDescriptor descriptor);
/// <summary>
/// Releases a Razor page.
/// </summary>
/// <param name="descriptor">The <see cref="CompiledPageActionDescriptor"/>.</param>
/// <returns>The delegate used to release the created page.</returns>
Action<PageContext, object> CreatePageDisposer(CompiledPageActionDescriptor descriptor);
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public interface IPageLoader
{
Type Load(PageActionDescriptor actionDescriptor);
}
}

View File

@ -0,0 +1,96 @@
// 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.Linq.Expressions;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
/// <summary>
/// <see cref="IPageActivatorProvider"/> that uses type activation to create Pages.
/// </summary>
public class DefaultPageActivator : IPageActivatorProvider
{
private readonly Action<PageContext, object> _disposer = Dispose;
/// <inheritdoc />
public virtual Func<PageContext, object> CreateActivator(CompiledPageActionDescriptor actionDescriptor)
{
if (actionDescriptor == null)
{
throw new ArgumentNullException(nameof(actionDescriptor));
}
var pageTypeInfo = actionDescriptor.PageTypeInfo?.AsType();
if (pageTypeInfo == null)
{
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(actionDescriptor.PageTypeInfo),
nameof(actionDescriptor)),
nameof(actionDescriptor));
}
return CreatePageFactory(pageTypeInfo);
}
public virtual Action<PageContext, object> CreateReleaser(CompiledPageActionDescriptor actionDescriptor)
{
if (actionDescriptor == null)
{
throw new ArgumentNullException(nameof(actionDescriptor));
}
if (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo))
{
return _disposer;
}
return null;
}
private static Func<PageContext, object> CreatePageFactory(Type pageTypeInfo)
{
var parameter = Expression.Parameter(typeof(PageContext), "pageContext");
// new Page();
var newExpression = Expression.New(pageTypeInfo);
// () => new Page();
var pageFactory = Expression
.Lambda<Func<PageContext, object>>(newExpression, parameter)
.Compile();
return pageFactory;
}
private static void Dispose(PageContext context, object page)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (page == null)
{
throw new ArgumentNullException(nameof(page));
}
((IDisposable)page).Dispose();
}
private static void NullDisposer(PageContext context, object page)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (page == null)
{
throw new ArgumentNullException(nameof(page));
}
// No-op
}
}
}

View File

@ -0,0 +1,79 @@
// 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.Diagnostics;
using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultPageFactory : IPageFactoryProvider
{
private readonly IPageActivatorProvider _pageActivator;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly RazorPagePropertyActivator.PropertyValueAccessors _propertyAccessors;
public DefaultPageFactory(
IPageActivatorProvider pageActivator,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
IJsonHelper jsonHelper,
DiagnosticSource diagnosticSource,
HtmlEncoder htmlEncoder,
IModelExpressionProvider modelExpressionProvider)
{
_pageActivator = pageActivator;
_modelMetadataProvider = metadataProvider;
_propertyAccessors = new RazorPagePropertyActivator.PropertyValueAccessors
{
UrlHelperAccessor = context => urlHelperFactory.GetUrlHelper(context),
JsonHelperAccessor = context => jsonHelper,
DiagnosticSourceAccessor = context => diagnosticSource,
HtmlEncoderAccessor = context => htmlEncoder,
ModelExpressionProviderAccessor = context => modelExpressionProvider,
};
}
public virtual Func<PageContext, object> CreatePageFactory(CompiledPageActionDescriptor actionDescriptor)
{
if (!typeof(Page).GetTypeInfo().IsAssignableFrom(actionDescriptor.PageTypeInfo))
{
throw new InvalidOperationException(Resources.FormatActivatedInstance_MustBeAnInstanceOf(
_pageActivator.GetType().FullName,
typeof(Page).FullName));
}
var activatorFactory = _pageActivator.CreateActivator(actionDescriptor);
var modelType = actionDescriptor.ModelTypeInfo?.AsType() ?? actionDescriptor.PageTypeInfo.AsType();
var propertyActivator = new RazorPagePropertyActivator(
actionDescriptor.PageTypeInfo.AsType(),
modelType,
_modelMetadataProvider,
_propertyAccessors);
return (context) =>
{
var page = (Page)activatorFactory(context);
page.PageContext = context;
propertyActivator.Activate(page, context);
return page;
};
}
public virtual Action<PageContext, object> CreatePageDisposer(CompiledPageActionDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
return _pageActivator.CreateReleaser(descriptor);
}
}
}

View File

@ -0,0 +1,28 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageActionInvoker : IActionInvoker
{
private readonly PageActionInvokerCacheEntry _cacheEntry;
private readonly ActionContext _actionContext;
public PageActionInvoker(
PageActionInvokerCacheEntry cacheEntry,
ActionContext actionContext)
{
_cacheEntry = cacheEntry;
_actionContext = actionContext;
}
public Task InvokeAsync()
{
return TaskCache.CompletedTask;
}
}
}

View File

@ -0,0 +1,34 @@
// 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 Microsoft.AspNetCore.Mvc.Filters;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageActionInvokerCacheEntry
{
public PageActionInvokerCacheEntry(
CompiledPageActionDescriptor actionDescriptor,
Func<PageContext, object> pageFactory,
Action<PageContext, object> releasePage,
Func<PageContext, IFilterMetadata[]> filterProvider)
{
ActionDescriptor = actionDescriptor;
PageFactory = pageFactory;
ReleasePage = releasePage;
FilterProvider = filterProvider;
}
public CompiledPageActionDescriptor ActionDescriptor { get; }
public Func<PageContext, object> PageFactory { get; }
/// <summary>
/// The action invoked to release a page. This may be <c>null</c>.
/// </summary>
public Action<PageContext, object> ReleasePage { get; }
Func<PageContext, IFilterMetadata[]> FilterProvider { get; }
}
}

View File

@ -0,0 +1,127 @@
// 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.Concurrent;
using System.Collections.Generic;
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.RazorPages.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageActionInvokerProvider : IActionInvokerProvider
{
private const string ModelPropertyName = "Model";
private readonly IPageLoader _loader;
private readonly IPageFactoryProvider _pageFactoryProvider;
private readonly IActionDescriptorCollectionProvider _collectionProvider;
private readonly IFilterProvider[] _filterProviders;
private volatile InnerCache _currentCache;
public PageActionInvokerProvider(
IPageLoader loader,
IPageFactoryProvider pageFactoryProvider,
IActionDescriptorCollectionProvider collectionProvider,
IEnumerable<IFilterProvider> filterProviders)
{
_loader = loader;
_collectionProvider = collectionProvider;
_pageFactoryProvider = pageFactoryProvider;
_filterProviders = filterProviders.ToArray();
}
public int Order { get; } = -1000;
public void OnProvidersExecuting(ActionInvokerProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var actionDescriptor = context.ActionContext.ActionDescriptor as PageActionDescriptor;
if (actionDescriptor == null)
{
return;
}
var cacheEntry = GetOrAddCacheEntry(context, actionDescriptor);
context.Result = new PageActionInvoker(cacheEntry, context.ActionContext);
}
public void OnProvidersExecuted(ActionInvokerProviderContext context)
{
}
private InnerCache CurrentCache
{
get
{
var current = _currentCache;
var actionDescriptors = _collectionProvider.ActionDescriptors;
if (current == null || current.Version != actionDescriptors.Version)
{
current = new InnerCache(actionDescriptors.Version);
_currentCache = current;
}
return current;
}
}
// Internal for unit testing
internal PageActionInvokerCacheEntry GetOrAddCacheEntry(
ActionInvokerProviderContext context,
PageActionDescriptor actionDescriptor)
{
var cache = CurrentCache;
PageActionInvokerCacheEntry cacheEntry;
if (!cache.Entries.TryGetValue(actionDescriptor, out cacheEntry))
{
cacheEntry = CreateCacheEntry(context);
cacheEntry = cache.Entries.GetOrAdd(actionDescriptor, cacheEntry);
}
return cacheEntry;
}
private PageActionInvokerCacheEntry CreateCacheEntry(ActionInvokerProviderContext context)
{
var actionDescriptor = (PageActionDescriptor)context.ActionContext.ActionDescriptor;
var compiledType = _loader.Load(actionDescriptor).GetTypeInfo();
var modelType = compiledType.GetProperty(ModelPropertyName)?.PropertyType.GetTypeInfo();
var compiledActionDescriptor = new CompiledPageActionDescriptor(actionDescriptor)
{
ModelTypeInfo = modelType,
PageTypeInfo = compiledType,
};
return new PageActionInvokerCacheEntry(
compiledActionDescriptor,
_pageFactoryProvider.CreatePageFactory(compiledActionDescriptor),
_pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor),
PageFilterFactoryProvider.GetFilterFactory(_filterProviders, context));
}
private class InnerCache
{
public InnerCache(int version)
{
Version = version;
}
public ConcurrentDictionary<ActionDescriptor, PageActionInvokerCacheEntry> Entries { get; } =
new ConcurrentDictionary<ActionDescriptor, PageActionInvokerCacheEntry>();
public int Version { get; }
}
}
}

View File

@ -0,0 +1,135 @@
// 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.Threading;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public static class PageFilterFactoryProvider
{
public static Func<ActionContext, IFilterMetadata[]> GetFilterFactory(
IFilterProvider[] filterProviders,
ActionInvokerProviderContext actionInvokerProviderContext)
{
if (filterProviders == null)
{
throw new ArgumentNullException(nameof(filterProviders));
}
if (actionInvokerProviderContext == null)
{
throw new ArgumentNullException(nameof(actionInvokerProviderContext));
}
var actionDescriptor = actionInvokerProviderContext.ActionContext.ActionDescriptor;
// staticFilterItems is captured as part of the closure.We evaluate it once to determine
// which of the staticFilters are reusable.
var staticFilterItems = new FilterItem[actionDescriptor.FilterDescriptors.Count];
for (var i = 0; i < actionDescriptor.FilterDescriptors.Count; i++)
{
staticFilterItems[i] = new FilterItem(actionDescriptor.FilterDescriptors[i]);
}
var internalFilterFactory = GetFilterFactory(filterProviders);
var allFilterItems = new List<FilterItem>(staticFilterItems);
// Execute the filter factory to determine which static filters can be cached.
var filters = internalFilterFactory(allFilterItems, actionInvokerProviderContext.ActionContext);
// Cache the filter items based on the following criteria
// 1. Are created statically (ex: via filter attributes, added to global filter list etc.)
// 2. Are re-usable
for (var i = 0; i < staticFilterItems.Length; i++)
{
var item = staticFilterItems[i];
if (!item.IsReusable)
{
item.Filter = null;
}
}
return (actionContext) =>
{
// Reuse the filters cached outside the closure for the very first run. This avoids re-running
// filters twice the first time we cache for a page.
var cachedFilters = Interlocked.Exchange(ref filters, null);
if (cachedFilters != null)
{
return cachedFilters;
}
// Create a separate collection as we want to hold onto the statically defined filter items
// in order to cache them
var filterItems = new List<FilterItem>(staticFilterItems.Length);
for (var i = 0; i < staticFilterItems.Length; i++)
{
// Deep copy the cached filter items as filter providers could modify them
var filterItem = staticFilterItems[i];
filterItems.Add(new FilterItem(filterItem.Descriptor)
{
Filter = filterItem.Filter,
IsReusable = filterItem.IsReusable
});
}
return internalFilterFactory(filterItems, actionContext);
};
}
private static Func<IList<FilterItem>, ActionContext, IFilterMetadata[]> GetFilterFactory(
IFilterProvider[] filterProviders)
{
return (filterItems, actionContext) =>
{
// Execute providers
var filterContext = new FilterProviderContext(actionContext, filterItems);
for (var i = 0; i < filterProviders.Length; i++)
{
filterProviders[i].OnProvidersExecuting(filterContext);
}
for (var i = filterProviders.Length - 1; i >= 0; i--)
{
filterProviders[i].OnProvidersExecuted(filterContext);
}
// Extract filter instances from statically defined filters and filter providers
var count = 0;
for (var i = 0; i < filterItems.Count; i++)
{
if (filterItems[i].Filter != null)
{
count++;
}
}
if (count == 0)
{
return EmptyArray<IFilterMetadata>.Instance;
}
else
{
var filters = new IFilterMetadata[count];
var filterIndex = 0;
for (int i = 0; i < filterItems.Count; i++)
{
var filter = filterItems[i].Filter;
if (filter != null)
{
filters[filterIndex++] = filter;
}
}
return filters;
}
};
}
}
}

View File

@ -12,8 +12,9 @@
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<DisabledCustomTools>.resx</DisabledCustomTools>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>

View File

@ -0,0 +1,53 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
/// <summary>
/// A base class for a Razor page.
/// </summary>
public abstract class Page : IRazorPage
{
/// <inheritdoc />
public IHtmlContent BodyContent { get; set; }
/// <inheritdoc />
public bool IsLayoutBeingRendered { get; set; }
/// <inheritdoc />
public string Layout { get; set; }
/// <inheritdoc />
public string Path { get; set; }
/// <inheritdoc />
public IDictionary<string, RenderAsyncDelegate> PreviousSectionWriters { get; set; }
/// <inheritdoc />
public IDictionary<string, RenderAsyncDelegate> SectionWriters { get; }
/// <summary>
/// The <see cref="PageContext"/>.
/// </summary>
public PageContext PageContext { get; set; }
/// <inheritdoc />
public ViewContext ViewContext { get; set; }
/// <inheritdoc />
public void EnsureRenderedBodyOrSections()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public abstract Task ExecuteAsync();
}
}

View File

@ -4,13 +4,36 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
[DebuggerDisplay("{" + nameof(ViewEnginePath) + "}")]
public class PageActionDescriptor : ActionDescriptor
{
/// <summary>
/// Initializes a new instance of <see cref="PageActionDescriptor"/>.
/// </summary>
public PageActionDescriptor()
{
}
/// <summary>
/// A copy constructor for <see cref="PageActionDescriptor"/>.
/// </summary>
/// <param name="other">The <see cref="PageActionDescriptor"/> to copy from.</param>
public PageActionDescriptor(PageActionDescriptor other)
{
RelativePath = other.RelativePath;
ViewEnginePath = other.ViewEnginePath;
}
/// <summary>
/// Gets or sets the application root relative path for the page.
/// </summary>
public string RelativePath { get; set; }
/// <summary>
/// Gets or sets the path relative to the base path for page discovery.
/// </summary>
public string ViewEnginePath { get; set; }
}
}

View File

@ -0,0 +1,53 @@
// 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.IO;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
/// <summary>
/// The context associated with the current request for a Razor page.
/// </summary>
public class PageContext : ViewContext
{
private CompiledPageActionDescriptor _actionDescriptor;
/// <summary>
/// Creates an empty <see cref="ViewContext"/>.
/// </summary>
/// <remarks>
/// The default constructor is provided for unit test purposes only.
/// </remarks>
public PageContext()
{
}
public PageContext(
ActionContext actionContext,
ViewDataDictionary viewData,
ITempDataDictionary tempDataDictionary,
HtmlHelperOptions htmlHelperOptions)
: base(actionContext, NullView.Instance, viewData, tempDataDictionary, TextWriter.Null, htmlHelperOptions)
{
}
/// <summary>
/// Gets or sets the <see cref="PageActionDescriptor"/>.
/// </summary>
public new CompiledPageActionDescriptor ActionDescriptor
{
get
{
return _actionDescriptor;
}
set
{
_actionDescriptor = value;
base.ActionDescriptor = value;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-us")]
[assembly: AssemblyCompany("Microsoft Corporation.")]

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
}
/// <summary>
/// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.
/// The @page directive for the Razor page at {0} cannot override the relative path prefix.
/// </summary>
internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0)
{
@ -42,6 +42,54 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
return GetString("RazorProject_PathMustStartWithForwardSlash");
}
/// <summary>
/// The '{0}' property of '{1}' must not be null.
/// </summary>
internal static string PropertyOfTypeCannotBeNull
{
get { return GetString("PropertyOfTypeCannotBeNull"); }
}
/// <summary>
/// The '{0}' property of '{1}' must not be null.
/// </summary>
internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1);
}
/// <summary>
/// Page created by '{0}' must be an instance of '{1}'.
/// </summary>
internal static string ActivatedInstance_MustBeAnInstanceOf
{
get { return GetString("ActivatedInstance_MustBeAnInstanceOf"); }
}
/// <summary>
/// Page created by '{0}' must be an instance of '{1}'.
/// </summary>
internal static string FormatActivatedInstance_MustBeAnInstanceOf(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ActivatedInstance_MustBeAnInstanceOf"), p0, p1);
}
/// <summary>
/// The Razor page type '{0}' does not have a parameterless constructor.
/// </summary>
internal static string PageActivator_TypeDoesNotHaveParameterlessConstructor
{
get { return GetString("PageActivator_TypeDoesNotHaveParameterlessConstructor"); }
}
/// <summary>
/// The Razor page type '{0}' does not have a parameterless constructor.
/// </summary>
internal static string FormatPageActivator_TypeDoesNotHaveParameterlessConstructor(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PageActivator_TypeDoesNotHaveParameterlessConstructor"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -123,4 +123,10 @@
<data name="RazorProject_PathMustStartWithForwardSlash" xml:space="preserve">
<value>Path must begin with a forward slash '/'.</value>
</data>
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value>
</data>
<data name="ActivatedInstance_MustBeAnInstanceOf" xml:space="preserve">
<value>Page created by '{0}' must be an instance of '{1}'.</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
{
{
"description": "ASP.NET Core MVC Razor Pages.",
"version": "1.0.0-*",
"packOptions": {
@ -22,10 +22,18 @@
"xmlDoc": true
},
"dependencies": {
"Microsoft.AspNetCore.Mvc.ViewFeatures": {
"Microsoft.AspNetCore.Mvc.Razor": {
"target": "project"
},
"Microsoft.AspNetCore.Razor.Evolution": "1.0.0-*",
"Microsoft.Extensions.PropertyActivator.Sources": {
"version": "1.2.0-*",
"type": "build"
},
"Microsoft.Extensions.PropertyHelper.Sources": {
"version": "1.2.0-*",
"type": "build"
},
"NETStandard.Library": "1.6.2-*"
},
"frameworks": {

View File

@ -0,0 +1,155 @@
// 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;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultPageActivatorTest
{
[Fact]
public void CreateActivator_ThrowsIfPageTypeInfoIsNull()
{
// Arrange
var descriptor = new CompiledPageActionDescriptor();
var activator = new DefaultPageActivator();
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => activator.CreateActivator(descriptor),
"actionDescriptor",
"The 'PageTypeInfo' property of 'actionDescriptor' must not be null.");
}
[Theory]
[InlineData(typeof(TestPage))]
[InlineData(typeof(PageWithMultipleConstructors))]
public void CreateActivator_ReturnsFactoryForPage(Type type)
{
// Arrange
var pageContext = new PageContext();
var descriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = type.GetTypeInfo(),
};
var activator = new DefaultPageActivator();
// Act
var factory = activator.CreateActivator(descriptor);
var instance = factory(pageContext);
// Assert
Assert.NotNull(instance);
Assert.IsType(type, instance);
}
[Fact]
public void CreateActivator_ThrowsIfTypeDoesNotHaveParameterlessConstructor()
{
// Arrange
var descriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(PageWithoutParameterlessConstructor).GetTypeInfo(),
};
var pageContext = new PageContext();
var activator = new DefaultPageActivator();
// Act & Assert
Assert.Throws<ArgumentException>(() => activator.CreateActivator(descriptor));
}
[Theory]
[InlineData(typeof(TestPage))]
[InlineData(typeof(object))]
public void CreateReleaser_ReturnsNullForPagesThatDoNotImplementDisposable(Type pageType)
{
// Arrange
var context = new PageContext();
var activator = new DefaultPageActivator();
var page = new TestPage();
// Act
var releaser = activator.CreateReleaser(new CompiledPageActionDescriptor
{
PageTypeInfo = pageType.GetTypeInfo()
});
// Assert
Assert.Null(releaser);
}
[Fact]
public void CreateReleaser_CreatesDelegateThatDisposesDisposableTypes()
{
// Arrange
var context = new PageContext();
var activator = new DefaultPageActivator();
var page = new DisposablePage();
// Act & Assert
var disposer = activator.CreateReleaser(new CompiledPageActionDescriptor
{
PageTypeInfo = page.GetType().GetTypeInfo()
});
Assert.NotNull(disposer);
disposer(context, page);
// Assert
Assert.True(page.Disposed);
}
private class TestPage : Page
{
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class PageWithMultipleConstructors : Page
{
public PageWithMultipleConstructors(int x)
{
}
public PageWithMultipleConstructors()
{
}
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class PageWithoutParameterlessConstructor : Page
{
public PageWithoutParameterlessConstructor(ILogger logger)
{
}
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class DisposablePage : TestPage, IDisposable
{
public bool Disposed { get; private set; }
public void Dispose()
{
Disposed = true;
}
}
}
}

View File

@ -0,0 +1,307 @@
// 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.Diagnostics;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultPageFactoryProviderTest
{
[Fact]
public void CreatePage_ThrowsIfActivatedInstanceIsNotAnInstanceOfRazorPage()
{
// Arrange
var descriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(object).GetTypeInfo(),
};
var pageActivator = CreateActivator();
var factoryProvider = CreatePageFactory();
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => factoryProvider.CreatePageFactory(descriptor));
Assert.Equal(
$"Page created by '{pageActivator.GetType()}' must be an instance of '{typeof(Page)}'.",
ex.Message);
}
[Fact]
public void PageFactorySetsPageContext()
{
// Arrange
var descriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(TestPage).GetTypeInfo(),
};
var pageContext = new PageContext();
var factoryProvider = CreatePageFactory();
// Act
var factory = factoryProvider.CreatePageFactory(descriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<TestPage>(instance);
Assert.Same(pageContext, testPage.PageContext);
}
[Fact]
public void PageFactorySetsPropertiesWithRazorInject()
{
// Arrange
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(TestPage).GetTypeInfo(),
}
};
var urlHelperFactory = new Mock<IUrlHelperFactory>();
var urlHelper = Mock.Of<IUrlHelper>();
urlHelperFactory.Setup(f => f.GetUrlHelper(pageContext))
.Returns(urlHelper)
.Verifiable();
var htmlEncoder = HtmlEncoder.Create();
var factoryProvider = CreatePageFactory(
urlHelperFactory: urlHelperFactory.Object,
htmlEncoder: htmlEncoder);
// Act
var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<TestPage>(instance);
Assert.Same(urlHelper, testPage.UrlHelper);
Assert.Same(htmlEncoder, testPage.HtmlEncoder);
Assert.NotNull(testPage.ViewData);
}
[Fact]
public void PageFactorySetViewDataWithModelTypeWhenNotNull()
{
// Arrange
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(ViewDataTestPage).GetTypeInfo(),
ModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo(),
},
};
var factoryProvider = CreatePageFactory();
// Act
var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<ViewDataTestPage>(instance);
Assert.NotNull(testPage.ViewData);
}
[Fact]
public void PageFactorySetsNonGenericViewDataDictionary()
{
// Arrange
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(NonGenericViewDataTestPage).GetTypeInfo()
},
};
var factoryProvider = CreatePageFactory();
// Act
var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<NonGenericViewDataTestPage>(instance);
Assert.NotNull(testPage.ViewData);
}
[Fact]
public void PageFactorySetsNestedVidewDataDictionaryWhenContextHasANonNullDictionary()
{
// Arrange
var modelMetadataProvider = new EmptyModelMetadataProvider();
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(TestPage).GetTypeInfo()
},
ViewData = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary())
{
{ "test-key", "test-value" },
}
};
var factoryProvider = CreatePageFactory();
// Act
var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<TestPage>(instance);
Assert.NotNull(testPage.ViewData);
Assert.Equal("test-value", testPage.ViewData["test-key"]);
}
[Fact]
public void PageFactoryDoesNotBindPropertiesWithNoRazorInjectAttribute()
{
// Arrange
var serviceProvider = new ServiceCollection()
.AddSingleton<ILogger>(NullLogger.Instance)
.BuildServiceProvider();
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
PageTypeInfo = typeof(PropertiesWithoutRazorInject).GetTypeInfo()
},
HttpContext = new DefaultHttpContext
{
RequestServices = serviceProvider,
},
};
var factoryProvider = CreatePageFactory();
// Act
var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor);
var instance = factory(pageContext);
// Assert
var testPage = Assert.IsType<PropertiesWithoutRazorInject>(instance);
Assert.Null(testPage.DiagnosticSourceWithoutInject);
Assert.NotNull(testPage.DiagnosticSourceWithInject);
Assert.Null(testPage.LoggerWithoutInject);
Assert.NotNull(testPage.LoggerWithInject);
Assert.Null(testPage.ModelExpressionProviderWithoutInject);
Assert.NotNull(testPage.ModelExpressionProviderWithInject);
}
private static DefaultPageFactory CreatePageFactory(
IPageActivatorProvider pageActivator = null,
IModelMetadataProvider provider = null,
IUrlHelperFactory urlHelperFactory = null,
IJsonHelper jsonHelper = null,
DiagnosticSource diagnosticSource = null,
HtmlEncoder htmlEncoder = null,
IModelExpressionProvider modelExpressionProvider = null)
{
return new DefaultPageFactory(
pageActivator ?? CreateActivator(),
provider ?? Mock.Of<IModelMetadataProvider>(),
urlHelperFactory ?? Mock.Of<IUrlHelperFactory>(),
jsonHelper ?? Mock.Of<IJsonHelper>(),
diagnosticSource ?? new DiagnosticListener("Microsoft.AspNetCore.Mvc.RazorPages"),
htmlEncoder ?? HtmlEncoder.Default,
modelExpressionProvider ?? Mock.Of<IModelExpressionProvider>());
}
private static IPageActivatorProvider CreateActivator()
{
var activator = new Mock<IPageActivatorProvider>();
activator.Setup(a => a.CreateActivator(It.IsAny<CompiledPageActionDescriptor>()))
.Returns((CompiledPageActionDescriptor descriptor) =>
{
return (context) => Activator.CreateInstance(descriptor.PageTypeInfo.AsType());
});
return activator.Object;
}
private class TestPage : Page
{
[RazorInject]
public IUrlHelper UrlHelper { get; set; }
[RazorInject]
public HtmlEncoder HtmlEncoder { get; set; }
[RazorInject]
public ViewDataDictionary<TestPage> ViewData { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class NonGenericViewDataTestPage : Page
{
[RazorInject]
public ViewDataDictionary ViewData { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class ViewDataTestPage : Page
{
[RazorInject]
public ViewDataDictionary<ViewDataTestPageModel> ViewData { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class ViewDataTestPageModel
{
}
private class PropertiesWithoutRazorInject : Page
{
public IModelExpressionProvider ModelExpressionProviderWithoutInject { get; set; }
[RazorInject]
public IModelExpressionProvider ModelExpressionProviderWithInject { get; set; }
public DiagnosticSource DiagnosticSourceWithoutInject { get; set; }
[RazorInject]
public DiagnosticSource DiagnosticSourceWithInject { get; set; }
public ILogger LoggerWithoutInject { get; set; }
[RazorInject]
public ILogger LoggerWithInject { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,131 @@
// 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 Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageInvokerProviderTest
{
[Fact]
public void GetOrAddCacheEntry_PopulatesCacheEntry()
{
// Arrange
var descriptor = new PageActionDescriptor
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
};
Func<PageContext, object> factory = _ => null;
Action<PageContext, object> releaser = (_, __) => { };
var loader = new Mock<IPageLoader>();
loader.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(typeof(object));
var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1);
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
var factoryProvider = new Mock<IPageFactoryProvider>();
factoryProvider.Setup(f => f.CreatePageFactory(It.IsAny<CompiledPageActionDescriptor>()))
.Returns(factory);
factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny<CompiledPageActionDescriptor>()))
.Returns(releaser);
var invokerProvider = new PageActionInvokerProvider(
loader.Object,
factoryProvider.Object,
actionDescriptorProvider.Object,
new IFilterProvider[0]);
var context = new ActionInvokerProviderContext(new ActionContext
{
ActionDescriptor = descriptor,
});
// Act
var entry = invokerProvider.GetOrAddCacheEntry(context, descriptor);
// Assert
Assert.NotNull(entry);
var compiledPageActionDescriptor = Assert.IsType<CompiledPageActionDescriptor>(entry.ActionDescriptor);
Assert.Equal(descriptor.RelativePath, compiledPageActionDescriptor.RelativePath);
Assert.Same(factory, entry.PageFactory);
Assert.Same(releaser, entry.ReleasePage);
}
[Fact]
public void GetOrAddCacheEntry_CachesEntries()
{
// Arrange
var descriptor = new PageActionDescriptor
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
};
var loader = new Mock<IPageLoader>();
loader.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(typeof(object));
var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1);
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
var invokerProvider = new PageActionInvokerProvider(
loader.Object,
Mock.Of<IPageFactoryProvider>(),
actionDescriptorProvider.Object,
new IFilterProvider[0]);
var context = new ActionInvokerProviderContext(new ActionContext
{
ActionDescriptor = descriptor,
});
// Act
var entry1 = invokerProvider.GetOrAddCacheEntry(context, descriptor);
var entry2 = invokerProvider.GetOrAddCacheEntry(context, descriptor);
// Assert
Assert.Same(entry1, entry2);
}
[Fact]
public void GetOrAddCacheEntry_UpdatesEntriesWhenActionDescriptorProviderCollectionIsUpdated()
{
// Arrange
var descriptor = new PageActionDescriptor
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
};
var descriptorCollection1 = new ActionDescriptorCollection(new[] { descriptor }, version: 1);
var descriptorCollection2 = new ActionDescriptorCollection(new[] { descriptor }, version: 2);
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
actionDescriptorProvider.SetupSequence(p => p.ActionDescriptors)
.Returns(descriptorCollection1)
.Returns(descriptorCollection2);
var loader = new Mock<IPageLoader>();
loader.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(typeof(object));
var invokerProvider = new PageActionInvokerProvider(
loader.Object,
Mock.Of<IPageFactoryProvider>(),
actionDescriptorProvider.Object,
new IFilterProvider[0]);
var context = new ActionInvokerProviderContext(new ActionContext
{
ActionDescriptor = descriptor,
});
// Act
var entry1 = invokerProvider.GetOrAddCacheEntry(context, descriptor);
var entry2 = invokerProvider.GetOrAddCacheEntry(context, descriptor);
// Assert
Assert.NotSame(entry1, entry2);
}
}
}

View File

@ -0,0 +1,284 @@
// 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.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageFilterFactoryProviderTest
{
[Fact]
public void FilterFactory_ReturnsNoFilters_IfNoFiltersAreSpecified()
{
// Arrange
var filterProviders = new IFilterProvider[0];
var actionInvokerProviderContext = GetInvokerContext();
// Act
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
filterProviders,
actionInvokerProviderContext);
var filters1 = filterFactory(actionInvokerProviderContext.ActionContext);
var filters2 = filterFactory(actionInvokerProviderContext.ActionContext);
// Assert
Assert.Empty(filters1);
Assert.Empty(filters2);
}
[Fact]
public void FilterFactory_ReturnsNoFilters_IfAllFiltersAreRemoved()
{
// Arrange
var filterProvider = new TestFilterProvider(
context => context.Results.Clear());
var filter = new FilterDescriptor(new TypeFilterAttribute(typeof(object)), FilterScope.Global);
var actionInvokerProviderContext = GetInvokerContext(filter);
// Act
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
new[] { filterProvider },
actionInvokerProviderContext);
var filters1 = filterFactory(actionInvokerProviderContext.ActionContext);
var filters2 = filterFactory(actionInvokerProviderContext.ActionContext);
// Assert
Assert.Empty(filters1);
Assert.Empty(filters2);
}
[Fact]
public void FilterFactory_CachesAllFilters()
{
// Arrange
var staticFilter1 = new TestFilter();
var staticFilter2 = new TestFilter();
var actionInvokerProviderContext = GetInvokerContext(new[]
{
new FilterDescriptor(staticFilter1, FilterScope.Action),
new FilterDescriptor(staticFilter2, FilterScope.Action),
});
var filterProviders = new[] { new DefaultFilterProvider() };
// Act - 1
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
filterProviders,
actionInvokerProviderContext);
var request1Filters = filterFactory(actionInvokerProviderContext.ActionContext);
// Assert - 1
Assert.Collection(
request1Filters,
f => Assert.Same(staticFilter1, f),
f => Assert.Same(staticFilter2, f));
// Act - 2
var request2Filters = filterFactory(actionInvokerProviderContext.ActionContext);
// Assert - 2
Assert.Collection(
request2Filters,
f => Assert.Same(staticFilter1, f),
f => Assert.Same(staticFilter2, f));
}
[Fact]
public void FilterFactory_CachesFilterFromFactory()
{
// Arrange
var staticFilter = new TestFilter();
var actionInvokerProviderContext = GetInvokerContext(new[]
{
new FilterDescriptor(new TestFilterFactory() { IsReusable = true }, FilterScope.Action),
new FilterDescriptor(staticFilter, FilterScope.Action),
});
var filterProviders = new[] { new DefaultFilterProvider() };
// Act & Assert
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
filterProviders,
actionInvokerProviderContext);
var filters = filterFactory(actionInvokerProviderContext.ActionContext);
Assert.Equal(2, filters.Length);
var cachedFactoryCreatedFilter = Assert.IsType<TestFilter>(filters[0]); // Created by factory
Assert.Same(staticFilter, filters[1]); // Cached and the same statically created filter instance
for (var i = 0; i < 5; i++)
{
filters = filterFactory(actionInvokerProviderContext.ActionContext);
var currentFactoryCreatedFilter = filters[0];
Assert.Same(currentFactoryCreatedFilter, cachedFactoryCreatedFilter); // Cached
Assert.Same(staticFilter, filters[1]); // Cached
}
}
[Fact]
public void FilterFactory_DoesNotCacheFiltersWithIsReusableFalse()
{
// Arrange
var staticFilter = new TestFilter();
var actionInvokerProviderContext = GetInvokerContext(new[]
{
new FilterDescriptor(new TestFilterFactory() { IsReusable = false }, FilterScope.Action),
new FilterDescriptor(staticFilter, FilterScope.Action),
});
var filterProviders = new[] { new DefaultFilterProvider() };
// Act & Assert
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
filterProviders,
actionInvokerProviderContext);
IFilterMetadata previousFactoryCreatedFilter = null;
for (var i = 0; i < 5; i++)
{
var filters = filterFactory(actionInvokerProviderContext.ActionContext);
var currentFactoryCreatedFilter = filters[0];
Assert.NotSame(currentFactoryCreatedFilter, previousFactoryCreatedFilter); // Never Cached
Assert.Same(staticFilter, filters[1]); // Cached
previousFactoryCreatedFilter = currentFactoryCreatedFilter;
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void FilterFactory_FiltersAddedByFilterProviders_AreNeverCached(bool reusable)
{
// Arrange
var customFilterProvider = new TestFilterProvider(
providerExecuting: (providerContext) =>
{
var filter = new TestFilter(providerContext.ActionContext.HttpContext.Items["name"] as string);
providerContext.Results.Add(
new FilterItem(new FilterDescriptor(filter, FilterScope.Global), filter)
{
IsReusable = reusable
});
});
var staticFilter = new TestFilter();
var actionInvokerProviderContext = GetInvokerContext(new[]
{
new FilterDescriptor(new TestFilterFactory() { IsReusable = false }, FilterScope.Action),
new FilterDescriptor(staticFilter, FilterScope.Action),
});
var actionContext = actionInvokerProviderContext.ActionContext;
var filterProviders = new IFilterProvider[] { new DefaultFilterProvider(), customFilterProvider };
// Act - 1
actionContext.HttpContext.Items["name"] = "foo";
var filterFactory = PageFilterFactoryProvider.GetFilterFactory(
filterProviders,
actionInvokerProviderContext);
var filters = filterFactory(actionContext);
// Assert - 1
Assert.Equal(3, filters.Length);
var request1Filter1 = Assert.IsType<TestFilter>(filters[0]); // Created by factory
Assert.Same(staticFilter, filters[1]); // Cached and the same statically created filter instance
var request1Filter3 = Assert.IsType<TestFilter>(filters[2]); // Created by custom filter provider
Assert.Equal("foo", request1Filter3.Data);
// Act - 2
actionContext.HttpContext.Items["name"] = "bar";
filters = filterFactory(actionContext);
// Assert -2
Assert.Equal(3, filters.Length);
var request2Filter1 = Assert.IsType<TestFilter>(filters[0]);
Assert.NotSame(request1Filter1, request2Filter1); // Created by factory
Assert.Same(staticFilter, filters[1]); // Cached and the same statically created filter instance
var request2Filter3 = Assert.IsType<TestFilter>(filters[2]);
Assert.NotSame(request1Filter3, request2Filter3); // Created by custom filter provider again
Assert.Equal("bar", request2Filter3.Data);
}
private class TestFilter : IFilterMetadata
{
public TestFilter()
{
}
public TestFilter(string data)
{
Data = data;
}
public string Data { get; }
}
private class TestFilterFactory : IFilterFactory
{
private TestFilter _testFilter;
public bool IsReusable { get; set; }
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (IsReusable)
{
if (_testFilter == null)
{
_testFilter = new TestFilter();
}
return _testFilter;
}
else
{
return new TestFilter();
}
}
}
private class TestFilterProvider : IFilterProvider
{
private readonly Action<FilterProviderContext> _providerExecuting;
private readonly Action<FilterProviderContext> _providerExecuted;
public TestFilterProvider(
Action<FilterProviderContext> providerExecuting,
Action<FilterProviderContext> providerExecuted = null,
int order = 0)
{
_providerExecuting = providerExecuting;
_providerExecuted = providerExecuted;
Order = order;
}
public int Order { get; }
public void OnProvidersExecuting(FilterProviderContext context)
{
_providerExecuting?.Invoke(context);
}
public void OnProvidersExecuted(FilterProviderContext context)
{
_providerExecuted?.Invoke(context);
}
}
private static ActionInvokerProviderContext GetInvokerContext(params FilterDescriptor[] filters)
{
var actionDescriptor = new PageActionDescriptor
{
FilterDescriptors = new List<FilterDescriptor>(filters ?? Enumerable.Empty<FilterDescriptor>())
};
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), actionDescriptor);
return new ActionInvokerProviderContext(actionContext);
}
}
}

View File

@ -1,6 +1,7 @@
{
"buildOptions": {
"warningsAsErrors": true
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"dotnet-test-xunit": "2.2.0-*",
@ -9,6 +10,7 @@
"target": "project"
},
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Microsoft.Extensions.DependencyInjection": "1.2.0-*",
"Moq": "4.6.36-*",
"xunit": "2.2.0-*"
},