diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionMetadataProvider.cs index bcee213b7d..bd26352bee 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionMetadataProvider.cs @@ -50,6 +50,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal // Property/field access is always legal var memberExpression = (MemberExpression)expression.Body; propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null; + if (string.Equals(propertyName, "Model", StringComparison.Ordinal) && + memberExpression.Type == typeof(TModel) && + memberExpression.Expression.NodeType == ExpressionType.Constant) + { + // Special case the Model property in RazorPage. (m => Model) should behave identically + // to (m => m). But do the more complicated thing for (m => m.Model) since that is a slightly + // different beast.) + return FromModel(viewData, metadataProvider); + } + containerType = memberExpression.Expression.Type; legalExpression = true; break; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html index 722650400f..e3a28d41de 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.Encoded.html @@ -70,7 +70,7 @@
- Male + Male Female
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html index 447f44e8fe..eb6bbd1870 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Order.html @@ -70,7 +70,7 @@
- Male + Male Female
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs index 2396740c06..043237bc9c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -20,6 +21,37 @@ namespace Microsoft.AspNetCore.Mvc.Razor { public class RazorPageCreateModelExpressionTest { + public static TheoryData IdentityExpressions + { + get + { + return new TheoryData, string> + { + // m => m + { page => page.CreateModelExpression1(), string.Empty }, + // m => Model + { page => page.CreateModelExpression2(), string.Empty }, + }; + } + } + + public static TheoryData NotQuiteIdentityExpressions + { + get + { + return new TheoryData, string, Type> + { + // m => m.Model + { page => page.CreateModelExpression1(), "Model", typeof(RecursiveModel) }, + // m => ViewData.Model + { page => page.CreateModelExpression2(), "ViewData.Model", typeof(RecursiveModel) }, + // m => ViewContext.ViewData.Model + // This property has type object because ViewData is not exposed as ViewDataDictionary. + { page => page.CreateModelExpression3(), "ViewContext.ViewData.Model", typeof(object) }, + }; + } + } + public static TheoryData>, string> IntExpressions { get @@ -50,6 +82,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } + [Theory] + [MemberData(nameof(IdentityExpressions))] + public void CreateModelExpression_ReturnsExpectedMetadata_IdentityExpressions( + Func createModelExpression, + string expectedName) + { + // Arrange + var viewContext = CreateViewContext(); + var modelExplorer = viewContext.ViewData.ModelExplorer.GetExplorerForProperty( + nameof(RazorPageCreateModelExpressionModel.Name)); + var viewData = new ViewDataDictionary(viewContext.ViewData) + { + ModelExplorer = modelExplorer, + }; + viewContext.ViewData = viewData; + + var page = CreateIdentityPage(viewContext); + + // Act + var modelExpression = createModelExpression(page); + + // Assert + Assert.NotNull(modelExpression); + Assert.Equal(expectedName, modelExpression.Name); + Assert.Same(modelExplorer, modelExpression.ModelExplorer); + } + + [Theory] + [MemberData(nameof(NotQuiteIdentityExpressions))] + public void CreateModelExpression_ReturnsExpectedMetadata_NotQuiteIdentityExpressions( + Func createModelExpression, + string expectedName, + Type expectedType) + { + // Arrange + var viewContext = CreateViewContext(); + var viewData = new ViewDataDictionary(viewContext.ViewData); + viewContext.ViewData = viewData; + var modelExplorer = viewData.ModelExplorer; + + var page = CreateNotQuiteIdentityPage(viewContext); + + // Act + var modelExpression = createModelExpression(page); + + // Assert + Assert.NotNull(modelExpression); + Assert.Equal(expectedName, modelExpression.Name); + Assert.NotNull(modelExpression.ModelExplorer); + Assert.NotSame(modelExplorer, modelExpression.ModelExplorer); + Assert.NotNull(modelExpression.Metadata); + Assert.Equal(ModelMetadataKind.Property, modelExpression.Metadata.MetadataKind); + Assert.Equal(expectedType, modelExpression.Metadata.ModelType); + } + [Theory] [MemberData(nameof(IntExpressions))] public void CreateModelExpression_ReturnsExpectedMetadata_IntExpressions( @@ -57,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor string expectedName) { // Arrange - var viewContext = CreateViewContext(model: null); + var viewContext = CreateViewContext(); var page = CreatePage(viewContext); // Act @@ -77,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor string expectedName) { // Arrange - var viewContext = CreateViewContext(model: null); + var viewContext = CreateViewContext(); var page = CreatePage(viewContext); // Act @@ -90,6 +177,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor Assert.Equal(expectedName, result.Name); } + private static IdentityRazorPage CreateIdentityPage(ViewContext viewContext) + { + return new IdentityRazorPage + { + ViewContext = viewContext, + ViewData = (ViewDataDictionary)viewContext.ViewData, + }; + } + + public static NotQuiteIdentityRazorPage CreateNotQuiteIdentityPage(ViewContext viewContext) + { + return new NotQuiteIdentityRazorPage + { + ViewContext = viewContext, + ViewData = (ViewDataDictionary)viewContext.ViewData, + }; + } + private static TestRazorPage CreatePage(ViewContext viewContext) { return new TestRazorPage @@ -99,42 +204,68 @@ namespace Microsoft.AspNetCore.Mvc.Razor }; } - private static ViewContext CreateViewContext(RazorPageCreateModelExpressionModel model) + private static ViewContext CreateViewContext() { - return CreateViewContext(model, new TestModelMetadataProvider()); - } + var provider = new TestModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(provider); + serviceCollection.AddSingleton(); - private static ViewContext CreateViewContext( - RazorPageCreateModelExpressionModel model, - IModelMetadataProvider provider) - { - var viewData = new ViewDataDictionary(provider) + var httpContext = new DefaultHttpContext { - Model = model, + RequestServices = serviceCollection.BuildServiceProvider(), }; - - var serviceProvider = new Mock(); - serviceProvider - .Setup(real => real.GetService(typeof(IModelMetadataProvider))) - .Returns(provider); - serviceProvider - .Setup(real => real.GetService(typeof(ExpressionTextCache))) - .Returns(new ExpressionTextCache()); - - var httpContext = new Mock(); - httpContext - .SetupGet(real => real.RequestServices) - .Returns(serviceProvider.Object); - - var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); return new ViewContext( actionContext, - view: Mock.Of(), - viewData: viewData, - tempData: Mock.Of(), - writer: new StringWriter(), - htmlHelperOptions: new HtmlHelperOptions()); + NullView.Instance, + viewData, + Mock.Of(), + new StringWriter(), + new HtmlHelperOptions()); + } + + public class IdentityRazorPage : RazorPage + { + public ModelExpression CreateModelExpression1() + { + return CreateModelExpression(m => m); + } + + public ModelExpression CreateModelExpression2() + { + return CreateModelExpression(m => Model); + } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + public class NotQuiteIdentityRazorPage : RazorPage + { + public ModelExpression CreateModelExpression1() + { + return CreateModelExpression(m => m.Model); + } + + public ModelExpression CreateModelExpression2() + { + return CreateModelExpression(m => ViewData.Model); + } + + public ModelExpression CreateModelExpression3() + { + return CreateModelExpression(m => ViewContext.ViewData.Model); + } + + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } } private class TestRazorPage : RazorPage @@ -145,6 +276,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor } } + public class RecursiveModel + { + public RecursiveModel Model { get; set; } + } + public class RazorPageCreateModelExpressionModel { public int Id { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionMetadataProviderTest.cs index 6c754ee58f..2b912cfa67 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionMetadataProviderTest.cs @@ -1,13 +1,91 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public class ExpressionMetadataProviderTest { + [Fact] + public void FromLambdaExpression_GetsExpectedMetadata_ForIdentityExpression() + { + // Arrange + var provider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + + // Act + var explorer = ExpressionMetadataProvider.FromLambdaExpression(m => m, viewData, provider); + + // Assert + Assert.NotNull(explorer); + Assert.NotNull(explorer.Metadata); + Assert.Equal(ModelMetadataKind.Type, explorer.Metadata.MetadataKind); + Assert.Equal(typeof(TestModel), explorer.ModelType); + Assert.Null(explorer.Model); + } + + [Fact] + public void FromLambdaExpression_GetsExpectedMetadata_ForPropertyExpression() + { + // Arrange + var provider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + + // Act + var explorer = ExpressionMetadataProvider.FromLambdaExpression(m => m.SelectedCategory, viewData, provider); + + // Assert + Assert.NotNull(explorer); + Assert.NotNull(explorer.Metadata); + Assert.Equal(ModelMetadataKind.Property, explorer.Metadata.MetadataKind); + Assert.Equal(typeof(Category), explorer.ModelType); + Assert.Null(explorer.Model); + } + + [Fact] + public void FromLambdaExpression_GetsExpectedMetadata_ForIndexerExpression() + { + // Arrange + var provider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + + // Act + var explorer = ExpressionMetadataProvider.FromLambdaExpression(m => m[23], viewData, provider); + + // Assert + Assert.NotNull(explorer); + Assert.NotNull(explorer.Metadata); + Assert.Equal(ModelMetadataKind.Type, explorer.Metadata.MetadataKind); + Assert.Equal(typeof(TestModel), explorer.ModelType); + Assert.Null(explorer.Model); + } + + [Fact] + public void FromLambdaExpression_GetsExpectedMetadata_ForLongerExpression() + { + // Arrange + var provider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + var index = 42; + + // Act + var explorer = ExpressionMetadataProvider.FromLambdaExpression( + m => m[index].SelectedCategory.CategoryId, + viewData, + provider); + + // Assert + Assert.NotNull(explorer); + Assert.NotNull(explorer.Metadata); + Assert.Equal(ModelMetadataKind.Property, explorer.Metadata.MetadataKind); + Assert.Equal(typeof(int), explorer.ModelType); + Assert.Null(explorer.Model); + } + [Fact] public void FromLambaExpression_SetsContainerAsExpected() { @@ -27,6 +105,31 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal Assert.Same(myModel, metadata.Container.Model); } + [Theory] + [InlineData(null, ModelMetadataKind.Type, typeof(TestModel))] + [InlineData("", ModelMetadataKind.Type, typeof(TestModel))] + [InlineData("SelectedCategory", ModelMetadataKind.Property, typeof(Category))] + [InlineData("SelectedCategory.CategoryName", ModelMetadataKind.Type, typeof(string))] + public void FromStringExpression_GetsExpectedMetadata( + string expression, + ModelMetadataKind expectedKind, + Type expectedType) + { + // Arrange + var provider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(provider); + + // Act + var explorer = ExpressionMetadataProvider.FromStringExpression(expression, viewData, provider); + + // Assert + Assert.NotNull(explorer); + Assert.NotNull(explorer.Metadata); + Assert.Equal(expectedKind, explorer.Metadata.MetadataKind); + Assert.Equal(expectedType, explorer.ModelType); + Assert.Null(explorer.Model); + } + [Fact] public void FromStringExpression_SetsContainerAsExpected() { @@ -53,6 +156,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal private class Category { public int CategoryId { get; set; } + public string CategoryName { get; set; } } }