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:
parent
010e8a4e37
commit
7a1ac034f9
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue