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

Commit

Permalink
Bug #1354 - ViewComponent View() fails on CoreCLR with IEnumerable<> …
Browse files Browse the repository at this point in the history
…passed in.

Fix - When the model is passed in to a View, ViewDataDictionary sets it. During this process, we recurse through all the properties and create FastPropertyGetters for each of them. In this case, since it is an enumerable, the properties which we recurse through are not the elements of the collection but the properties of the Enumerable instead. i.e - Enumerable.Current. Creating getters for these properties are not necessary. The fix moves the property iteration step to a place where the properties are actually requested.
- Splitting TypeInformation class into two and separating their caches appropriately.
  • Loading branch information
sornaks committed Dec 16, 2014
1 parent 2df482a commit 1570e19
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public abstract class AssociatedMetadataProvider<TModelMetadata> : IModelMetadataProvider
where TModelMetadata : ModelMetadata
{
private readonly ConcurrentDictionary<Type, TypeInformation> _typeInfoCache =
new ConcurrentDictionary<Type, TypeInformation>();
private readonly ConcurrentDictionary<Type, TModelMetadata> _typeInfoCache =
new ConcurrentDictionary<Type, TModelMetadata>();

private readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInformation>> _typePropertyInfoCache =
new ConcurrentDictionary<Type, Dictionary<string, PropertyInformation>>();

public IEnumerable<ModelMetadata> GetMetadataForProperties(object container, [NotNull] Type containerType)
{
Expand All @@ -26,23 +29,24 @@ public ModelMetadata GetMetadataForProperty(Func<object> modelAccessor,
{
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);
}

public ModelMetadata GetMetadataForType(Func<object> modelAccessor, [NotNull] Type modelType)
{
var prototype = GetTypeInformation(modelType).Prototype;
var prototype = GetTypeInformation(modelType);
return CreateMetadataFromPrototype(prototype, modelAccessor);
}

Expand All @@ -53,7 +57,7 @@ public ModelMetadata GetMetadataForParameter(
{
if (string.IsNullOrEmpty(parameterName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "parameterName");
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(parameterName));
}

var parameter = methodInfo.GetParameters().FirstOrDefault(
Expand Down Expand Up @@ -92,8 +96,9 @@ private ModelMetadata GetMetadataForParameterCore(Func<object> modelAccessor,

private IEnumerable<ModelMetadata> 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<object> modelAccessor = null;
Expand Down Expand Up @@ -122,47 +127,43 @@ private TModelMetadata CreatePropertyMetadata(Func<object> modelAccessor, Proper
return metadata;
}

private TypeInformation GetTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes = null)
private TModelMetadata GetTypeInformation(Type type, IEnumerable<Attribute> 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<Attribute> associatedAttributes)
private Dictionary<string, PropertyInformation> GetTypePropertyInformation(Type type)
{
var typeInfo = type.GetTypeInfo();
var attributes = typeInfo.GetCustomAttributes();
if (associatedAttributes != null)
// This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd
// to avoid the performance cost of creating instance delegates
Dictionary<string, PropertyInformation> typePropertyInfo;
if (!_typePropertyInfoCache.TryGetValue(type, out typePropertyInfo))
{
attributes = attributes.Concat(associatedAttributes);
typePropertyInfo = GetPropertiesLookup(type);
_typePropertyInfoCache.TryAdd(type, typePropertyInfo);
}
var info = new TypeInformation
{
Prototype = CreateMetadataPrototype(attributes,
containerType: null,
modelType: type,
propertyName: null)
};

var properties = new Dictionary<string, PropertyInformation>(StringComparer.Ordinal);
foreach (var propertyHelper in PropertyHelper.GetProperties(type))
return typePropertyInfo;
}

private TModelMetadata CreateTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes)
{
var attributes = type.GetTypeInfo().GetCustomAttributes();
if (associatedAttributes != null)
{
// 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));
}
attributes = attributes.Concat(associatedAttributes);
}

info.Properties = properties;
return info;
return CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null);
}

private PropertyInformation CreatePropertyInformation(Type containerType, PropertyHelper helper)
Expand All @@ -179,6 +180,21 @@ private PropertyInformation CreatePropertyInformation(Type containerType, Proper
};
}

private Dictionary<string, PropertyInformation> GetPropertiesLookup(Type containerType)
{
var properties = new Dictionary<string, PropertyInformation>(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<object> attributes,
Expand All @@ -200,12 +216,6 @@ private sealed class ParameterInformation
public TModelMetadata Prototype { get; set; }
}

private sealed class TypeInformation
{
public TModelMetadata Prototype { get; set; }
public Dictionary<string, PropertyInformation> Properties { get; set; }
}

private sealed class PropertyInformation
{
public PropertyHelper PropertyHelper { get; set; }
Expand Down
68 changes: 68 additions & 0 deletions test/Microsoft.AspNet.Mvc.Core.Test/ViewDataDictionaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +74,73 @@ public void SetModelUsesPassedInModelMetadataProvider()
metadataProvider.Verify();
}

// When SetModel is called, only GetMetadataForType from MetadataProvider is expected to be called.
[Fact]
public void SetModelCallsGetMetadataForTypeExactlyOnce()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
metadataProvider
.Setup(m => m.GetMetadataForType(It.IsAny<Func<object>>(), typeof(object)))
.Returns(new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)))
.Verifiable();
metadataProvider
.Setup(m => m.GetMetadataForType(It.IsAny<Func<object>>(), 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<Func<object>>(), typeof(object)), Times.Once());
// Verifies if GetMetadataForProperties and GetMetadataForProperty is not called.
metadataProvider.Verify(
m => m.GetMetadataForProperties(It.IsAny<Func<object>>(), typeof(object)), Times.Never());
metadataProvider.Verify(
m => m.GetMetadataForProperty(
It.IsAny<Func<object>>(), typeof(object), It.IsAny<string>()), Times.Never());
}

public static TheoryData<object> SetModelData
{
get
{
var model = new List<TestModel>()
{
new TestModel(),
new TestModel()
};

return new TheoryData<object>
{
{ 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()
{
Expand Down
23 changes: 23 additions & 0 deletions test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ public async Task ViewComponents_SupportsValueType()
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("<p>Hello</p><p>World</p><p>Sample</p><p>Test</p>"
+ "<p>Hello</p><p>World</p><p>" + linqQueryType + "</p><p>Test</p>", body.Trim());
}

[Theory]
[InlineData("ViewComponentWebSite.Namespace1.SameName")]
[InlineData("ViewComponentWebSite.Namespace2.SameName")]
Expand Down
16 changes: 16 additions & 0 deletions test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ public async Task RazorView_ExecutesPartialPagesWithCorrectContext()
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()
{
Expand Down
12 changes: 12 additions & 0 deletions test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -37,6 +38,17 @@ public IActionResult ViewWithPartial()
return View(model);
}

public IActionResult ViewWithPartialTakingModelFromIEnumerable()
{
var model = new List<Person>()
{
new Person() { Name = "Hello" },
new Person() { Name = "World" }
};

return View(model);
}

public ViewResult ViewPassesViewDataToLayout()
{
ViewData["Title"] = "Controller title";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@model Person
@Html.DisplayFor(m => m.Name)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@using RazorWebSite
@model IEnumerable<Person>
@foreach (var item in Model)
{@await Html.PartialAsync("_PartialWithModelFromEnumerable", item)}
50 changes: 50 additions & 0 deletions test/WebSites/ViewComponentWebSite/EnumerableViewComponent.cs
Original file line number Diff line number Diff line change
@@ -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<SampleModel>()
{
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<SelectManySampleModel>
{
new SelectManySampleModel {
TestModel =
new List<SampleModel> { new SampleModel { Prop1 = "Hello", Prop2 = "World" } } },
new SelectManySampleModel {
TestModel =
new List<SampleModel> { new SampleModel{ Prop1 = linqQueryType, Prop2 = "Test" } } }
};

return View(selectManySampleModelList.SelectMany(a => a.TestModel));
};

return View(modelList.Select(e => e));
}
}
}
Loading

0 comments on commit 1570e19

Please sign in to comment.