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:
parent
b30020a655
commit
7127bb5dbb
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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("~/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue