From dbff416be60ad6accccb4afa33785e7db660152e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 18 Dec 2017 11:43:09 -0800 Subject: [PATCH] Add support for running conventions on controller properties, Razor Page parameter and properties Fixes #6935 --- .../IParameterModelBaseConvention.cs | 21 + .../IParameterModelConvention.cs | 8 +- .../ApplicationModels/ParameterModel.cs | 39 +- .../ApplicationModels/ParameterModelBase.cs | 52 +++ .../ApplicationModels/PropertyModel.cs | 49 +-- .../ApplicationModelConventionExtensions.cs | 64 +++- .../Internal/ApplicationModelConventions.cs | 37 +- .../IPageHandlerModelConvention.cs | 17 + .../ApplicationModels/PageParameterModel.cs | 31 +- .../ApplicationModels/PagePropertyModel.cs | 40 +- .../PageConventionCollectionExtensions.cs | 39 ++ .../Internal/DefaultPageLoader.cs | 60 ++- ...pplicationModelConventionExtensionsTest.cs | 259 +++++++++++-- .../Internal/DefaultPageLoaderTest.cs | 361 ++++++++++++++++-- 14 files changed, 884 insertions(+), 193 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelBaseConvention.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModelBase.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageHandlerModelConvention.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelBaseConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelBaseConvention.cs new file mode 100644 index 0000000000..2457b89f95 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelBaseConvention.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Allows customization of the properties and parameters on controllers and Razor Pages. + /// + /// + /// To use this interface, create an class which implements the interface and + /// place it on an action method parameter. + /// + public interface IParameterModelBaseConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply(ParameterModelBase parameter); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelConvention.cs index 3904bdf73a..eddbbe383c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelConvention.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/IParameterModelConvention.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { /// - /// Allows customization of the . + /// Allows customization of the . /// /// /// To use this interface, create an class which implements the interface and @@ -15,10 +15,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// public interface IParameterModelConvention { - /// - /// Called to apply the convention to the . - /// - /// The . void Apply(ParameterModel parameter); } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModel.cs index bc1ed07a39..970a026a8a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModel.cs @@ -5,33 +5,22 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { [DebuggerDisplay("ParameterModel: Name={ParameterName}")] - public class ParameterModel : ICommonModel, IBindingModel + public class ParameterModel : ParameterModelBase, ICommonModel { public ParameterModel( ParameterInfo parameterInfo, IReadOnlyList attributes) + : base(parameterInfo?.ParameterType, attributes) { - if (parameterInfo == null) - { - throw new ArgumentNullException(nameof(parameterInfo)); - } - - if (attributes == null) - { - throw new ArgumentNullException(nameof(attributes)); - } - - ParameterInfo = parameterInfo; - Properties = new Dictionary(); - Attributes = new List(attributes); + ParameterInfo = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo)); } public ParameterModel(ParameterModel other) + : base(other) { if (other == null) { @@ -39,27 +28,23 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } Action = other.Action; - 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 ActionModel Action { get; set; } - public IReadOnlyList Attributes { get; } + public new IDictionary Properties => base.Properties; - public IDictionary Properties { get; } + public new IReadOnlyList Attributes => base.Attributes; MemberInfo ICommonModel.MemberInfo => ParameterInfo.Member; - string ICommonModel.Name => ParameterName; - public ParameterInfo ParameterInfo { get; } - public string ParameterName { get; set; } - - public BindingInfo BindingInfo { get; set; } + public string ParameterName + { + get => Name; + set => Name = value; + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModelBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModelBase.cs new file mode 100644 index 0000000000..7937d8b3c7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ParameterModelBase.cs @@ -0,0 +1,52 @@ +// 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 Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// A model type for reading and manipulation properties and parameters. + /// + /// Derived instances of this type represent properties and parameters for controllers, and Razor Pages. + /// + /// + public abstract class ParameterModelBase : IBindingModel + { + protected ParameterModelBase( + Type parameterType, + IReadOnlyList attributes) + { + ParameterType = parameterType ?? throw new ArgumentNullException(nameof(parameterType)); + Attributes = new List(attributes ?? throw new ArgumentNullException(nameof(attributes))); + + Properties = new Dictionary(); + } + + protected ParameterModelBase(ParameterModelBase other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + ParameterType = other.ParameterType; + Attributes = new List(other.Attributes); + BindingInfo = other.BindingInfo == null ? null : new BindingInfo(other.BindingInfo); + Name = other.Name; + Properties = new Dictionary(other.Properties); + } + + public IReadOnlyList Attributes { get; } + + public IDictionary Properties { get; } + + public Type ParameterType { get; } + + public string Name { get; protected set; } + + public BindingInfo BindingInfo { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/PropertyModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/PropertyModel.cs index 3db6a8cccc..8f26d0648a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/PropertyModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/PropertyModel.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// A type which is used to represent a property in a . /// [DebuggerDisplay("PropertyModel: Name={PropertyName}")] - public class PropertyModel : ICommonModel, IBindingModel + public class PropertyModel : ParameterModelBase, ICommonModel, IBindingModel { /// /// Creates a new instance of . @@ -23,20 +23,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public PropertyModel( PropertyInfo propertyInfo, IReadOnlyList attributes) + : base(propertyInfo?.PropertyType, attributes) { - if (propertyInfo == null) - { - throw new ArgumentNullException(nameof(propertyInfo)); - } - - if (attributes == null) - { - throw new ArgumentNullException(nameof(attributes)); - } - - PropertyInfo = propertyInfo; - Properties = new Dictionary(); - Attributes = new List(attributes); + PropertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); } /// @@ -44,6 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// /// The which needs to be copied. public PropertyModel(PropertyModel other) + : base(other) { if (other == null) { @@ -51,11 +41,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } Controller = other.Controller; - Attributes = new List(other.Attributes); BindingInfo = BindingInfo == null ? null : new BindingInfo(other.BindingInfo); PropertyInfo = other.PropertyInfo; - PropertyName = other.PropertyName; - Properties = new Dictionary(other.Properties); } /// @@ -63,30 +50,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// public ControllerModel Controller { get; set; } - /// - /// Gets any attributes which are annotated on the property. - /// - public IReadOnlyList Attributes { get; } - - public IDictionary Properties { get; } - MemberInfo ICommonModel.MemberInfo => PropertyInfo; - string ICommonModel.Name => PropertyName; + public new IDictionary Properties => base.Properties; - /// - /// Gets or sets the associated with this model. - /// - public BindingInfo BindingInfo { get; set; } + public new IReadOnlyList Attributes => base.Attributes; - /// - /// Gets the underlying . - /// public PropertyInfo PropertyInfo { get; } - /// - /// Gets or sets the name of the property represented by this model. - /// - public string PropertyName { get; set; } + public string PropertyName + { + get => Name; + set => Name = value; + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/ApplicationModelConventionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/ApplicationModelConventionExtensions.cs index b23f9d7bed..15f25d474c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/ApplicationModelConventionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/ApplicationModelConventionExtensions.cs @@ -2,8 +2,8 @@ // 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.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace Microsoft.Extensions.DependencyInjection @@ -18,7 +18,8 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The list of s. /// The type to remove. - public static void RemoveType(this IList list) where TApplicationModelConvention : IApplicationModelConvention + public static void RemoveType(this IList list) + where TApplicationModelConvention : IApplicationModelConvention { if (list == null) { @@ -127,17 +128,36 @@ namespace Microsoft.Extensions.DependencyInjection conventions.Add(new ParameterApplicationModelConvention(parameterModelConvention)); } + /// + /// Adds a to all properties and parameters in the application. + /// + /// The list of + /// in . + /// The which needs to be + /// added. + public static void Add( + this IList conventions, + IParameterModelBaseConvention parameterModelConvention) + { + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (parameterModelConvention == null) + { + throw new ArgumentNullException(nameof(parameterModelConvention)); + } + + conventions.Add(new ParameterBaseApplicationModelConvention(parameterModelConvention)); + } + private class ParameterApplicationModelConvention : IApplicationModelConvention { private readonly IParameterModelConvention _parameterModelConvention; public ParameterApplicationModelConvention(IParameterModelConvention parameterModelConvention) { - if (parameterModelConvention == null) - { - throw new ArgumentNullException(nameof(parameterModelConvention)); - } - _parameterModelConvention = parameterModelConvention; } @@ -167,6 +187,36 @@ namespace Microsoft.Extensions.DependencyInjection } } + private class ParameterBaseApplicationModelConvention : + IApplicationModelConvention, IParameterModelBaseConvention + { + private readonly IParameterModelBaseConvention _parameterBaseModelConvention; + + public ParameterBaseApplicationModelConvention(IParameterModelBaseConvention parameterModelBaseConvention) + { + _parameterBaseModelConvention = parameterModelBaseConvention; + } + + /// + public void Apply(ApplicationModel application) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + } + + void IParameterModelBaseConvention.Apply(ParameterModelBase parameterModel) + { + if (parameterModel == null) + { + throw new ArgumentNullException(nameof(parameterModel)); + } + + _parameterBaseModelConvention.Apply(parameterModel); + } + } + private class ActionApplicationModelConvention : IApplicationModelConvention { private readonly IActionModelConvention _actionModelConvention; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApplicationModelConventions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApplicationModelConventions.cs index d4662940d9..108890f5ce 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApplicationModelConventions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApplicationModelConventions.cs @@ -39,8 +39,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal convention.Apply(applicationModel); } + var controllers = applicationModel.Controllers.ToArray(); // First apply the conventions from attributes in decreasing order of scope. - foreach (var controller in applicationModel.Controllers) + foreach (var controller in controllers) { // ToArray is needed here to prevent issues with modifying the attributes collection // while iterating it. @@ -54,7 +55,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal controllerConvention.Apply(controller); } - foreach (var action in controller.Actions) + var actions = controller.Actions.ToArray(); + foreach (var action in actions) { // ToArray is needed here to prevent issues with modifying the attributes collection // while iterating it. @@ -68,7 +70,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal actionConvention.Apply(action); } - foreach (var parameter in action.Parameters) + var parameters = action.Parameters.ToArray(); + foreach (var parameter in parameters) { // ToArray is needed here to prevent issues with modifying the attributes collection // while iterating it. @@ -81,9 +84,35 @@ namespace Microsoft.AspNetCore.Mvc.Internal { parameterConvention.Apply(parameter); } + + var parameterBaseConventions = GetConventions(conventions, parameter.Attributes); + foreach (var parameterConvention in parameterBaseConventions) + { + parameterConvention.Apply(parameter); + } + } + } + + var properties = controller.ControllerProperties.ToArray(); + foreach (var property in properties) + { + var parameterBaseConventions = GetConventions(conventions, property.Attributes); + + foreach (var parameterConvention in parameterBaseConventions) + { + parameterConvention.Apply(property); } } } } + + private static IEnumerable GetConventions( + IEnumerable conventions, + IReadOnlyList attributes) + { + return Enumerable.Concat( + conventions.OfType(), + attributes.OfType()); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageHandlerModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageHandlerModelConvention.cs new file mode 100644 index 0000000000..df2b0c7a69 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageHandlerModelConvention.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 . + /// + public interface IPageHandlerModelConvention : IPageConvention + { + /// + /// Called to apply the convention to the . + /// + /// The . + void Apply(PageHandlerModel model); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs index c4547411c0..0b4851b1ee 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageParameterModel.cs @@ -5,29 +5,32 @@ 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 class PageParameterModel : ParameterModelBase, ICommonModel, IBindingModel { public PageParameterModel( ParameterInfo parameterInfo, IReadOnlyList attributes) + : base(parameterInfo?.ParameterType, attributes) { - ParameterInfo = parameterInfo ?? throw new ArgumentNullException(nameof(parameterInfo)); + if (parameterInfo == null) + { + throw new ArgumentNullException(nameof(parameterInfo)); + } if (attributes == null) { throw new ArgumentNullException(nameof(attributes)); } - Properties = new Dictionary(); - Attributes = new List(attributes); + ParameterInfo = parameterInfo; } public PageParameterModel(PageParameterModel other) + : base(other) { if (other == null) { @@ -35,27 +38,19 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } 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; } + public string ParameterName + { + get => Name; + set => Name = value; + } } } \ 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 index f76d7da366..5af225dee6 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PagePropertyModel.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// Represents a property in a . /// [DebuggerDisplay("PagePropertyModel: Name={PropertyName}")] - public class PagePropertyModel : ICommonModel, IBindingModel + public class PagePropertyModel : ParameterModelBase, ICommonModel { /// /// Creates a new instance of . @@ -23,10 +23,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public PagePropertyModel( PropertyInfo propertyInfo, IReadOnlyList attributes) + : base(propertyInfo?.PropertyType, attributes) { PropertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo)); - Properties = new Dictionary(); - Attributes = new List(attributes) ?? throw new ArgumentNullException(nameof(attributes)); } /// @@ -34,6 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// /// The which needs to be copied. public PagePropertyModel(PagePropertyModel other) + : base(other) { if (other == null) { @@ -41,11 +41,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } 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); } /// @@ -53,31 +50,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// 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; + public PropertyInfo PropertyInfo { get; } + + public string PropertyName + { + get => Name; + set => Name = value; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/PageConventionCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/PageConventionCollectionExtensions.cs index 6daa490f9f..577c8b5ed3 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/PageConventionCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/PageConventionCollectionExtensions.cs @@ -143,6 +143,30 @@ namespace Microsoft.Extensions.DependencyInjection return conventions; } + /// + /// Adds the specified to . + /// The added convention will apply to all handler properties and parameters on handler methods. + /// + /// The to configure. + /// The to apply. + /// The . + public static PageConventionCollection Add(this PageConventionCollection conventions, IParameterModelBaseConvention convention) + { + if (conventions == null) + { + throw new ArgumentNullException(nameof(conventions)); + } + + if (convention == null) + { + throw new ArgumentNullException(nameof(convention)); + } + + var adapter = new ParameterModelBaseConventionAdapter(convention); + conventions.Add(adapter); + return conventions; + } + /// /// Adds a to all pages under the specified area folder. /// @@ -461,5 +485,20 @@ namespace Microsoft.Extensions.DependencyInjection }); }; } + + private class ParameterModelBaseConventionAdapter : IPageConvention, IParameterModelBaseConvention + { + private readonly IParameterModelBaseConvention _convention; + + public ParameterModelBaseConventionAdapter(IParameterModelBaseConvention convention) + { + _convention = convention; + } + + public void Apply(ParameterModelBase parameter) + { + _convention.Apply(parameter); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index e31ed6d32b..23a57a786e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { private readonly IPageApplicationModelProvider[] _applicationModelProviders; private readonly IViewCompilerProvider _viewCompilerProvider; - private readonly IPageApplicationModelConvention[] _conventions; + private readonly PageConventionCollection _conventions; private readonly FilterCollection _globalFilters; public DefaultPageLoader( @@ -30,9 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal .OrderBy(p => p.Order) .ToArray(); _viewCompilerProvider = viewCompilerProvider; - _conventions = pageOptions.Value.Conventions - .OfType() - .ToArray(); + _conventions = pageOptions.Value.Conventions; _globalFilters = mvcOptions.Value.Filters; } @@ -60,12 +58,58 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _applicationModelProviders[i].OnProvidersExecuted(context); } - for (var i = 0; i < _conventions.Length; i++) - { - _conventions[i].Apply(context.PageApplicationModel); - } + ApplyConventions(_conventions, context.PageApplicationModel); return CompiledPageActionDescriptorBuilder.Build(context.PageApplicationModel, _globalFilters); } + + internal static void ApplyConventions( + PageConventionCollection conventions, + PageApplicationModel pageApplicationModel) + { + var applicationModelConventions = GetConventions(pageApplicationModel.HandlerTypeAttributes); + foreach (var convention in applicationModelConventions) + { + convention.Apply(pageApplicationModel); + } + + var handlers = pageApplicationModel.HandlerMethods.ToArray(); + foreach (var handlerModel in handlers) + { + var handlerModelConventions = GetConventions(handlerModel.Attributes); + foreach (var convention in handlerModelConventions) + { + convention.Apply(handlerModel); + } + + var parameterModels = handlerModel.Parameters.ToArray(); + foreach (var parameterModel in parameterModels) + { + var parameterModelConventions = GetConventions(parameterModel.Attributes); + foreach (var convention in parameterModelConventions) + { + convention.Apply(parameterModel); + } + } + } + + var properties = pageApplicationModel.HandlerProperties.ToArray(); + foreach (var propertyModel in properties) + { + var propertyModelConventions = GetConventions(propertyModel.Attributes); + foreach (var convention in propertyModelConventions) + { + convention.Apply(propertyModel); + } + } + + IEnumerable GetConventions( + IReadOnlyList attributes) + { + return Enumerable.Concat( + conventions.OfType(), + attributes.OfType()); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/ApplicationModelConventionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/ApplicationModelConventionExtensionsTest.cs index 134fdfb688..51811324e8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/ApplicationModelConventionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/DependencyInjection/ApplicationModelConventionExtensionsTest.cs @@ -18,8 +18,16 @@ namespace Microsoft.Extensions.DependencyInjection { // Arrange var app = new ApplicationModel(); - app.Controllers.Add(new ControllerModel(typeof(HelloController).GetTypeInfo(), new List())); - app.Controllers.Add(new ControllerModel(typeof(WorldController).GetTypeInfo(), new List())); + var controllerType = typeof(HelloController); + var controllerModel = new ControllerModel(controllerType.GetTypeInfo(), Array.Empty()); + app.Controllers.Add(controllerModel); + + var actionModel = new ActionModel(controllerType.GetMethod(nameof(HelloController.GetInfo)), Array.Empty()); + controllerModel.Actions.Add(actionModel); + var parameterModel = new ParameterModel( + controllerType.GetMethod(nameof(HelloController.GetInfo)).GetParameters()[0], + Array.Empty()); + actionModel.Parameters.Add(parameterModel); var options = new MvcOptions(); options.Conventions.Add(new SimpleParameterConvention()); @@ -28,18 +36,9 @@ namespace Microsoft.Extensions.DependencyInjection options.Conventions[0].Apply(app); // Assert - foreach (var controller in app.Controllers) - { - foreach (var action in controller.Actions) - { - foreach (var parameter in action.Parameters) - { - var kvp = Assert.Single(parameter.Properties); - Assert.Equal("TestProperty", kvp.Key); - Assert.Equal("TestValue", kvp.Value); - } - } - } + var kvp = Assert.Single(parameterModel.Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); } [Fact] @@ -47,8 +46,28 @@ namespace Microsoft.Extensions.DependencyInjection { // Arrange var app = new ApplicationModel(); - app.Controllers.Add(new ControllerModel(typeof(HelloController).GetTypeInfo(), new List())); - app.Controllers.Add(new ControllerModel(typeof(WorldController).GetTypeInfo(), new List())); + var controllerType1 = typeof(HelloController).GetTypeInfo(); + var actionMethod1 = controllerType1.GetMethod(nameof(HelloController.GetHello)); + var controllerModel1 = new ControllerModel(controllerType1, Array.Empty()) + { + Actions = + { + new ActionModel(actionMethod1, Array.Empty()), + } + }; + + var controllerType2 = typeof(WorldController).GetTypeInfo(); + var actionMethod2 = controllerType2.GetMethod(nameof(WorldController.GetWorld)); + var controllerModel2 = new ControllerModel(controllerType2, Array.Empty()) + { + Actions = + { + new ActionModel(actionMethod2, Array.Empty()), + }, + }; + + app.Controllers.Add(controllerModel1); + app.Controllers.Add(controllerModel2); var options = new MvcOptions(); options.Conventions.Add(new SimpleActionConvention()); @@ -57,15 +76,76 @@ namespace Microsoft.Extensions.DependencyInjection options.Conventions[0].Apply(app); // Assert - foreach (var controller in app.Controllers) + var kvp = Assert.Single(controllerModel1.Actions[0].Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); + + kvp = Assert.Single(controllerModel2.Actions[0].Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); + } + + [Fact] + public void AddedParameterConvention_AppliesToAllPropertiesAndParameters() + { + // Arrange + var app = new ApplicationModel(); + var controllerType1 = typeof(HelloController).GetTypeInfo(); + var parameterModel1 = new ParameterModel( + controllerType1.GetMethod(nameof(HelloController.GetInfo)).GetParameters()[0], + Array.Empty()); + var actionMethod1 = controllerType1.GetMethod(nameof(HelloController.GetInfo)); + var property1 = controllerType1.GetProperty(nameof(HelloController.Property1)); + var controllerModel1 = new ControllerModel(controllerType1, Array.Empty()) { - foreach (var action in controller.Actions) + ControllerProperties = { - var kvp = Assert.Single(action.Properties); - Assert.Equal("TestProperty", kvp.Key); - Assert.Equal("TestValue", kvp.Value); + new PropertyModel(property1, Array.Empty()), + }, + Actions = + { + new ActionModel(actionMethod1, Array.Empty()) + { + Parameters = + { + parameterModel1, + } + } } - } + }; + + var controllerType2 = typeof(WorldController).GetTypeInfo(); + var property2 = controllerType2.GetProperty(nameof(WorldController.Property2)); + var controllerModel2 = new ControllerModel(controllerType2, Array.Empty()) + { + ControllerProperties = + { + new PropertyModel(property2, Array.Empty()), + }, + }; + + app.Controllers.Add(controllerModel1); + app.Controllers.Add(controllerModel2); + + var options = new MvcOptions(); + var convention = new SimplePropertyConvention(); + options.Conventions.Add(convention); + + // Act + ApplicationModelConventions.ApplyConventions(app, options.Conventions); + + // Assert + var kvp = Assert.Single(controllerModel1.ControllerProperties[0].Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); + + kvp = Assert.Single(controllerModel2.ControllerProperties[0].Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); + + kvp = Assert.Single(controllerModel1.Actions[0].Parameters[0].Properties); + Assert.Equal("TestProperty", kvp.Key); + Assert.Equal("TestValue", kvp.Value); } [Fact] @@ -74,8 +154,8 @@ namespace Microsoft.Extensions.DependencyInjection // Arrange var options = new MvcOptions(); var app = new ApplicationModel(); - app.Controllers.Add(new ControllerModel(typeof(HelloController).GetTypeInfo(), new List())); - app.Controllers.Add(new ControllerModel(typeof(WorldController).GetTypeInfo(), new List())); + app.Controllers.Add(new ControllerModel(typeof(HelloController).GetTypeInfo(), Array.Empty())); + app.Controllers.Add(new ControllerModel(typeof(WorldController).GetTypeInfo(), Array.Empty())); options.Conventions.Add(new SimpleControllerConvention()); // Act @@ -115,7 +195,7 @@ namespace Microsoft.Extensions.DependencyInjection // Arrange var applicationModel = new ApplicationModel(); applicationModel.Controllers.Add( - new ControllerModel(typeof(HelloController).GetTypeInfo(), new List()) + new ControllerModel(typeof(HelloController).GetTypeInfo(), Array.Empty()) { Application = applicationModel }); @@ -128,18 +208,36 @@ namespace Microsoft.Extensions.DependencyInjection ApplicationModelConventions.ApplyConventions(applicationModel, conventions); } + [Fact] + public void ApplicationModelConventions_CopiesControllerModelCollectionOnApply_WhenRegisteredAsAnAttribute() + { + // Arrange + var controllerModelConvention = new ControllerModelCollectionModifyingConvention(); + var applicationModel = new ApplicationModel(); + applicationModel.Controllers.Add( + new ControllerModel(typeof(HelloController).GetTypeInfo(), new[] { controllerModelConvention }) + { + Application = applicationModel + }); + + var conventions = new List(); + + // Act & Assert + ApplicationModelConventions.ApplyConventions(applicationModel, conventions); + } + [Fact] public void ApplicationModelConventions_CopiesActionModelCollectionOnApply() { // Arrange var controllerType = typeof(HelloController).GetTypeInfo(); var applicationModel = new ApplicationModel(); - var controllerModel = new ControllerModel(controllerType, new List()) + var controllerModel = new ControllerModel(controllerType, Array.Empty()) { Application = applicationModel }; controllerModel.Actions.Add( - new ActionModel(controllerType.GetMethod(nameof(HelloController.GetHello)), new List()) + new ActionModel(controllerType.GetMethod(nameof(HelloController.GetHello)), Array.Empty()) { Controller = controllerModel }); @@ -153,25 +251,74 @@ namespace Microsoft.Extensions.DependencyInjection ApplicationModelConventions.ApplyConventions(applicationModel, conventions); } + [Fact] + public void ApplicationModelConventions_CopiesPropertyModelCollectionOnApply() + { + // Arrange + var controllerType = typeof(HelloController).GetTypeInfo(); + var applicationModel = new ApplicationModel(); + var controllerModel = new ControllerModel(controllerType, Array.Empty()) + { + Application = applicationModel + }; + controllerModel.ControllerProperties.Add( + new PropertyModel(controllerType.GetProperty(nameof(HelloController.Property1)), Array.Empty()) + { + Controller = controllerModel + }); + applicationModel.Controllers.Add(controllerModel); + + var propertyModelConvention = new ParameterModelBaseConvention(); + var conventions = new List(); + conventions.Add(propertyModelConvention); + + // Act & Assert + ApplicationModelConventions.ApplyConventions(applicationModel, conventions); + } + + [Fact] + public void ApplicationModelConventions_CopiesPropertyModelCollectionOnApply_WhenAppliedViaAttributes() + { + // Arrange + var propertyModelConvention = new ParameterModelBaseConvention(); + var controllerType = typeof(HelloController).GetTypeInfo(); + var applicationModel = new ApplicationModel(); + var controllerModel = new ControllerModel(controllerType, Array.Empty()) + { + Application = applicationModel + }; + controllerModel.ControllerProperties.Add( + new PropertyModel(controllerType.GetProperty(nameof(HelloController.Property1)), new[] { propertyModelConvention }) + { + Controller = controllerModel + }); + applicationModel.Controllers.Add(controllerModel); + + var conventions = new List(); + + // Act & Assert + ApplicationModelConventions.ApplyConventions(applicationModel, conventions); + } + [Fact] public void ApplicationModelConventions_CopiesParameterModelCollectionOnApply() { // Arrange var controllerType = typeof(HelloController).GetTypeInfo(); var app = new ApplicationModel(); - var controllerModel = new ControllerModel(controllerType, new List()) + var controllerModel = new ControllerModel(controllerType, Array.Empty()) { Application = app }; app.Controllers.Add(controllerModel); - var actionModel = new ActionModel(controllerType.GetMethod(nameof(HelloController.GetInfo)), new List()) + var actionModel = new ActionModel(controllerType.GetMethod(nameof(HelloController.GetInfo)), Array.Empty()) { Controller = controllerModel }; controllerModel.Actions.Add(actionModel); var parameterModel = new ParameterModel( controllerType.GetMethod(nameof(HelloController.GetInfo)).GetParameters()[0], - new List()) + Array.Empty()) { Action = actionModel }; @@ -185,6 +332,37 @@ namespace Microsoft.Extensions.DependencyInjection ApplicationModelConventions.ApplyConventions(app, conventions); } + [Fact] + public void ApplicationModelConventions_CopiesParameterModelCollectionOnApply_WhenRegisteredViaAttribute() + { + // Arrange + var parameterModelConvention = new ParameterModelCollectionModifyingConvention(); + var controllerType = typeof(HelloController).GetTypeInfo(); + var app = new ApplicationModel(); + var controllerModel = new ControllerModel(controllerType, Array.Empty()) + { + Application = app + }; + app.Controllers.Add(controllerModel); + var actionModel = new ActionModel(controllerType.GetMethod(nameof(HelloController.GetInfo)), Array.Empty()) + { + Controller = controllerModel + }; + controllerModel.Actions.Add(actionModel); + var parameterModel = new ParameterModel( + controllerType.GetMethod(nameof(HelloController.GetInfo)).GetParameters()[0], + new[] { parameterModelConvention }) + { + Action = actionModel + }; + actionModel.Parameters.Add(parameterModel); + + var conventions = new List(); + + // Act & Assert + ApplicationModelConventions.ApplyConventions(app, conventions); + } + [Fact] public void GenericRemoveType_RemovesAllOfType() { @@ -222,6 +400,8 @@ namespace Microsoft.Extensions.DependencyInjection private class HelloController { + public string Property1 { get; set; } + public string GetHello() { return "Hello"; @@ -235,6 +415,8 @@ namespace Microsoft.Extensions.DependencyInjection private class WorldController { + public string Property2 { get; set; } + public string GetWorld() { return "World!"; @@ -257,6 +439,14 @@ namespace Microsoft.Extensions.DependencyInjection } } + private class SimplePropertyConvention : IParameterModelBaseConvention + { + public void Apply(ParameterModelBase action) + { + action.Properties.Add("TestProperty", "TestValue"); + } + } + private class SimpleControllerConvention : IControllerModelConvention { public void Apply(ControllerModel controller) @@ -289,6 +479,15 @@ namespace Microsoft.Extensions.DependencyInjection } } + private class ParameterModelBaseConvention : IParameterModelBaseConvention + { + public void Apply(ParameterModelBase modelBase) + { + var property = (PropertyModel)modelBase; + property.Controller.ControllerProperties.Remove(property); + } + } + private class ParameterModelCollectionModifyingConvention : IParameterModelConvention { public void Apply(ParameterModel parameter) diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs index 169a0db20f..d31359df2f 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageLoaderTest.cs @@ -1,11 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -28,8 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var provider2 = new Mock(); 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]); + var pageApplicationModel1 = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var pageApplicationModel2 = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); provider1.Setup(p => p.OnProvidersExecuting(It.IsAny())) .Callback((PageApplicationModelProviderContext c) => @@ -103,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal .Callback((PageApplicationModelProviderContext c) => { Assert.Equal(1, sequence++); - c.PageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new object[0]); + c.PageApplicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); }) .Verifiable(); @@ -148,45 +150,346 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void Load_InvokesApplicationModelConventions() + public void ApplyConventions_InvokesApplicationModelConventions() { // Arrange var descriptor = new PageActionDescriptor(); + var model = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); - 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) => - { - c.PageApplicationModel = model; - }); - var providers = new[] { provider.Object }; - - var razorPagesOptions = Options.Create(new RazorPagesOptions()); - var mvcOptions = Options.Create(new MvcOptions()); var convention = new Mock(); convention.Setup(c => c.Apply(It.IsAny())) .Callback((PageApplicationModel m) => { Assert.Same(model, m); - }); - razorPagesOptions.Value.Conventions.Add(convention.Object); - - var loader = new DefaultPageLoader( - providers, - compilerProvider, - razorPagesOptions, - mvcOptions); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection + { + convention.Object, + }; // Act - var result = loader.Load(new PageActionDescriptor()); + DefaultPageLoader.ApplyConventions(conventionCollection, model); // Assert convention.Verify(); } + [Fact] + public void ApplyConventions_InvokesApplicationModelConventions_SpecifiedOnHandlerType() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var handlerConvention = new Mock(); + var model = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), new[] { handlerConvention.Object }); + + var globalConvention = new Mock(); + globalConvention.Setup(c => c.Apply(It.IsAny())) + .Callback((PageApplicationModel m) => + { + Assert.Same(model, m); + }) + .Verifiable(); + + handlerConvention.Setup(c => c.Apply(It.IsAny())) + .Callback((PageApplicationModel m) => + { + Assert.Same(model, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection + { + globalConvention.Object, + }; + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, model); + + // Assert + globalConvention.Verify(); + handlerConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesHandlerModelConventions() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var handlerModelConvention = new Mock(); + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, new[] { handlerModelConvention.Object }); + + applicationModel.HandlerMethods.Add(handlerModel); + + handlerModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((PageHandlerModel m) => + { + Assert.Same(handlerModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + handlerModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesHandlerModelConventions_DefinedGlobally() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + applicationModel.HandlerMethods.Add(handlerModel); + + var handlerModelConvention = new Mock(); + handlerModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((PageHandlerModel m) => + { + Assert.Same(handlerModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection { handlerModelConvention.Object }; + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + handlerModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_RemovingHandlerAsPartOfHandlerModelConvention_Works() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var handlerModelConvention = new Mock(); + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, new[] { handlerModelConvention.Object }) + { + Page = applicationModel, + }; + + applicationModel.HandlerMethods.Add(handlerModel); + + handlerModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((PageHandlerModel m) => + { + m.Page.HandlerMethods.Remove(m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + handlerModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesParameterModelConventions() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var parameterInfo = methodInfo.GetParameters()[0]; + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + var parameterModelConvention = new Mock(); + var parameterModel = new PageParameterModel(parameterInfo, new[] { parameterModelConvention.Object }); + + applicationModel.HandlerMethods.Add(handlerModel); + handlerModel.Parameters.Add(parameterModel); + + parameterModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + Assert.Same(parameterModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + parameterModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesParameterModelConventions_DeclaredGlobally() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var parameterInfo = methodInfo.GetParameters()[0]; + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + var parameterModel = new PageParameterModel(parameterInfo, Array.Empty()); + + applicationModel.HandlerMethods.Add(handlerModel); + handlerModel.Parameters.Add(parameterModel); + + var parameterModelConvention = new Mock(); + parameterModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + Assert.Same(parameterModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection { parameterModelConvention.Object }; + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + parameterModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_RemovingParameterModelAsPartOfConventionWorks() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var parameterInfo = methodInfo.GetParameters()[0]; + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + var parameterModelConvention = new Mock(); + var parameterModel = new PageParameterModel(parameterInfo, new[] { parameterModelConvention.Object }) + { + Handler = handlerModel, + }; + + applicationModel.HandlerMethods.Add(handlerModel); + handlerModel.Parameters.Add(parameterModel); + + parameterModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + var model = Assert.IsType(m); + model.Handler.Parameters.Remove(model); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + parameterModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesPropertyModelConventions() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var propertyInfo = GetType().GetProperty(nameof(TestProperty), BindingFlags.Instance | BindingFlags.NonPublic); + var parameterInfo = methodInfo.GetParameters()[0]; + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + var parameterModel = new PageParameterModel(parameterInfo, Array.Empty()); + var propertyModelConvention = new Mock(); + var propertyModel = new PagePropertyModel(propertyInfo, new[] { propertyModelConvention.Object }); + + applicationModel.HandlerMethods.Add(handlerModel); + applicationModel.HandlerProperties.Add(propertyModel); + handlerModel.Parameters.Add(parameterModel); + + propertyModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + Assert.Same(propertyModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + propertyModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_InvokesPropertyModelConventions_DeclaredGlobally() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var methodInfo = GetType().GetMethod(nameof(OnGet), BindingFlags.Instance | BindingFlags.NonPublic); + var propertyInfo = GetType().GetProperty(nameof(TestProperty), BindingFlags.Instance | BindingFlags.NonPublic); + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var handlerModel = new PageHandlerModel(methodInfo, Array.Empty()); + var propertyModel = new PagePropertyModel(propertyInfo, Array.Empty()); + + applicationModel.HandlerMethods.Add(handlerModel); + applicationModel.HandlerProperties.Add(propertyModel); + + var propertyModelConvention = new Mock(); + propertyModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + Assert.Same(propertyModel, m); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection { propertyModelConvention.Object }; + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + propertyModelConvention.Verify(); + } + + [Fact] + public void ApplyConventions_RemovingPropertyModelAsPartOfConvention_Works() + { + // Arrange + var descriptor = new PageActionDescriptor(); + var propertyInfo = GetType().GetProperty(nameof(TestProperty), BindingFlags.Instance | BindingFlags.NonPublic); + + var applicationModel = new PageApplicationModel(descriptor, typeof(object).GetTypeInfo(), Array.Empty()); + var propertyModelConvention = new Mock(); + var propertyModel = new PagePropertyModel(propertyInfo, new[] { propertyModelConvention.Object }) + { + Page = applicationModel, + }; + + applicationModel.HandlerProperties.Add(propertyModel); + + propertyModelConvention.Setup(p => p.Apply(It.IsAny())) + .Callback((ParameterModelBase m) => + { + var model = Assert.IsType(m); + model.Page.HandlerProperties.Remove(model); + }) + .Verifiable(); + var conventionCollection = new PageConventionCollection(); + + // Act + DefaultPageLoader.ApplyConventions(conventionCollection, applicationModel); + + // Assert + propertyModelConvention.Verify(); + } + private static IViewCompilerProvider GetCompilerProvider() { var descriptor = new CompiledViewDescriptor @@ -202,5 +505,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal .Returns(compiler.Object); return compilerProvider.Object; } + + private void OnGet(string parameter) + { + } + + private string TestProperty { get; set; } } }