From fa18eba97b232db3ccb1b8163cd0cd062dca4646 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 20 Jun 2017 14:25:22 -0700 Subject: [PATCH 1/2] Add support for specifying filters on page models. Fixes #6334 --- .../IPageRouteModelConvention.cs | 17 + .../IPageRouteModelProvider.cs | 44 + .../ApplicationModels/PageApplicationModel.cs | 85 +- .../PageApplicationModelProviderContext.cs | 23 +- .../ApplicationModels/PageHandlerModel.cs | 98 +++ .../ApplicationModels/PageParameterModel.cs | 61 ++ .../ApplicationModels/PagePropertyModel.cs | 83 ++ .../ApplicationModels/PageRouteModel.cs | 77 ++ .../PageRouteModelProviderContext.cs | 18 + .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 10 +- .../RazorPagesOptionsExtensions.cs | 69 +- .../PageActionDescriptorProvider.cs | 45 +- ...thorizationPageApplicationModelProvider.cs | 48 ++ .../CompiledPageActionDescriptorBuilder.cs | 119 +++ ...r.cs => CompiledPageRouteModelProvider.cs} | 24 +- .../DefaultPageApplicationModelProvider.cs | 353 ++++++++ .../Internal/DefaultPageLoader.cs | 254 +----- .../Internal/PageActionInvokerProvider.cs | 9 +- .../PageFilterApplicationModelProvider.cs | 21 +- .../Internal/PageSelectorModel.cs | 2 +- ... => RazorProjectPageRouteModelProvider.cs} | 16 +- ...Microsoft.AspNetCore.Mvc.RazorPages.csproj | 1 + .../RazorPagesOptions.cs | 12 +- .../RazorPagesTest.cs | 24 + .../RazorPagesWithBasePathTest.cs | 10 + .../MvcRazorPagesMvcBuilderExtensionsTest.cs | 12 +- .../RazorPagesOptionsExtensionsTest.cs | 70 +- .../PageActionDescriptorProviderTest.cs | 175 +--- ...izationPageApplicationModelProviderTest.cs | 150 ++++ ...CompiledPageActionDescriptorBuilderTest.cs | 311 +++++++ ... => CompiledPageRouteModelProviderTest.cs} | 28 +- ...DefaultPageApplicationModelProviderTest.cs | 788 ++++++++++++++++++ .../Internal/DefaultPageLoaderTest.cs | 647 ++------------ .../Internal/PageActionInvokerProviderTest.cs | 12 +- .../PageFilterApplicationModelProviderTest.cs | 29 +- .../Internal/PocoModel.cs | 17 + ...RazorProjectPageRouteModelProviderTest.cs} | 32 +- .../MvcServiceCollectionExtensionsTest.cs | 12 +- .../HelloWorldWithPageModelHandler.cs | 1 + .../RazorPagesWebSite/ModelWithAuthFilter.cs | 15 + .../ModelWithAuthFilter.cshtml | 4 + .../RazorPagesWebSite/ModelWithPageFilter.cs | 44 + .../ModelWithPageFilter.cshtml | 4 + .../Conventions/AuthFolder/AnonymousModel.cs | 15 + .../AuthFolder/AnonymousViaModel.cshtml | 3 + 45 files changed, 2750 insertions(+), 1142 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelConvention.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageHandlerModel.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelProviderContext.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AuthorizationPageApplicationModelProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs rename src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/{CompiledPageApplicationModelProvider.cs => CompiledPageRouteModelProvider.cs} (80%) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs rename src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/{RazorProjectPageApplicationModelProvider.cs => RazorProjectPageRouteModelProvider.cs} (74%) create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs rename test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/{CompiledPageApplicationModelProviderTest.cs => CompiledPageRouteModelProviderTest.cs} (83%) create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PocoModel.cs rename test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/{RazorProjectPageApplicationModelProviderTest.cs => RazorProjectPageRouteModelProviderTest.cs} (84%) create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cs create mode 100644 test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cshtml create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousModel.cs create mode 100644 test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousViaModel.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelConvention.cs new file mode 100644 index 0000000000..cfa332a465 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelConvention.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Allows customization of the of the . + /// + public interface IPageRouteModelConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply(PageRouteModel model); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelProvider.cs new file mode 100644 index 0000000000..300d8ecf9c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageRouteModelProvider.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Builds or modifies an for Razor Page routing. + /// + public interface IPageRouteModelProvider + { + /// + /// Gets the order value for determining the order of execution of providers. Providers execute in + /// ascending numeric value of the property. + /// + /// + /// + /// Providers are executed in an ordering determined by an ascending sort of the property. + /// A provider with a lower numeric value of will have its + /// called before that of a provider with a higher numeric value of + /// . The method is called in the reverse ordering after + /// all calls to . A provider with a lower numeric value of + /// will have its method called after that of a provider + /// with a higher numeric value of . + /// + /// + /// If two providers have the same numeric value of , then their relative execution order + /// is undefined. + /// + /// + int Order { get; } + + /// + /// Executed for the first pass of building instances. See . + /// + /// The . + void OnProvidersExecuting(PageRouteModelProviderContext context); + + /// + /// Executed for the second pass of building instances. See . + /// + /// The . + void OnProvidersExecuted(PageRouteModelProviderContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs index dd075bc91b..dee4f299fd 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { @@ -16,26 +19,21 @@ public class PageApplicationModel /// /// Initializes a new instance of . /// - /// The application relative path of the page. - /// The path relative to the base path for page discovery. - public PageApplicationModel(string relativePath, string viewEnginePath) + public PageApplicationModel( + PageActionDescriptor actionDescriptor, + TypeInfo handlerType, + IReadOnlyList handlerAttributes) { - if (relativePath == null) - { - throw new ArgumentNullException(nameof(relativePath)); - } - - if (viewEnginePath == null) - { - throw new ArgumentNullException(nameof(viewEnginePath)); - } - - RelativePath = relativePath; - ViewEnginePath = viewEnginePath; + ActionDescriptor = actionDescriptor ?? throw new ArgumentNullException(nameof(actionDescriptor)); + HandlerType = handlerType; Filters = new List(); - Properties = new Dictionary(); - Selectors = new List(); + Properties = new CopyOnWriteDictionary( + actionDescriptor.Properties, + EqualityComparer.Default); + HandlerMethods = new List(); + HandlerProperties = new List(); + HandlerTypeAttributes = handlerAttributes; } /// @@ -49,24 +47,38 @@ public PageApplicationModel(PageApplicationModel other) throw new ArgumentNullException(nameof(other)); } - RelativePath = other.RelativePath; - ViewEnginePath = other.ViewEnginePath; + ActionDescriptor = other.ActionDescriptor; + HandlerType = other.HandlerType; + PageType = other.PageType; + ModelType = other.ModelType; Filters = new List(other.Filters); Properties = new Dictionary(other.Properties); - Selectors = new List(other.Selectors.Select(m => new SelectorModel(m))); + HandlerMethods = new List(other.HandlerMethods.Select(m => new PageHandlerModel(m))); + HandlerProperties = new List(other.HandlerProperties.Select(p => new PagePropertyModel(p))); + HandlerTypeAttributes = other.HandlerTypeAttributes; } + /// + /// Gets the . + /// + public PageActionDescriptor ActionDescriptor { get; } + /// /// Gets the application root relative path for the page. /// - public string RelativePath { get; } + public string RelativePath => ActionDescriptor.RelativePath; /// /// Gets the path relative to the base path for page discovery. /// - public string ViewEnginePath { get; } + public string ViewEnginePath => ActionDescriptor.ViewEnginePath; + + /// + /// Gets the route template for the page. + /// + public string RouteTemplate => ActionDescriptor.AttributeRouteInfo?.Template; /// /// Gets the applicable instances. @@ -79,8 +91,33 @@ public PageApplicationModel(PageApplicationModel other) public IDictionary Properties { get; } /// - /// Gets the instances. + /// Gets or sets the of the Razor page. + /// + public TypeInfo PageType { get; set; } + + /// + /// Gets or sets the of the Razor page model. + /// + public TypeInfo ModelType { get; set; } + + /// + /// Gets the of the handler. + /// + public TypeInfo HandlerType { get; } + + /// + /// Gets the sequence of attributes declared on . + /// + public IReadOnlyList HandlerTypeAttributes { get; } + + /// + /// Gets the sequence of instances. + /// + public IList HandlerMethods { get; } + + /// + /// Gets the sequence of instances on . /// - public IList Selectors { get; } + public IList HandlerProperties { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs index b73f88c168..cccb6824e5 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs @@ -1,7 +1,8 @@ // 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.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.RazorPages; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { @@ -10,9 +11,25 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// public class PageApplicationModelProviderContext { + public PageApplicationModelProviderContext(PageActionDescriptor descriptor, TypeInfo pageTypeInfo) + { + ActionDescriptor = descriptor; + PageType = pageTypeInfo; + } + + /// + /// Gets the . + /// + public PageActionDescriptor ActionDescriptor { get; } + + /// + /// Gets the page . + /// + public TypeInfo PageType { get; } + /// - /// Gets the instances. + /// Gets or sets the . /// - public IList Results { get; } = new List(); + public PageApplicationModel PageApplicationModel { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageHandlerModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageHandlerModel.cs new file mode 100644 index 0000000000..eae43995f2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageHandlerModel.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Represents a handler in a . + /// + [DebuggerDisplay("PageHandlerModel: Name={" + nameof(PageHandlerModel.Name) + "}")] + public class PageHandlerModel : ICommonModel + { + /// + /// Creates a new . + /// + /// The for the handler. + /// Any attributes annotated on the handler method. + public PageHandlerModel( + MethodInfo handlerMethod, + IReadOnlyList attributes) + { + MethodInfo = handlerMethod ?? throw new ArgumentNullException(nameof(handlerMethod)); + Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes)); + + Parameters = new List(); + Properties = new Dictionary(); + } + + /// + /// Creats a new instance of from a given . + /// + /// The which needs to be copied. + public PageHandlerModel(PageHandlerModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + MethodInfo = other.MethodInfo; + HandlerName = other.HandlerName; + HttpMethod = other.HttpMethod; + Name = other.Name; + + Page = other.Page; + + // These are just metadata, safe to create new collections + Attributes = new List(other.Attributes); + Properties = new Dictionary(other.Properties); + + // Make a deep copy of other 'model' types. + Parameters = new List(other.Parameters.Select(p => new PageParameterModel(p) { Handler = this })); + } + + /// + /// Gets the for the handler. + /// + public MethodInfo MethodInfo { get; } + + /// + /// Gets or sets the HTTP method supported by this handler. + /// + public string HttpMethod { get; set; } + + /// + /// Gets or sets the handler method name. + /// + public string HandlerName { get; set; } + + /// + /// Gets or sets a descriptive name for the handler. + /// + public string Name { get; set; } + + /// + /// Gets the seqeunce of instances. + /// + public IList Parameters { get; } + + /// + /// Gets or sets the . + /// + public PageApplicationModel Page { get; set; } + + /// + public IReadOnlyList Attributes { get; } + + /// + public IDictionary Properties { get; } + + MemberInfo ICommonModel.MemberInfo => MethodInfo; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs new file mode 100644 index 0000000000..c4547411c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + [DebuggerDisplay("PageParameterModel: Name={ParameterName}")] + public class PageParameterModel : ICommonModel, IBindingModel + { + public PageParameterModel( + ParameterInfo parameterInfo, + IReadOnlyList attributes) + { + ParameterInfo = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo)); + + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + + Properties = new Dictionary(); + Attributes = new List(attributes); + } + + public PageParameterModel(PageParameterModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + Handler = other.Handler; + Attributes = new List(other.Attributes); + BindingInfo = other.BindingInfo == null ? null : new BindingInfo(other.BindingInfo); + ParameterInfo = other.ParameterInfo; + ParameterName = other.ParameterName; + Properties = new Dictionary(other.Properties); + } + + public PageHandlerModel Handler { get; set; } + + public IReadOnlyList Attributes { get; } + + public IDictionary Properties { get; } + + MemberInfo ICommonModel.MemberInfo => ParameterInfo.Member; + + string ICommonModel.Name => ParameterName; + + public ParameterInfo ParameterInfo { get; } + + public string ParameterName { get; set; } + + public BindingInfo BindingInfo { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs new file mode 100644 index 0000000000..ac31e8d6bd --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Represents a property in a . + /// + [DebuggerDisplay("PagePropertyModel: Name={PropertyName}")] + public class PagePropertyModel : ICommonModel, IBindingModel + { + /// + /// Creates a new instance of . + /// + /// The for the underlying property. + /// Any attributes which are annotated on the property. + public PagePropertyModel( + PropertyInfo propertyInfo, + IReadOnlyList attributes) + { + PropertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); + Properties = new Dictionary(); + Attributes = new List(attributes) ?? throw new ArgumentNullException(nameof(attributes)); + } + + /// + /// Creats a new instance of from a given . + /// + /// The which needs to be copied. + public PagePropertyModel(PagePropertyModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + Page = other.Page; + Attributes = new List(other.Attributes); + BindingInfo = BindingInfo == null ? null : new BindingInfo(other.BindingInfo); + PropertyInfo = other.PropertyInfo; + PropertyName = other.PropertyName; + Properties = new Dictionary(other.Properties); + } + + /// + /// Gets or sets the this is associated with. + /// + public PageApplicationModel Page { get; set; } + + /// + /// Gets any attributes which are annotated on the property. + /// + public IReadOnlyList Attributes { get; } + + /// + public IDictionary Properties { get; } + + /// + /// Gets or sets the associated with this model. + /// + public BindingInfo BindingInfo { get; set; } + + /// + /// Gets the underlying . + /// + public PropertyInfo PropertyInfo { get; } + + /// + /// Gets or sets the name of the property represented by this model. + /// + public string PropertyName { get; set; } + + MemberInfo ICommonModel.MemberInfo => PropertyInfo; + + string ICommonModel.Name => PropertyName; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs new file mode 100644 index 0000000000..8cc6c413af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs @@ -0,0 +1,77 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// A model component for routing RazorPages. + /// + public class PageRouteModel + { + /// + /// Initializes a new instance of . + /// + /// The application relative path of the page. + /// The path relative to the base path for page discovery. + public PageRouteModel(string relativePath, string viewEnginePath) + { + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + if (viewEnginePath == null) + { + throw new ArgumentNullException(nameof(viewEnginePath)); + } + + RelativePath = relativePath; + ViewEnginePath = viewEnginePath; + + Properties = new Dictionary(); + Selectors = new List(); + } + + /// + /// A copy constructor for . + /// + /// The to copy from. + public PageRouteModel(PageRouteModel other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + RelativePath = other.RelativePath; + ViewEnginePath = other.ViewEnginePath; + + Properties = new Dictionary(other.Properties); + Selectors = new List(other.Selectors.Select(m => new SelectorModel(m))); + } + + /// + /// Gets the application root relative path for the page. + /// + public string RelativePath { get; } + + /// + /// Gets the path relative to the base path for page discovery. + /// + public string ViewEnginePath { get; } + + /// + /// Stores arbitrary metadata properties associated with the . + /// + public IDictionary Properties { get; } + + /// + /// Gets the instances. + /// + public IList Selectors { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelProviderContext.cs new file mode 100644 index 0000000000..966eb3876b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelProviderContext.cs @@ -0,0 +1,18 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// A context object for . + /// + public class PageRouteModelProviderContext + { + /// + /// Gets the instances. + /// + public IList RouteModels { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 6df8579ecd..bc2b8f4770 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Internal; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -88,9 +87,14 @@ internal static void AddServices(IServiceCollection services) ServiceDescriptor.Singleton()); services.TryAddSingleton(); services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + ServiceDescriptor.Singleton()); services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + ServiceDescriptor.Singleton()); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs index c7b46332db..4c8f69223e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs @@ -32,7 +32,7 @@ public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, throw new ArgumentNullException(nameof(factory)); } - options.Conventions.Add(new FolderConvention("/", model => model.Filters.Add(factory(model)))); + options.ApplicationModelConventions.Add(new FolderApplicationModelConvention("/", model => model.Filters.Add(factory(model)))); return options; } @@ -54,7 +54,7 @@ public static RazorPagesOptions ConfigureFilter(this RazorPagesOptions options, throw new ArgumentNullException(nameof(filter)); } - options.Conventions.Add(new FolderConvention("/", model => model.Filters.Add(filter))); + options.ApplicationModelConventions.Add(new FolderApplicationModelConvention("/", model => model.Filters.Add(filter))); return options; } @@ -77,7 +77,7 @@ public static RazorPagesOptions AllowAnonymousToPage(this RazorPagesOptions opti } var anonymousFilter = new AllowAnonymousFilter(); - options.Conventions.Add(new PageConvention(pageName, model => model.Filters.Add(anonymousFilter))); + options.ApplicationModelConventions.Add(new PageApplicationModelConvention(pageName, model => model.Filters.Add(anonymousFilter))); return options; } @@ -100,7 +100,7 @@ public static RazorPagesOptions AllowAnonymousToFolder(this RazorPagesOptions op } var anonymousFilter = new AllowAnonymousFilter(); - options.Conventions.Add(new FolderConvention(folderPath, model => model.Filters.Add(anonymousFilter))); + options.ApplicationModelConventions.Add(new FolderApplicationModelConvention(folderPath, model => model.Filters.Add(anonymousFilter))); return options; } @@ -124,7 +124,7 @@ public static RazorPagesOptions AuthorizePage(this RazorPagesOptions options, st } var authorizeFilter = new AuthorizeFilter(policy); - options.Conventions.Add(new PageConvention(pageName, model => model.Filters.Add(authorizeFilter))); + options.ApplicationModelConventions.Add(new PageApplicationModelConvention(pageName, model => model.Filters.Add(authorizeFilter))); return options; } @@ -157,7 +157,7 @@ public static RazorPagesOptions AuthorizeFolder(this RazorPagesOptions options, } var authorizeFilter = new AuthorizeFilter(policy); - options.Conventions.Add(new FolderConvention(folderPath, model => model.Filters.Add(authorizeFilter))); + options.ApplicationModelConventions.Add(new FolderApplicationModelConvention(folderPath, model => model.Filters.Add(authorizeFilter))); return options; } @@ -198,7 +198,7 @@ public static RazorPagesOptions AddPageRoute(this RazorPagesOptions options, str throw new ArgumentNullException(nameof(route)); } - options.Conventions.Add(new PageConvention(pageName, model => + options.RouteModelConventions.Add(new PageRouteModelConvention(pageName, model => { // Use the route specified in MapPageRoute for outbound routing. foreach (var selector in model.Selectors) @@ -218,12 +218,59 @@ public static RazorPagesOptions AddPageRoute(this RazorPagesOptions options, str return options; } - private class PageConvention : IPageApplicationModelConvention + private class PageRouteModelConvention : IPageRouteModelConvention + { + private readonly string _path; + private readonly Action _action; + + public PageRouteModelConvention(string path, Action action) + { + _path = path; + _action = action; + } + + public void Apply(PageRouteModel model) + { + if (string.Equals(model.ViewEnginePath, _path, StringComparison.OrdinalIgnoreCase)) + { + _action(model); + } + } + } + + private class FolderRouteModelConvention : IPageRouteModelConvention + { + private readonly string _folderPath; + private readonly Action _action; + + public FolderRouteModelConvention(string folderPath, Action action) + { + _folderPath = folderPath.TrimEnd('/'); + _action = action; + } + + public void Apply(PageRouteModel model) + { + var viewEnginePath = model.ViewEnginePath; + + var applyConvention = _folderPath == "/" || + (viewEnginePath.Length > _folderPath.Length && + viewEnginePath.StartsWith(_folderPath, StringComparison.OrdinalIgnoreCase) && + viewEnginePath[_folderPath.Length] == '/'); + + if (applyConvention) + { + _action(model); + } + } + } + + private class PageApplicationModelConvention : IPageApplicationModelConvention { private readonly string _path; private readonly Action _action; - public PageConvention(string path, Action action) + public PageApplicationModelConvention(string path, Action action) { _path = path; _action = action; @@ -238,12 +285,12 @@ public void Apply(PageApplicationModel model) } } - private class FolderConvention : IPageApplicationModelConvention + private class FolderApplicationModelConvention : IPageApplicationModelConvention { private readonly string _folderPath; private readonly Action _action; - public FolderConvention(string folderPath, Action action) + public FolderApplicationModelConvention(string folderPath, Action action) { _folderPath = folderPath.TrimEnd('/'); _action = action; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index 816020e60b..0c4970b5cf 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -14,16 +14,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class PageActionDescriptorProvider : IActionDescriptorProvider { - private readonly List _applicationModelProviders; + private readonly IPageRouteModelProvider[] _routeModelProviders; private readonly MvcOptions _mvcOptions; private readonly RazorPagesOptions _pagesOptions; public PageActionDescriptorProvider( - IEnumerable pageMetadataProviders, + IEnumerable pageRouteModelProviders, IOptions mvcOptionsAccessor, IOptions pagesOptionsAccessor) { - _applicationModelProviders = pageMetadataProviders.OrderBy(p => p.Order).ToList(); + _routeModelProviders = pageRouteModelProviders.OrderBy(p => p.Order).ToArray(); _mvcOptions = mvcOptionsAccessor.Value; _pagesOptions = pagesOptionsAccessor.Value; } @@ -32,51 +32,40 @@ public PageActionDescriptorProvider( public void OnProvidersExecuting(ActionDescriptorProviderContext context) { - var pageApplicationModels = BuildModel(); + var pageRouteModels = BuildModel(); - for (var i = 0; i < pageApplicationModels.Count; i++) + for (var i = 0; i < pageRouteModels.Count; i++) { - AddActionDescriptors(context.Results, pageApplicationModels[i]); + AddActionDescriptors(context.Results, pageRouteModels[i]); } } - protected IList BuildModel() + protected IList BuildModel() { - var context = new PageApplicationModelProviderContext(); + var context = new PageRouteModelProviderContext(); - for (var i = 0; i < _applicationModelProviders.Count; i++) + for (var i = 0; i < _routeModelProviders.Length; i++) { - _applicationModelProviders[i].OnProvidersExecuting(context); + _routeModelProviders[i].OnProvidersExecuting(context); } - for (var i = _applicationModelProviders.Count - 1; i >= 0; i--) + for (var i = _routeModelProviders.Length - 1; i >= 0; i--) { - _applicationModelProviders[i].OnProvidersExecuted(context); + _routeModelProviders[i].OnProvidersExecuted(context); } - return context.Results; + return context.RouteModels; } public void OnProvidersExecuted(ActionDescriptorProviderContext context) { } - private void AddActionDescriptors(IList actions, PageApplicationModel model) + private void AddActionDescriptors(IList actions, PageRouteModel model) { - for (var i = 0; i < _pagesOptions.Conventions.Count; i++) + for (var i = 0; i < _pagesOptions.RouteModelConventions.Count; i++) { - _pagesOptions.Conventions[i].Apply(model); - } - - var filters = new List(_mvcOptions.Filters.Count + model.Filters.Count); - for (var i = 0; i < _mvcOptions.Filters.Count; i++) - { - filters.Add(new FilterDescriptor(_mvcOptions.Filters[i], FilterScope.Global)); - } - - for (var i = 0; i < model.Filters.Count; i++) - { - filters.Add(new FilterDescriptor(model.Filters[i], FilterScope.Action)); + _pagesOptions.RouteModelConventions[i].Apply(model); } foreach (var selector in model.Selectors) @@ -92,7 +81,7 @@ private void AddActionDescriptors(IList actions, PageApplicati SuppressPathMatching = selector.AttributeRouteModel.SuppressPathMatching, }, DisplayName = $"Page: {model.ViewEnginePath}", - FilterDescriptors = filters, + FilterDescriptors = Array.Empty(), Properties = new Dictionary(model.Properties), RelativePath = model.RelativePath, RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AuthorizationPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AuthorizationPageApplicationModelProvider.cs new file mode 100644 index 0000000000..ad047c1ede --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/AuthorizationPageApplicationModelProvider.cs @@ -0,0 +1,48 @@ +// 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; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class AuthorizationPageApplicationModelProvider : IPageApplicationModelProvider + { + private readonly IAuthorizationPolicyProvider _policyProvider; + + public AuthorizationPageApplicationModelProvider(IAuthorizationPolicyProvider policyProvider) + { + _policyProvider = policyProvider; + } + + // The order is set to execute after the DefaultPageApplicationModelProvider. + public int Order => -1000 + 10; + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var pageModel = context.PageApplicationModel; + var authorizeData = pageModel.HandlerTypeAttributes.OfType().ToArray(); + if (authorizeData.Length > 0) + { + pageModel.Filters.Add(AuthorizationApplicationModelProvider.GetFilter(_policyProvider, authorizeData)); + } + foreach (var attribute in pageModel.HandlerTypeAttributes.OfType()) + { + pageModel.Filters.Add(new AllowAnonymousFilter()); + } + } + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs new file mode 100644 index 0000000000..c529c7bb4f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs @@ -0,0 +1,119 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + /// + /// Constructs a from an . + /// + public static class CompiledPageActionDescriptorBuilder + { + /// + /// Creates a from the specified . + /// + /// The . + /// The . + public static CompiledPageActionDescriptor Build(PageApplicationModel applicationModel) + { + var boundProperties = CreateBoundProperties(applicationModel); + var filters = applicationModel.Filters + .Select(f => new FilterDescriptor(f, FilterScope.Action)) + .ToArray(); + var handlerMethods = CreateHandlerMethods(applicationModel); + + var actionDescriptor = applicationModel.ActionDescriptor; + return new CompiledPageActionDescriptor(actionDescriptor) + { + ActionConstraints = actionDescriptor.ActionConstraints, + AttributeRouteInfo = actionDescriptor.AttributeRouteInfo, + BoundProperties = boundProperties, + FilterDescriptors = filters, + HandlerMethods = handlerMethods, + HandlerTypeInfo = applicationModel.HandlerType, + ModelTypeInfo = applicationModel.ModelType, + RouteValues = actionDescriptor.RouteValues, + PageTypeInfo = applicationModel.PageType, + Properties = applicationModel.Properties, + }; + } + + // Internal for unit testing + internal static HandlerMethodDescriptor[] CreateHandlerMethods(PageApplicationModel applicationModel) + { + var handlerModels = applicationModel.HandlerMethods; + var handlerDescriptors = new HandlerMethodDescriptor[handlerModels.Count]; + + for (var i = 0; i < handlerDescriptors.Length; i++) + { + var handlerModel = handlerModels[i]; + + handlerDescriptors[i] = new HandlerMethodDescriptor + { + HttpMethod = handlerModel.HttpMethod, + Name = handlerModel.HandlerName, + MethodInfo = handlerModel.MethodInfo, + Parameters = CreateHandlerParameters(handlerModel), + }; + } + + return handlerDescriptors; + } + + // internal for unit testing + internal static HandlerParameterDescriptor[] CreateHandlerParameters(PageHandlerModel handlerModel) + { + var methodParameters = handlerModel.Parameters; + var parameters = new HandlerParameterDescriptor[methodParameters.Count]; + + for (var i = 0; i < parameters.Length; i++) + { + var parameterModel = methodParameters[i]; + + parameters[i] = new HandlerParameterDescriptor + { + BindingInfo = parameterModel.BindingInfo, + Name = parameterModel.ParameterName, + ParameterInfo = parameterModel.ParameterInfo, + ParameterType = parameterModel.ParameterInfo.ParameterType, + }; + } + + return parameters; + } + + // internal for unit testing + internal static PageBoundPropertyDescriptor[] CreateBoundProperties(PageApplicationModel applicationModel) + { + var results = new List(); + var properties = applicationModel.HandlerProperties; + for (var i = 0; i < properties.Count; i++) + { + var propertyModel = properties[i]; + + // Only add properties which are explicitly marked to bind. + if (propertyModel.BindingInfo == null) + { + continue; + } + + var descriptor = new PageBoundPropertyDescriptor + { + Property = propertyModel.PropertyInfo, + Name = propertyModel.PropertyName, + BindingInfo = propertyModel.BindingInfo, + ParameterType = propertyModel.PropertyInfo.PropertyType, + }; + + results.Add(descriptor); + } + + return results.ToArray(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs similarity index 80% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs index 61779b3a03..e8110ecf9f 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs @@ -12,14 +12,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class CompiledPageApplicationModelProvider : IPageApplicationModelProvider + public class CompiledPageRouteModelProvider : IPageRouteModelProvider { private readonly object _cacheLock = new object(); private readonly ApplicationPartManager _applicationManager; private readonly RazorPagesOptions _pagesOptions; - private List _cachedApplicationModels; + private List _cachedModels; - public CompiledPageApplicationModelProvider( + public CompiledPageRouteModelProvider( ApplicationPartManager applicationManager, IOptions pagesOptionsAccessor) { @@ -29,17 +29,17 @@ public CompiledPageApplicationModelProvider( public int Order => -1000; - public void OnProvidersExecuting(PageApplicationModelProviderContext context) + public void OnProvidersExecuting(PageRouteModelProviderContext context) { EnsureCache(); - for (var i = 0; i < _cachedApplicationModels.Count; i++) + for (var i = 0; i < _cachedModels.Count; i++) { - var pageModel = _cachedApplicationModels[i]; - context.Results.Add(new PageApplicationModel(pageModel)); + var pageModel = _cachedModels[i]; + context.RouteModels.Add(new PageRouteModel(pageModel)); } } - public void OnProvidersExecuted(PageApplicationModelProviderContext context) + public void OnProvidersExecuted(PageRouteModelProviderContext context) { } @@ -47,7 +47,7 @@ private void EnsureCache() { lock (_cacheLock) { - if (_cachedApplicationModels != null) + if (_cachedModels != null) { return; } @@ -58,7 +58,7 @@ private void EnsureCache() rootDirectory = rootDirectory + "/"; } - var cachedApplicationModels = new List(); + var cachedApplicationModels = new List(); foreach (var viewDescriptor in GetViewDescriptors(_applicationManager)) { if (!viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase)) @@ -67,14 +67,14 @@ private void EnsureCache() } var viewEnginePath = GetViewEnginePath(rootDirectory, viewDescriptor.RelativePath); - var model = new PageApplicationModel(viewDescriptor.RelativePath, viewEnginePath); + var model = new PageRouteModel(viewDescriptor.RelativePath, viewEnginePath); var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute; PageSelectorModel.PopulateDefaults(model, pageAttribute.RouteTemplate); cachedApplicationModels.Add(model); } - _cachedApplicationModels = cachedApplicationModels; + _cachedModels = cachedApplicationModels; } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs new file mode 100644 index 0000000000..dc92f83e14 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs @@ -0,0 +1,353 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageApplicationModelProvider : IPageApplicationModelProvider + { + private const string ModelPropertyName = "Model"; + private readonly FilterCollection _globalFilters; + + /// + /// Initializes a new instance of . + /// + /// + public DefaultPageApplicationModelProvider(IOptions mvcOptions) + { + _globalFilters = mvcOptions.Value.Filters; + } + + /// + public int Order => -1000; + + /// + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.PageApplicationModel = CreateModel(context.ActionDescriptor, context.PageType); + } + + /// + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + /// + /// Creates a for the given . + /// + /// The . + /// The . + /// A for the given . + protected virtual PageApplicationModel CreateModel( + PageActionDescriptor actionDescriptor, + TypeInfo pageTypeInfo) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException(nameof(actionDescriptor)); + } + + if (pageTypeInfo == null) + { + throw new ArgumentNullException(nameof(pageTypeInfo)); + } + + // Pages always have a model type. If it's not set explicitly by the developer using + // @model, it will be the same as the page type. + var modelTypeInfo = pageTypeInfo.GetProperty(ModelPropertyName)?.PropertyType?.GetTypeInfo(); + + // Now we want to find the handler methods. If the model defines any handlers, then we'll use those, + // otherwise look at the page itself (unless the page IS the model, in which case we already looked). + TypeInfo handlerType; + + var handlerModels = modelTypeInfo == null ? null : CreateHandlerModels(modelTypeInfo); + if (handlerModels?.Count > 0) + { + handlerType = modelTypeInfo; + } + else + { + handlerType = pageTypeInfo.GetTypeInfo(); + handlerModels = CreateHandlerModels(pageTypeInfo); + } + + var handlerTypeAttributes = handlerType.GetCustomAttributes(inherit: true); + var pageModel = new PageApplicationModel( + actionDescriptor, + handlerType, + handlerTypeAttributes) + { + PageType = pageTypeInfo, + ModelType = modelTypeInfo, + }; + + for (var i = 0; i < handlerModels.Count; i++) + { + var handlerModel = handlerModels[i]; + handlerModel.Page = pageModel; + pageModel.HandlerMethods.Add(handlerModel); + } + + PopulateHandlerProperties(pageModel); + + for (var i = 0; i < _globalFilters.Count; i++) + { + pageModel.Filters.Add(_globalFilters[i]); + } + + for (var i = 0; i < handlerTypeAttributes.Length; i++) + { + if (handlerTypeAttributes[i] is IFilterMetadata filter) + { + pageModel.Filters.Add(filter); + } + } + + return pageModel; + } + + // Internal for unit testing + internal void PopulateHandlerProperties(PageApplicationModel pageModel) + { + var properties = PropertyHelper.GetVisibleProperties(pageModel.HandlerType.AsType()); + for (var i = 0; i < properties.Length; i++) + { + var propertyModel = CreatePropertyModel(properties[i].Property); + if (propertyModel != null) + { + propertyModel.Page = pageModel; + pageModel.HandlerProperties.Add(propertyModel); + } + } + } + + // Internal for unit testing + internal IList CreateHandlerModels(TypeInfo handlerTypeInfo) + { + var methods = handlerTypeInfo.GetMethods(); + var results = new List(); + + for (var i = 0; i < methods.Length; i++) + { + var handler = CreateHandlerModel(methods[i]); + if (handler != null) + { + results.Add(handler); + } + } + + return results; + } + + /// + /// Creates a for the specified .s + /// + /// The . + /// The . + protected virtual PageHandlerModel CreateHandlerModel(MethodInfo method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (!IsHandler(method)) + { + return null; + } + + if (method.IsDefined(typeof(NonHandlerAttribute))) + { + return null; + } + + if (method.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute))) + { + return null; + } + + if (!TryParseHandlerMethod(method.Name, out var httpMethod, out var handlerName)) + { + return null; + } + + var handlerModel = new PageHandlerModel( + method, + method.GetCustomAttributes(inherit: false)) + { + Name = method.Name, + HandlerName = handlerName, + HttpMethod = httpMethod, + }; + + var methodParameters = handlerModel.MethodInfo.GetParameters(); + + for (var i = 0; i < methodParameters.Length; i++) + { + var parameter = methodParameters[i]; + var parameterModel = CreateParameterModel(parameter); + parameterModel.Handler = handlerModel; + + handlerModel.Parameters.Add(parameterModel); + } + + return handlerModel; + } + + /// + /// Creates a for the specified . + /// + /// The . + /// The . + protected virtual PageParameterModel CreateParameterModel(ParameterInfo parameter) + { + if (parameter == null) + { + throw new ArgumentNullException(nameof(parameter)); + } + + return new PageParameterModel(parameter, parameter.GetCustomAttributes(inherit: true)) + { + BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes()), + ParameterName = parameter.Name, + }; + } + + /// + /// Creates a for the . + /// + /// The . + /// The . + protected virtual PagePropertyModel CreatePropertyModel(PropertyInfo property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + var attributes = property.GetCustomAttributes(inherit: true); + var bindingInfo = BindingInfo.GetBindingInfo(attributes); + + var model = new PagePropertyModel(property, property.GetCustomAttributes(inherit: true)) + { + PropertyName = property.Name, + BindingInfo = bindingInfo, + }; + + return model; + } + + /// + /// Determines if the specified is a handler. + /// + /// The . + /// true if the is a handler. Otherwise false. + /// + /// Override this method to provide custom logic to determine which methods are considered handlers. + /// + protected virtual bool IsHandler(MethodInfo methodInfo) + { + // The SpecialName bit is set to flag members that are treated in a special way by some compilers + // (such as property accessors and operator overloading methods). + if (methodInfo.IsSpecialName) + { + return false; + } + + // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. + if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + { + return false; + } + + if (methodInfo.IsStatic) + { + return false; + } + + if (methodInfo.IsAbstract) + { + return false; + } + + if (methodInfo.IsConstructor) + { + return false; + } + + if (methodInfo.IsGenericMethod) + { + return false; + } + + return methodInfo.IsPublic; + } + + internal static bool TryParseHandlerMethod(string methodName, out string httpMethod, out string handler) + { + httpMethod = null; + handler = null; + + // Handler method names always start with "On" + if (!methodName.StartsWith("On") || methodName.Length <= "On".Length) + { + return false; + } + + // Now we parse the method name according to our conventions to determine the required HTTP method + // and optional 'handler name'. + // + // Valid names look like: + // - OnGet + // - OnPost + // - OnFooBar + // - OnTraceAsync + // - OnPostEditAsync + + var start = "On".Length; + var length = methodName.Length; + if (methodName.EndsWith("Async", StringComparison.Ordinal)) + { + length -= "Async".Length; + } + + if (start == length) + { + // There are no additional characters. This is "On" or "OnAsync". + return false; + } + + // The http method follows "On" and is required to be at least one character. We use casing + // to determine where it ends. + var handlerNameStart = start + 1; + for (; handlerNameStart < length; handlerNameStart++) + { + if (char.IsUpper(methodName[handlerNameStart])) + { + break; + } + } + + httpMethod = methodName.Substring(start, handlerNameStart - start); + + // The handler name follows the http method and is optional. It includes everything up to the end + // excluding the "Async" suffix (if present). + handler = handlerNameStart == length ? null : methodName.Substring(handlerNameStart, length - handlerNameStart); + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index df4409b993..f0c588e932 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -3,267 +3,61 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; -using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class DefaultPageLoader : IPageLoader { - private const string ModelPropertyName = "Model"; - + private readonly IPageApplicationModelProvider[] _applicationModelProviders; private readonly IViewCompilerProvider _viewCompilerProvider; + private readonly RazorPagesOptions _options; - public DefaultPageLoader(IViewCompilerProvider viewCompilerProvider) + public DefaultPageLoader( + IEnumerable applicationModelProviders, + IViewCompilerProvider viewCompilerProvider, + IOptions pageOptions) { + _applicationModelProviders = applicationModelProviders.ToArray(); _viewCompilerProvider = viewCompilerProvider; + _options = pageOptions.Value; } private IViewCompiler Compiler => _viewCompilerProvider.GetCompiler(); public CompiledPageActionDescriptor Load(PageActionDescriptor actionDescriptor) { - var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); - var viewDescriptor = compileTask.GetAwaiter().GetResult(); - var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute; - - return CreateDescriptor(actionDescriptor, pageAttribute); - } - - // Internal for unit testing - internal static CompiledPageActionDescriptor CreateDescriptor( - PageActionDescriptor actionDescriptor, - RazorPageAttribute pageAttribute) - { - var pageType = pageAttribute.ViewType.GetTypeInfo(); - - // Pages always have a model type. If it's not set explicitly by the developer using - // @model, it will be the same as the page type. - var modelType = pageAttribute.ViewType.GetProperty(ModelPropertyName)?.PropertyType?.GetTypeInfo(); - - // Now we want to find the handler methods. If the model defines any handlers, then we'll use those, - // otherwise look at the page itself (unless the page IS the model, in which case we already looked). - TypeInfo handlerType; - - var handlerMethods = modelType == null ? null : CreateHandlerMethods(modelType); - if (handlerMethods?.Length > 0) - { - handlerType = modelType; - } - else - { - handlerType = pageType; - handlerMethods = CreateHandlerMethods(pageType); - } - - var boundProperties = CreateBoundProperties(handlerType); - - return new CompiledPageActionDescriptor(actionDescriptor) - { - ActionConstraints = actionDescriptor.ActionConstraints, - AttributeRouteInfo = actionDescriptor.AttributeRouteInfo, - BoundProperties = boundProperties, - FilterDescriptors = actionDescriptor.FilterDescriptors, - HandlerMethods = handlerMethods, - HandlerTypeInfo = handlerType, - ModelTypeInfo = modelType, - RouteValues = actionDescriptor.RouteValues, - PageTypeInfo = pageType, - Properties = actionDescriptor.Properties, - }; - } - - internal static HandlerMethodDescriptor[] CreateHandlerMethods(TypeInfo type) - { - var methods = type.GetMethods(); - var results = new List(); - - for (var i = 0; i < methods.Length; i++) - { - var method = methods[i]; - if (!IsValidHandlerMethod(method)) - { - continue; - } - - if (method.IsDefined(typeof(NonHandlerAttribute))) - { - continue; - } - - if (method.DeclaringType.GetTypeInfo().IsDefined(typeof(PagesBaseClassAttribute))) - { - continue; - } - - if (!TryParseHandlerMethod(method.Name, out var httpMethod, out var handler)) - { - continue; - } - - var parameters = CreateHandlerParameters(method); - - var handlerMethodDescriptor = new HandlerMethodDescriptor() - { - MethodInfo = method, - Name = handler, - HttpMethod = httpMethod, - Parameters = parameters, - }; - - results.Add(handlerMethodDescriptor); - } - - return results.ToArray(); - } - - // Internal for testing - internal static bool TryParseHandlerMethod(string methodName, out string httpMethod, out string handler) - { - httpMethod = null; - handler = null; - - // Handler method names always start with "On" - if (!methodName.StartsWith("On") || methodName.Length <= "On".Length) - { - return false; - } - - // Now we parse the method name according to our conventions to determine the required HTTP method - // and optional 'handler name'. - // - // Valid names look like: - // - OnGet - // - OnPost - // - OnFooBar - // - OnTraceAsync - // - OnPostEditAsync - - var start = "On".Length; - var length = methodName.Length; - if (methodName.EndsWith("Async", StringComparison.Ordinal)) - { - length -= "Async".Length; - } - - if (start == length) - { - // There are no additional characters. This is "On" or "OnAsync". - return false; - } - - // The http method follows "On" and is required to be at least one character. We use casing - // to determine where it ends. - var handlerNameStart = start + 1; - for (; handlerNameStart < length; handlerNameStart++) - { - if (char.IsUpper(methodName[handlerNameStart])) - { - break; - } - } - - httpMethod = methodName.Substring(start, handlerNameStart - start); - - // The handler name follows the http method and is optional. It includes everything up to the end - // excluding the "Async" suffix (if present). - handler = handlerNameStart == length ? null : methodName.Substring(handlerNameStart, length - handlerNameStart); - return true; - } - - private static bool IsValidHandlerMethod(MethodInfo methodInfo) - { - // The SpecialName bit is set to flag members that are treated in a special way by some compilers - // (such as property accessors and operator overloading methods). - if (methodInfo.IsSpecialName) - { - return false; - } - - // Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. - if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + if (actionDescriptor == null) { - return false; + throw new ArgumentNullException(nameof(actionDescriptor)); } - if (methodInfo.IsStatic) - { - return false; - } - - if (methodInfo.IsAbstract) - { - return false; - } + var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); + var viewDescriptor = compileTask.GetAwaiter().GetResult(); + var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute; - if (methodInfo.IsConstructor) + var context = new PageApplicationModelProviderContext(actionDescriptor, pageAttribute.ViewType.GetTypeInfo()); + for (var i = 0; i < _applicationModelProviders.Length; i++) { - return false; + _applicationModelProviders[i].OnProvidersExecuting(context); } - if (methodInfo.IsGenericMethod) + for (var i = _applicationModelProviders.Length - 1; i >= 0; i--) { - return false; + _applicationModelProviders[i].OnProvidersExecuted(context); } - return methodInfo.IsPublic; - } - - // Internal for testing - internal static HandlerParameterDescriptor[] CreateHandlerParameters(MethodInfo methodInfo) - { - var methodParameters = methodInfo.GetParameters(); - var parameters = new HandlerParameterDescriptor[methodParameters.Length]; - - for (var i = 0; i < methodParameters.Length; i++) + for (var i = 0; i < _options.ApplicationModelConventions.Count; i++) { - var parameter = methodParameters[i]; - - parameters[i] = new HandlerParameterDescriptor() - { - BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes()), - Name = parameter.Name, - ParameterInfo = parameter, - ParameterType = parameter.ParameterType, - }; - } - - return parameters; - } - - // Internal for testing - internal static PageBoundPropertyDescriptor[] CreateBoundProperties(TypeInfo type) - { - var properties = PropertyHelper.GetVisibleProperties(type.AsType()); - - var results = new List(); - for (var i = 0; i < properties.Length; i++) - { - var property = properties[i]; - var bindingInfo = BindingInfo.GetBindingInfo(property.Property.GetCustomAttributes()); - - // If there's no binding info then that means there are no model binding attributes on the - // property. So we won't bind this property. - if (bindingInfo == null) - { - continue; - } - - var descriptor = new PageBoundPropertyDescriptor() - { - BindingInfo = bindingInfo, - Name = property.Name, - Property = property.Property, - ParameterType = property.Property.PropertyType, - }; - - results.Add(descriptor); + _options.ApplicationModelConventions[i].Apply(context.PageApplicationModel); } - return results.ToArray(); + return CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 99b7a7a991..ff9863135c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -94,11 +94,11 @@ public void OnProvidersExecuting(ActionInvokerProviderContext context) } var cache = CurrentCache; - PageActionInvokerCacheEntry cacheEntry; - IFilterMetadata[] filters; - if (!cache.Entries.TryGetValue(actionDescriptor, out cacheEntry)) + if (!cache.Entries.TryGetValue(actionDescriptor, out var cacheEntry)) { + actionContext.ActionDescriptor = _loader.Load(actionDescriptor); + var filterFactoryResult = FilterFactory.GetAllFilters(_filterProviders, actionContext); filters = filterFactoryResult.Filters; cacheEntry = CreateCacheEntry(context, filterFactoryResult.CacheableFilters); @@ -166,8 +166,7 @@ private PageActionInvokerCacheEntry CreateCacheEntry( ActionInvokerProviderContext context, FilterItem[] cachedFilters) { - var actionDescriptor = (PageActionDescriptor)context.ActionContext.ActionDescriptor; - var compiledActionDescriptor = _loader.Load(actionDescriptor); + var compiledActionDescriptor = (CompiledPageActionDescriptor)context.ActionContext.ActionDescriptor; var viewDataFactory = ViewDataDictionaryFactory.CreateFactory(compiledActionDescriptor.ModelTypeInfo); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterApplicationModelProvider.cs index 24ba99ff26..75a4abfd0e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageFilterApplicationModelProvider.cs @@ -3,20 +3,16 @@ using System; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageFilterApplicationModelProvider : IPageApplicationModelProvider { - /// This order ensures that runs after - /// and . - /// + // The order is set to execute after the DefaultPageApplicationModelProvider. public int Order => -1000 + 10; public void OnProvidersExecuted(PageApplicationModelProviderContext context) { - // Do nothing } public void OnProvidersExecuting(PageApplicationModelProviderContext context) @@ -26,16 +22,13 @@ public void OnProvidersExecuting(PageApplicationModelProviderContext context) throw new ArgumentNullException(nameof(context)); } - for (var i = 0; i < context.Results.Count; i++) - { - var pageApplicationModel = context.Results[i]; - - // Support for [TempData] on properties - pageApplicationModel.Filters.Add(new PageSaveTempDataPropertyFilterFactory()); + var pageApplicationModel = context.PageApplicationModel; - // Always require an antiforgery token on post - pageApplicationModel.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - } + // Support for [TempData] on properties + pageApplicationModel.Filters.Add(new PageSaveTempDataPropertyFilterFactory()); + + // Always require an antiforgery token on post + pageApplicationModel.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs index 35e2bb4bd0..f8db3320ad 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs @@ -11,7 +11,7 @@ public static class PageSelectorModel { private const string IndexFileName = "Index.cshtml"; - public static void PopulateDefaults(PageApplicationModel model, string routeTemplate) + public static void PopulateDefaults(PageRouteModel model, string routeTemplate) { if (AttributeRouteModel.IsOverridePattern(routeTemplate)) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs similarity index 74% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageApplicationModelProvider.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs index d80651ad27..3b27adf383 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs @@ -9,29 +9,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class RazorProjectPageApplicationModelProvider : IPageApplicationModelProvider + public class RazorProjectPageRouteModelProvider : IPageRouteModelProvider { private readonly RazorProject _project; private readonly RazorPagesOptions _pagesOptions; private readonly ILogger _logger; - public RazorProjectPageApplicationModelProvider( + public RazorProjectPageRouteModelProvider( RazorProject razorProject, IOptions pagesOptionsAccessor, ILoggerFactory loggerFactory) { _project = razorProject; _pagesOptions = pagesOptionsAccessor.Value; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); } public int Order => -1000; - public void OnProvidersExecuted(PageApplicationModelProviderContext context) + public void OnProvidersExecuted(PageRouteModelProviderContext context) { } - public void OnProvidersExecuting(PageApplicationModelProviderContext context) + public void OnProvidersExecuting(PageRouteModelProviderContext context) { foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory)) { @@ -47,12 +47,12 @@ public void OnProvidersExecuting(PageApplicationModelProviderContext context) continue; } - var pageApplicationModel = new PageApplicationModel( + var routeModel = new PageRouteModel( relativePath: item.CombinedPath, viewEnginePath: item.PathWithoutExtension); - PageSelectorModel.PopulateDefaults(pageApplicationModel, routeTemplate); + PageSelectorModel.PopulateDefaults(routeModel, routeTemplate); - context.Results.Add(pageApplicationModel); + context.RouteModels.Add(routeModel); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj index 8b5913ca2a..83ab12f8f7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Microsoft.AspNetCore.Mvc.RazorPages.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs index 5fd0321190..e562414a7a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages.Internal; namespace Microsoft.AspNetCore.Mvc.RazorPages { @@ -15,10 +17,16 @@ public class RazorPagesOptions private string _root = "/Pages"; /// - /// Gets a list of instances that will be applied to + /// Gets a list of instances that will be applied to /// the when discovering Razor Pages. /// - public IList Conventions { get; } = new List(); + public IList RouteModelConventions { get; } = new List(); + + /// + /// Gets a list of instances that will be applied to + /// the when discovering Razor Pages. + /// + public IList ApplicationModelConventions { get; } = new List(); /// /// Application relative path used as the root of discovery for Razor Page files. diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index fdbf5893d0..6edff8f491 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -1050,6 +1050,30 @@ public async Task PagesCanByRoutedToApplicationRoot_ViaAddPageRoute() Assert.StartsWith(expected, response.Trim()); } + [Fact] + public async Task AuthFiltersAppliedToPageModel_AreExecuted() + { + // Act + var response = await Client.GetAsync("/ModelWithAuthFilter"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/Login?ReturnUrl=%2FModelWithAuthFilter", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task PageFiltersAppliedToPageModel_AreExecuted() + { + // Arrange + var expected = "Hello from OnGetEdit"; + + // Act + var response = await Client.GetStringAsync("/ModelWithPageFilter"); + + // Assert + Assert.Equal(expected, response.Trim()); + } + private async Task AddAntiforgeryHeaders(HttpRequestMessage request) { var getResponse = await Client.GetAsync(request.RequestUri); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 711e221ab3..92bcfb86c9 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -115,6 +115,16 @@ public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_For_Folders() Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuthFolder", response.Headers.Location.PathAndQuery); } + [Fact] + public async Task AuthConvention_AppliedToFolders_CanByOverridenByFiltersOnModel() + { + // Act + var response = await Client.GetStringAsync("/Conventions/AuthFolder/AnonymousViaModel"); + + // Assert + Assert.Equal("Hello from Anonymous", response.Trim()); + } + [Fact] public async Task ViewStart_IsDiscoveredWhenRootDirectoryIsSpecified() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs index 548fc09dbd..0d57132c92 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/MvcRazorPagesMvcBuilderExtensionsTest.cs @@ -18,21 +18,23 @@ public void AddRazorPagesOptions_AddsApplicationModelConventions() { // Arrange var services = new ServiceCollection().AddOptions(); - var expected = Mock.Of(); + var applicationModelConvention = Mock.Of(); + var routeModelConvention = Mock.Of(); var builder = new MvcBuilder(services, new ApplicationPartManager()); builder.AddRazorPagesOptions(options => { - options.Conventions.Add(expected); + options.ApplicationModelConventions.Add(applicationModelConvention); + options.RouteModelConventions.Add(routeModelConvention); }); var serviceProvider = services.BuildServiceProvider(); var accessor = serviceProvider.GetRequiredService>(); - // Act - var conventions = accessor.Value.Conventions; + // Act & Assert + var conventions = accessor.Value.ApplicationModelConventions; // Assert Assert.Collection(conventions, - convention => Assert.Same(expected, convention)); + convention => Assert.Same(applicationModelConvention, convention)); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs index 9741a0456f..99d45ad3b0 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/DependencyInjection/RazorPagesOptionsExtensionsTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Authorization; @@ -21,9 +22,9 @@ public void AddFilter_AddsFiltersToAllPages() var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -44,9 +45,9 @@ public void AuthorizePage_AddsAllowAnonymousFilterToSpecificPage() var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -79,9 +80,9 @@ public void AuthorizePage_AddsAllowAnonymousFilterToPagesUnderFolder(string fold var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -117,9 +118,9 @@ public void AuthorizePage_AddsAuthorizeFilterWithPolicyToSpecificPage() var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -146,9 +147,9 @@ public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToSpecificPage() var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -177,9 +178,9 @@ public void AuthorizePage_AddsAuthorizeFilterWithPolicyToPagesUnderFolder(string var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -214,9 +215,9 @@ public void AuthorizePage_AddsAuthorizeFilterWithoutPolicyToPagesUnderFolder(str var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), - new PageApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), - new PageApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), + CreateApplicationModel("/Pages/Index.cshtml", "/Index.cshtml"), + CreateApplicationModel("/Pages/Users/Account.cshtml", "/Users/Account.cshtml"), + CreateApplicationModel("/Pages/Users/Contact.cshtml", "/Users/Contact.cshtml"), }; // Act @@ -249,7 +250,7 @@ public void AddPageRoute_AddsRouteToSelector() var options = new RazorPagesOptions(); var models = new[] { - new PageApplicationModel("/Pages/Index.cshtml", "/Index.cshtml") + new PageRouteModel("/Pages/Index.cshtml", "/Index.cshtml") { Selectors = { @@ -257,7 +258,7 @@ public void AddPageRoute_AddsRouteToSelector() CreateSelectorModel(""), } }, - new PageApplicationModel("/Pages/About.cshtml", "/About.cshtml") + new PageRouteModel("/Pages/About.cshtml", "/About.cshtml") { Selectors = { @@ -316,9 +317,19 @@ private static SelectorModel CreateSelectorModel(string template, bool suppressL }; } + private static void ApplyConventions(RazorPagesOptions options, PageRouteModel[] models) + { + foreach (var convention in options.RouteModelConventions) + { + foreach (var model in models) + { + convention.Apply(model); + } + } + } private static void ApplyConventions(RazorPagesOptions options, PageApplicationModel[] models) { - foreach (var convention in options.Conventions) + foreach (var convention in options.ApplicationModelConventions) { foreach (var model in models) { @@ -326,5 +337,16 @@ private static void ApplyConventions(RazorPagesOptions options, PageApplicationM } } } + + private PageApplicationModel CreateApplicationModel(string relativePath, string viewEnginePath) + { + var descriptor = new PageActionDescriptor + { + ViewEnginePath = viewEnginePath, + RelativePath = relativePath, + }; + + return new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs index 95069bc9de..03e7ca778e 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -4,11 +4,8 @@ using System; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Internal; -using Microsoft.AspNetCore.Mvc.RazorPages.Internal; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.Options; using Moq; @@ -22,7 +19,7 @@ public class PageActionDescriptorProviderTest public void GetDescriptors_DoesNotAddDescriptorsIfNoApplicationModelsAreDiscovered() { // Arrange - var applicationModelProvider = new TestPageApplicationModelProvider(); + var applicationModelProvider = new TestPageRouteModelProvider(); var provider = new PageActionDescriptorProvider( new[] { applicationModelProvider }, GetAccessor(), @@ -40,7 +37,7 @@ public void GetDescriptors_DoesNotAddDescriptorsIfNoApplicationModelsAreDiscover public void GetDescriptors_AddsDescriptorsForModelWithSelector() { // Arrange - var model = new PageApplicationModel("/Test.cshtml", "/Test") + var model = new PageRouteModel("/Test.cshtml", "/Test") { Selectors = { @@ -53,7 +50,7 @@ public void GetDescriptors_AddsDescriptorsForModelWithSelector() } } }; - var applicationModelProvider = new TestPageApplicationModelProvider(model); + var applicationModelProvider = new TestPageRouteModelProvider(model); var provider = new PageActionDescriptorProvider( new[] { applicationModelProvider }, GetAccessor(), @@ -75,15 +72,15 @@ public void GetDescriptors_AddsDescriptorsForModelWithSelector() public void GetDescriptors_AddsActionDescriptorForEachSelector() { // Arrange - var applicationModelProvider = new TestPageApplicationModelProvider( - new PageApplicationModel("/base-path/Test.cshtml", "/base-path/Test") + var applicationModelProvider = new TestPageRouteModelProvider( + new PageRouteModel("/base-path/Test.cshtml", "/base-path/Test") { Selectors = { CreateSelectorModel("base-path/Test/Home") } }, - new PageApplicationModel("/base-path/Index.cshtml", "/base-path/Index") + new PageRouteModel("/base-path/Index.cshtml", "/base-path/Index") { Selectors = { @@ -91,7 +88,7 @@ public void GetDescriptors_AddsActionDescriptorForEachSelector() CreateSelectorModel("base-path/"), } }, - new PageApplicationModel("/base-path/Admin/Index.cshtml", "/base-path/Admin/Index") + new PageRouteModel("/base-path/Admin/Index.cshtml", "/base-path/Admin/Index") { Selectors = { @@ -99,7 +96,7 @@ public void GetDescriptors_AddsActionDescriptorForEachSelector() CreateSelectorModel("base-path/Admin"), } }, - new PageApplicationModel("/base-path/Admin/User.cshtml", "/base-path/Admin/User") + new PageRouteModel("/base-path/Admin/User.cshtml", "/base-path/Admin/User") { Selectors = { @@ -143,8 +140,8 @@ private static SelectorModel CreateSelectorModel(string template) public void GetDescriptors_AddsMultipleDescriptorsForPageWithMultipleSelectors() { // Arrange - var applicationModelProvider = new TestPageApplicationModelProvider( - new PageApplicationModel("/Catalog/Details/Index.cshtml", "/Catalog/Details/Index") + var applicationModelProvider = new TestPageRouteModelProvider( + new PageRouteModel("/Catalog/Details/Index.cshtml", "/Catalog/Details/Index") { Selectors = { @@ -180,142 +177,9 @@ public void GetDescriptors_AddsMultipleDescriptorsForPageWithMultipleSelectors() }); } - [Fact] - public void GetDescriptors_ImplicitFilters() - { - // Arrange - var options = new MvcOptions(); - var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); - var filterProvider = new PageFilterApplicationModelProvider(); - var provider = new PageActionDescriptorProvider( - new IPageApplicationModelProvider[] { applicationModelProvider, filterProvider }, - GetAccessor(options), - GetRazorPagesOptions()); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var result = Assert.Single(context.Results); - var descriptor = Assert.IsType(result); - Assert.Collection( - descriptor.FilterDescriptors, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }); - } - - [Fact] - public void GetDescriptors_AddsGlobalFilters() + private static PageRouteModel CreateModel() { - // Arrange - var filter1 = Mock.Of(); - var filter2 = Mock.Of(); - var options = new MvcOptions(); - options.Filters.Add(filter1); - options.Filters.Add(filter2); - var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); - var filterProvider = new PageFilterApplicationModelProvider(); - var provider = new PageActionDescriptorProvider( - new IPageApplicationModelProvider[] { applicationModelProvider, filterProvider }, - GetAccessor(options), - GetRazorPagesOptions()); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var result = Assert.Single(context.Results); - var descriptor = Assert.IsType(result); - Assert.Collection( - descriptor.FilterDescriptors, - filterDescriptor => - { - Assert.Equal(FilterScope.Global, filterDescriptor.Scope); - Assert.Same(filter1, filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Global, filterDescriptor.Scope); - Assert.Same(filter2, filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }); - } - - [Fact] - public void GetDescriptors_AddsFiltersAddedByConvention() - { - // Arrange - var globalFilter = Mock.Of(); - var localFilter = Mock.Of(); - var options = new MvcOptions(); - options.Filters.Add(globalFilter); - var convention = new Mock(); - convention.Setup(c => c.Apply(It.IsAny())) - .Callback((PageApplicationModel model) => - { - model.Filters.Add(localFilter); - }); - var razorOptions = GetRazorPagesOptions(); - razorOptions.Value.Conventions.Add(convention.Object); - var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); - var filterProvider = new PageFilterApplicationModelProvider(); - var provider = new PageActionDescriptorProvider( - new IPageApplicationModelProvider[] { applicationModelProvider, filterProvider }, - GetAccessor(options), - razorOptions); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var result = Assert.Single(context.Results); - var descriptor = Assert.IsType(result); - Assert.Collection(descriptor.FilterDescriptors, - filterDescriptor => - { - Assert.Equal(FilterScope.Global, filterDescriptor.Scope); - Assert.Same(globalFilter, filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.IsType(filterDescriptor.Filter); - }, - filterDescriptor => - { - Assert.Equal(FilterScope.Action, filterDescriptor.Scope); - Assert.Same(localFilter, filterDescriptor.Filter); - }); - } - - private static PageApplicationModel CreateModel() - { - return new PageApplicationModel("/Home.cshtml", "/Home") + return new PageRouteModel("/Home.cshtml", "/Home") { Selectors = { @@ -353,28 +217,27 @@ private static RazorProjectItem GetProjectItem(string basePath, string path, str return new FileProviderRazorProjectItem(testFileInfo, basePath, path); } - private class TestPageApplicationModelProvider : IPageApplicationModelProvider + private class TestPageRouteModelProvider : IPageRouteModelProvider { - private readonly PageApplicationModel[] _models; + private readonly PageRouteModel[] _models; - public TestPageApplicationModelProvider(params PageApplicationModel[] models) + public TestPageRouteModelProvider(params PageRouteModel[] models) { - _models = models ?? Array.Empty(); + _models = models ?? Array.Empty(); } public int Order => -1000; - public void OnProvidersExecuted(PageApplicationModelProviderContext context) + public void OnProvidersExecuted(PageRouteModelProviderContext context) { } - public void OnProvidersExecuting(PageApplicationModelProviderContext context) + public void OnProvidersExecuting(PageRouteModelProviderContext context) { foreach (var model in _models) { - context.Results.Add(model); + context.RouteModels.Add(model); } - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..ef7cc860c1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs @@ -0,0 +1,150 @@ +// 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.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Authorization; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class AuthorizationPageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_IgnoresAttributesOnHandlerMethods() + { + // Arrange + var policyProvider = new DefaultAuthorizationPolicyProvider(new TestOptionsManager()); + var autorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var typeInfo = typeof(PageWiithAuthorizeHandlers).GetTypeInfo(); + var context = GetApplicationProviderContext(typeInfo); + + // Act + autorizationProvider.OnProvidersExecuting(context); + + // Assert + Assert.Empty(context.PageApplicationModel.Filters); + } + + private class PageWiithAuthorizeHandlers + { + public ModelWuthAuthorizeHandlers Model => null; + } + + public class ModelWuthAuthorizeHandlers + { + [Authorize] + public void OnGet() + { + } + } + + [Fact] + public void OnProvidersExecuting_AddsAuthorizeFilter_IfModelHasAuthorizationAttributes() + { + // Arrange + var policyProvider = new DefaultAuthorizationPolicyProvider(new TestOptionsManager()); + var autorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var context = GetApplicationProviderContext(typeof(TestPage).GetTypeInfo()); + + // Act + autorizationProvider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f)); + } + + private class TestPage + { + public TestModel Model => null; + } + + [Authorize] + private class TestModel + { + public virtual void OnGet() + { + } + } + + [Fact] + public void OnProvidersExecuting_CollatesAttributesFromInheritedTypes() + { + // Arrange + var options = new TestOptionsManager(); + options.Value.AddPolicy("Base", policy => policy.RequireClaim("Basic").RequireClaim("Basic2")); + options.Value.AddPolicy("Derived", policy => policy.RequireClaim("Derived")); + + var policyProvider = new DefaultAuthorizationPolicyProvider(options); + var autorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + + var context = GetApplicationProviderContext(typeof(TestPageWithDerivedModel).GetTypeInfo()); + + // Act + autorizationProvider.OnProvidersExecuting(context); + + // Assert + var authorizeFilter = Assert.IsType(Assert.Single(context.PageApplicationModel.Filters)); + // Basic + Basic2 + Derived authorize + Assert.Equal(3, authorizeFilter.Policy.Requirements.Count); + } + + private class TestPageWithDerivedModel + { + public DeriviedModel Model => null; + } + + [Authorize(Policy = "Base")] + public class BaseModel + { + } + + [Authorize(Policy = "Derived")] + private class DeriviedModel : BaseModel + { + public virtual void OnGet() + { + } + } + + [Fact] + public void OnProvidersExecuting_AddsAllowAnonymousFilter() + { + // Arrange + var policyProvider = new DefaultAuthorizationPolicyProvider(new TestOptionsManager()); + var autorizationProvider = new AuthorizationPageApplicationModelProvider(policyProvider); + var context = GetApplicationProviderContext(typeof(PageWithAnonymousModel).GetTypeInfo()); + + // Act + autorizationProvider.OnProvidersExecuting(context); + + // Assert + Assert.Collection( + context.PageApplicationModel.Filters, + f => Assert.IsType(f)); + } + + private class PageWithAnonymousModel + { + public AnonymousModel Model => null; + } + + [AllowAnonymous] + public class AnonymousModel + { + public void OnGet() { } + } + + private static PageApplicationModelProviderContext GetApplicationProviderContext(TypeInfo typeInfo) + { + var defaultProvider = new DefaultPageApplicationModelProvider(new TestOptionsManager()); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + defaultProvider.OnProvidersExecuting(context); + return context; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs new file mode 100644 index 0000000000..ca13fb3f37 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs @@ -0,0 +1,311 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class CompiledPageActionDescriptorBuilderTest + { + [Fact] + public void CreateDescriptor_CopiesPropertiesFromPageActionDescriptor() + { + // Arrange + var actionDescriptor = new PageActionDescriptor + { + ActionConstraints = new List(), + AttributeRouteInfo = new AttributeRouteInfo(), + FilterDescriptors = new List(), + RelativePath = "/Foo", + RouteValues = new Dictionary(), + ViewEnginePath = "/Pages/Foo", + }; + var handlerTypeInfo = typeof(object).GetTypeInfo(); + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]); + + // Act + var actual = CompiledPageActionDescriptorBuilder.Build(pageApplicationModel); + + // Assert + Assert.Same(actionDescriptor.ActionConstraints, actual.ActionConstraints); + Assert.Same(actionDescriptor.AttributeRouteInfo, actual.AttributeRouteInfo); + Assert.Same(actionDescriptor.RelativePath, actual.RelativePath); + Assert.Same(actionDescriptor.RouteValues, actual.RouteValues); + Assert.Same(actionDescriptor.ViewEnginePath, actual.ViewEnginePath); + } + + [Fact] + public void CreateDescriptor_CopiesPropertiesFromPageApplicationModel() + { + // Arrange + var actionDescriptor = new PageActionDescriptor + { + ActionConstraints = new List(), + AttributeRouteInfo = new AttributeRouteInfo(), + FilterDescriptors = new List(), + RelativePath = "/Foo", + RouteValues = new Dictionary(), + ViewEnginePath = "/Pages/Foo", + }; + var handlerTypeInfo = typeof(TestModel).GetTypeInfo(); + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + { + PageType = typeof(TestPage).GetTypeInfo(), + ModelType = typeof(TestModel).GetTypeInfo(), + Filters = + { + Mock.Of(), + Mock.Of(), + }, + HandlerMethods = + { + new PageHandlerModel(handlerTypeInfo.GetMethod(nameof(TestModel.OnGet)), new object[0]), + }, + HandlerProperties = + { + new PagePropertyModel(handlerTypeInfo.GetProperty(nameof(TestModel.Property)), new object[0]) + { + BindingInfo = new BindingInfo(), + }, + } + }; + + // Act + var actual = CompiledPageActionDescriptorBuilder.Build(pageApplicationModel); + + // Assert + Assert.Same(pageApplicationModel.PageType, actual.PageTypeInfo); + Assert.Same(pageApplicationModel.ModelType, actual.ModelTypeInfo); + Assert.Same(pageApplicationModel.HandlerType, actual.HandlerTypeInfo); + Assert.Same(pageApplicationModel.Properties, actual.Properties); + Assert.Equal(pageApplicationModel.Filters, actual.FilterDescriptors.Select(f => f.Filter)); + Assert.Equal(pageApplicationModel.HandlerMethods.Select(p => p.MethodInfo), actual.HandlerMethods.Select(p => p.MethodInfo)); + Assert.Equal(pageApplicationModel.HandlerProperties.Select(p => p.PropertyName), actual.BoundProperties.Select(p => p.Name)); + } + + private class TestPage + { + public TestModel Model { get; } = new TestModel(); + + [BindProperty] + public string Property { get; set; } + + public void OnGet() + { + + } + } + + private class TestModel + { + [BindProperty] + public string Property { get; set; } + + public void OnGet() + { + + } + } + + [Fact] + public void CreateHandlerMethods_CopiesPropertiesFromHandlerModel() + { + // Arrange + var actionDescriptor = new PageActionDescriptor(); + var handlerTypeInfo = typeof(ModelWithHandler).GetTypeInfo(); + var handlerModel = new PageHandlerModel(handlerTypeInfo.GetMethod(nameof(ModelWithHandler.OnGetCustomerAsync)), new object[0]) + { + HttpMethod = "GET", + HandlerName = "Customer", + }; + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + { + HandlerMethods = + { + handlerModel, + } + }; + + // Act + var handlerDescriptors = CompiledPageActionDescriptorBuilder.CreateHandlerMethods(pageApplicationModel); + + // Assert + Assert.Collection( + handlerDescriptors, + d => + { + Assert.Equal(handlerModel.MethodInfo, d.MethodInfo); + Assert.Equal(handlerModel.HttpMethod, d.HttpMethod); + Assert.Equal(handlerModel.HandlerName, d.Name); + }); + } + + private class ModelWithHandler + { + public void OnGetCustomerAsync() + { + } + } + + [Fact] + public void CreateHandlerMethods_CopiesParameterDecriptorsFromParameterModel() + { + // Arrange + var actionDescriptor = new PageActionDescriptor(); + var handlerTypeInfo = typeof(HandlerWithParameters).GetTypeInfo(); + var handlerMethod = handlerTypeInfo.GetMethod(nameof(HandlerWithParameters.OnPost)); + var parameters = handlerMethod.GetParameters(); + var parameterModel1 = new PageParameterModel(parameters[0], new object[0]) + { + ParameterName = "test-name" + }; + var parameterModel2 = new PageParameterModel(parameters[1], new object[0]) + { + BindingInfo = new BindingInfo(), + }; + var handlerModel = new PageHandlerModel(handlerMethod, new object[0]) + { + Parameters = + { + parameterModel1, + parameterModel2, + } + }; + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + { + HandlerMethods = + { + handlerModel, + } + }; + + // Act + var handlerDescriptors = CompiledPageActionDescriptorBuilder.CreateHandlerMethods(pageApplicationModel); + + // Assert + Assert.Collection( + Assert.Single(handlerDescriptors).Parameters, + p => + { + Assert.Equal(parameters[0], p.ParameterInfo); + Assert.Equal(typeof(string), p.ParameterType); + Assert.Equal(parameterModel1.ParameterName, p.Name); + }, + p => + { + Assert.Equal(parameters[1], p.ParameterInfo); + Assert.Equal(typeof(int), p.ParameterType); + Assert.Same(parameterModel2.BindingInfo, p.BindingInfo); + }); + } + + private class HandlerWithParameters + { + public void OnPost(string param1, [FromRoute(Name = "id")] int param2) + { + } + } + + [Fact] + public void CreateBoundProperties_CopiesPropertyDescriptorsFromPagePropertyModel() + { + // Arrange + var actionDescriptor = new PageActionDescriptor(); + var handlerTypeInfo = typeof(HandlerWithProperty).GetTypeInfo(); + var propertyModel = new PagePropertyModel( + handlerTypeInfo.GetProperty(nameof(HandlerWithProperty.Property)), + new object[0]) + { + PropertyName = nameof(HandlerWithProperty.Property), + BindingInfo = new BindingInfo(), + }; + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + { + HandlerProperties = + { + propertyModel, + } + }; + + // Act + var propertyDescriptors = CompiledPageActionDescriptorBuilder.CreateBoundProperties(pageApplicationModel); + + // Assert + Assert.Collection( + propertyDescriptors, + p => + { + Assert.Same(propertyModel.PropertyName, p.Name); + Assert.Same(typeof(int), p.ParameterType); + Assert.Same(propertyModel.PropertyInfo, p.Property); + Assert.Same(propertyModel.BindingInfo, p.BindingInfo); + }); + } + + private class HandlerWithProperty + { + [BindProperty] + public int Property { get; set; } + } + + [Fact] + public void CreateBoundProperties_IgnoresPropertiesWithoutBindingInfo() + { + // Arrange + var actionDescriptor = new PageActionDescriptor(); + var handlerTypeInfo = typeof(HandlerWithIgnoredProperties).GetTypeInfo(); + var propertyModel1 = new PagePropertyModel( + handlerTypeInfo.GetProperty(nameof(HandlerWithIgnoredProperties.Property)), + new object[0]) + { + PropertyName = nameof(HandlerWithIgnoredProperties.Property), + BindingInfo = new BindingInfo(), + }; + var propertyModel2 = new PagePropertyModel( + handlerTypeInfo.GetProperty(nameof(HandlerWithIgnoredProperties.IgnoreMe)), + new object[0]) + { + PropertyName = nameof(HandlerWithIgnoredProperties.IgnoreMe), + }; + var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + { + HandlerProperties = + { + propertyModel1, + propertyModel2, + } + }; + + // Act + var propertyDescriptors = CompiledPageActionDescriptorBuilder.CreateBoundProperties(pageApplicationModel); + + // Assert + Assert.Collection( + propertyDescriptors, + p => + { + Assert.Same(propertyModel1.PropertyName, p.Name); + Assert.Same(typeof(int), p.ParameterType); + Assert.Same(propertyModel1.PropertyInfo, p.Property); + Assert.Same(propertyModel1.BindingInfo, p.BindingInfo); + }); + } + + private class HandlerWithIgnoredProperties + { + [BindProperty] + public int Property { get; set; } + + public string IgnoreMe { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs similarity index 83% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs index c3807a15d2..c9b298a7df 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class CompiledPageApplicationModelProviderTest + public class CompiledPageRouteModelProviderTest { [Fact] public void OnProvidersExecuting_AddsModelsForCompiledViews() @@ -22,14 +22,14 @@ public void OnProvidersExecuting_AddsModelsForCompiledViews() GetDescriptor("/Pages/About.cshtml"), GetDescriptor("/Pages/Home.cshtml", "some-prefix"), }; - var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); - var context = new PageApplicationModelProviderContext(); + var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions()); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, result => { Assert.Equal("/Pages/About.cshtml", result.RelativePath); @@ -55,14 +55,14 @@ public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRo GetDescriptor("/Pages/Index.cshtml"), GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"), }; - var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions { RootDirectory = "/" }); - var context = new PageApplicationModelProviderContext(); + var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions { RootDirectory = "/" }); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, result => { Assert.Equal("/Pages/Index.cshtml", result.RelativePath); @@ -90,14 +90,14 @@ public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage() GetDescriptor("/Pages/Index.cshtml"), GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"), }; - var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); - var context = new PageApplicationModelProviderContext(); + var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions()); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, result => { Assert.Equal("/Pages/Index.cshtml", result.RelativePath); @@ -125,8 +125,8 @@ public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() GetDescriptor("/Pages/Index.cshtml"), GetDescriptor("/Pages/Home.cshtml", "/some-prefix"), }; - var provider = new TestCompiledPageApplicationModelProvider(descriptors, new RazorPagesOptions()); - var context = new PageApplicationModelProviderContext(); + var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions()); + var context = new PageRouteModelProviderContext(); // Act & Assert var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); @@ -143,11 +143,11 @@ private static CompiledViewDescriptor GetDescriptor(string path, string prefix = }; } - public class TestCompiledPageApplicationModelProvider : CompiledPageApplicationModelProvider + public class TestCompiledPageRouteModelProvider : CompiledPageRouteModelProvider { private readonly IEnumerable _descriptors; - public TestCompiledPageApplicationModelProvider(IEnumerable descriptors, RazorPagesOptions options) + public TestCompiledPageRouteModelProvider(IEnumerable descriptors, RazorPagesOptions options) : base(new ApplicationPartManager(), new TestOptionsManager(options)) { _descriptors = descriptors; diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..d0f3835231 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs @@ -0,0 +1,788 @@ +// 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; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public partial class DefaultPageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_SetsPageAsHandlerType_IfModelPropertyDoesNotExist() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(TestPage).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Same(context.PageApplicationModel.PageType, context.PageApplicationModel.HandlerType); + } + + [Fact] + public void OnProvidersExecuting_SetsPageAsHandlerType_IfModelTypeDoesNotHaveAnyHandlers() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithModelWithoutHandlers).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Same(context.PageApplicationModel.PageType, context.PageApplicationModel.HandlerType); + } + + [Fact] + public void OnProvidersExecuting_SetsModelAsHandlerType() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithModel).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Same(typeof(TestPageModel).GetTypeInfo(), context.PageApplicationModel.HandlerType); + } + + [Fact] + public void OnProvidersExecuting_DiscoversPropertiesFromPage() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(TestPage).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName), + property => + { + Assert.Equal(typeInfo.GetProperty(nameof(TestPage.Property1)), property.PropertyInfo); + Assert.Null(property.BindingInfo); + Assert.Equal(nameof(TestPage.Property1), property.PropertyName); + }, + property => + { + Assert.Equal(typeInfo.GetProperty(nameof(TestPage.Property2)), property.PropertyInfo); + Assert.Equal(nameof(TestPage.Property2), property.PropertyName); + Assert.NotNull(property.BindingInfo); + Assert.Equal(BindingSource.Path, property.BindingInfo.BindingSource); + }); + } + + [Fact] + public void OnProvidersExecuting_DiscoversHandlersFromPage() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithModelWithoutHandlers).GetTypeInfo(); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerMethods.OrderBy(p => p.Name), + handler => + { + var name = nameof(PageWithModelWithoutHandlers.OnGet); + Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo); + Assert.Equal(name, handler.Name); + Assert.Equal("Get", handler.HttpMethod); + Assert.Null(handler.HandlerName); + }, + handler => + { + var name = nameof(PageWithModelWithoutHandlers.OnPostAsync); + Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo); + Assert.Equal(name, handler.Name); + Assert.Equal("Post", handler.HttpMethod); + Assert.Null(handler.HandlerName); + }, + handler => + { + var name = nameof(PageWithModelWithoutHandlers.OnPostDeleteCustomerAsync); + Assert.Equal(typeInfo.GetMethod(name), handler.MethodInfo); + Assert.Equal(name, handler.Name); + Assert.Equal("Post", handler.HttpMethod); + Assert.Equal("DeleteCustomer", handler.HandlerName); + }); + } + + [Fact] + public void OnProvidersExecuting_DiscoversPropertiesFromModel() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithModel).GetTypeInfo(); + var modelType = typeof(TestPageModel); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerProperties.OrderBy(p => p.PropertyName), + property => + { + var name = nameof(TestPageModel.Property1); + Assert.Equal(modelType.GetProperty(name), property.PropertyInfo); + Assert.Null(property.BindingInfo); + Assert.Equal(name, property.PropertyName); + }, + property => + { + var name = nameof(TestPageModel.Property2); + Assert.Equal(modelType.GetProperty(name), property.PropertyInfo); + Assert.Equal(name, property.PropertyName); + Assert.NotNull(property.BindingInfo); + Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource); + }); + } + + [Fact] + public void OnProvidersExecuting_DiscoversHandlersFromModel() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithModel).GetTypeInfo(); + var modelType = typeof(TestPageModel); + var descriptor = new PageActionDescriptor(); + var context = new PageApplicationModelProviderContext(descriptor, typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.PageApplicationModel); + Assert.Collection( + context.PageApplicationModel.HandlerMethods.OrderBy(p => p.Name), + handler => + { + var name = nameof(TestPageModel.OnGetUser); + Assert.Equal(modelType.GetMethod(name), handler.MethodInfo); + Assert.Equal(name, handler.Name); + Assert.Equal("Get", handler.HttpMethod); + Assert.Equal("User", handler.HandlerName); + }); + } + + // We want to test the the 'empty' page has no bound properties, and no handler methods. + [Fact] + public void OnProvidersExecuting_EmptyPage() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(EmptyPage).GetTypeInfo(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var pageModel = context.PageApplicationModel; + Assert.Empty(pageModel.HandlerProperties.Where(p => p.BindingInfo != null)); + Assert.Empty(pageModel.HandlerMethods); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.HandlerType); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.ModelType); + Assert.Same(typeof(EmptyPage).GetTypeInfo(), pageModel.PageType); + } + + // We want to test the the 'empty' page and pagemodel has no bound properties, and no handler methods. + [Fact] + public void OnProvidersExecuting_EmptyPageModel() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(EmptyPageWithPageModel).GetTypeInfo(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var pageModel = context.PageApplicationModel; + Assert.Empty(pageModel.HandlerProperties.Where(p => p.BindingInfo != null)); + Assert.Empty(pageModel.HandlerMethods); + Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), pageModel.HandlerType); + Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.ModelType); + Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), pageModel.PageType); + } + + private class EmptyPage : Page + { + // Copied from generated code + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; + public EmptyPage Model => ViewData.Model; + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class EmptyPageWithPageModel : Page + { + // Copied from generated code + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; + public EmptyPageModel Model => ViewData.Model; + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + private class EmptyPageModel : PageModel + { + } + + [Fact] // If the model has handler methods, we prefer those. + public void CreateDescriptor_FindsHandlerMethod_OnModel() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(); + var modelType = typeof(ModelWithHandler); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var pageModel = context.PageApplicationModel; + Assert.Collection( + pageModel.HandlerProperties, + p => Assert.Equal(modelType.GetProperty(nameof(ModelWithHandler.BindMe)), p.PropertyInfo)); + + Assert.Collection( + pageModel.HandlerMethods, + p => Assert.Equal(modelType.GetMethod(nameof(ModelWithHandler.OnGet)), p.MethodInfo)); + + Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), pageModel.HandlerType); + Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), pageModel.ModelType); + Assert.Same(typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(), pageModel.PageType); + } + + private class ModelWithHandler + { + [ModelBinder] + public int BindMe { get; set; } + + public void OnGet() { } + } + + private class PageWithHandlerThatGetsIgnored + { + public ModelWithHandler Model => null; + + [ModelBinder] + public int IgnoreMe { get; set; } + + public void OnPost() { } + } + + + [Fact] // If the model has no handler methods, we look at the page instead. + public void OnProvidersExecuting_FindsHandlerMethodOnPage_WhenModelHasNoHandlers() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithHandler).GetTypeInfo(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeInfo); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var pageModel = context.PageApplicationModel; + Assert.Collection( + pageModel.HandlerProperties.OrderBy(p => p.PropertyName), + p => Assert.Equal(typeInfo.GetProperty(nameof(PageWithHandler.BindMe)), p.PropertyInfo), + p => Assert.Equal(typeInfo.GetProperty(nameof(PageWithHandler.Model)), p.PropertyInfo)); + + Assert.Collection( + pageModel.HandlerMethods, + p => Assert.Equal(typeInfo.GetMethod(nameof(PageWithHandler.OnGet)), p.MethodInfo)); + + Assert.Same(typeof(PageWithHandler).GetTypeInfo(), pageModel.HandlerType); + Assert.Same(typeof(PocoModel).GetTypeInfo(), pageModel.ModelType); + Assert.Same(typeof(PageWithHandler).GetTypeInfo(), pageModel.PageType); + } + + private class PageWithHandler + { + public PocoModel Model => null; + + [ModelBinder] + public int BindMe { get; set; } + + public void OnGet() { } + } + + [Fact] + public void CreateHandlerModels_DiscoversHandlersFromBaseType() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(InheritsMethods).GetTypeInfo(); + var baseType = typeof(TestSetPageModel); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Collection( + handlerModels.OrderBy(h => h.MethodInfo.DeclaringType.Name).ThenBy(h => h.MethodInfo.Name), + handler => + { + Assert.Equal(nameof(InheritsMethods.OnGet), handler.MethodInfo.Name); + Assert.Equal(typeInfo, handler.MethodInfo.DeclaringType.GetTypeInfo()); + }, + handler => + { + Assert.Equal(nameof(TestSetPageModel.OnGet), handler.MethodInfo.Name); + Assert.Equal(baseType, handler.MethodInfo.DeclaringType); + }, + handler => + { + Assert.Equal(nameof(TestSetPageModel.OnPost), handler.MethodInfo.Name); + Assert.Equal(baseType, handler.MethodInfo.DeclaringType); + }); + } + + private class TestSetPageModel + { + public void OnGet() + { + } + + public void OnPost() + { + } + } + + private class InheritsMethods : TestSetPageModel + { + public new void OnGet() + { + } + } + + [Fact] + public void CreateHandlerModels_IgnoresNonPublicMethods() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(ProtectedModel).GetTypeInfo(); + var baseType = typeof(TestSetPageModel); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Empty(handlerModels); + } + + private class ProtectedModel + { + protected void OnGet() + { + } + + private void OnPost() + { + } + } + + [Fact] + public void CreateHandlerModels_IgnoreGenericTypeParameters() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(GenericClassModel).GetTypeInfo(); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Empty(handlerModels); + } + + private class GenericClassModel + { + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerModels_IgnoresStaticMethods() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageModelWithStaticHandler).GetTypeInfo(); + var expected = typeInfo.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Collection( + handlerModels, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private class PageModelWithStaticHandler + { + public static void OnGet(string name) + { + } + + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerModels_IgnoresAbstractMethods() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageModelWithAbstractMethod).GetTypeInfo(); + var expected = typeInfo.GetMethod(nameof(PageModelWithAbstractMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Collection( + handlerModels, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private abstract class PageModelWithAbstractMethod + { + public abstract void OnPost(string name); + + public void OnGet() + { + } + } + + [Fact] + public void CreateHandlerModels_IgnoresMethodWithNonHandlerAttribute() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithNonHandlerMethod).GetTypeInfo(); + var expected = typeInfo.GetMethod(nameof(PageWithNonHandlerMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Collection( + handlerModels, + handler => Assert.Same(expected, handler.MethodInfo)); + } + + private class PageWithNonHandlerMethod + { + [NonHandler] + public void OnPost(string name) { } + + public void OnGet() + { + } + } + + // There are more tests for the parsing elsewhere, this is just testing that it's wired + // up to the model. + [Fact] + public void CreateHandlerModel_ParsesMethod() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageModelWithHandlerNames).GetTypeInfo(); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + Assert.Collection( + handlerModels.OrderBy(h => h.MethodInfo.Name), + handler => + { + Assert.Same(typeInfo.GetMethod(nameof(PageModelWithHandlerNames.OnPutDeleteAsync)), handler.MethodInfo); + Assert.Equal("Put", handler.HttpMethod); + Assert.Equal("Delete", handler.HandlerName); + }); + } + + private class PageModelWithHandlerNames + { + public void OnPutDeleteAsync() + { + } + + public void Foo() // This isn't a valid handler name. + { + } + } + + [Fact] + public void CreateHandlerMethods_AddsParameterDescriptors() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(PageWithHandlerParameters).GetTypeInfo(); + var expected = typeInfo.GetMethod(nameof(PageWithHandlerParameters.OnPost)); + + // Act + var handlerModels = provider.CreateHandlerModels(typeInfo); + + // Assert + var handler = Assert.Single(handlerModels); + + Assert.Collection( + handler.Parameters, + p => + { + Assert.NotNull(p.ParameterInfo); + Assert.Equal(typeof(string), p.ParameterInfo.ParameterType); + Assert.Equal("name", p.ParameterName); + }, + p => + { + Assert.NotNull(p.ParameterInfo); + Assert.Equal(typeof(int), p.ParameterInfo.ParameterType); + Assert.Equal("id", p.ParameterName); + Assert.Equal("personId", p.BindingInfo.BinderModelName); + }); + } + + private class PageWithHandlerParameters + { + public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { } + } + + // We're using PropertyHelper from Common to find the properties here, which implements + // out standard set of semantics for properties that the framework interacts with. + // + // One of the desirable consequences of that is we only find 'visible' properties. We're not + // retesting all of the details of PropertyHelper here, just the visibility part as a quick check + // that we're using PropertyHelper as expected. + [Fact] + public void PopulateHandlerProperties_UsesPropertyHelpers_ToFindProperties() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(HidesAProperty).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]); + + // Act + provider.PopulateHandlerProperties(pageModel); + + // Assert + var properties = pageModel.HandlerProperties; + Assert.Collection( + properties, + p => + { + Assert.Equal(typeof(HidesAProperty).GetTypeInfo(), p.PropertyInfo.DeclaringType.GetTypeInfo()); + }); + } + + private class HasAHiddenProperty + { + [BindProperty] + public int Property { get; set; } + } + + private class HidesAProperty : HasAHiddenProperty + { + [BindProperty] + public new int Property { get; set; } + } + + [Fact] + public void PopulateHandlerProperties_SupportsGet_OnProperty() + { + // Arrange + var provider = new TestPageApplicationModelProvider(); + var typeInfo = typeof(ModelSupportsGetOnProperty).GetTypeInfo(); + var pageModel = new PageApplicationModel(new PageActionDescriptor(), typeInfo, new object[0]); + + // Act + provider.PopulateHandlerProperties(pageModel); + + // Assert + var properties = pageModel.HandlerProperties; + Assert.Collection( + properties.OrderBy(p => p.PropertyName), + p => + { + Assert.Equal(typeInfo.GetProperty(nameof(ModelSupportsGetOnProperty.Property)), p.PropertyInfo); + Assert.NotNull(p.BindingInfo.RequestPredicate); + Assert.True(p.BindingInfo.RequestPredicate(new ActionContext + { + HttpContext = new DefaultHttpContext + { + Request = + { + Method ="GET", + } + } + })); + }); + } + + private class ModelSupportsGetOnProperty + { + [BindProperty(SupportsGet = true)] + public int Property { get; set; } + } + + [Theory] + [InlineData("Foo")] + [InlineData("On")] + [InlineData("OnAsync")] + [InlineData("Async")] + public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName) + { + // Act + var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + + // Assert + Assert.False(result); + Assert.Null(httpMethod); + Assert.Null(handler); + } + + [Theory] + [InlineData("OnG", "G", null)] + [InlineData("OnGAsync", "G", null)] + [InlineData("OnPOST", "P", "OST")] + [InlineData("OnPOSTAsync", "P", "OST")] + [InlineData("OnDeleteFoo", "Delete", "Foo")] + [InlineData("OnDeleteFooAsync", "Delete", "Foo")] + [InlineData("OnMadeupLongHandlerName", "Madeup", "LongHandlerName")] + [InlineData("OnMadeupLongHandlerNameAsync", "Madeup", "LongHandlerName")] + public void TryParseHandler_ParsesHandlerNames_ValidData(string methodName, string expectedHttpMethod, string expectedHandler) + { + // Arrange + + // Act + var result = DefaultPageApplicationModelProvider.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + + // Assert + Assert.True(result); + Assert.Equal(expectedHttpMethod, httpMethod); + Assert.Equal(expectedHandler, handler); + } + + private class TestPageApplicationModelProvider : DefaultPageApplicationModelProvider + { + public TestPageApplicationModelProvider(IOptions mvcOptions = null) + : base(mvcOptions : new TestOptionsManager()) + { + } + } + + private class TestPage + { + public string Property1 { get; set; } + + [FromRoute] + public object Property2 { get; set; } + } + + private class PageWithModelWithoutHandlers : Page + { + public ModelWithoutHandler Model { get; } + + public override Task ExecuteAsync() => throw new NotImplementedException(); + + public void OnGet() { } + + public void OnPostAsync() { } + + public void OnPostDeleteCustomerAsync() { } + + public class ModelWithoutHandler + { + } + } + + private class PageWithModel : Page + { + public TestPageModel Model { get; } + + public override Task ExecuteAsync() => throw new NotImplementedException(); + } + + private class TestPageModel + { + public string Property1 { get; set; } + + [FromQuery] + public string Property2 { get; set; } + + public void OnGetUser() { } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs index 893e241a59..29a7268f97 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs @@ -1,16 +1,11 @@ // 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 System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -18,600 +13,126 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public class DefaultPageLoaderTest { [Fact] - public void CreateDescriptor_CopiesPropertiesFromBaseClass() + public void Load_InvokesApplicationModelProviders() { // Arrange - var expected = new PageActionDescriptor() // We only copy the properties that are meaningful for pages. - { - ActionConstraints = new List(), - AttributeRouteInfo = new AttributeRouteInfo(), - FilterDescriptors = new List(), - RelativePath = "/Foo", - RouteValues = new Dictionary(), - ViewEnginePath = "/Pages/Foo", - }; - - // Act - var actual = DefaultPageLoader.CreateDescriptor(expected, - new RazorPageAttribute(expected.RelativePath, typeof(EmptyPage), "")); - - // Assert - Assert.Same(expected.ActionConstraints, actual.ActionConstraints); - Assert.Same(expected.AttributeRouteInfo, actual.AttributeRouteInfo); - Assert.Same(expected.FilterDescriptors, actual.FilterDescriptors); - Assert.Same(expected.Properties, actual.Properties); - Assert.Same(expected.RelativePath, actual.RelativePath); - Assert.Same(expected.RouteValues, actual.RouteValues); - Assert.Same(expected.ViewEnginePath, actual.ViewEnginePath); - } - - // We want to test the the 'empty' page has no bound properties, and no handler methods. - [Fact] - public void CreateDescriptor_EmptyPage() - { - // Arrange - var type = typeof(EmptyPage); - - // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), - new RazorPageAttribute("/Pages/Index", type, "")); - - // Assert - Assert.Empty(result.BoundProperties); - Assert.Empty(result.HandlerMethods); - Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.HandlerTypeInfo); - Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.ModelTypeInfo); - Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.PageTypeInfo); - } - - // We want to test the the 'empty' page and pagemodel has no bound properties, and no handler methods. - [Fact] - public void CreateDescriptor_EmptyPageModel() - { - // Arrange - var type = typeof(EmptyPageWithPageModel); - - // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), - new RazorPageAttribute("/Pages/Index", type, "")); - - // Assert - Assert.Empty(result.BoundProperties); - Assert.Empty(result.HandlerMethods); - Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.HandlerTypeInfo); - Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), result.ModelTypeInfo); - Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.PageTypeInfo); - } - - private class EmptyPage : Page - { - // Copied from generated code - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; - public EmptyPage Model => ViewData.Model; - - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private class EmptyPageWithPageModel : Page - { - // Copied from generated code - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; - public EmptyPageModel Model => ViewData.Model; - - public override Task ExecuteAsync() - { - throw new NotImplementedException(); - } - } - - private class EmptyPageModel : PageModel - { - } - - [Fact] // If the model has handler methods, we prefer those. - public void CreateDescriptor_FindsHandlerMethod_OnModel() - { - // Arrange - var type = typeof(PageWithHandlerThatGetsIgnored); - - // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), - new RazorPageAttribute("/Pages/Index", type, "")); + var descriptor = new PageActionDescriptor(); - // Assert - Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); - Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); - Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.HandlerTypeInfo); - Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.ModelTypeInfo); - Assert.Same(typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(), result.PageTypeInfo); - } + var compilerProvider = GetCompilerProvider(); - private class ModelWithHandler - { - [ModelBinder] - public int BindMe { get; set; } + var options = new TestOptionsManager(); - public void OnGet() { } - } + var provider1 = new Mock(); + var provider2 = new Mock(); - private class PageWithHandlerThatGetsIgnored - { - public ModelWithHandler Model => null; - - [ModelBinder] - public int IgnoreMe { get; set; } - - public void OnPost() { } - } - - - [Fact] // If the model has no handler methods, we look at the page instead. - public void CreateDescriptor_FindsHandlerMethodOnPage_WhenModelHasNoHandlers() - { - // Arrange - var type = typeof(PageWithHandler); - - // Act - var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), - new RazorPageAttribute("/Pages/Index", type, "")); - - // Assert - Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); - Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); - Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.HandlerTypeInfo); - Assert.Same(typeof(PocoModel).GetTypeInfo(), result.ModelTypeInfo); - Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.PageTypeInfo); - } - - private class PocoModel - { - // Just a plain ol' model, nothing to see here. + var sequence = 0; + var pageApplicationModel1 = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]); + var pageApplicationModel2 = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]); - [ModelBinder] - public int IgnoreMe { get; set; } - } - - private class PageWithHandler - { - public PocoModel Model => null; - - [ModelBinder] - public int BindMe { get; set; } - - public void OnGet() { } - } - - [Fact] - public void CreateHandlerMethods_DiscoversHandlersFromBaseType() - { - // Arrange - var type = typeof(InheritsMethods).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Collection( - results.OrderBy(h => h.MethodInfo.Name).ToArray(), - (handler) => + provider1.Setup(p => p.OnProvidersExecuting(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => { - Assert.Equal("OnGet", handler.MethodInfo.Name); - Assert.Equal(typeof(InheritsMethods), handler.MethodInfo.DeclaringType); - }, - (handler) => + Assert.Equal(0, sequence++); + Assert.Null(c.PageApplicationModel); + c.PageApplicationModel = pageApplicationModel1; + }) + .Verifiable(); + + provider2.Setup(p => p.OnProvidersExecuting(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => { - Assert.Equal("OnGet", handler.MethodInfo.Name); - Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); - }, - (handler) => + Assert.Equal(1, sequence++); + Assert.Same(pageApplicationModel1, c.PageApplicationModel); + c.PageApplicationModel = pageApplicationModel2; + }) + .Verifiable(); + + provider1.Setup(p => p.OnProvidersExecuted(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => { - Assert.Equal("OnPost", handler.MethodInfo.Name); - Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); - }); - } - - private class TestSetPageModel - { - public void OnGet() - { - } - - public void OnPost() - { - } - } - - private class TestSetPageWithModel - { - public TestSetPageModel Model { get; set; } - } - - private class InheritsMethods : TestSetPageModel - { - public new void OnGet() - { - } - } - - [Fact] - public void CreateHandlerMethods_IgnoresNonPublicMethods() - { - // Arrange - var type = typeof(ProtectedModel).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Empty(results); - } - - private class ProtectedModel - { - protected void OnGet() - { - } - - private void OnPost() - { - } - } - - [Fact] - public void CreateHandlerMethods_IgnoreGenericTypeParameters() - { - // Arrange - var type = typeof(GenericClassModel).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Empty(results); - } - - private class GenericClassModel - { - public void OnGet() - { - } - } - - [Fact] - public void CreateHandlerMethods_IgnoresStaticMethods() - { - // Arrange - var type = typeof(PageModelWithStaticHandler).GetTypeInfo(); - var expected = type.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance); + Assert.Equal(3, sequence++); + Assert.Same(pageApplicationModel2, c.PageApplicationModel); + }) + .Verifiable(); - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Collection( - results, - handler => Assert.Same(expected, handler.MethodInfo)); - } - - private class PageModelWithStaticHandler - { - public static void OnGet(string name) - { - } - - public void OnGet() - { - } - } - - [Fact] - public void CreateHandlerMethods_IgnoresAbstractMethods() - { - // Arrange - var type = typeof(PageModelWithAbstractMethod).GetTypeInfo(); - var expected = type.GetMethod(nameof(PageModelWithAbstractMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Collection( - results, - handler => Assert.Same(expected, handler.MethodInfo)); - } - - private abstract class PageModelWithAbstractMethod - { - public abstract void OnPost(string name); - - public void OnGet() - { - } - } - - [Fact] - public void CreateHandlerMethods_IgnoresMethodWithNonHandlerAttribute() - { - // Arrange - var type = typeof(PageWithNonHandlerMethod).GetTypeInfo(); - var expected = type.GetMethod(nameof(PageWithNonHandlerMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Collection( - results, - handler => Assert.Same(expected, handler.MethodInfo)); - } - - private class PageWithNonHandlerMethod - { - [NonHandler] - public void OnPost(string name) { } - - public void OnGet() - { - } - } - - // There are more tests for the parsing elsewhere, this is just testing that it's wired - // up to the descriptor. - [Fact] - public void CreateHandlerMethods_ParsesMethod() - { - // Arrange - var type = typeof(PageModelWithHandlerNames).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - Assert.Collection( - results.OrderBy(h => h.MethodInfo.Name), - handler => + provider2.Setup(p => p.OnProvidersExecuted(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => { - Assert.Same(type.GetMethod(nameof(PageModelWithHandlerNames.OnPutDeleteAsync)), handler.MethodInfo); - Assert.Equal("Put", handler.HttpMethod); - Assert.Equal("Delete", handler.Name.ToString()); - }); - } - - private class PageModelWithHandlerNames - { - public void OnPutDeleteAsync() - { - } + Assert.Equal(2, sequence++); + Assert.Same(pageApplicationModel2, c.PageApplicationModel); + }) + .Verifiable(); - public void Foo() // This isn't a valid handler name. + var providers = new[] { - } - } - - [Fact] - public void CreateHandlerMethods_AddsParameterDescriptors() - { - // Arrange - var type = typeof(PageWithHandlerParameters).GetTypeInfo(); - var expected = type.GetMethod(nameof(PageWithHandlerParameters.OnPost), BindingFlags.Public | BindingFlags.Instance); - - // Act - var results = DefaultPageLoader.CreateHandlerMethods(type); - - // Assert - var handler = Assert.Single(results); - - Assert.Collection( - handler.Parameters, - p => - { - Assert.Equal(typeof(string), p.ParameterType); - Assert.NotNull(p.ParameterInfo); - Assert.Equal("name", p.Name); - }, - p => - { - Assert.Equal(typeof(int), p.ParameterType); - Assert.NotNull(p.ParameterInfo); - Assert.Equal("id", p.Name); - Assert.Equal("personId", p.BindingInfo.BinderModelName); - }); - } - - private class PageWithHandlerParameters - { - public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { } - } + provider1.Object, provider2.Object + }; - // We're using PropertyHelper from Common to find the properties here, which implements - // out standard set of semantics for properties that the framework interacts with. - // - // One of the desirable consequences of that is we only find 'visible' properties. We're not - // retesting all of the details of PropertyHelper here, just the visibility part as a quick check - // that we're using PropertyHelper as expected. - [Fact] - public void CreateBoundProperties_UsesPropertyHelpers_ToFindProperties() - { - // Arrange - var type = typeof(HidesAProperty).GetTypeInfo(); + var loader = new DefaultPageLoader( + providers, + compilerProvider, + options); // Act - var results = DefaultPageLoader.CreateBoundProperties(type); + var result = loader.Load(new PageActionDescriptor()); // Assert - Assert.Collection( - results.OrderBy(p => p.Property.Name), - p => - { - Assert.Equal(typeof(HidesAProperty).GetTypeInfo(), p.Property.DeclaringType.GetTypeInfo()); - }); + provider1.Verify(); + provider2.Verify(); } - private class HasAHiddenProperty - { - [BindProperty] - public int Property { get; set; } - } - - private class HidesAProperty : HasAHiddenProperty - { - [BindProperty] - public new int Property { get; set; } - } - - // We're using BindingInfo to make property binding opt-in here. We're not going to retest - // all of the semantics of BindingInfo here, as that's covered elsewhere. [Fact] - public void CreateBoundProperties_UsesBindingInfo_ToFindProperties() + public void Load_InvokesApplicationModelConventions() { // Arrange - var type = typeof(ModelWithBindingInfoProperty).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateBoundProperties(type); + var descriptor = new PageActionDescriptor(); - // Assert - Assert.Collection( - results.OrderBy(p => p.Property.Name), - p => + var compilerProvider = GetCompilerProvider(); + + var model = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]); + var provider = new Mock(); + provider.Setup(p => p.OnProvidersExecuting(It.IsAny())) + .Callback((PageApplicationModelProviderContext c) => { - Assert.Equal("Property", p.Property.Name); + c.PageApplicationModel = model; }); - } - - private class ModelWithBindingInfoProperty - { - [ModelBinder] - public int Property { get; set; } + var providers = new[] { provider.Object }; - public int IgnoreMe { get; set; } - } - - // Additionally [BindProperty] on a property can opt-in a property - [Fact] - public void CreateBoundProperties_UsesBindPropertyAttribute_ToFindProperties() - { - // Arrange - var type = typeof(ModelWithBindProperty).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateBoundProperties(type); - - // Assert - Assert.Collection( - results.OrderBy(p => p.Property.Name), - p => - { - Assert.Equal("Property", p.Property.Name); - }); - } - - private class ModelWithBindProperty - { - [BindProperty] - public int Property { get; set; } - - public int IgnoreMe { get; set; } - } - - [Fact] - public void CreateBoundProperties_SupportsGet_OnProperty() - { - // Arrange - var type = typeof(ModelSupportsGetOnProperty).GetTypeInfo(); - - // Act - var results = DefaultPageLoader.CreateBoundProperties(type); - - // Assert - Assert.Collection( - results.OrderBy(p => p.Property.Name), - p => + var options = new TestOptionsManager(); + var convention = new Mock(); + convention.Setup(c => c.Apply(It.IsAny())) + .Callback((PageApplicationModel m) => { - Assert.Equal("Property", p.Property.Name); - Assert.NotNull(p.BindingInfo.RequestPredicate); - Assert.True(p.BindingInfo.RequestPredicate(new ActionContext() - { - HttpContext = new DefaultHttpContext() - { - Request = - { - Method ="GET", - } - } - })); + Assert.Same(model, m); }); - } - - private class ModelSupportsGetOnProperty - { - [BindProperty(SupportsGet = true)] - public int Property { get; set; } + options.Value.ApplicationModelConventions.Add(convention.Object); - public int IgnoreMe { get; set; } - } - - [Theory] - [InlineData("Foo")] - [InlineData("On")] - [InlineData("OnAsync")] - [InlineData("Async")] - public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName) - { - // Arrange + var loader = new DefaultPageLoader( + providers, + compilerProvider, + options); // Act - var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + var result = loader.Load(new PageActionDescriptor()); // Assert - Assert.False(result); - Assert.Null(httpMethod); - Assert.Null(handler); + convention.Verify(); } - [Theory] - [InlineData("OnG", "G", null)] - [InlineData("OnGAsync", "G", null)] - [InlineData("OnPOST", "P", "OST")] - [InlineData("OnPOSTAsync", "P", "OST")] - [InlineData("OnDeleteFoo", "Delete", "Foo")] - [InlineData("OnDeleteFooAsync", "Delete", "Foo")] - [InlineData("OnMadeupLongHandlerName", "Madeup", "LongHandlerName")] - [InlineData("OnMadeupLongHandlerNameAsync", "Madeup", "LongHandlerName")] - public void TryParseHandler_ParsesHandlerNames_ValidData(string methodName, string expectedHttpMethod, string expectedHandler) + private static IViewCompilerProvider GetCompilerProvider() { - // Arrange - - // Act - var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); + var descriptor = new CompiledViewDescriptor + { + ViewAttribute = new RazorPageAttribute("/Views/Index.cshtml", typeof(object), null), + }; - // Assert - Assert.True(result); - Assert.Equal(expectedHttpMethod, httpMethod); - Assert.Equal(expectedHandler, handler); + var compiler = new Mock(); + compiler.Setup(c => c.CompileAsync(It.IsAny())) + .ReturnsAsync(descriptor); + var compilerProvider = new Mock(); + compilerProvider.Setup(p => p.GetCompiler()) + .Returns(compiler.Object); + return compilerProvider.Object; } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 70b86ff7c6..1d3b7d2185 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -18,11 +17,9 @@ using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -235,7 +232,7 @@ public void OnProvidersExecuting_CachesEntries() loader.Object, CreateActionDescriptorCollection(descriptor)); - var context = new ActionInvokerProviderContext(new ActionContext() + var context = new ActionInvokerProviderContext(new ActionContext { ActionDescriptor = descriptor, HttpContext = new DefaultHttpContext(), @@ -251,6 +248,12 @@ public void OnProvidersExecuting_CachesEntries() var entry1 = actionInvoker.CacheEntry; // Act - 2 + context = new ActionInvokerProviderContext(new ActionContext + { + ActionDescriptor = descriptor, + HttpContext = new DefaultHttpContext(), + RouteData = new RouteData(), + }); invokerProvider.OnProvidersExecuting(context); // Assert - 2 @@ -445,6 +448,7 @@ private static CompiledPageActionDescriptor CreateCompiledPageActionDescriptor( HandlerTypeInfo = modelTypeInfo ?? pageTypeInfo, ModelTypeInfo = modelTypeInfo ?? pageTypeInfo, PageTypeInfo = pageTypeInfo, + FilterDescriptors = Array.Empty(), }; } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterApplicationModelProviderTest.cs index ddb169f2b0..3e184a9154 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageFilterApplicationModelProviderTest.cs @@ -1,8 +1,8 @@ // 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; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal @@ -10,30 +10,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public class PageFilterApplicationModelProviderTest { [Fact] - public void OnProvidersExecuting_AddsFiltersToModels() + public void OnProvidersExecuting_AddsFiltersToModel() { // Arrange - var applicationModel1 = new PageApplicationModel("/Home.cshtml", "/Home.cshtml"); - var applicationModel2 = new PageApplicationModel("/About.cshtml", "/About.cshtml"); - var modelProvider = new PageFilterApplicationModelProvider(); - var context = new PageApplicationModelProviderContext + var actionDescriptor = new PageActionDescriptor(); + var applicationModel = new PageApplicationModel( + actionDescriptor, + typeof(object).GetTypeInfo(), + new object[0]); + var applicationModelProvider = new PageFilterApplicationModelProvider(); + var context = new PageApplicationModelProviderContext(new PageActionDescriptor(), typeof(object).GetTypeInfo()) { - Results = - { - applicationModel1, - applicationModel2, - } + PageApplicationModel = applicationModel, }; // Act - modelProvider.OnProvidersExecuting(context); + applicationModelProvider.OnProvidersExecuting(context); // Assert - Assert.Collection(applicationModel1.Filters, - filter => Assert.IsType(filter), - filter => Assert.IsType(filter)); - - Assert.Collection(applicationModel2.Filters, + Assert.Collection(applicationModel.Filters, filter => Assert.IsType(filter), filter => Assert.IsType(filter)); } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PocoModel.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PocoModel.cs new file mode 100644 index 0000000000..1c33b0a5bc --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PocoModel.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public partial class DefaultPageApplicationModelProviderTest + { + private class PocoModel + { + // Just a plain ol' model, nothing to see here. + + [ModelBinder] + public int IgnoreMe { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs similarity index 84% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs index a4f9995f37..95fea9b11f 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageRouteModelProviderTest.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { - public class RazorProjectPageApplicationModelProviderTest + public class RazorProjectPageRouteModelProviderTest { [Fact] public void OnProvidersExecuting_ReturnsPagesWithPageDirective() @@ -27,14 +27,14 @@ public void OnProvidersExecuting_ReturnsPagesWithPageDirective() var optionsManager = new TestOptionsManager(); optionsManager.Value.RootDirectory = "/"; - var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance); - var context = new PageApplicationModelProviderContext(); + var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, model => { Assert.Equal("/Pages/Home.cshtml", model.RelativePath); @@ -61,14 +61,14 @@ public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPages() var optionsManager = new TestOptionsManager(); optionsManager.Value.RootDirectory = "/"; - var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance); - var context = new PageApplicationModelProviderContext(); + var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, model => { Assert.Equal("/Pages/Admin/Index.cshtml", model.RelativePath); @@ -99,12 +99,12 @@ public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() var optionsManager = new TestOptionsManager(); optionsManager.Value.RootDirectory = "/"; - var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance); - var context = new PageApplicationModelProviderContext(); + var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); + var context = new PageRouteModelProviderContext(); // Act & Assert var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal("The route for the page at '/Index.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.", + Assert.Equal("The route for the page at '/Index.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.", ex.Message); } @@ -125,14 +125,14 @@ public void OnProvidersExecuting_SkipsPagesStartingWithUnderscore() var optionsManager = new TestOptionsManager(); optionsManager.Value.RootDirectory = "/"; - var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance); - var context = new PageApplicationModelProviderContext(); + var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, model => { Assert.Equal("/Pages/Home.cshtml", model.RelativePath); @@ -163,14 +163,14 @@ public void OnProvidersExecuting_DiscoversFilesUnderBasePath() var optionsManager = new TestOptionsManager(); optionsManager.Value.RootDirectory = "/Pages"; - var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager, NullLoggerFactory.Instance); - var context = new PageApplicationModelProviderContext(); + var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance); + var context = new PageRouteModelProviderContext(); // Act provider.OnProvidersExecuting(context); // Assert - Assert.Collection(context.Results, + Assert.Collection(context.RouteModels, model => { Assert.Equal("/Pages/Index.cshtml", model.RelativePath); diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 058c0dd19f..9b89a8fe78 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -422,12 +422,20 @@ private Dictionary MutliRegistrationServiceTypes typeof(JsonPatchOperationsArrayProvider), } }, + { + typeof(IPageRouteModelProvider), + new[] + { + typeof(CompiledPageRouteModelProvider), + typeof(RazorProjectPageRouteModelProvider), + } + }, { typeof(IPageApplicationModelProvider), new[] { - typeof(CompiledPageApplicationModelProvider), - typeof(RazorProjectPageApplicationModelProvider), + typeof(AuthorizationPageApplicationModelProvider), + typeof(DefaultPageApplicationModelProvider), typeof(PageFilterApplicationModelProvider), } }, diff --git a/test/WebSites/RazorPagesWebSite/HelloWorldWithPageModelHandler.cs b/test/WebSites/RazorPagesWebSite/HelloWorldWithPageModelHandler.cs index d6622314da..ec8dc27137 100644 --- a/test/WebSites/RazorPagesWebSite/HelloWorldWithPageModelHandler.cs +++ b/test/WebSites/RazorPagesWebSite/HelloWorldWithPageModelHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace RazorPagesWebSite diff --git a/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs b/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs new file mode 100644 index 0000000000..8be7783920 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + [Authorize] + public class ModelWithAuthFilter : PageModel + { + public IActionResult OnGet() => Page(); + } +} diff --git a/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml b/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml new file mode 100644 index 0000000000..8862a69efa --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithAuthFilter.cshtml @@ -0,0 +1,4 @@ +@page +@model RazorPagesWebSite.ModelWithAuthFilter + +Can't see me diff --git a/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cs b/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cs new file mode 100644 index 0000000000..f56b01d629 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cs @@ -0,0 +1,44 @@ +// 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; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace RazorPagesWebSite +{ + [HandlerChangingPageFilter] + public class ModelWithPageFilter + { + public string Message { get; private set; } + + public void OnGet() + { + Message = $"Hello from {nameof(OnGet)}"; + } + + public void OnGetEdit() + { + Message = $"Hello from {nameof(OnGetEdit)}"; + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class HandlerChangingPageFilterAttribute : Attribute, IPageFilter + { + public void OnPageHandlerSelected(PageHandlerSelectedContext context) + { + context.HandlerMethod = context.ActionDescriptor.HandlerMethods.First(m => m.Name == "Edit"); + } + + public void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + } + + public void OnPageHandlerExecuted(PageHandlerExecutedContext context) + { + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cshtml b/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cshtml new file mode 100644 index 0000000000..05809eaac4 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/ModelWithPageFilter.cshtml @@ -0,0 +1,4 @@ +@page +@model RazorPagesWebSite.ModelWithPageFilter + +@Model.Message diff --git a/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousModel.cs b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousModel.cs new file mode 100644 index 0000000000..fa96abbdbf --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousModel.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authorization; + +namespace RazorPagesWebSite +{ + [AllowAnonymous] + public class AnonymousModel + { + public void OnGet() + { + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousViaModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousViaModel.cshtml new file mode 100644 index 0000000000..a5f469d689 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/AnonymousViaModel.cshtml @@ -0,0 +1,3 @@ +@page +@model AnonymousModel +Hello from Anonymous \ No newline at end of file From 0879b35e66aca1b594d8017df41dfd188d92ea7e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 28 Jun 2017 12:58:58 -0700 Subject: [PATCH 2/2] Refactor duplicate code --- .../RazorPagesOptionsExtensions.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs index 4c8f69223e..bbc9582f9e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/RazorPagesOptionsExtensions.cs @@ -253,12 +253,7 @@ public void Apply(PageRouteModel model) { var viewEnginePath = model.ViewEnginePath; - var applyConvention = _folderPath == "/" || - (viewEnginePath.Length > _folderPath.Length && - viewEnginePath.StartsWith(_folderPath, StringComparison.OrdinalIgnoreCase) && - viewEnginePath[_folderPath.Length] == '/'); - - if (applyConvention) + if (PathBelongsToFolder(_folderPath, viewEnginePath)) { _action(model); } @@ -300,16 +295,24 @@ public void Apply(PageApplicationModel model) { var viewEnginePath = model.ViewEnginePath; - var applyConvention = _folderPath == "/" || - (viewEnginePath.Length > _folderPath.Length && - viewEnginePath.StartsWith(_folderPath, StringComparison.OrdinalIgnoreCase) && - viewEnginePath[_folderPath.Length] == '/'); - - if (applyConvention) + if (PathBelongsToFolder(_folderPath, viewEnginePath)) { _action(model); } } } + + private static bool PathBelongsToFolder(string folderPath, string viewEnginePath) + { + if (folderPath == "/") + { + // Root directory covers everything. + return true; + } + + return viewEnginePath.Length > folderPath.Length && + viewEnginePath.StartsWith(folderPath, StringComparison.OrdinalIgnoreCase) && + viewEnginePath[folderPath.Length] == '/'; + } } }