Add DeclaredModelType to Razor pages

* This allows razor pages to override their model type with a model that
  extends the declared model type through the page application model.
This commit is contained in:
Javier Calvarro Nelson 2018-01-24 13:48:26 -08:00
parent b30020a655
commit 7127bb5dbb
20 changed files with 427 additions and 43 deletions

View File

@ -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<object> on the current type.
var viewDataDictionaryModelType = modelType ?? typeof(object);
var viewDataDictionaryModelType = declaredModelType ?? typeof(object);
if (viewDataDictionaryModelType != null)
{

View File

@ -23,13 +23,26 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
PageActionDescriptor actionDescriptor,
TypeInfo handlerType,
IReadOnlyList<object> handlerAttributes)
: this(actionDescriptor, handlerType, handlerType, handlerAttributes)
{
}
/// <summary>
/// Initializes a new instance of <see cref="PageApplicationModel"/>.
/// </summary>
public PageApplicationModel(
PageActionDescriptor actionDescriptor,
TypeInfo declaredModelType,
TypeInfo handlerType,
IReadOnlyList<object> handlerAttributes)
{
ActionDescriptor = actionDescriptor ?? throw new ArgumentNullException(nameof(actionDescriptor));
DeclaredModelType = declaredModelType;
HandlerType = handlerType;
Filters = new List<IFilterMetadata>();
Properties = new CopyOnWriteDictionary<object, object>(
actionDescriptor.Properties,
actionDescriptor.Properties,
EqualityComparer<object>.Default);
HandlerMethods = new List<PageHandlerModel>();
HandlerProperties = new List<PagePropertyModel>();
@ -56,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
Properties = new Dictionary<object, object>(other.Properties);
HandlerMethods = new List<PageHandlerModel>(other.HandlerMethods.Select(m => new PageHandlerModel(m)));
HandlerProperties = new List<PagePropertyModel>(other.HandlerProperties.Select(p => new PagePropertyModel(p)));
HandlerProperties = new List<PagePropertyModel>(other.HandlerProperties.Select(p => new PagePropertyModel(p)));
HandlerTypeAttributes = other.HandlerTypeAttributes;
}
@ -109,7 +122,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public TypeInfo PageType { get; set; }
/// <summary>
/// Gets or sets the <see cref="TypeInfo"/> of the Razor page model.
/// Gets the declared model <see cref="TypeInfo"/> of the model for the page.
/// Typically this <see cref="TypeInfo"/> will be the type specified by the @model directive
/// in the razor page.
/// </summary>
public TypeInfo DeclaredModelType { get; }
/// <summary>
/// Gets or sets the runtime model <see cref="TypeInfo"/> of the model for the razor page.
/// This is the <see cref="TypeInfo"/> that will be used at runtime to instantiate and populate
/// the model property of the page.
/// </summary>
public TypeInfo ModelType { get; set; }

View File

@ -42,7 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
public TypeInfo HandlerTypeInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="TypeInfo"/> of the model.
/// Gets or sets the declared model <see cref="TypeInfo"/> of the model for the page.
/// Typically this <see cref="TypeInfo"/> will be the type specified by the @model directive
/// in the razor page.
/// </summary>
public TypeInfo DeclaredModelTypeInfo { get; set; }
/// <summary>
/// Gets or sets the runtime model <see cref="TypeInfo"/> of the model for the razor page.
/// This is the <see cref="TypeInfo"/> that will be used at runtime to instantiate and populate
/// the model property of the page.
/// </summary>
public TypeInfo ModelTypeInfo { get; set; }

View File

@ -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);

View File

@ -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();
}
}
}
}

View File

@ -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)
{

View File

@ -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);

View File

@ -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);
/// <summary>
/// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.
/// </summary>
internal static string PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable
{
get => GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable");
}
/// <summary>
/// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.
/// </summary>
internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"), p0);
/// <summary>
/// The '{0}' property of '{1}' must not be null.
/// </summary>
@ -178,6 +164,20 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
internal static string FormatInvalidValidPageName(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValidPageName"), p0);
/// <summary>
/// The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'.
/// </summary>
internal static string InvalidActionDescriptorModelType
{
get => GetString("InvalidActionDescriptorModelType");
}
/// <summary>
/// The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'.
/// </summary>
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);

View File

@ -150,4 +150,7 @@
<data name="InvalidValidPageName" xml:space="preserve">
<value>'{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".</value>
</data>
<data name="InvalidActionDescriptorModelType" xml:space="preserve">
<value>The model type for '{0}' is of type '{1}' which is not assignable to its declared model type '{2}'.</value>
</data>
</root>

View File

@ -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 =
@"<a href=""/Accounts/Manage/RenderPartials"">Link inside area</a>
<a href=""/Products/List/old/20"">Link to external area</a>
<a href=""/Accounts"">Link to area action</a>
@ -336,7 +337,7 @@ Hello from page";
public async Task PagesInAreas_CanGenerateRelativeLinks()
{
// Arrange
var expected =
var expected =
@"<a href=""/Accounts/PageWithRouteTemplate/1"">Parent directory</a>
<a href=""/Accounts/Manage/RenderPartials"">Sibling directory</a>
<a href=""/Products/List"">Go back to root of different area</a>";
@ -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<TUser> 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("<h2>User</h2>", 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<string, string>
{
["__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<string, string>
{
["__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());
}
}
}

View File

@ -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<TestModel>>(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<object>(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<TestModel>(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<TestModel>(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<object>(modelMetadataProvider, new ModelStateDictionary());
@ -193,5 +222,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
private class TestModel
{
}
private class DerivedTestModel : TestModel
{
}
}
}

View File

@ -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<ViewDataTestPage>(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; }

View File

@ -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<IActionConstraintMetadata>(),
AttributeRouteInfo = new AttributeRouteInfo(),
FilterDescriptors = new List<FilterDescriptor>(),
RelativePath = "/Foo",
RouteValues = new Dictionary<string, string>(),
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<IFilterMetadata>(),
Mock.Of<IFilterMetadata>(),
},
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<InvalidOperationException>(() =>
CompiledPageActionDescriptorBuilder.Build(pageApplicationModel, globalFilters));
}
[Fact]
public void CreateDescriptor_AddsGlobalFiltersWithTheRightScope()
{

View File

@ -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);
}

View File

@ -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<PageContext, ViewContext, object> factory = (a, b) => null;
@ -102,7 +102,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var loader = new Mock<IPageLoader>();
loader
.Setup(l => l.Load(It.IsAny<PageActionDescriptor>()))
.Returns(CreateCompiledPageActionDescriptor(descriptor, pageType: typeof(PageWithModel)));
.Returns(CreateCompiledPageActionDescriptor(
descriptor,
pageType: typeof(PageWithModel),
modelType: typeof(DerivedTestPageModel)));
var pageFactoryProvider = new Mock<IPageFactoryProvider>();
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<FilterDescriptor>(),
@ -522,5 +532,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
}
}
private class DerivedTestPageModel : TestPageModel
{
}
}
}

View File

@ -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<User>).GetTypeInfo();
}
}
}
}

View File

@ -0,0 +1,36 @@
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using RazorPagesWebSite
@model CustomModelTypeModel
@{
ViewData["Title"] = "Register";
}
<h2>@ViewData["Title"]</h2>
<h2>@ViewData["UserType"]</h2>
<div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
</form>
</div>
</div>

View File

@ -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<TUser> : CustomModelTypeModel where TUser : User
{
private readonly ILogger<CustomModelTypeModel<TUser>> _logger;
public CustomModelTypeModel(ILogger<CustomModelTypeModel<TUser>> 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("~/");
}
}
}

View File

@ -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();
}

View File

@ -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());
});
}