From 2ff80ffb4918757718b48ba97a7aad338f52e9a7 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 7 Feb 2017 12:44:27 -0800 Subject: [PATCH] Porting DefaultPageHandlerMethodSelector and ExecutorFactory --- samples/MvcSandbox/Models/TestModel.cs | 16 +- samples/MvcSandbox/Pages/Index.cshtml | 8 +- ....cs => IPageApplicationModelConvention.cs} | 8 +- .../{PageModel.cs => PageApplicationModel.cs} | 14 +- .../CompiledPageActionDescriptor.cs | 4 + .../MvcRazorPagesMvcCoreBuilderExtensions.cs | 1 + .../PageActionDescriptorProvider.cs | 2 +- .../Infrastructure/PageArgumentBinder.cs | 20 ++ .../Internal/DefaultPageArgumentBinder.cs | 82 +++++++ .../DefaultPageHandlerMethodSelector.cs | 138 ++++++++++- .../Internal/ExecutorFactory.cs | 223 +++++++++++++++++- .../Internal/PageActionInvoker.cs | 1 + .../Internal/PageActionInvokerProvider.cs | 27 ++- .../Page.cs | 25 ++ .../PageContext.cs | 27 +++ .../PageModel.cs | 58 +++++ .../PageActionDescriptorProviderTest.cs | 4 +- .../Internal/PageActionInvokerProviderTest.cs | 2 +- 18 files changed, 634 insertions(+), 26 deletions(-) rename src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/{IPageModelConvention.cs => IPageApplicationModelConvention.cs} (65%) rename src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/{PageModel.cs => PageApplicationModel.cs} (84%) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageArgumentBinder.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageArgumentBinder.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs diff --git a/samples/MvcSandbox/Models/TestModel.cs b/samples/MvcSandbox/Models/TestModel.cs index 49c72dc2aa..a3233fd0e8 100644 --- a/samples/MvcSandbox/Models/TestModel.cs +++ b/samples/MvcSandbox/Models/TestModel.cs @@ -1,9 +1,19 @@ - +// 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.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + namespace MvcSandbox { - public class TestModel + public class TestModel : PageModel { - public string Name { get; set; } + public string Name { get; private set; } = "World"; + public IActionResult OnPost(string name) + { + Name = name; + return View(); + } } } diff --git a/samples/MvcSandbox/Pages/Index.cshtml b/samples/MvcSandbox/Pages/Index.cshtml index 1cc8dea003..702b1005ce 100644 --- a/samples/MvcSandbox/Pages/Index.cshtml +++ b/samples/MvcSandbox/Pages/Index.cshtml @@ -3,9 +3,15 @@
-

RazorPages Test

+

RazorPages says Hello @Model.Name!

  • This file should give you a quick view of a Mvc Raor Page in action.
+ +
+ + +
+
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelConvention.cs similarity index 65% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelConvention.cs index 752cd81f49..2dbe608d8c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageModelConvention.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelConvention.cs @@ -4,14 +4,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { /// - /// Allows customization of the of the . + /// Allows customization of the of the . /// public interface IPageModelConvention { /// - /// Called to apply the convention to the . + /// Called to apply the convention to the . /// - /// The . - void Apply(PageModel model); + /// The . + void Apply(PageApplicationModel model); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs similarity index 84% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs index 9c080aed1c..a5ff863070 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs @@ -11,14 +11,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels /// /// Application model component for RazorPages. /// - public class PageModel + public class PageApplicationModel { /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// /// The application relative path of the page. /// The path relative to the base path for page discovery. - public PageModel(string relativePath, string viewEnginePath) + public PageApplicationModel(string relativePath, string viewEnginePath) { if (relativePath == null) { @@ -39,10 +39,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } /// - /// A copy constructor for . + /// A copy constructor for . /// - /// The to copy from. - public PageModel(PageModel other) + /// The to copy from. + public PageApplicationModel(PageApplicationModel other) { if (other == null) { @@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public IList Filters { get; } /// - /// Stores arbitrary metadata properties associated with the . + /// Stores arbitrary metadata properties associated with the . /// public IDictionary Properties { get; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs index e879bb4a0b..feb6298f9b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs @@ -1,7 +1,9 @@ // 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.Infrastructure; namespace Microsoft.AspNetCore.Mvc.RazorPages { @@ -36,5 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// Gets or sets the of the model. /// public TypeInfo ModelTypeInfo { get; set; } + + public IList HandlerMethods { get; } = new List(); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 7c2da38b2b..826f154af7 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -66,6 +66,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index a64ee5cdf7..b9333afe6c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private void AddActionDescriptors(IList actions, RazorProjectItem item, string template) { - var model = new PageModel(item.CombinedPath, item.PathWithoutExtension); + var model = new PageApplicationModel(item.CombinedPath, item.PathWithoutExtension); var routePrefix = item.BasePath == "/" ? item.PathWithoutExtension : item.BasePath + item.PathWithoutExtension; model.Selectors.Add(CreateSelectorModel(routePrefix, template)); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageArgumentBinder.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageArgumentBinder.cs new file mode 100644 index 0000000000..4b76587999 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageArgumentBinder.cs @@ -0,0 +1,20 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public abstract class PageArgumentBinder + { + public async Task BindModelAsync(PageContext context, Type type, object defaultValue, string name) + { + var result = await BindAsync(context, value: null, name: name, type: type); + return result.IsModelSet ? result.Model : defaultValue; + } + + protected abstract Task BindAsync(PageContext context, object value, string name, Type type); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageArgumentBinder.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageArgumentBinder.cs new file mode 100644 index 0000000000..9f40c7544d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageArgumentBinder.cs @@ -0,0 +1,82 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class DefaultPageArgumentBinder : PageArgumentBinder + { + private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly IModelBinderFactory _modelBinderFactory; + private readonly IObjectModelValidator _validator; + + public DefaultPageArgumentBinder( + IModelMetadataProvider modelMetadataProvider, + IModelBinderFactory modelBinderFactory, + IObjectModelValidator validator) + { + _modelMetadataProvider = modelMetadataProvider; + _modelBinderFactory = modelBinderFactory; + _validator = validator; + } + + protected override async Task BindAsync(PageContext pageContext, object value, string name, Type type) + { + var factories = pageContext.ValueProviderFactories; + var valueProviderFactoryContext = new ValueProviderFactoryContext(pageContext); + for (var i = 0; i < factories.Count; i++) + { + var factory = factories[i]; + await factory.CreateValueProviderAsync(valueProviderFactoryContext); + } + + var valueProvider = new CompositeValueProvider(valueProviderFactoryContext.ValueProviders); + + var metadata = _modelMetadataProvider.GetMetadataForType(type); + var binder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext() + { + BindingInfo = null, + Metadata = metadata, + CacheToken = null, + }); + + var modelBindingContext = DefaultModelBindingContext.CreateBindingContext( + pageContext, + valueProvider, + metadata, + null, + name); + modelBindingContext.Model = value; + + if (modelBindingContext.ValueProvider.ContainsPrefix(name)) + { + // We have a match for the parameter name, use that as that prefix. + modelBindingContext.ModelName = name; + } + else + { + // No match, fallback to empty string as the prefix. + modelBindingContext.ModelName = string.Empty; + } + + await binder.BindModelAsync(modelBindingContext); + + var result = modelBindingContext.Result; + if (result.IsModelSet) + { + _validator.Validate( + pageContext, + modelBindingContext.ValidationState, + modelBindingContext.ModelName, + result.Model); + } + + return result; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs index a53b713aad..3092ce2738 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs @@ -1,13 +1,143 @@ -using System; -using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +// 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 +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageHandlerMethodSelector : IPageHandlerMethodSelector { public HandlerMethodDescriptor Select(PageContext context) { + var handlers = new List(context.ActionDescriptor.HandlerMethods.Count); + for (var i = 0; i < context.ActionDescriptor.HandlerMethods.Count; i++) + { + handlers.Add(HandlerMethodAndMetadata.Create(context.ActionDescriptor.HandlerMethods[i])); + } + + for (var i = handlers.Count - 1; i >= 0; i--) + { + var handler = handlers[i]; + + if (handler.HttpMethod != null && + !string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + { + handlers.RemoveAt(i); + } + } + + var formaction = Convert.ToString(context.RouteData.Values["formaction"]); + + for (var i = handlers.Count - 1; i >= 0; i--) + { + var handler = handlers[i]; + + if (handler.Formaction != null && + !string.Equals(handler.Formaction, formaction, StringComparison.OrdinalIgnoreCase)) + { + handlers.RemoveAt(i); + } + } + + var ambiguousMatches = (List)null; + var best = (HandlerMethodAndMetadata?)null; + for (var i = 2; i >= 0; i--) + { + for (var j = 0; j < handlers.Count; j++) + { + var handler = handlers[j]; + if (handler.GetScore() == i) + { + if (best == null) + { + best = handler; + continue; + } + + if (ambiguousMatches == null) + { + ambiguousMatches = new List(); + ambiguousMatches.Add(best.Value.Handler); + } + + ambiguousMatches.Add(handler.Handler); + } + } + + if (ambiguousMatches != null) + { + throw new InvalidOperationException($"Selecting a handler is ambiguous! Matches: {string.Join(", ", ambiguousMatches)}"); + } + + if (best != null) + { + return best.Value.Handler; + } + } + return null; } + + // Bad prototype substring implementation :) + private struct HandlerMethodAndMetadata + { + public static HandlerMethodAndMetadata Create(HandlerMethodDescriptor handler) + { + var name = handler.Method.Name; + + string httpMethod; + if (name.StartsWith("OnGet", StringComparison.Ordinal)) + { + httpMethod = "GET"; + } + else if (name.StartsWith("OnPost", StringComparison.Ordinal)) + { + httpMethod = "POST"; + } + else + { + httpMethod = null; + } + + var formactionStart = httpMethod?.Length + 2 ?? 0; + var formactionLength = name.EndsWith("Async", StringComparison.Ordinal) + ? name.Length - formactionStart - "Async".Length + : name.Length - formactionStart; + + var formaction = formactionLength == 0 ? null : name.Substring(formactionStart, formactionLength); + + return new HandlerMethodAndMetadata(handler, httpMethod, formaction); + } + + public HandlerMethodAndMetadata(HandlerMethodDescriptor handler, string httpMethod, string formaction) + { + Handler = handler; + HttpMethod = httpMethod; + Formaction = formaction; + } + + public HandlerMethodDescriptor Handler { get; } + + public string HttpMethod { get; } + + public string Formaction { get; } + + public int GetScore() + { + if (Formaction != null) + { + return 2; + } + else if (HttpMethod != null) + { + return 1; + } + else + { + return 0; + } + } + } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs index 25c07afe09..1a535a0e90 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/ExecutorFactory.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; @@ -11,7 +12,227 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public static Func> Create(MethodInfo method) { - throw new NotImplementedException(); + return new Executor() + { + Method = method, + }.Execute; + } + + private class Executor + { + public MethodInfo Method { get; set; } + + public async Task Execute(Page page, object model) + { + var handler = HandlerMethod.Create(Method); + + var receiver = Method.DeclaringType.IsAssignableFrom(page.GetType()) ? page : model; + + var arguments = new object[handler.Parameters.Length]; + for (var i = 0; i < handler.Parameters.Length; i++) + { + var parameter = handler.Parameters[i]; + arguments[i] = await page.Binder.BindModelAsync( + page.PageContext, + parameter.Type, + parameter.DefaultValue, + parameter.Name); + } + + var result = await handler.Execute(receiver, arguments); + return result; + } + } + + private class HandlerParameter + { + public string Name { get; set; } + + public Type Type { get; set; } + + public object DefaultValue { get; set; } + } + + private abstract class HandlerMethod + { + public static HandlerMethod Create(MethodInfo method) + { + var methodParameters = method.GetParameters(); + var parameters = new HandlerParameter[methodParameters.Length]; + + for (var i = 0; i < methodParameters.Length; i++) + { + parameters[i] = new HandlerParameter() + { + DefaultValue = methodParameters[i].HasDefaultValue ? methodParameters[i].DefaultValue : null, + Name = methodParameters[i].Name, + Type = methodParameters[i].ParameterType, + }; + } + + if (method.ReturnType == typeof(Task)) + { + return new NonGenericTaskHandlerMethod(parameters, method); + } + else if (method.ReturnType == typeof(void)) + { + return new VoidHandlerMethod(parameters, method); + } + else if ( + method.ReturnType.IsConstructedGenericType && + method.ReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) && + typeof(IActionResult).IsAssignableFrom(method.ReturnType.GetTypeInfo().GetGenericArguments()[0])) + { + return new GenericTaskHandlerMethod(parameters, method); + } + else if (typeof(IActionResult).IsAssignableFrom(method.ReturnType)) + { + return new ActionResultHandlerMethod(parameters, method); + } + else + { + throw new InvalidOperationException("unsupported handler method return type"); + } + } + + protected static Expression[] Unpack(Expression arguments, HandlerParameter[] parameters) + { + var unpackExpressions = new Expression[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + unpackExpressions[i] = Expression.Convert(Expression.ArrayIndex(arguments, Expression.Constant(i)), parameters[i].Type); + } + + return unpackExpressions; + } + + protected HandlerMethod(HandlerParameter[] parameters) + { + Parameters = parameters; + } + + public HandlerParameter[] Parameters { get; } + + public abstract Task Execute(object receiver, object[] arguments); + } + + private class NonGenericTaskHandlerMethod : HandlerMethod + { + private readonly Func _thunk; + + public NonGenericTaskHandlerMethod(HandlerParameter[] parameters, MethodInfo method) + : base(parameters) + { + var receiver = Expression.Parameter(typeof(object), "receiver"); + var arguments = Expression.Parameter(typeof(object[]), "arguments"); + + _thunk = Expression.Lambda>( + Expression.Call( + Expression.Convert(receiver, method.DeclaringType), + method, + Unpack(arguments, parameters)), + receiver, + arguments).Compile(); + } + + public override async Task Execute(object receiver, object[] arguments) + { + await _thunk(receiver, arguments); + return null; + } + } + + private class GenericTaskHandlerMethod : HandlerMethod + { + private static readonly MethodInfo ConvertMethod = typeof(GenericTaskHandlerMethod).GetMethod( + nameof(Convert), + BindingFlags.NonPublic | BindingFlags.Static); + + private readonly Func> _thunk; + + public GenericTaskHandlerMethod(HandlerParameter[] parameters, MethodInfo method) + : base(parameters) + { + var receiver = Expression.Parameter(typeof(object), "receiver"); + var arguments = Expression.Parameter(typeof(object[]), "arguments"); + + _thunk = Expression.Lambda>>( + Expression.Call( + ConvertMethod.MakeGenericMethod(method.ReturnType.GenericTypeArguments), + Expression.Convert( + Expression.Call( + Expression.Convert(receiver, method.DeclaringType), + method, + Unpack(arguments, parameters)), + typeof(object))), + receiver, + arguments).Compile(); + } + + public override async Task Execute(object receiver, object[] arguments) + { + var result = await _thunk(receiver, arguments); + return (IActionResult)result; + } + + private static async Task Convert(object taskAsObject) + { + var task = (Task)taskAsObject; + return (object)await task; + } + } + + private class VoidHandlerMethod : HandlerMethod + { + private readonly Action _thunk; + + public VoidHandlerMethod(HandlerParameter[] parameters, MethodInfo method) + : base(parameters) + { + var receiver = Expression.Parameter(typeof(object), "receiver"); + var arguments = Expression.Parameter(typeof(object[]), "arguments"); + + _thunk = Expression.Lambda>( + Expression.Call( + Expression.Convert(receiver, method.DeclaringType), + method, + Unpack(arguments, parameters)), + receiver, + arguments).Compile(); + } + + public override Task Execute(object receiver, object[] arguments) + { + _thunk(receiver, arguments); + return Task.FromResult(null); + } + } + + private class ActionResultHandlerMethod : HandlerMethod + { + private readonly Func _thunk; + + public ActionResultHandlerMethod(HandlerParameter[] parameters, MethodInfo method) + : base(parameters) + { + var receiver = Expression.Parameter(typeof(object), "receiver"); + var arguments = Expression.Parameter(typeof(object[]), "arguments"); + + _thunk = Expression.Lambda>( + Expression.Convert( + Expression.Call( + Expression.Convert(receiver, method.DeclaringType), + method, + Unpack(arguments, parameters)), + typeof(IActionResult)), + receiver, + arguments).Compile(); + } + + public override Task Execute(object receiver, object[] arguments) + { + return Task.FromResult(_thunk(receiver, arguments)); + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs index 1683f61130..15f39db944 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvoker.cs @@ -304,6 +304,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var actionDescriptor = _pageContext.ActionDescriptor; _page = (Page)CacheEntry.PageFactory(_pageContext); _pageContext.Page = _page; + _pageContext.ValueProviderFactories = _valueProviderFactories; IRazorPage[] pageStarts; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 49a479077f..b0ccfb16fa 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -48,9 +48,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal IRazorPageFactoryProvider razorPageFactoryProvider, IActionDescriptorCollectionProvider collectionProvider, IEnumerable filterProviders, - IEnumerable valueProviderFactories, IModelMetadataProvider modelMetadataProvider, ITempDataDictionaryFactory tempDataFactory, + IOptions mvcOptions, IOptions htmlHelperOptions, IPageHandlerMethodSelector selector, RazorProject razorProject, @@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _razorPageFactoryProvider = razorPageFactoryProvider; _collectionProvider = collectionProvider; _filterProviders = filterProviders.ToArray(); - _valueProviderFactories = valueProviderFactories.ToArray(); + _valueProviderFactories = mvcOptions.Value.ValueProviderFactories.ToArray(); _modelMetadataProvider = modelMetadataProvider; _tempDataFactory = tempDataFactory; _htmlHelperOptions = htmlHelperOptions.Value; @@ -180,6 +180,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { modelFactory = _modelFactoryProvider.CreateModelFactory(compiledActionDescriptor); modelReleaser = _modelFactoryProvider.CreateModelDisposer(compiledActionDescriptor); + + if (modelType != compiledType) + { + // If the model and page type are different discover handler methods on the model as well. + PopulateHandlerMethodDescriptors(modelType, compiledActionDescriptor); + } } var pageStartFactories = GetPageStartFactories(compiledActionDescriptor); @@ -210,6 +216,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return pageStartFactories; } + private static void PopulateHandlerMethodDescriptors(TypeInfo type, CompiledPageActionDescriptor actionDescriptor) + { + var methods = type.GetMethods(); + for (var i = 0; i < methods.Length; i++) + { + var method = methods[i]; + if (method.Name.StartsWith("OnGet", StringComparison.Ordinal) || + method.Name.StartsWith("OnPost", StringComparison.Ordinal)) + { + actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor() + { + Method = method, + }); + } + } + } + private class InnerCache { public InnerCache(int version) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs index bc926861a0..5514035d79 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Page.cs @@ -9,6 +9,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; @@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public abstract class Page : RazorPageBase, IRazorPage { private IUrlHelper _urlHelper; + private PageArgumentBinder _binder; /// public IHtmlContent BodyContent { get; set; } @@ -61,6 +63,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages [RazorInject] public HtmlEncoder HtmlEncoder { get; set; } + public PageArgumentBinder Binder + { + get + { + if (_binder == null) + { + _binder = PageContext.HttpContext.RequestServices.GetRequiredService(); + } + + return _binder; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _binder = value; + } + } + protected override HtmlEncoder Encoder => HtmlEncoder; protected override TextWriter Writer => ViewContext.Writer; diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs index 576d62092e..e45a6e94e1 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContext.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -19,6 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages { private CompiledPageActionDescriptor _actionDescriptor; private Page _page; + private IList _valueProviderFactories; /// /// Creates an empty . @@ -80,5 +82,30 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// Gets or sets the applicable _PageStart instances. /// public IReadOnlyList PageStarts { get; set; } + + /// + /// Gets or sets the list of instances for the current request. + /// + public virtual IList ValueProviderFactories + { + get + { + if (_valueProviderFactories == null) + { + _valueProviderFactories = new List(); + } + + return _valueProviderFactories; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _valueProviderFactories = value; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs new file mode 100644 index 0000000000..5688c20d9a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageModel.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + public abstract class PageModel + { + private PageArgumentBinder _binder; + + public PageArgumentBinder Binder + { + get + { + if (_binder == null) + { + _binder = PageContext.HttpContext.RequestServices.GetRequiredService(); + } + + return _binder; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _binder = value; + } + } + + public Page Page => PageContext.Page; + + [PageContext] + public PageContext PageContext { get; set; } + + public ModelStateDictionary ModelState => PageContext.ModelState; + + public ViewDataDictionary ViewData => PageContext?.ViewData; + + protected IActionResult Redirect(string url) + { + return new RedirectResult(url); + } + + protected IActionResult View() + { + return new PageViewResult(Page); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs index 91b75d9a6e..7d2b91048b 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -242,8 +242,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var options = new MvcOptions(); options.Filters.Add(globalFilter); var convention = new Mock(); - convention.Setup(c => c.Apply(It.IsAny())) - .Callback((PageModel model) => + convention.Setup(c => c.Apply(It.IsAny())) + .Callback((PageApplicationModel model) => { model.Filters.Add(localFilter); }); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index e2482230d1..1616c1112d 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -278,9 +278,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal razorPageFactoryProvider ?? Mock.Of(), actionDescriptorProvider, new IFilterProvider[0], - new IValueProviderFactory[0], new EmptyModelMetadataProvider(), tempDataFactory.Object, + new TestOptionsManager(), new TestOptionsManager(), Mock.Of(), razorProject,