diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs new file mode 100644 index 0000000000..67b90cc6a4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs @@ -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.GetPropertiesToActivate( + pageType, + typeof(RazorInjectAttribute), + propertyInfo => CreateActivateInfo(propertyInfo, propertyValueAccessors), + includeNonPublic: true); + } + + private PropertyActivator[] 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 if the ViewContext.ViewData is not set or the type of + // ViewContext.ViewData is an incompatible type. + if (context.ViewData == null) + { + // Create ViewDataDictionary(IModelMetadataProvider, ModelStateDictionary). + return CreateViewDataRoot(context.ModelState); + } + else if (context.ViewData.GetType() != ViewDataDictionaryType) + { + // Create ViewDataDictionary(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(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(castNewCall, parameterExpression); + return lambda.Compile(); + } + + private static PropertyActivator CreateActivateInfo( + PropertyInfo property, + PropertyValueAccessors valueAccessors) + { + Func 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 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(property, valueAccessor); + } + + public class PropertyValueAccessors + { + public Func UrlHelperAccessor { get; set; } + + public Func JsonHelperAccessor { get; set; } + + public Func DiagnosticSourceAccessor { get; set; } + + public Func HtmlEncoderAccessor { get; set; } + + public Func ModelExpressionProviderAccessor { get; set; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs index 563b67e205..f84278b49d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageActivator.cs @@ -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 { /// 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 private const string ModelPropertyName = "Model"; - private readonly ConcurrentDictionary _activationInfo; + private readonly ConcurrentDictionary _activationInfo; private readonly IModelMetadataProvider _metadataProvider; // Value accessors for common singleton properties activated in a RazorPage. - private Func _urlHelperAccessor; - private Func _jsonHelperAccessor; - private Func _diagnosticSourceAccessor; - private Func _htmlEncoderAccessor; - private Func _modelExpressionProviderAccessor; + private readonly RazorPagePropertyActivator.PropertyValueAccessors _propertyAccessors; /// /// Initializes a new instance of the class. @@ -48,13 +36,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor HtmlEncoder htmlEncoder, IModelExpressionProvider modelExpressionProvider) { - _activationInfo = new ConcurrentDictionary(); + _activationInfo = new ConcurrentDictionary(); _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, + }; } /// @@ -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 if the ViewContext.ViewData is not set or the type of - // ViewContext.ViewData is an incompatible type. - if (context.ViewData == null) - { - // Create ViewDataDictionary(IModelMetadataProvider, ModelStateDictionary). - return activationInfo.CreateViewDataRoot( - _metadataProvider, - context.ModelState); - } - else if (context.ViewData.GetType() != activationInfo.ViewDataDictionaryType) - { - // Create ViewDataDictionary(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 - 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.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(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(castNewCall, parameters); - return lambda.Compile(); - } - - - private PropertyActivator CreateActivateInfo(PropertyInfo property) - { - Func 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 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 + 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(property, valueAccessor); - } - - private class PageActivationInfo - { - public PropertyActivator[] PropertyActivators { get; set; } - - public Type ViewDataDictionaryType { get; set; } - - public CreateViewDataNested CreateViewDataNested { get; set; } - - public CreateViewDataRoot CreateViewDataRoot { get; set; } - - public Action ViewDataDictionarySetter { get; set; } + propertyActivator.Activate(page, context); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs new file mode 100644 index 0000000000..e879bb4a0b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs @@ -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 +{ + /// + /// A for a compiled Razor page. + /// + public class CompiledPageActionDescriptor : PageActionDescriptor + { + /// + /// Initializes an empty . + /// + public CompiledPageActionDescriptor() + { + } + + /// + /// Initializes a new instance of + /// from the specified instance. + /// + /// The . + public CompiledPageActionDescriptor(PageActionDescriptor actionDescriptor) + : base(actionDescriptor) + { + } + + /// + /// Gets or sets the of the page. + /// + public TypeInfo PageTypeInfo { get; set; } + + /// + /// Gets or sets the of the model. + /// + public TypeInfo ModelTypeInfo { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageActivatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageActivatorProvider.cs new file mode 100644 index 0000000000..98ebb5a915 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageActivatorProvider.cs @@ -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 +{ + /// + /// Provides methods to create a Razor page. + /// + public interface IPageActivatorProvider + { + /// + /// Creates a Razor page activator. + /// + /// The . + /// The delegate used to activate the page. + Func CreateActivator(CompiledPageActionDescriptor descriptor); + + /// + /// Releases a Razor page. + /// + /// The . + /// The delegate used to dispose the activated page. + Action CreateReleaser(CompiledPageActionDescriptor descriptor); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageFactoryProvider.cs new file mode 100644 index 0000000000..dff6addb1a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageFactoryProvider.cs @@ -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 +{ + /// + /// Provides methods for creation and disposal of Razor pages. + /// + public interface IPageFactoryProvider + { + /// + /// Creates a factory for producing Razor pages for the specified . + /// + /// The . + /// The Razor page factory. + Func CreatePageFactory(CompiledPageActionDescriptor descriptor); + + /// + /// Releases a Razor page. + /// + /// The . + /// The delegate used to release the created page. + Action CreatePageDisposer(CompiledPageActionDescriptor descriptor); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs new file mode 100644 index 0000000000..09b61fe86a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/IPageLoader.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public interface IPageLoader + { + Type Load(PageActionDescriptor actionDescriptor); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs new file mode 100644 index 0000000000..53b58d42ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs @@ -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 +{ + /// + /// that uses type activation to create Pages. + /// + public class DefaultPageActivator : IPageActivatorProvider + { + private readonly Action _disposer = Dispose; + + /// + public virtual Func 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 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 CreatePageFactory(Type pageTypeInfo) + { + var parameter = Expression.Parameter(typeof(PageContext), "pageContext"); + + // new Page(); + var newExpression = Expression.New(pageTypeInfo); + + // () => new Page(); + var pageFactory = Expression + .Lambda>(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 + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs new file mode 100644 index 0000000000..cadd93913d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs @@ -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 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 CreatePageDisposer(CompiledPageActionDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return _pageActivator.CreateReleaser(descriptor); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs new file mode 100644 index 0000000000..75ba5121be --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs new file mode 100644 index 0000000000..c6b356922c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerCacheEntry.cs @@ -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 pageFactory, + Action releasePage, + Func filterProvider) + { + ActionDescriptor = actionDescriptor; + PageFactory = pageFactory; + ReleasePage = releasePage; + FilterProvider = filterProvider; + } + + public CompiledPageActionDescriptor ActionDescriptor { get; } + + public Func PageFactory { get; } + + /// + /// The action invoked to release a page. This may be null. + /// + public Action ReleasePage { get; } + + Func FilterProvider { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs new file mode 100644 index 0000000000..004c536012 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -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 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 Entries { get; } = + new ConcurrentDictionary(); + + public int Version { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterFactoryProvider.cs new file mode 100644 index 0000000000..486911e9f1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterFactoryProvider.cs @@ -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 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(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(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, 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.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; + } + }; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.xproj b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.xproj index 35edf3918f..a1b62b1be5 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.xproj +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.xproj @@ -12,8 +12,9 @@ .\obj .\bin\ v4.6 + .resx - + 2.0 diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs new file mode 100644 index 0000000000..a1bf56d1a2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -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 +{ + /// + /// A base class for a Razor page. + /// + public abstract class Page : IRazorPage + { + /// + public IHtmlContent BodyContent { get; set; } + + /// + public bool IsLayoutBeingRendered { get; set; } + + /// + public string Layout { get; set; } + + /// + public string Path { get; set; } + + /// + public IDictionary PreviousSectionWriters { get; set; } + + /// + public IDictionary SectionWriters { get; } + + /// + /// The . + /// + public PageContext PageContext { get; set; } + + /// + public ViewContext ViewContext { get; set; } + + /// + public void EnsureRenderedBodyOrSections() + { + throw new NotImplementedException(); + } + + /// + public abstract Task ExecuteAsync(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs index bcaa23235b..19650a12ad 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageActionDescriptor.cs @@ -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 { + /// + /// Initializes a new instance of . + /// + public PageActionDescriptor() + { + } + + /// + /// A copy constructor for . + /// + /// The to copy from. + public PageActionDescriptor(PageActionDescriptor other) + { + RelativePath = other.RelativePath; + ViewEnginePath = other.ViewEnginePath; + } + + /// + /// Gets or sets the application root relative path for the page. + /// public string RelativePath { get; set; } + /// + /// Gets or sets the path relative to the base path for page discovery. + /// public string ViewEnginePath { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs new file mode 100644 index 0000000000..965c33189b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs @@ -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 +{ + /// + /// The context associated with the current request for a Razor page. + /// + public class PageContext : ViewContext + { + private CompiledPageActionDescriptor _actionDescriptor; + + /// + /// Creates an empty . + /// + /// + /// The default constructor is provided for unit test purposes only. + /// + public PageContext() + { + } + + public PageContext( + ActionContext actionContext, + ViewDataDictionary viewData, + ITempDataDictionary tempDataDictionary, + HtmlHelperOptions htmlHelperOptions) + : base(actionContext, NullView.Instance, viewData, tempDataDictionary, TextWriter.Null, htmlHelperOptions) + { + } + + /// + /// Gets or sets the . + /// + public new CompiledPageActionDescriptor ActionDescriptor + { + get + { + return _actionDescriptor; + } + set + { + _actionDescriptor = value; + base.ActionDescriptor = value; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs index 1a8b56c5a1..2860a31bf7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/AssemblyInfo.cs @@ -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.")] diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 07d93675dc..a5637e77f7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } /// - /// 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. /// internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0) { @@ -42,6 +42,54 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages return GetString("RazorProject_PathMustStartWithForwardSlash"); } + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string PropertyOfTypeCannotBeNull + { + get { return GetString("PropertyOfTypeCannotBeNull"); } + } + + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1); + } + + /// + /// Page created by '{0}' must be an instance of '{1}'. + /// + internal static string ActivatedInstance_MustBeAnInstanceOf + { + get { return GetString("ActivatedInstance_MustBeAnInstanceOf"); } + } + + /// + /// Page created by '{0}' must be an instance of '{1}'. + /// + internal static string FormatActivatedInstance_MustBeAnInstanceOf(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ActivatedInstance_MustBeAnInstanceOf"), p0, p1); + } + + /// + /// The Razor page type '{0}' does not have a parameterless constructor. + /// + internal static string PageActivator_TypeDoesNotHaveParameterlessConstructor + { + get { return GetString("PageActivator_TypeDoesNotHaveParameterlessConstructor"); } + } + + /// + /// The Razor page type '{0}' does not have a parameterless constructor. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index 2679a194f5..ee0b86b563 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -123,4 +123,10 @@ Path must begin with a forward slash '/'. + + The '{0}' property of '{1}' must not be null. + + + Page created by '{0}' must be an instance of '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json index da8dc20b17..5819fcbcee 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/project.json @@ -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": { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs new file mode 100644 index 0000000000..6d15679713 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs @@ -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(() => 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; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs new file mode 100644 index 0000000000..eadb731cb4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs @@ -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(() => 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(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(); + var urlHelper = Mock.Of(); + 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(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(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(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(instance); + Assert.NotNull(testPage.ViewData); + Assert.Equal("test-value", testPage.ViewData["test-key"]); + } + + [Fact] + public void PageFactoryDoesNotBindPropertiesWithNoRazorInjectAttribute() + { + // Arrange + var serviceProvider = new ServiceCollection() + .AddSingleton(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(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(), + urlHelperFactory ?? Mock.Of(), + jsonHelper ?? Mock.Of(), + diagnosticSource ?? new DiagnosticListener("Microsoft.AspNetCore.Mvc.RazorPages"), + htmlEncoder ?? HtmlEncoder.Default, + modelExpressionProvider ?? Mock.Of()); + } + + private static IPageActivatorProvider CreateActivator() + { + var activator = new Mock(); + activator.Setup(a => a.CreateActivator(It.IsAny())) + .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 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 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(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs new file mode 100644 index 0000000000..35895b9707 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -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 factory = _ => null; + Action releaser = (_, __) => { }; + var loader = new Mock(); + loader.Setup(l => l.Load(It.IsAny())) + .Returns(typeof(object)); + var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var actionDescriptorProvider = new Mock(); + actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var factoryProvider = new Mock(); + factoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) + .Returns(factory); + factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) + .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(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(); + loader.Setup(l => l.Load(It.IsAny())) + .Returns(typeof(object)); + var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var actionDescriptorProvider = new Mock(); + actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + + var invokerProvider = new PageActionInvokerProvider( + loader.Object, + Mock.Of(), + 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(); + actionDescriptorProvider.SetupSequence(p => p.ActionDescriptors) + .Returns(descriptorCollection1) + .Returns(descriptorCollection2); + + var loader = new Mock(); + loader.Setup(l => l.Load(It.IsAny())) + .Returns(typeof(object)); + var invokerProvider = new PageActionInvokerProvider( + loader.Object, + Mock.Of(), + actionDescriptorProvider.Object, + new IFilterProvider[0]); + var context = new ActionInvokerProviderContext(new ActionContext + { + ActionDescriptor = descriptor, + }); + + // Act + var entry1 = invokerProvider.GetOrAddCacheEntry(context, descriptor); + var entry2 = invokerProvider.GetOrAddCacheEntry(context, descriptor); + + // Assert + Assert.NotSame(entry1, entry2); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterFactoryProviderTest.cs new file mode 100644 index 0000000000..5fa888e1e6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterFactoryProviderTest.cs @@ -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(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(filters[0]); // Created by factory + Assert.Same(staticFilter, filters[1]); // Cached and the same statically created filter instance + var request1Filter3 = Assert.IsType(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(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(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 _providerExecuting; + private readonly Action _providerExecuted; + + public TestFilterProvider( + Action providerExecuting, + Action 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(filters ?? Enumerable.Empty()) + }; + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), actionDescriptor); + return new ActionInvokerProviderContext(actionContext); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json index 260310c4c4..3f143e5b96 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/project.json @@ -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-*" },