Special-case use of `razorPage.Model` property in `ExpressionMetadataProvider`

- #3978
- better-aligns `ExpressionMetadataProvider` with `ExpressionHelper`

nit: mock less in `RazorPageCreateModelExpressionTest`
This commit is contained in:
Doug Bunting 2016-04-14 12:15:21 -07:00
parent 010e8a4e37
commit 7a1ac034f9
5 changed files with 284 additions and 34 deletions

View File

@ -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<TModel>. (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;

View File

@ -70,7 +70,7 @@
</div>
<div>
<label class="order" for="HtmlEncode[[Customer_Gender]]">HtmlEncode[[Gender]]</label>
<input type="HtmlEncode[[radio]]" value="HtmlEncode[[Male]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The Model field is required.]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" /> Male
<input type="HtmlEncode[[radio]]" value="HtmlEncode[[Male]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The Gender field is required.]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" /> Male
<input type="HtmlEncode[[radio]]" value="HtmlEncode[[Female]]" checked="HtmlEncode[[checked]]" id="HtmlEncode[[Customer_Gender]]" name="HtmlEncode[[Customer.Gender]]" /> Female
<span class="HtmlEncode[[field-validation-valid]]" data-valmsg-for="HtmlEncode[[Customer.Gender]]" data-valmsg-replace="HtmlEncode[[true]]"></span>
</div>

View File

@ -70,7 +70,7 @@
</div>
<div>
<label class="order" for="Customer_Gender">Gender</label>
<input type="radio" value="Male" data-val="true" data-val-required="The Model field is required." id="Customer_Gender" name="Customer.Gender" /> Male
<input type="radio" value="Male" data-val="true" data-val-required="The Gender field is required." id="Customer_Gender" name="Customer.Gender" /> Male
<input type="radio" value="Female" checked="checked" id="Customer_Gender" name="Customer.Gender" /> Female
<span class="field-validation-valid" data-valmsg-for="Customer.Gender" data-valmsg-replace="true"></span>
</div>

View File

@ -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<Func<IdentityRazorPage, ModelExpression>, string>
{
// m => m
{ page => page.CreateModelExpression1(), string.Empty },
// m => Model
{ page => page.CreateModelExpression2(), string.Empty },
};
}
}
public static TheoryData NotQuiteIdentityExpressions
{
get
{
return new TheoryData<Func<NotQuiteIdentityRazorPage, ModelExpression>, 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<TModel>.
{ page => page.CreateModelExpression3(), "ViewContext.ViewData.Model", typeof(object) },
};
}
}
public static TheoryData<Expression<Func<RazorPageCreateModelExpressionModel, int>>, string> IntExpressions
{
get
@ -50,6 +82,61 @@ namespace Microsoft.AspNetCore.Mvc.Razor
}
}
[Theory]
[MemberData(nameof(IdentityExpressions))]
public void CreateModelExpression_ReturnsExpectedMetadata_IdentityExpressions(
Func<IdentityRazorPage, ModelExpression> createModelExpression,
string expectedName)
{
// Arrange
var viewContext = CreateViewContext();
var modelExplorer = viewContext.ViewData.ModelExplorer.GetExplorerForProperty(
nameof(RazorPageCreateModelExpressionModel.Name));
var viewData = new ViewDataDictionary<string>(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<NotQuiteIdentityRazorPage, ModelExpression> createModelExpression,
string expectedName,
Type expectedType)
{
// Arrange
var viewContext = CreateViewContext();
var viewData = new ViewDataDictionary<RecursiveModel>(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<string>)viewContext.ViewData,
};
}
public static NotQuiteIdentityRazorPage CreateNotQuiteIdentityPage(ViewContext viewContext)
{
return new NotQuiteIdentityRazorPage
{
ViewContext = viewContext,
ViewData = (ViewDataDictionary<RecursiveModel>)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<RazorPageCreateModelExpressionModel>(provider);
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IModelMetadataProvider>(provider);
serviceCollection.AddSingleton<ExpressionTextCache, ExpressionTextCache>();
private static ViewContext CreateViewContext(
RazorPageCreateModelExpressionModel model,
IModelMetadataProvider provider)
{
var viewData = new ViewDataDictionary<RazorPageCreateModelExpressionModel>(provider)
var httpContext = new DefaultHttpContext
{
Model = model,
RequestServices = serviceCollection.BuildServiceProvider(),
};
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider
.Setup(real => real.GetService(typeof(IModelMetadataProvider)))
.Returns(provider);
serviceProvider
.Setup(real => real.GetService(typeof(ExpressionTextCache)))
.Returns(new ExpressionTextCache());
var httpContext = new Mock<HttpContext>();
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<IView>(),
viewData: viewData,
tempData: Mock.Of<ITempDataDictionary>(),
writer: new StringWriter(),
htmlHelperOptions: new HtmlHelperOptions());
NullView.Instance,
viewData,
Mock.Of<ITempDataDictionary>(),
new StringWriter(),
new HtmlHelperOptions());
}
public class IdentityRazorPage : RazorPage<string>
{
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<RecursiveModel>
{
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<RazorPageCreateModelExpressionModel>
@ -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; }

View File

@ -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<TestModel>(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<TestModel>(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<TestModel[]>(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<TestModel[]>(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<TestModel>(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; }
}
}