Merge branch 'release' into dev

This commit is contained in:
sornaks 2014-12-16 14:52:16 -08:00
commit 07043ce1a9
12 changed files with 304 additions and 41 deletions

View File

@ -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)
{
@ -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<object> 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<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;
@ -122,47 +127,43 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
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();
// 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))
{
typePropertyInfo = GetPropertiesLookup(type);
_typePropertyInfoCache.TryAdd(type, typePropertyInfo);
}
return typePropertyInfo;
}
private TModelMetadata CreateTypeInformation(Type type, IEnumerable<Attribute> 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<string, PropertyInformation>(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<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,
@ -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<string, PropertyInformation> Properties { get; set; }
}
private sealed class PropertyInformation
{
public PropertyHelper PropertyHelper { get; set; }

View File

@ -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<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()
{

View File

@ -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("<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")]

View File

@ -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()
{

View File

@ -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<Person>()
{
new Person() { Name = "Hello" },
new Person() { Name = "World" }
};
return View(model);
}
public ViewResult ViewPassesViewDataToLayout()
{
ViewData["Title"] = "Controller title";

View File

@ -0,0 +1,2 @@
@model Person
@Html.DisplayFor(m => m.Name)

View File

@ -0,0 +1,4 @@
@using RazorWebSite
@model IEnumerable<Person>
@foreach (var item in Model)
{@await Html.PartialAsync("_PartialWithModelFromEnumerable", item)}

View File

@ -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));
}
}
}

View File

@ -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<SampleModel> ModelList { get; set; }
public HomeController()
{
ModelList = new List<SampleModel>()
{
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<SelectManySampleModel>
{
new SelectManySampleModel {
TestModel =
new List<SampleModel> { ModelList.ElementAt(0) } },
new SelectManySampleModel {
TestModel =
new List<SampleModel> { ModelList.ElementAt(1) } }
};
ViewBag.LinqQueryType = "SelectMany";
return View("ViewComponentWithEnumerableModel", selectManySampleModelList.SelectMany(s => s.TestModel));
}
}
}

View File

@ -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<SampleModel> TestModel { get; set; }
}
}

View File

@ -0,0 +1,3 @@
@model IEnumerable<SampleModel>
@foreach (var m in Model)
{<p>@m.Prop1</p><p>@m.Prop2</p>}

View File

@ -0,0 +1,4 @@
@model IEnumerable<SampleModel>
@foreach (var m in Model)
{<p>@m.Prop1</p><p>@m.Prop2</p>}
@Component.Invoke("Enumerable", ViewBag.LinqQueryType)