Add IPageFactory and IPageActivator
This commit is contained in:
parent
9146fce4ec
commit
2b8233932a
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||
{
|
||||
public interface IPageLoader
|
||||
{
|
||||
Type Load(PageActionDescriptor actionDescriptor);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.")]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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-*"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue