Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit a23307e

Browse files
committed
Check for properties that can't befound
If you give ModelExpressionProvider a lambda with a private property you'll end up here. This wasn't common before, but it seems like users are more likely to try it with pages. Model Metadata and Model Binding don't handle private properties, so supporting it in Model Expressions seems less than useful. This isn't a breaking change because this case would have resulted in a null-ref. Addresses #6400
1 parent c351712 commit a23307e

File tree

7 files changed

+213
-11
lines changed

7 files changed

+213
-11
lines changed

src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionMetadataProvider.cs

+25-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Globalization;
67
using System.Linq.Expressions;
78
using System.Reflection;
@@ -60,7 +61,12 @@ public static ModelExplorer FromLambdaExpression<TModel, TResult>(
6061
return FromModel(viewData, metadataProvider);
6162
}
6263

63-
containerType = memberExpression.Expression.Type;
64+
// memberExpression.Expression can be null when this is a static field or property.
65+
//
66+
// This can be the case if the expression is like (m => Person.Name) where Name is a static field
67+
// or property on the Person type.
68+
containerType = memberExpression.Expression?.Type;
69+
6470
legalExpression = true;
6571
break;
6672

@@ -86,23 +92,31 @@ public static ModelExplorer FromLambdaExpression<TModel, TResult>(
8692
}
8793
};
8894

89-
ModelMetadata metadata;
90-
if (propertyName == null)
91-
{
92-
// Ex:
93-
// m => 5 (arbitrary expression)
94-
// m => foo (arbitrary expression)
95-
// m => m.Widgets[0] (expression ending with non-property-access)
96-
metadata = metadataProvider.GetMetadataForType(typeof(TResult));
97-
}
98-
else
95+
ModelMetadata metadata = null;
96+
if (containerType != null && propertyName != null)
9997
{
10098
// Ex:
10199
// m => m.Color (simple property access)
102100
// m => m.Color.Red (nested property access)
103101
// m => m.Widgets[0].Size (expression ending with property-access)
104102
metadata = metadataProvider.GetMetadataForType(containerType).Properties[propertyName];
105103
}
104+
105+
if (metadata == null)
106+
{
107+
// Ex:
108+
// m => 5 (arbitrary expression)
109+
// m => foo (arbitrary expression)
110+
// m => m.Widgets[0] (expression ending with non-property-access)
111+
//
112+
// This can also happen for any case where we cannot retrieve a model metadata.
113+
// This will happen for:
114+
// - fields
115+
// - statics
116+
// - non-visibility (internal/private)
117+
metadata = metadataProvider.GetMetadataForType(typeof(TResult));
118+
Debug.Assert(metadata != null);
119+
}
106120

107121
return viewData.ModelExplorer.GetExplorerForExpression(metadata, modelAccessor);
108122
}

test/Microsoft.AspNetCore.Mvc.FunctionalTests/HtmlGenerationTest.cs

+26
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,32 @@ public async Task EditorTemplateWithSpecificModel_RendersWithCorrectMetadata()
523523
Assert.Equal(expected, response, ignoreLineEndingDifferences: true);
524524
}
525525

526+
// We want to make sure that for 'wierd' model expressions involving:
527+
// - fields
528+
// - statics
529+
// - private
530+
//
531+
// These tests verify that we don't throw, and can evaluate the expression to get the model
532+
// value. One quirk of behavior for these cases is that we can't return a correct model metadata
533+
// instance (this is true for anything other than a public instance property). We're not overly
534+
// concerned with that, and so the accuracy of the model metadata is is not verified by the test.
535+
[Theory]
536+
[InlineData("GetWeirdWithHtmlHelpers")]
537+
[InlineData("GetWeirdWithTagHelpers")]
538+
public async Task WeirdModelExpressions_CanAccessModelValues(string action)
539+
{
540+
// Arrange
541+
var url = "http://localhost/HtmlGeneration_WeirdExpressions/" + action;
542+
543+
// Act
544+
var response = await Client.GetStringAsync(url);
545+
546+
// Assert
547+
Assert.Contains("Hello, Field World!", response);
548+
Assert.Contains("Hello, Static World!", response);
549+
Assert.Contains("Hello, Private World!", response);
550+
}
551+
526552
private static HttpRequestMessage RequestWithLocale(string url, string locale)
527553
{
528554
var request = new HttpRequestMessage(HttpMethod.Get, url);

test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionMetadataProviderTest.cs

+72
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
1010
{
1111
public class ExpressionMetadataProviderTest
1212
{
13+
private string PrivateProperty { get; set; }
14+
15+
public static string StaticProperty { get; set; }
16+
17+
public string Field = "Hello";
18+
1319
[Fact]
1420
public void FromLambdaExpression_GetsExpectedMetadata_ForIdentityExpression()
1521
{
@@ -148,6 +154,72 @@ public void FromStringExpression_SetsContainerAsExpected()
148154
Assert.Same(myModel, metadata.Container.Model);
149155
}
150156

157+
// A private property can't be found by the model metadata provider, so return the property's type
158+
// as a best-effort mechanism.
159+
[Fact]
160+
public void FromLambdaExpression_ForPrivateProperty_ReturnsMetadataOfExpressionType()
161+
{
162+
// Arrange
163+
var provider = new EmptyModelMetadataProvider();
164+
var viewData = new ViewDataDictionary<ExpressionMetadataProviderTest>(provider);
165+
166+
// Act
167+
var explorer = ExpressionMetadataProvider.FromLambdaExpression(
168+
m => m.PrivateProperty,
169+
viewData,
170+
provider);
171+
172+
// Assert
173+
Assert.NotNull(explorer);
174+
Assert.NotNull(explorer.Metadata);
175+
Assert.Equal(ModelMetadataKind.Type, explorer.Metadata.MetadataKind);
176+
Assert.Equal(typeof(string), explorer.ModelType);
177+
}
178+
179+
// A static property can't be found by the model metadata provider, so return the property's type
180+
// as a best-effort mechanism.
181+
[Fact]
182+
public void FromLambdaExpression_ForStaticProperty_ReturnsMetadataOfExpressionType()
183+
{
184+
// Arrange
185+
var provider = new EmptyModelMetadataProvider();
186+
var viewData = new ViewDataDictionary<ExpressionMetadataProviderTest>(provider);
187+
188+
// Act
189+
var explorer = ExpressionMetadataProvider.FromLambdaExpression(
190+
m => ExpressionMetadataProviderTest.StaticProperty,
191+
viewData,
192+
provider);
193+
194+
// Assert
195+
Assert.NotNull(explorer);
196+
Assert.NotNull(explorer.Metadata);
197+
Assert.Equal(ModelMetadataKind.Type, explorer.Metadata.MetadataKind);
198+
Assert.Equal(typeof(string), explorer.ModelType);
199+
}
200+
201+
// A field can't be found by the model metadata provider, so return the field's type
202+
// as a best-effort mechanism.
203+
[Fact]
204+
public void FromLambdaExpression_ForField_ReturnsMetadataOfExpressionType()
205+
{
206+
// Arrange
207+
var provider = new EmptyModelMetadataProvider();
208+
var viewData = new ViewDataDictionary<ExpressionMetadataProviderTest>(provider);
209+
210+
// Act
211+
var explorer = ExpressionMetadataProvider.FromLambdaExpression(
212+
m => m.Field,
213+
viewData,
214+
provider);
215+
216+
// Assert
217+
Assert.NotNull(explorer);
218+
Assert.NotNull(explorer.Metadata);
219+
Assert.Equal(ModelMetadataKind.Type, explorer.Metadata.MetadataKind);
220+
Assert.Equal(typeof(string), explorer.ModelType);
221+
}
222+
151223
private class TestModel
152224
{
153225
public Category SelectedCategory { get; set; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using HtmlGenerationWebSite.Models;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
namespace HtmlGenerationWebSite.Controllers
9+
{
10+
public class HtmlGeneration_WeirdExpressionsController : Controller
11+
{
12+
public IActionResult GetWeirdWithHtmlHelpers()
13+
{
14+
return View(new WeirdModel());
15+
}
16+
17+
public IActionResult GetWeirdWithTagHelpers()
18+
{
19+
return View(new WeirdModel());
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.ComponentModel.DataAnnotations;
5+
6+
namespace HtmlGenerationWebSite.Models
7+
{
8+
public class WeirdModel
9+
{
10+
public string Field = "Hello, Field World!";
11+
12+
public static string StaticProperty { get; set; } = "Hello, Static World!";
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
2+
@using HtmlGenerationWebSite.Models
3+
@model WeirdModel
4+
5+
@functions{
6+
private string PrivateProperty { get; set; } = "Hello, Private World!";
7+
}
8+
9+
<html>
10+
<body>
11+
<div>
12+
<p>Field</p>
13+
@Html.LabelFor(m => m.Field)
14+
@Html.TextBoxFor(m => m.Field)
15+
</div>
16+
<div>
17+
<p>Static Property</p>
18+
@Html.LabelFor(m => WeirdModel.StaticProperty)
19+
@Html.TextBoxFor(m => WeirdModel.StaticProperty)
20+
</div>
21+
<div>
22+
<p>Private Property</p>
23+
@Html.LabelFor(m => PrivateProperty)
24+
@Html.TextBoxFor(m => PrivateProperty)
25+
</div>
26+
</body>
27+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
2+
@using HtmlGenerationWebSite.Models
3+
@model WeirdModel
4+
5+
@functions{
6+
private string PrivateProperty { get; set; } = "Hello, Private World!";
7+
}
8+
9+
<html>
10+
<body>
11+
<div>
12+
<p>Field</p>
13+
<label asp-for="Field" />
14+
<input asp-for="Field" />
15+
</div>
16+
<div>
17+
<p>Static Property</p>
18+
<label asp-for="@WeirdModel.StaticProperty" />
19+
<input asp-for="@WeirdModel.StaticProperty" />
20+
</div>
21+
<div>
22+
<p>Private Property</p>
23+
<label asp-for="@PrivateProperty" />
24+
<input asp-for="@PrivateProperty" />
25+
</div>
26+
</body>
27+
</html>

0 commit comments

Comments
 (0)