diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs index e3958d36ce..b163088361 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorPagePropertyActivator.cs @@ -24,14 +24,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal public RazorPagePropertyActivator( Type pageType, - Type modelType, + Type declaredModelType, IModelMetadataProvider metadataProvider, PropertyValueAccessors propertyValueAccessors) { _metadataProvider = metadataProvider; // In the absence of a model on the current type, we'll attempt to use ViewDataDictionary on the current type. - var viewDataDictionaryModelType = modelType ?? typeof(object); + var viewDataDictionaryModelType = declaredModelType ?? typeof(object); if (viewDataDictionaryModelType != null) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs index bdf0654dc0..5077e26a27 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs @@ -23,13 +23,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels PageActionDescriptor actionDescriptor, TypeInfo handlerType, IReadOnlyList handlerAttributes) + : this(actionDescriptor, handlerType, handlerType, handlerAttributes) + { + } + + /// + /// Initializes a new instance of . + /// + public PageApplicationModel( + PageActionDescriptor actionDescriptor, + TypeInfo declaredModelType, + TypeInfo handlerType, + IReadOnlyList handlerAttributes) { ActionDescriptor = actionDescriptor ?? throw new ArgumentNullException(nameof(actionDescriptor)); + DeclaredModelType = declaredModelType; HandlerType = handlerType; Filters = new List(); Properties = new CopyOnWriteDictionary( - actionDescriptor.Properties, + actionDescriptor.Properties, EqualityComparer.Default); HandlerMethods = new List(); HandlerProperties = new List(); @@ -56,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Properties = new Dictionary(other.Properties); HandlerMethods = new List(other.HandlerMethods.Select(m => new PageHandlerModel(m))); - HandlerProperties = new List(other.HandlerProperties.Select(p => new PagePropertyModel(p))); + HandlerProperties = new List(other.HandlerProperties.Select(p => new PagePropertyModel(p))); HandlerTypeAttributes = other.HandlerTypeAttributes; } @@ -109,7 +122,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public TypeInfo PageType { get; set; } /// - /// Gets or sets the of the Razor page model. + /// Gets the declared model of the model for the page. + /// Typically this will be the type specified by the @model directive + /// in the razor page. + /// + public TypeInfo DeclaredModelType { get; } + + /// + /// Gets or sets the runtime model of the model for the razor page. + /// This is the that will be used at runtime to instantiate and populate + /// the model property of the page. /// public TypeInfo ModelType { get; set; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs index 8eecaa6772..cd81ea8888 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/CompiledPageActionDescriptor.cs @@ -42,7 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages public TypeInfo HandlerTypeInfo { get; set; } /// - /// Gets or sets the of the model. + /// Gets or sets the declared model of the model for the page. + /// Typically this will be the type specified by the @model directive + /// in the razor page. + /// + public TypeInfo DeclaredModelTypeInfo { get; set; } + + /// + /// Gets or sets the runtime model of the model for the razor page. + /// This is the that will be used at runtime to instantiate and populate + /// the model property of the page. /// public TypeInfo ModelTypeInfo { get; set; } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs index ed068bfd8c..709921ab28 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactoryProvider.cs @@ -50,10 +50,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } var activatorFactory = _pageActivator.CreateActivator(actionDescriptor); - var modelType = actionDescriptor.ModelTypeInfo?.AsType() ?? actionDescriptor.PageTypeInfo.AsType(); + var declaredModelType = actionDescriptor.DeclaredModelTypeInfo?.AsType() ?? actionDescriptor.PageTypeInfo.AsType(); var propertyActivator = new RazorPagePropertyActivator( actionDescriptor.PageTypeInfo.AsType(), - modelType, + declaredModelType, _modelMetadataProvider, _propertyAccessors); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs index 8fec2fd4a8..29f7214448 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageActionDescriptorBuilder.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; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -31,6 +32,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal .ToArray(); var handlerMethods = CreateHandlerMethods(applicationModel); + if (applicationModel.ModelType != null && applicationModel.DeclaredModelType != null && + !applicationModel.DeclaredModelType.IsAssignableFrom(applicationModel.ModelType)) + { + var message = Resources.FormatInvalidActionDescriptorModelType( + applicationModel.ActionDescriptor.DisplayName, + applicationModel.ModelType.Name, + applicationModel.DeclaredModelType.Name); + + throw new InvalidOperationException(message); + } + var actionDescriptor = applicationModel.ActionDescriptor; return new CompiledPageActionDescriptor(actionDescriptor) { @@ -40,6 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal FilterDescriptors = filters, HandlerMethods = handlerMethods, HandlerTypeInfo = applicationModel.HandlerType, + DeclaredModelTypeInfo = applicationModel.DeclaredModelType, ModelTypeInfo = applicationModel.ModelType, RouteValues = actionDescriptor.RouteValues, PageTypeInfo = applicationModel.PageType, @@ -120,4 +133,4 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return results.ToArray(); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs index de7e25ddb5..0db3f1e45d 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageApplicationModelProvider.cs @@ -75,6 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } var modelTypeInfo = modelProperty.PropertyType.GetTypeInfo(); + var declaredModelType = modelTypeInfo; // Now we want figure out which type is the handler type. TypeInfo handlerType; @@ -90,6 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var handlerTypeAttributes = handlerType.GetCustomAttributes(inherit: true); var pageModel = new PageApplicationModel( actionDescriptor, + declaredModelType, handlerType, handlerTypeAttributes) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index ab0afacf9d..3c4917abf2 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { var compiledActionDescriptor = (CompiledPageActionDescriptor)context.ActionContext.ActionDescriptor; - var viewDataFactory = ViewDataDictionaryFactory.CreateFactory(compiledActionDescriptor.ModelTypeInfo); + var viewDataFactory = ViewDataDictionaryFactory.CreateFactory(compiledActionDescriptor.DeclaredModelTypeInfo); var pageFactory = _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor); var pageDisposer = _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index c2cd73be9f..eec543945b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -10,20 +10,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNetCore.Mvc.RazorPages.Resources", typeof(Resources).GetTypeInfo().Assembly); - /// - /// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. - /// - internal static string PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable - { - get => GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"); - } - - /// - /// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page. - /// - internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0) - => string.Format(CultureInfo.CurrentCulture, GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"), p0); - /// /// The '{0}' property of '{1}' must not be null. /// @@ -178,6 +164,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages internal static string FormatInvalidValidPageName(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("InvalidValidPageName"), p0); + /// + /// The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'. + /// + internal static string InvalidActionDescriptorModelType + { + get => GetString("InvalidActionDescriptorModelType"); + } + + /// + /// The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'. + /// + internal static string FormatInvalidActionDescriptorModelType(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidActionDescriptorModelType"), p0, p1, p2); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index 90e0ea9300..2d9e11cd41 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -150,4 +150,7 @@ '{0}' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g "/Users/Edit". + + The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index b7fa5fa9ce..6868c14512 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.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.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -114,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuthFolder", response.Headers.Location.PathAndQuery); } - [Fact] + [Fact] public async Task AuthConvention_AppliedToFolders_CanByOverridenByFiltersOnModel() { // Act @@ -319,7 +320,7 @@ Hello from page"; public async Task PagesInAreas_CanGenerateLinksToControllersAndPages() { // Arrange - var expected = + var expected = @"Link inside area Link to external area Link to area action @@ -336,7 +337,7 @@ Hello from page"; public async Task PagesInAreas_CanGenerateRelativeLinks() { // Arrange - var expected = + var expected = @"Parent directory Sibling directory Go back to root of different area"; @@ -352,7 +353,7 @@ Hello from page"; public async Task PagesInAreas_CanDiscoverViewsFromAreaAndSharedDirectories() { // Arrange - var expected = + var expected = @"Layout in /Views/Shared Partial in /Areas/Accounts/Pages/Manage/ @@ -391,5 +392,76 @@ Hello from /Pages/Shared/"; // Assert Assert.Equal("Hello from AllowAnonymous", response.Trim()); } + + // These test is important as it covers a feature that allows razor pages to use a different + // model at runtime that wasn't known at compile time. Like a non-generic model used at compile + // time and overrided at runtime with a closed-generic model that performs the actual implementation. + // An example of this is how the Identity UI library defines a base page model in their views, + // like how the Register.cshtml view defines its model as RegisterModel and then, at runtime it replaces + // that model with RegisterModel where TUser is the type of the user used to configure identity. + [Fact] + public async Task PageConventions_CanBeUsedToCustomizeTheModelType() + { + // Act + var response = await Client.GetAsync("/CustomModelTypeModel"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("

User

", content); + } + + [Fact] + public async Task PageConventions_CustomizedModelCanPostToHandlers() + { + // Arrange + var getPage = await Client.GetAsync("/CustomModelTypeModel"); + var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); + var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); + + var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); + message.Content = new FormUrlEncodedContent(new Dictionary + { + ["__RequestVerificationToken"] = token, + ["ConfirmPassword"] = "", + ["Password"] = "", + ["Email"] = "" + }); + message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("is required.", content); + } + + [Fact] + public async Task PageConventions_CustomizedModelCanWorkWithModelState() + { + // Arrange + var getPage = await Client.GetAsync("/CustomModelTypeModel"); + var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); + var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); + + var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); + message.Content = new FormUrlEncodedContent(new Dictionary + { + ["__RequestVerificationToken"] = token, + ["Email"] = "javi@example.com", + ["Password"] = "Password.12$", + ["ConfirmPassword"] = "Password.12$", + }); + message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/", response.Headers.Location.ToString()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs index 779fc7df26..c0221fc68d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorPagePropertyActivatorTest.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal // Arrange var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: null, + declaredModelType: null, metadataProvider: new TestModelMetadataProvider(), propertyValueAccessors: null); var viewContext = new ViewContext(); @@ -55,7 +55,36 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var modelMetadataProvider = new TestModelMetadataProvider(); var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: typeof(TestModel), + declaredModelType: typeof(TestModel), + metadataProvider: modelMetadataProvider, + propertyValueAccessors: null); + var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) + { + { "test-key", "test-value" }, + }; + var viewContext = new ViewContext + { + ViewData = original, + }; + + // Act + var viewDataDictionary = activator.CreateViewDataDictionary(viewContext); + + // Assert + Assert.NotNull(viewDataDictionary); + Assert.NotSame(original, viewDataDictionary); + Assert.IsType>(viewDataDictionary); + Assert.Equal("test-value", viewDataDictionary["test-key"]); + } + + [Fact] + public void CreateViewDataDictionary_UsesDeclaredTypeOverModelType_WhenCreatingTheViewDataDictionary() + { + // Arrange + var modelMetadataProvider = new TestModelMetadataProvider(); + var activator = new RazorPagePropertyActivator( + typeof(TestPage), + declaredModelType: typeof(TestModel), metadataProvider: modelMetadataProvider, propertyValueAccessors: null); var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) @@ -84,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var modelMetadataProvider = new TestModelMetadataProvider(); var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: typeof(TestModel), + declaredModelType: typeof(TestModel), metadataProvider: modelMetadataProvider, propertyValueAccessors: null); var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) @@ -113,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var modelMetadataProvider = new TestModelMetadataProvider(); var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: null, + declaredModelType: null, metadataProvider: modelMetadataProvider, propertyValueAccessors: null); var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) @@ -142,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var modelMetadataProvider = new TestModelMetadataProvider(); var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: typeof(TestModel), + declaredModelType: typeof(TestModel), metadataProvider: modelMetadataProvider, propertyValueAccessors: null); var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()) @@ -169,7 +198,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var modelMetadataProvider = new TestModelMetadataProvider(); var activator = new RazorPagePropertyActivator( typeof(TestPage), - modelType: null, + declaredModelType: null, metadataProvider: modelMetadataProvider, propertyValueAccessors: null); var original = new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()); @@ -193,5 +222,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private class TestModel { } + + private class DerivedTestModel : TestModel + { + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs index cc6a78e72d..6b337a93eb 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryProviderTest.cs @@ -110,6 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var descriptor = new CompiledPageActionDescriptor { PageTypeInfo = typeof(ViewDataTestPage).GetTypeInfo(), + DeclaredModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo(), ModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo() }; descriptor.RelativePath = "/this/is/a/path.cshtml"; @@ -139,6 +140,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure ActionDescriptor = new CompiledPageActionDescriptor { PageTypeInfo = typeof(ViewDataTestPage).GetTypeInfo(), + DeclaredModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo(), ModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo(), }, }; @@ -156,6 +158,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.NotNull(testPage.ViewData); } + [Fact] + public void PageFactorySetViewDataWithDeclaredModelTypeWhenNotNull() + { + // Arrange + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(ViewDataTestPage).GetTypeInfo(), + DeclaredModelTypeInfo = typeof(ViewDataTestPageModel).GetTypeInfo(), + ModelTypeInfo = typeof(DerivedViewDataTestPageModel).GetTypeInfo(), + }, + }; + + var viewContext = new ViewContext(); + + var factoryProvider = CreatePageFactory(); + + // Act + var factory = factoryProvider.CreatePageFactory(pageContext.ActionDescriptor); + var instance = factory(pageContext, viewContext); + + // Assert + var testPage = Assert.IsType(instance); + Assert.NotNull(testPage.ViewData); + } + [Fact] public void PageFactorySetsNonGenericViewDataDictionary() { @@ -334,6 +363,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { } + private class DerivedViewDataTestPageModel : ViewDataTestPageModel + { + } + + private class PropertiesWithoutRazorInject : Page { public IModelExpressionProvider ModelExpressionProviderWithoutInject { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs index e6b45a8c84..2d08aee3d8 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageActionDescriptorBuilderTest.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; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -58,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ViewEnginePath = "/Pages/Foo", }; var handlerTypeInfo = typeof(TestModel).GetTypeInfo(); - var pageApplicationModel = new PageApplicationModel(actionDescriptor, handlerTypeInfo, new object[0]) + var pageApplicationModel = new PageApplicationModel(actionDescriptor, typeof(TestModel).GetTypeInfo(), handlerTypeInfo, new object[0]) { PageType = typeof(TestPage).GetTypeInfo(), ModelType = typeof(TestModel).GetTypeInfo(), @@ -86,6 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal // Assert Assert.Same(pageApplicationModel.PageType, actual.PageTypeInfo); + Assert.Same(pageApplicationModel.DeclaredModelType, actual.DeclaredModelTypeInfo); Assert.Same(pageApplicationModel.ModelType, actual.ModelTypeInfo); Assert.Same(pageApplicationModel.HandlerType, actual.HandlerTypeInfo); Assert.Same(pageApplicationModel.Properties, actual.Properties); @@ -94,6 +96,48 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(pageApplicationModel.HandlerProperties.Select(p => p.PropertyName), actual.BoundProperties.Select(p => p.Name)); } + [Fact] + public void CreateDescriptor_ThrowsIfModelIsNotCompatibleWithDeclaredModel() + { + // 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, typeof(TestModel).GetTypeInfo(), handlerTypeInfo, new object[0]) + { + PageType = typeof(TestPage).GetTypeInfo(), + ModelType = typeof(string).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(), + }, + } + }; + var globalFilters = new FilterCollection(); + + // Act & Assert + var actual = Assert.Throws(() => + CompiledPageActionDescriptorBuilder.Build(pageApplicationModel, globalFilters)); + } + [Fact] public void CreateDescriptor_AddsGlobalFiltersWithTheRightScope() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs index afc0c41b1a..196cf7f2eb 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageApplicationModelProviderTest.cs @@ -470,8 +470,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageModel = context.PageApplicationModel; Assert.Empty(pageModel.HandlerProperties.Where(p => p.BindingInfo != null)); Assert.Empty(pageModel.HandlerMethods); - Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.HandlerType); + Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.DeclaredModelType); Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.ModelType); + Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), pageModel.HandlerType); Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), pageModel.PageType); } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 1f8eb972e1..02e3086e98 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var descriptor = new PageActionDescriptor { RelativePath = "Path1", - FilterDescriptors = new FilterDescriptor[0], + FilterDescriptors = new FilterDescriptor[0] }; Func factory = (a, b) => null; @@ -102,7 +102,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var loader = new Mock(); loader .Setup(l => l.Load(It.IsAny())) - .Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel))); + .Returns(CreateCompiledPageActionDescriptor( + descriptor, + pageType: typeof(PageWithModel), + modelType: typeof(DerivedTestPageModel))); var pageFactoryProvider = new Mock(); pageFactoryProvider @@ -322,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void GetViewStartFactories_FindsFullHeirarchy() + public void GetViewStartFactories_FindsFullHierarchy() { // Arrange @@ -437,20 +440,27 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private static CompiledPageActionDescriptor CreateCompiledPageActionDescriptor( PageActionDescriptor descriptor, - Type pageType = null) + Type pageType = null, + Type modelType = null) { pageType = pageType ?? typeof(object); var pageTypeInfo = pageType.GetTypeInfo(); - TypeInfo modelTypeInfo = null; + var modelTypeInfo = modelType?.GetTypeInfo(); + TypeInfo declaredModelTypeInfo = null; if (pageType != null) { - modelTypeInfo = pageTypeInfo.GetProperty("Model")?.PropertyType.GetTypeInfo(); + declaredModelTypeInfo = pageTypeInfo.GetProperty("Model")?.PropertyType.GetTypeInfo(); + if (modelTypeInfo == null) + { + modelTypeInfo = declaredModelTypeInfo; + } } return new CompiledPageActionDescriptor(descriptor) { HandlerTypeInfo = modelTypeInfo ?? pageTypeInfo, + DeclaredModelTypeInfo = declaredModelTypeInfo ?? pageTypeInfo, ModelTypeInfo = modelTypeInfo ?? pageTypeInfo, PageTypeInfo = pageTypeInfo, FilterDescriptors = Array.Empty(), @@ -522,5 +532,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { } } + + private class DerivedTestPageModel : TestPageModel + { + } } } diff --git a/test/WebSites/RazorPagesWebSite/Conventions/CustomModelTypeConvention.cs b/test/WebSites/RazorPagesWebSite/Conventions/CustomModelTypeConvention.cs new file mode 100644 index 0000000000..c37ce99a8d --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Conventions/CustomModelTypeConvention.cs @@ -0,0 +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.ApplicationModels; +using System.Reflection; + +namespace RazorPagesWebSite.Conventions +{ + internal class CustomModelTypeConvention : IPageApplicationModelConvention + { + public void Apply(PageApplicationModel model) + { + if (model.ModelType == typeof(CustomModelTypeModel)) + { + model.ModelType = typeof(CustomModelTypeModel).GetTypeInfo(); + } + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml new file mode 100644 index 0000000000..46819452da --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml @@ -0,0 +1,36 @@ +@page +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using RazorPagesWebSite +@model CustomModelTypeModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

@ViewData["UserType"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
diff --git a/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs new file mode 100644 index 0000000000..e32a2e83c4 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/CustomModelTypeModel.cshtml.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace RazorPagesWebSite +{ + public class CustomModelTypeModel : PageModel + { + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public virtual void OnGet(string returnUrl = null) + { + throw new NotImplementedException(); + } + + public virtual IActionResult OnPostAsync(string returnUrl = null) + { + throw new NotImplementedException(); + } + } + + public class User + { + } + + internal class CustomModelTypeModel : CustomModelTypeModel where TUser : User + { + private readonly ILogger> _logger; + + public CustomModelTypeModel(ILogger> logger) + { + _logger = logger; + } + + public override void OnGet(string returnUrl = null) + { + // We only care about being able to resolve the service from DI. + // The line below is just to make the compiler happy. + _logger.LogInformation(typeof(TUser).Name); + ViewData["UserType"] = typeof(TUser).Name; + ReturnUrl = returnUrl; + } + + public override IActionResult OnPostAsync(string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + return Redirect("~/"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Startup.cs b/test/WebSites/RazorPagesWebSite/Startup.cs index 718789325d..fe3cecc4e9 100644 --- a/test/WebSites/RazorPagesWebSite/Startup.cs +++ b/test/WebSites/RazorPagesWebSite/Startup.cs @@ -2,9 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Globalization; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using RazorPagesWebSite.Conventions; namespace RazorPagesWebSite { @@ -22,6 +23,7 @@ namespace RazorPagesWebSite options.Conventions.AllowAnonymousToPage("/Pages/Admin/Login"); options.Conventions.AddPageRoute("/HelloWorldWithRoute", "Different-Route/{text}"); options.Conventions.AddPageRoute("/Pages/NotTheRoot", string.Empty); + options.Conventions.Add(new CustomModelTypeConvention()); }) .WithRazorPagesAtContentRoot(); } diff --git a/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs b/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs index 4947f62e91..394710fb3b 100644 --- a/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs +++ b/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; +using RazorPagesWebSite.Conventions; namespace RazorPagesWebSite { @@ -22,6 +23,7 @@ namespace RazorPagesWebSite options.Conventions.AuthorizeFolder("/Conventions/AuthFolder"); options.Conventions.AuthorizeAreaFolder("Accounts", "/RequiresAuth"); options.Conventions.AllowAnonymousToAreaPage("Accounts", "/RequiresAuth/AllowAnonymous"); + options.Conventions.Add(new CustomModelTypeConvention()); }); }