diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index c1ae24737a..94655e576a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -12,8 +12,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public abstract class AssociatedMetadataProvider : IModelMetadataProvider where TModelMetadata : ModelMetadata { - private readonly ConcurrentDictionary _typeInfoCache = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typeInfoCache = + new ConcurrentDictionary(); + + private readonly ConcurrentDictionary> _typePropertyInfoCache = + new ConcurrentDictionary>(); public IEnumerable GetMetadataForProperties(object container, [NotNull] Type containerType) { @@ -26,15 +29,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { if (string.IsNullOrEmpty(propertyName)) { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "propertyName"); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(propertyName)); } - var typeInfo = GetTypeInformation(containerType); + var typePropertyInfo = GetTypePropertyInformation(containerType); + PropertyInformation propertyInfo; - if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo)) + if (!typePropertyInfo.TryGetValue(propertyName, out propertyInfo)) { var message = Resources.FormatCommon_PropertyNotFound(containerType, propertyName); - throw new ArgumentException(message, "propertyName"); + throw new ArgumentException(message, nameof(propertyName)); } return CreatePropertyMetadata(modelAccessor, propertyInfo); @@ -42,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public ModelMetadata GetMetadataForType(Func modelAccessor, [NotNull] Type modelType) { - var prototype = GetTypeInformation(modelType).Prototype; + var prototype = GetTypeInformation(modelType); return CreateMetadataFromPrototype(prototype, modelAccessor); } @@ -53,7 +57,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { if (string.IsNullOrEmpty(parameterName)) { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "parameterName"); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(parameterName)); } var parameter = methodInfo.GetParameters().FirstOrDefault( @@ -92,8 +96,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private IEnumerable GetMetadataForPropertiesCore(object container, Type containerType) { - var typeInfo = GetTypeInformation(containerType); - foreach (var kvp in typeInfo.Properties) + var typePropertyInfo = GetTypePropertyInformation(containerType); + + foreach (var kvp in typePropertyInfo) { var propertyInfo = kvp.Value; Func modelAccessor = null; @@ -122,47 +127,43 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return metadata; } - private TypeInformation GetTypeInformation(Type type, IEnumerable associatedAttributes = null) + private TModelMetadata GetTypeInformation(Type type, IEnumerable associatedAttributes = null) { // This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd // to avoid the performance cost of creating instance delegates - TypeInformation typeInfo; + TModelMetadata typeInfo; if (!_typeInfoCache.TryGetValue(type, out typeInfo)) { typeInfo = CreateTypeInformation(type, associatedAttributes); _typeInfoCache.TryAdd(type, typeInfo); } + return typeInfo; } - private TypeInformation CreateTypeInformation(Type type, IEnumerable associatedAttributes) + private Dictionary GetTypePropertyInformation(Type type) { - var typeInfo = type.GetTypeInfo(); - var attributes = typeInfo.GetCustomAttributes(); + // This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd + // to avoid the performance cost of creating instance delegates + Dictionary typePropertyInfo; + if (!_typePropertyInfoCache.TryGetValue(type, out typePropertyInfo)) + { + typePropertyInfo = GetPropertiesLookup(type); + _typePropertyInfoCache.TryAdd(type, typePropertyInfo); + } + + return typePropertyInfo; + } + + private TModelMetadata CreateTypeInformation(Type type, IEnumerable associatedAttributes) + { + var attributes = type.GetTypeInfo().GetCustomAttributes(); if (associatedAttributes != null) { attributes = attributes.Concat(associatedAttributes); } - var info = new TypeInformation - { - Prototype = CreateMetadataPrototype(attributes, - containerType: null, - modelType: type, - propertyName: null) - }; - var properties = new Dictionary(StringComparer.Ordinal); - foreach (var propertyHelper in PropertyHelper.GetProperties(type)) - { - // Avoid re-generating a property descriptor if one has already been generated for the property name - if (!properties.ContainsKey(propertyHelper.Name)) - { - properties.Add(propertyHelper.Name, CreatePropertyInformation(type, propertyHelper)); - } - } - - info.Properties = properties; - return info; + return CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null); } private PropertyInformation CreatePropertyInformation(Type containerType, PropertyHelper helper) @@ -179,6 +180,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; } + private Dictionary GetPropertiesLookup(Type containerType) + { + var properties = new Dictionary(StringComparer.Ordinal); + foreach (var propertyHelper in PropertyHelper.GetProperties(containerType)) + { + // Avoid re-generating a property descriptor if one has already been generated for the property name + if (!properties.ContainsKey(propertyHelper.Name)) + { + properties.Add(propertyHelper.Name, CreatePropertyInformation(containerType, propertyHelper)); + } + } + + return properties; + } + private ParameterInformation CreateParameterInfo( Type parameterType, IEnumerable attributes, @@ -200,12 +216,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public TModelMetadata Prototype { get; set; } } - private sealed class TypeInformation - { - public TModelMetadata Prototype { get; set; } - public Dictionary Properties { get; set; } - } - private sealed class PropertyInformation { public PropertyHelper PropertyHelper { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ViewDataDictionaryTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ViewDataDictionaryTest.cs index e39a5cbc95..8ba6e0e9f5 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ViewDataDictionaryTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ViewDataDictionaryTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Testing; using Moq; @@ -73,6 +74,73 @@ namespace Microsoft.AspNet.Mvc.Core metadataProvider.Verify(); } + // When SetModel is called, only GetMetadataForType from MetadataProvider is expected to be called. + [Fact] + public void SetModelCallsGetMetadataForTypeExactlyOnce() + { + // Arrange + var metadataProvider = new Mock(MockBehavior.Strict); + metadataProvider + .Setup(m => m.GetMetadataForType(It.IsAny>(), typeof(object))) + .Returns(new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object))) + .Verifiable(); + metadataProvider + .Setup(m => m.GetMetadataForType(It.IsAny>(), typeof(TestModel))) + .Returns(new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(TestModel))) + .Verifiable(); + var modelState = new ModelStateDictionary(); + var viewData = new TestViewDataDictionary(metadataProvider.Object, modelState); + var model = new TestModel(); + + // Act + viewData.SetModelPublic(model); + + // Assert + Assert.NotNull(viewData.ModelMetadata); + // Verifies if the GetMetadataForType is called only once. + metadataProvider.Verify( + m => m.GetMetadataForType(It.IsAny>(), typeof(object)), Times.Once()); + // Verifies if GetMetadataForProperties and GetMetadataForProperty is not called. + metadataProvider.Verify( + m => m.GetMetadataForProperties(It.IsAny>(), typeof(object)), Times.Never()); + metadataProvider.Verify( + m => m.GetMetadataForProperty( + It.IsAny>(), typeof(object), It.IsAny()), Times.Never()); + } + + public static TheoryData SetModelData + { + get + { + var model = new List() + { + new TestModel(), + new TestModel() + }; + + return new TheoryData + { + { model.Select(t => t) }, + { model.Where(t => t != null) }, + { model.SelectMany(t => t.ToString()) }, + { model.Take(2) }, + { model.TakeWhile(t => t != null) }, + { model.Union(model) } + }; + } + } + + [Theory] + [MemberData(nameof(SetModelData))] + public void SetModelDoesNotThrowOnEnumerableModel(object model) + { + // Arrange + var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider()); + + // Act & Assert + Assert.DoesNotThrow(() => { vdd.Model = model; }); + } + [Fact] public void CopyConstructorInitalizesModelAndModelMetadataBasedOnSource() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs index 1080a64b02..e713f507fb 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs @@ -65,6 +65,29 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("10", body.Trim()); } + [Theory] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingWhere", "Where")] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingSelect", "Select")] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingSelectMany", "SelectMany")] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingTake", "Take")] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingTakeWhile", "TakeWhile")] + [InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingUnion", "Union")] + public async Task ViewComponents_SupportsEnumerableModel(string url, string linqQueryType) + { + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + // https://github.com/aspnet/Mvc/issues/1354 + // The invoked ViewComponent/View has a model which is an internal type implementing Enumerable. + // For ex - TestEnumerableObject.Select(t => t) returns WhereSelectListIterator + var body = await client.GetStringAsync(url); + + // Assert + Assert.Equal("

Hello

World

Sample

Test

" + + "

Hello

World

" + linqQueryType + "

Test

", body.Trim()); + } + [Theory] [InlineData("ViewComponentWebSite.Namespace1.SameName")] [InlineData("ViewComponentWebSite.Namespace2.SameName")] diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs index 3569843316..f87b3899a4 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -93,6 +93,22 @@ ViewWithNestedLayout-Content Assert.Equal(expected, body.Trim()); } + [Fact] + public async Task RazorView_DoesNotThrow_PartialViewWithEnumerableModel() + { + // Arrange + var expected = "HelloWorld"; + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act + var body = await client.GetStringAsync( + "http://localhost/ViewEngine/ViewWithPartialTakingModelFromIEnumerable"); + + // Assert + Assert.Equal(expected, body.Trim()); + } + [Fact] public async Task RazorView_PassesViewContextBetweenViewAndLayout() { diff --git a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs index 42efaf2227..c625844f3a 100644 --- a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs +++ b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc; namespace RazorWebSite.Controllers @@ -37,6 +38,17 @@ namespace RazorWebSite.Controllers return View(model); } + public IActionResult ViewWithPartialTakingModelFromIEnumerable() + { + var model = new List() + { + new Person() { Name = "Hello" }, + new Person() { Name = "World" } + }; + + return View(model); + } + public ViewResult ViewPassesViewDataToLayout() { ViewData["Title"] = "Controller title"; diff --git a/test/WebSites/RazorWebSite/Views/Shared/_PartialWithModelFromEnumerable.cshtml b/test/WebSites/RazorWebSite/Views/Shared/_PartialWithModelFromEnumerable.cshtml new file mode 100644 index 0000000000..8a4d1ce706 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Shared/_PartialWithModelFromEnumerable.cshtml @@ -0,0 +1,2 @@ +@model Person +@Html.DisplayFor(m => m.Name) \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartialTakingModelFromIEnumerable.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartialTakingModelFromIEnumerable.cshtml new file mode 100644 index 0000000000..952bf0fc0a --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithPartialTakingModelFromIEnumerable.cshtml @@ -0,0 +1,4 @@ +@using RazorWebSite +@model IEnumerable +@foreach (var item in Model) +{@await Html.PartialAsync("_PartialWithModelFromEnumerable", item)} \ No newline at end of file diff --git a/test/WebSites/ViewComponentWebSite/EnumerableViewComponent.cs b/test/WebSites/ViewComponentWebSite/EnumerableViewComponent.cs new file mode 100644 index 0000000000..158be48e56 --- /dev/null +++ b/test/WebSites/ViewComponentWebSite/EnumerableViewComponent.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; +using Microsoft.AspNet.Mvc; + +namespace ViewComponentWebSite +{ + public class EnumerableViewComponent : ViewComponent + { + public IViewComponentResult Invoke(string linqQueryType) + { + var modelList = new List() + { + new SampleModel { Prop1 = "Hello", Prop2 = "World" }, + new SampleModel { Prop1 = linqQueryType, Prop2 = "Test" }, + }; + + switch (linqQueryType) { + case "Where": + return View(modelList.Where(e => e != null)); + + case "Take": + return View(modelList.Take(2)); + + case "TakeWhile": + return View(modelList.TakeWhile(a => a != null)); + + case "Union": + return View(modelList.Union(modelList)); + + case "SelectMany": + var selectManySampleModelList = new List + { + new SelectManySampleModel { + TestModel = + new List { new SampleModel { Prop1 = "Hello", Prop2 = "World" } } }, + new SelectManySampleModel { + TestModel = + new List { new SampleModel{ Prop1 = linqQueryType, Prop2 = "Test" } } } + }; + + return View(selectManySampleModelList.SelectMany(a => a.TestModel)); + }; + + return View(modelList.Select(e => e)); + } + } +} \ No newline at end of file diff --git a/test/WebSites/ViewComponentWebSite/HomeController.cs b/test/WebSites/ViewComponentWebSite/HomeController.cs index 3e7b75a6e9..78ea705e8f 100644 --- a/test/WebSites/ViewComponentWebSite/HomeController.cs +++ b/test/WebSites/ViewComponentWebSite/HomeController.cs @@ -1,12 +1,25 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; using Microsoft.AspNet.Mvc; namespace ViewComponentWebSite { - public class HomeController + public class HomeController : Controller { + private IEnumerable ModelList { get; set; } + + public HomeController() + { + ModelList = new List() + { + new SampleModel { Prop1 = "Hello", Prop2 = "World" }, + new SampleModel { Prop1 = "Sample", Prop2 = "Test" }, + }; + } + public ViewResult ViewWithAsyncComponents() { return new ViewResult(); @@ -21,5 +34,51 @@ namespace ViewComponentWebSite { return new ViewResult(); } + + public ViewResult ViewComponentWithEnumerableModelUsingWhere() + { + ViewBag.LinqQueryType = "Where"; + return View("ViewComponentWithEnumerableModel", ModelList.Where(a => a != null)); + } + + public ViewResult ViewComponentWithEnumerableModelUsingSelect() + { + ViewBag.LinqQueryType = "Select"; + return View("ViewComponentWithEnumerableModel", ModelList.Select(a => a)); + } + + public ViewResult ViewComponentWithEnumerableModelUsingTake() + { + ViewBag.LinqQueryType = "Take"; + return View("ViewComponentWithEnumerableModel", ModelList.Take(2)); + } + + public ViewResult ViewComponentWithEnumerableModelUsingTakeWhile() + { + ViewBag.LinqQueryType = "TakeWhile"; + return View("ViewComponentWithEnumerableModel", ModelList.TakeWhile(a => a != null)); + } + + public ViewResult ViewComponentWithEnumerableModelUsingUnion() + { + ViewBag.LinqQueryType = "Union"; + return View("ViewComponentWithEnumerableModel", ModelList.Union(ModelList)); + } + + public ViewResult ViewComponentWithEnumerableModelUsingSelectMany() + { + var selectManySampleModelList = new List + { + new SelectManySampleModel { + TestModel = + new List { ModelList.ElementAt(0) } }, + new SelectManySampleModel { + TestModel = + new List { ModelList.ElementAt(1) } } + }; + + ViewBag.LinqQueryType = "SelectMany"; + return View("ViewComponentWithEnumerableModel", selectManySampleModelList.SelectMany(s => s.TestModel)); + } } } \ No newline at end of file diff --git a/test/WebSites/ViewComponentWebSite/SelectManySampleModel.cs b/test/WebSites/ViewComponentWebSite/SelectManySampleModel.cs new file mode 100644 index 0000000000..acac899f22 --- /dev/null +++ b/test/WebSites/ViewComponentWebSite/SelectManySampleModel.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; + +namespace ViewComponentWebSite +{ + public class SelectManySampleModel + { + public List TestModel { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/ViewComponentWebSite/Views/Shared/Components/Enumerable/Default.cshtml b/test/WebSites/ViewComponentWebSite/Views/Shared/Components/Enumerable/Default.cshtml new file mode 100644 index 0000000000..ce64a0f9f5 --- /dev/null +++ b/test/WebSites/ViewComponentWebSite/Views/Shared/Components/Enumerable/Default.cshtml @@ -0,0 +1,3 @@ +@model IEnumerable +@foreach (var m in Model) +{

@m.Prop1

@m.Prop2

} \ No newline at end of file diff --git a/test/WebSites/ViewComponentWebSite/Views/Shared/ViewComponentWithEnumerableModel.cshtml b/test/WebSites/ViewComponentWebSite/Views/Shared/ViewComponentWithEnumerableModel.cshtml new file mode 100644 index 0000000000..adaa146307 --- /dev/null +++ b/test/WebSites/ViewComponentWebSite/Views/Shared/ViewComponentWithEnumerableModel.cshtml @@ -0,0 +1,4 @@ +@model IEnumerable +@foreach (var m in Model) +{

@m.Prop1

@m.Prop2

} +@Component.Invoke("Enumerable", ViewBag.LinqQueryType) \ No newline at end of file