Merge branch 'release' into dev
This commit is contained in:
commit
07043ce1a9
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
@model Person
|
||||
@Html.DisplayFor(m => m.Name)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@using RazorWebSite
|
||||
@model IEnumerable<Person>
|
||||
@foreach (var item in Model)
|
||||
{@await Html.PartialAsync("_PartialWithModelFromEnumerable", item)}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@model IEnumerable<SampleModel>
|
||||
@foreach (var m in Model)
|
||||
{<p>@m.Prop1</p><p>@m.Prop2</p>}
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue