Correct `Type.IsAssignableFrom()` polarity

- #3482
 - see new tests; many failed without fixes in the product code
 - add support for binding `IFormFileCollection` properties
 - make `FormFileModelBinder` / `HeaderModelBinder` collection handling consistent w/ `GenericModelBinder`++
  - see also dupe bug #4129 which describes some of the prior inconsistencies
  - add checks around creating collections and leaving non-top-level collections `null` (not empty)
 - move smarts down to `ModelBindingHelper.GetCompatibleCollection<T>()` (was `ConvertValuesToCollectionType<T>()`)
  - add `ModelBindingHelper.CanGetCompatibleCollection()`
  - add fallback for cases like `public IEnumerable<T> Property { get; set; } = new T[0];`
- #4193
 - allow `Exception`s while activating collections to propagate
- part of #4181
 - `CollectionModelBinder` no longer creates an instance to check if it can create an instance
 - not a complete fix since it still creates unnecessary intermediate lists

nits:
- correct a few existing test names since nothing is not the same as `ModelBindingResult.Failed()`
- remove a couple of unnecessary `return` statements
- correct stale "optimized" comments
- explicit `(string)`
This commit is contained in:
Doug Bunting 2016-03-03 09:55:01 -08:00
parent ac58e8433c
commit d3c24637b1
18 changed files with 1307 additions and 219 deletions

View File

@ -7,9 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
#if NETSTANDARD1_3
using System.Reflection; using System.Reflection;
#endif
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -45,7 +43,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model); bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
return;
} }
return; return;
@ -85,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
if (valueProviderResult != ValueProviderResult.None) if (valueProviderResult != ValueProviderResult.None)
{ {
// If we did simple binding, then modelstate should be updated to reflect what we bound for ModelName. // If we did simple binding, then modelstate should be updated to reflect what we bound for ModelName.
// If we did complex binding, there will already be an entry for each index. // If we did complex binding, there will already be an entry for each index.
bindingContext.ModelState.SetModelValue( bindingContext.ModelState.SetModelValue(
bindingContext.ModelName, bindingContext.ModelName,
@ -93,13 +90,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model); bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
return;
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual bool CanCreateInstance(Type targetType) public virtual bool CanCreateInstance(Type targetType)
{ {
return CreateEmptyCollection(targetType) != null; if (targetType.IsAssignableFrom(typeof(List<TElement>)))
{
// Simple case such as ICollection<TElement>, IEnumerable<TElement> and IList<TElement>.
return true;
}
return targetType.GetTypeInfo().IsClass &&
!targetType.GetTypeInfo().IsAbstract &&
typeof(ICollection<TElement>).IsAssignableFrom(targetType);
} }
/// <summary> /// <summary>
@ -126,15 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <returns>An instance of <paramref name="targetType"/>.</returns> /// <returns>An instance of <paramref name="targetType"/>.</returns>
protected object CreateInstance(Type targetType) protected object CreateInstance(Type targetType)
{ {
try return Activator.CreateInstance(targetType);
{
return Activator.CreateInstance(targetType);
}
catch (Exception)
{
// Details of exception are not important.
return null;
}
} }
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value // Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value

View File

@ -113,10 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return collection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); return collection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
} }
var newCollection = CreateInstance(targetType); return base.ConvertToCollectionType(targetType, collection);
CopyToModel(newCollection, collection);
return newCollection;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -128,7 +125,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new Dictionary<TKey, TValue>(); return new Dictionary<TKey, TValue>();
} }
return CreateInstance(targetType); return base.CreateEmptyCollection(targetType);
}
public override bool CanCreateInstance(Type targetType)
{
if (targetType.IsAssignableFrom(typeof(Dictionary<TKey, TValue>)))
{
// Simple case such as IDictionary<TKey, TValue>.
return true;
}
return base.CanCreateInstance(targetType);
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
#if NETSTANDARD1_3 #if NETSTANDARD1_3
@ -12,7 +13,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
@ -30,72 +30,111 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
// This method is optimized to use cached tasks when possible and avoid allocating // This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile // using Task.FromResult or async state machines.
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.ModelType != typeof(IFormFile) && var modelType = bindingContext.ModelType;
!typeof(IEnumerable<IFormFile>).IsAssignableFrom(bindingContext.ModelType)) if (modelType != typeof(IFormFile) && !typeof(IEnumerable<IFormFile>).IsAssignableFrom(modelType))
{ {
// Not a type this model binder supports. Let other binders run.
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
return BindModelCoreAsync(bindingContext); var createFileCollection = modelType == typeof(IFormFileCollection) &&
!bindingContext.ModelMetadata.IsReadOnly;
if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext))
{
// Silently fail and stop other model binders running if unable to create an instance or use the
// current instance.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
}
ICollection<IFormFile> postedFiles;
if (createFileCollection)
{
postedFiles = new List<IFormFile>();
}
else
{
postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);
}
return BindModelCoreAsync(bindingContext, postedFiles);
} }
private async Task BindModelCoreAsync(ModelBindingContext bindingContext) private async Task BindModelCoreAsync(ModelBindingContext bindingContext, ICollection<IFormFile> postedFiles)
{ {
// If we're at the top level, then use the FieldName (paramter or property name). Debug.Assert(postedFiles != null);
// If we're at the top level, then use the FieldName (parameter or property name).
// This handles the fact that there will be nothing in the ValueProviders for this parameter // This handles the fact that there will be nothing in the ValueProviders for this parameter
// and so we'll do the right thing even though we 'fell-back' to the empty prefix. // and so we'll do the right thing even though we 'fell-back' to the empty prefix.
var modelName = bindingContext.IsTopLevelObject var modelName = bindingContext.IsTopLevelObject
? bindingContext.BinderModelName ?? bindingContext.FieldName ? bindingContext.BinderModelName ?? bindingContext.FieldName
: bindingContext.ModelName; : bindingContext.ModelName;
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
object value; object value;
if (bindingContext.ModelType == typeof(IFormFile)) if (bindingContext.ModelType == typeof(IFormFile))
{ {
var postedFiles = await GetFormFilesAsync(modelName, bindingContext); if (postedFiles.Count == 0)
value = postedFiles.FirstOrDefault();
}
else if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(bindingContext.ModelType))
{
var postedFiles = await GetFormFilesAsync(modelName, bindingContext);
value = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, postedFiles);
}
else
{
// This binder does not support the requested type.
Debug.Fail("We shouldn't be called without a matching type.");
return;
}
if (value == null)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return;
}
else
{
bindingContext.ValidationState.Add(value, new ValidationStateEntry()
{ {
Key = modelName, // Silently fail if the named file does not exist in the request.
SuppressValidation = true bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
}); return;
}
bindingContext.ModelState.SetModelValue( value = postedFiles.First();
modelName,
rawValue: null,
attemptedValue: null);
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, value);
return;
} }
else
{
if (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject)
{
// Silently fail if no files match. Will bind to an empty collection (treat empty as a success
// case and not reach here) if binding to a top-level object.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return;
}
// Perform any final type mangling needed.
var modelType = bindingContext.ModelType;
if (modelType == typeof(IFormFile[]))
{
Debug.Assert(postedFiles is List<IFormFile>);
value = ((List<IFormFile>)postedFiles).ToArray();
}
else if (modelType == typeof(IFormFileCollection))
{
Debug.Assert(postedFiles is List<IFormFile>);
value = new FileCollection((List<IFormFile>)postedFiles);
}
else
{
value = postedFiles;
}
}
bindingContext.ValidationState.Add(value, new ValidationStateEntry()
{
Key = modelName,
SuppressValidation = true
});
bindingContext.ModelState.SetModelValue(
modelName,
rawValue: null,
attemptedValue: null);
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, value);
} }
private async Task<List<IFormFile>> GetFormFilesAsync(string modelName, ModelBindingContext bindingContext) private async Task GetFormFilesAsync(
string modelName,
ModelBindingContext bindingContext,
ICollection<IFormFile> postedFiles)
{ {
var request = bindingContext.OperationBindingContext.HttpContext.Request; var request = bindingContext.OperationBindingContext.HttpContext.Request;
var postedFiles = new List<IFormFile>();
if (request.HasFormContentType) if (request.HasFormContentType)
{ {
var form = await request.ReadFormAsync(); var form = await request.ReadFormAsync();
@ -114,8 +153,45 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
} }
} }
}
return postedFiles; private class FileCollection : ReadOnlyCollection<IFormFile>, IFormFileCollection
{
public FileCollection(List<IFormFile> list)
: base(list)
{
}
public IFormFile this[string name] => GetFile(name);
public IFormFile GetFile(string name)
{
for (var i = 0; i < Items.Count; i++)
{
var file = Items[i];
if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase))
{
return file;
}
}
return null;
}
public IReadOnlyList<IFormFile> GetFiles(string name)
{
var files = new List<IFormFile>();
for (var i = 0; i < Items.Count; i++)
{
var file = Items[i];
if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase))
{
files.Add(file);
}
}
return files;
}
} }
} }
} }

View File

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
#if NETSTANDARD1_3 #if NETSTANDARD1_3
using System.Reflection; using System.Reflection;
#endif #endif
@ -28,8 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
// This method is optimized to use cached tasks when possible and avoid allocating // This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile // using Task.FromResult or async state machines.
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource; var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null || if (allowedBindingSource == null ||
@ -41,34 +38,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
var request = bindingContext.OperationBindingContext.HttpContext.Request; var request = bindingContext.OperationBindingContext.HttpContext.Request;
var modelMetadata = bindingContext.ModelMetadata;
// Property name can be null if the model metadata represents a type (rather than a property or parameter). // Property name can be null if the model metadata represents a type (rather than a property or parameter).
var headerName = bindingContext.FieldName; var headerName = bindingContext.FieldName;
object model = null;
if (bindingContext.ModelType == typeof(string)) object model;
if (ModelBindingHelper.CanGetCompatibleCollection<string>(bindingContext))
{ {
string value = request.Headers[headerName]; if (bindingContext.ModelType == typeof(string))
if (value != null)
{ {
model = value; var value = request.Headers[headerName];
model = (string)value;
}
else
{
var values = request.Headers.GetCommaSeparatedValues(headerName);
model = GetCompatibleCollection(bindingContext, values);
} }
} }
else if (typeof(IEnumerable<string>).IsAssignableFrom(bindingContext.ModelType)) else
{ {
var values = request.Headers.GetCommaSeparatedValues(headerName); // An unsupported datatype or a new collection is needed (perhaps because target type is an array) but
if (values.Length > 0) // can't assign it to the property.
{ model = null;
model = ModelBindingHelper.ConvertValuesToCollectionType(
bindingContext.ModelType,
values);
}
} }
if (model == null) if (model == null)
{ {
// Silently fail if unable to create an instance or use the current instance. Also reach here in the
// typeof(string) case if the header does not exist in the request and in the
// typeof(IEnumerable<string>) case if the header does not exist and this is not a top-level object.
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName); bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
} }
else else
{ {
@ -78,8 +78,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
request.Headers[headerName]); request.Headers[headerName]);
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model); bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
return TaskCache.CompletedTask;
} }
return TaskCache.CompletedTask;
}
private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
{
// Almost-always success if IsTopLevelObject.
if (!bindingContext.IsTopLevelObject && values.Length == 0)
{
return null;
}
if (bindingContext.ModelType.IsAssignableFrom(typeof(string[])))
{
// Array we already have is compatible.
return values;
}
var collection = ModelBindingHelper.GetCompatibleCollection<string>(bindingContext, values.Length);
for (int i = 0; i < values.Length; i++)
{
collection.Add(values[i]);
}
return collection;
} }
} }
} }

View File

@ -748,46 +748,146 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return (model is TModel) ? (TModel)model : default(TModel); return (model is TModel) ? (TModel)model : default(TModel);
} }
public static object ConvertValuesToCollectionType<T>(Type modelType, IList<T> values) /// <summary>
/// Gets an indication whether <see cref="M:GetCompatibleCollection{T}"/> is likely to return a usable
/// non-<c>null</c> value.
/// </summary>
/// <typeparam name="T">The element type of the <see cref="ICollection{T}"/> required.</typeparam>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// <c>true</c> if <see cref="M:GetCompatibleCollection{T}"/> is likely to return a usable non-<c>null</c>
/// value; <c>false</c> otherwise.
/// </returns>
/// <remarks>"Usable" in this context means the property can be set or its value reused.</remarks>
public static bool CanGetCompatibleCollection<T>(ModelBindingContext bindingContext)
{ {
// There's a limited set of collection types we can support here. var model = bindingContext.Model;
// var modelType = bindingContext.ModelType;
// For the simple cases - choose a T[] or List<T> if the destination type supports var writeable = !bindingContext.ModelMetadata.IsReadOnly;
// it. if (typeof(T).IsAssignableFrom(modelType))
//
// For more complex cases, if the destination type is a class and implements ICollection<T>
// then activate it and add the values.
//
// Otherwise just give up.
if (typeof(List<T>).IsAssignableFrom(modelType))
{ {
return new List<T>(values); // Scalar case. Existing model is not relevant and property must always be set. Will use a List<T>
// intermediate and set property to first element, if any.
return writeable;
} }
else if (typeof(T[]).IsAssignableFrom(modelType))
if (modelType == typeof(T[]))
{ {
return values.ToArray(); // Can't change the length of an existing array or replace it. Will use a List<T> intermediate and set
// property to an array created from that.
return writeable;
} }
else if (
if (!typeof(IEnumerable<T>).IsAssignableFrom(modelType))
{
// Not a supported collection.
return false;
}
var collection = model as ICollection<T>;
if (collection != null && !collection.IsReadOnly)
{
// Can use the existing collection.
return true;
}
// Most likely the model is null.
// Also covers the corner case where the model implements IEnumerable<T> but not ICollection<T> e.g.
// public IEnumerable<T> Property { get; set; } = new T[0];
if (modelType.IsAssignableFrom(typeof(List<T>)))
{
return writeable;
}
// Will we be able to activate an instance and use that?
return writeable &&
modelType.GetTypeInfo().IsClass && modelType.GetTypeInfo().IsClass &&
!modelType.GetTypeInfo().IsAbstract && !modelType.GetTypeInfo().IsAbstract &&
typeof(ICollection<T>).IsAssignableFrom(modelType)) typeof(ICollection<T>).IsAssignableFrom(modelType);
{ }
var result = (ICollection<T>)Activator.CreateInstance(modelType);
foreach (var value in values)
{
result.Add(value);
}
return result; /// <summary>
} /// Creates an <see cref="ICollection{T}"/> instance compatible with <paramref name="bindingContext"/>'s
else if (typeof(IEnumerable<T>).IsAssignableFrom(modelType)) /// <see cref="ModelBindingContext.ModelType"/>.
/// </summary>
/// <typeparam name="T">The element type of the <see cref="ICollection{T}"/> required.</typeparam>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// An <see cref="ICollection{T}"/> instance compatible with <paramref name="bindingContext"/>'s
/// <see cref="ModelBindingContext.ModelType"/>.
/// </returns>
/// <remarks>
/// Should not be called if <see cref="CanGetCompatibleCollection{T}"/> returned <c>false</c>.
/// </remarks>
public static ICollection<T> GetCompatibleCollection<T>(ModelBindingContext bindingContext)
{
return GetCompatibleCollection<T>(bindingContext, capacity: null);
}
/// <summary>
/// Creates an <see cref="ICollection{T}"/> instance compatible with <paramref name="bindingContext"/>'s
/// <see cref="ModelBindingContext.ModelType"/>.
/// </summary>
/// <typeparam name="T">The element type of the <see cref="ICollection{T}"/> required.</typeparam>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <param name="capacity">
/// Capacity for use when creating a <see cref="List{T}"/> instance. Not used when creating another type.
/// </param>
/// <returns>
/// An <see cref="ICollection{T}"/> instance compatible with <paramref name="bindingContext"/>'s
/// <see cref="ModelBindingContext.ModelType"/>.
/// </returns>
/// <remarks>
/// Should not be called if <see cref="CanGetCompatibleCollection{T}"/> returned <c>false</c>.
/// </remarks>
public static ICollection<T> GetCompatibleCollection<T>(ModelBindingContext bindingContext, int capacity)
{
return GetCompatibleCollection<T>(bindingContext, (int?)capacity);
}
private static ICollection<T> GetCompatibleCollection<T>(ModelBindingContext bindingContext, int? capacity)
{
var model = bindingContext.Model;
var modelType = bindingContext.ModelType;
// There's a limited set of collection types we can create here.
//
// For the simple cases: Choose List<T> if the destination type supports it (at least as an intermediary).
//
// For more complex cases: If the destination type is a class that implements ICollection<T>, then activate
// an instance and return that.
//
// Otherwise just give up.
if (typeof(T).IsAssignableFrom(modelType))
{ {
return values; return CreateList<T>(capacity);
} }
else
if (modelType == typeof(T[]))
{ {
return null; return CreateList<T>(capacity);
} }
// Does collection exist and can it be reused?
var collection = model as ICollection<T>;
if (collection != null && !collection.IsReadOnly)
{
collection.Clear();
return collection;
}
if (modelType.IsAssignableFrom(typeof(List<T>)))
{
return CreateList<T>(capacity);
}
return (ICollection<T>)Activator.CreateInstance(modelType);
}
private static List<T> CreateList<T>(int? capacity)
{
return capacity.HasValue ? new List<T>(capacity.Value) : new List<T>();
} }
/// <summary> /// <summary>
@ -860,7 +960,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null;
} }
if (value.GetType().IsAssignableFrom(type)) if (type.IsAssignableFrom(value.GetType()))
{ {
return value; return value;
} }
@ -917,7 +1017,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private static object ConvertSimpleType(object value, Type destinationType, CultureInfo culture) private static object ConvertSimpleType(object value, Type destinationType, CultureInfo culture)
{ {
if (value == null || value.GetType().IsAssignableFrom(destinationType)) if (value == null || destinationType.IsAssignableFrom(value.GetType()))
{ {
return value; return value;
} }

View File

@ -110,10 +110,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor
Func<ViewContext, object> valueAccessor; Func<ViewContext, object> valueAccessor;
if (typeof(ViewDataDictionary).IsAssignableFrom(property.PropertyType)) if (typeof(ViewDataDictionary).IsAssignableFrom(property.PropertyType))
{ {
// Logic looks reversed in condition above but is OK. Support only properties of base
// ViewDataDictionary type and activationInfo.ViewDataDictionaryType. VDD<AnotherType> will fail when
// assigning to the property (InvalidCastException) and that's fine.
valueAccessor = context => context.ViewData; valueAccessor = context => context.ViewData;
} }
else if (typeof(IUrlHelper).IsAssignableFrom(property.PropertyType)) else if (property.PropertyType == typeof(IUrlHelper))
{ {
// W.r.t. specificity of above condition: Users are much more likely to inject their own
// IUrlHelperFactory than to create a class implementing IUrlHelper (or a sub-interface) and inject
// that. But the second scenario is supported. (Note the class must implement ICanHasViewContext.)
valueAccessor = context => valueAccessor = context =>
{ {
var serviceProvider = context.HttpContext.RequestServices; var serviceProvider = context.HttpContext.RequestServices;

View File

@ -115,13 +115,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
} }
else else
{ {
// Will invoke synchronously. Method must not return void, Task or Task<T>.
if (selectedMethod.ReturnType == typeof(void)) if (selectedMethod.ReturnType == typeof(void))
{ {
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_ShouldReturnValue( throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(
SyncMethodName, SyncMethodName,
componentName)); componentName));
} }
else if (selectedMethod.ReturnType.IsAssignableFrom(typeof(Task))) else if (typeof(Task).IsAssignableFrom(selectedMethod.ReturnType))
{ {
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_CannotReturnTask( throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_CannotReturnTask(
SyncMethodName, SyncMethodName,

View File

@ -317,8 +317,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ typeof(List<int>), true }, { typeof(List<int>), true },
{ typeof(LinkedList<int>), true }, { typeof(LinkedList<int>), true },
{ typeof(ISet<int>), false }, { typeof(ISet<int>), false },
{ typeof(ListWithInternalConstructor<int>), false },
{ typeof(ListWithThrowingConstructor<int>), false },
}; };
} }
} }
@ -446,22 +444,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public string Name { get; set; } public string Name { get; set; }
} }
private class ListWithInternalConstructor<T> : List<T>
{
internal ListWithInternalConstructor()
: base()
{
}
}
private class ListWithThrowingConstructor<T> : List<T>
{
public ListWithThrowingConstructor()
: base()
{
throw new RankException("No, don't do this.");
}
}
} }
} }

View File

@ -388,9 +388,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
{ typeof(IDictionary<int, int>), true }, { typeof(IDictionary<int, int>), true },
{ typeof(Dictionary<int, int>), true }, { typeof(Dictionary<int, int>), true },
{ typeof(SortedDictionary<int, int>), true }, { typeof(SortedDictionary<int, int>), true },
{ typeof(IList<KeyValuePair<int, int>>), false }, { typeof(IList<KeyValuePair<int, int>>), true },
{ typeof(DictionaryWithInternalConstructor<int, int>), false }, { typeof(ISet<KeyValuePair<int, int>>), false },
{ typeof(DictionaryWithThrowingConstructor<int, int>), false },
}; };
} }
} }
@ -554,22 +553,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return $"{{{ Id }, '{ Name }'}}"; return $"{{{ Id }, '{ Name }'}}";
} }
} }
private class DictionaryWithInternalConstructor<TKey, TValue> : Dictionary<TKey, TValue>
{
internal DictionaryWithInternalConstructor()
: base()
{
}
}
private class DictionaryWithThrowingConstructor<TKey, TValue> : Dictionary<TKey, TValue>
{
public DictionaryWithThrowingConstructor()
: base()
{
throw new RankException("No, don't do this.");
}
}
} }
} }

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Xunit; using Xunit;
@ -43,9 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful() public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = GetTwoFiles();
formFiles.Add(GetMockFormFile("file", "file1.txt"));
formFiles.Add(GetMockFormFile("file", "file2.txt"));
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext); var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
var binder = new FormFileModelBinder(); var binder = new FormFileModelBinder();
@ -66,13 +63,38 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(2, files.Count); Assert.Equal(2, files.Count);
} }
[Theory]
[InlineData(typeof(IFormFile[]))]
[InlineData(typeof(ICollection<IFormFile>))]
[InlineData(typeof(IList<IFormFile>))]
[InlineData(typeof(IFormFileCollection))]
[InlineData(typeof(List<IFormFile>))]
[InlineData(typeof(LinkedList<IFormFile>))]
[InlineData(typeof(FileList))]
[InlineData(typeof(FormFileCollection))]
public async Task FormFileModelBinder_BindsFiles_ForCollectionsItCanCreate(Type destinationType)
{
// Arrange
var binder = new FormFileModelBinder();
var formFiles = GetTwoFiles();
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(destinationType, httpContext);
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.IsAssignableFrom(destinationType, result.Model);
Assert.Equal(formFiles, result.Model as IEnumerable<IFormFile>);
}
[Fact] [Fact]
public async Task FormFileModelBinder_ExpectSingleFile_BindFirstFile() public async Task FormFileModelBinder_ExpectSingleFile_BindFirstFile()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = GetTwoFiles();
formFiles.Add(GetMockFormFile("file", "file1.txt"));
formFiles.Add(GetMockFormFile("file", "file2.txt"));
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
var binder = new FormFileModelBinder(); var binder = new FormFileModelBinder();
@ -86,8 +108,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal("file1.txt", file.FileName); Assert.Equal("file1.txt", file.FileName);
} }
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(IEnumerable<string>))]
public async Task FormFileModelBinder_ReturnsNothing_ForUnsupportedDestinationTypes(Type destinationType)
{
// Arrange
var formFiles = GetTwoFiles();
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(destinationType, httpContext);
var binder = new FormFileModelBinder();
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.Equal(default(ModelBindingResult), result);
}
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsNothing_WhenNoFilePosted() public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = new FormFileCollection();
@ -100,11 +140,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result); Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model); Assert.Null(result.Model);
} }
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsNothing_WhenNamesDontMatch() public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = new FormFileCollection();
@ -118,6 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result); Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model); Assert.Null(result.Model);
} }
@ -151,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsNothing_WithEmptyContentDisposition() public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = new FormFileCollection();
@ -165,11 +207,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result); Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model); Assert.Null(result.Model);
} }
[Fact] [Fact]
public async Task FormFileModelBinder_ReturnsNothing_WithNoFileNameAndZeroLength() public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
{ {
// Arrange // Arrange
var formFiles = new FormFileCollection(); var formFiles = new FormFileCollection();
@ -183,15 +226,75 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.NotEqual(default(ModelBindingResult), result); Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model); Assert.Null(result.Model);
} }
[Fact]
public async Task FormFileModelBinder_ReturnsFailedResult_ForReadOnlyDestination()
{
// Arrange
var binder = new FormFileModelBinder();
var formFiles = GetTwoFiles();
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContextForReadOnlyArray(httpContext);
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model);
}
[Fact]
public async Task FormFileModelBinder_ReturnsFailedResult_ForCollectionsItCannotCreate()
{
// Arrange
var binder = new FormFileModelBinder();
var formFiles = GetTwoFiles();
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
var bindingContext = GetBindingContext(typeof(ISet<IFormFile>), httpContext);
// Act
var result = await binder.BindModelResultAsync(bindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Null(result.Model);
}
private static DefaultModelBindingContext GetBindingContextForReadOnlyArray(HttpContext httpContext)
{
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
.BindingDetails(bd => bd.BindingSource = BindingSource.Header);
var modelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithReadOnlyArray),
nameof(ModelWithReadOnlyArray.ArrayProperty));
return GetBindingContext(metadataProvider, modelMetadata, httpContext);
}
private static DefaultModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext) private static DefaultModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext)
{ {
var metadataProvider = new EmptyModelMetadataProvider(); var metadataProvider = new EmptyModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(modelType);
return GetBindingContext(metadataProvider, metadata, httpContext);
}
private static DefaultModelBindingContext GetBindingContext(
IModelMetadataProvider metadataProvider,
ModelMetadata metadata,
HttpContext httpContext)
{
var bindingContext = new DefaultModelBindingContext var bindingContext = new DefaultModelBindingContext
{ {
ModelMetadata = metadataProvider.GetMetadataForType(modelType), ModelMetadata = metadata,
ModelName = "file", ModelName = "file",
ModelState = new ModelStateDictionary(), ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext OperationBindingContext = new OperationBindingContext
@ -218,6 +321,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return httpContext.Object; return httpContext.Object;
} }
private static FormFileCollection GetTwoFiles()
{
var formFiles = new FormFileCollection
{
GetMockFormFile("file", "file1.txt"),
GetMockFormFile("file", "file2.txt"),
};
return formFiles;
}
private static IFormCollection GetMockFormCollection(FormFileCollection formFiles) private static IFormCollection GetMockFormCollection(FormFileCollection formFiles)
{ {
var formCollection = new Mock<IFormCollection>(); var formCollection = new Mock<IFormCollection>();
@ -233,5 +347,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return formFile.Object; return formFile.Object;
} }
private class ModelWithReadOnlyArray
{
public IFormFile[] ArrayProperty { get; }
}
private class FileList : List<IFormFile>
{
}
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Xunit; using Xunit;
@ -72,6 +73,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(headerValue, result.Model); Assert.Equal(headerValue, result.Model);
} }
[Theory]
[InlineData(typeof(IEnumerable<string>))]
[InlineData(typeof(ICollection<string>))]
[InlineData(typeof(IList<string>))]
[InlineData(typeof(List<string>))]
[InlineData(typeof(LinkedList<string>))]
[InlineData(typeof(StringList))]
public async Task HeaderBinder_BindsHeaders_ForCollectionsItCanCreate(Type destinationType)
{
// Arrange
var header = "Accept";
var headerValue = "application/json,text/json";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(destinationType);
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.True(result.IsModelSet);
Assert.IsAssignableFrom(destinationType, result.Model);
Assert.Equal(headerValue.Split(','), result.Model as IEnumerable<string>);
}
[Fact] [Fact]
public async Task HeaderBinder_ReturnsNothing_ForNullBindingSource() public async Task HeaderBinder_ReturnsNothing_ForNullBindingSource()
{ {
@ -116,11 +145,76 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(default(ModelBindingResult), result); Assert.Equal(default(ModelBindingResult), result);
} }
[Fact]
public async Task HeaderBinder_ReturnsFailedResult_ForReadOnlyDestination()
{
// Arrange
var header = "Accept";
var headerValue = "application/json,text/json";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContextForReadOnlyArray();
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Equal("modelName", result.Key);
Assert.Null(result.Model);
}
[Fact]
public async Task HeaderBinder_ReturnsFailedResult_ForCollectionsItCannotCreate()
{
// Arrange
var header = "Accept";
var headerValue = "application/json,text/json";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(typeof(ISet<string>));
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelResultAsync(modelBindingContext);
// Assert
Assert.NotEqual(default(ModelBindingResult), result);
Assert.False(result.IsModelSet);
Assert.Equal("modelName", result.Key);
Assert.Null(result.Model);
}
private static DefaultModelBindingContext GetBindingContext(Type modelType) private static DefaultModelBindingContext GetBindingContext(Type modelType)
{ {
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header); metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header);
var modelMetadata = metadataProvider.GetMetadataForType(modelType); var modelMetadata = metadataProvider.GetMetadataForType(modelType);
return GetBindingContext(metadataProvider, modelMetadata);
}
private static DefaultModelBindingContext GetBindingContextForReadOnlyArray()
{
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty<ModelWithReadOnlyArray>(nameof(ModelWithReadOnlyArray.ArrayProperty))
.BindingDetails(bd => bd.BindingSource = BindingSource.Header);
var modelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithReadOnlyArray),
nameof(ModelWithReadOnlyArray.ArrayProperty));
return GetBindingContext(metadataProvider, modelMetadata);
}
private static DefaultModelBindingContext GetBindingContext(
IModelMetadataProvider metadataProvider,
ModelMetadata modelMetadata)
{
var bindingContext = new DefaultModelBindingContext var bindingContext = new DefaultModelBindingContext
{ {
ModelMetadata = modelMetadata, ModelMetadata = modelMetadata,
@ -142,5 +236,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
return bindingContext; return bindingContext;
} }
private class ModelWithReadOnlyArray
{
public string[] ArrayProperty { get; }
}
private class StringList : List<string>
{
}
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq.Expressions; using System.Linq.Expressions;
@ -12,10 +13,8 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Test; using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Moq; using Moq;
using Xunit; using Xunit;
@ -550,10 +549,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ "ExcludedProperty", "ExcludedPropertyValue" } { "ExcludedProperty", "ExcludedPropertyValue" }
}; };
Func<ModelBindingContext, string, bool> includePredicate = Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) =>
(context, propertyName) => string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) || string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
var valueProvider = new TestValueProvider(values); var valueProvider = new TestValueProvider(values);
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -658,8 +656,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var binder = new StubModelBinder(); var binder = new StubModelBinder();
var model = new MyModel(); var model = new MyModel();
Func<ModelBindingContext, string, bool> includePredicate = Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) => true;
(context, propertyName) => true;
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>( var exception = await Assert.ThrowsAsync<ArgumentException>(
@ -1018,7 +1015,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
[Fact] [Fact]
public void ConvertToReturnsNullIfTrimmedValueIsEmptyString() public void ConvertToReturnsNull_IfConvertingNullToArrayType()
{ {
// Arrange // Arrange
@ -1244,21 +1241,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
() => ModelBindingHelper.ConvertTo("this-is-not-a-valid-value", destinationType)); () => ModelBindingHelper.ConvertTo("this-is-not-a-valid-value", destinationType));
} }
[Fact]
public void ConvertToThrowsIfNoConverterExists()
{
// Arrange
var destinationType = typeof(MyClassWithoutConverter);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ModelBindingHelper.ConvertTo("x", destinationType));
Assert.Equal("The parameter conversion from type 'System.String' to type " +
$"'{typeof(MyClassWithoutConverter).FullName}' " +
"failed because no type converter can convert between these types.",
ex.Message);
}
[Fact] [Fact]
public void ConvertToUsesProvidedCulture() public void ConvertToUsesProvidedCulture()
{ {
@ -1308,43 +1290,80 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
} }
// None of the types here have converters from MyClassWithoutConverter.
[Theory] [Theory]
[InlineData(typeof(TimeSpan))] [InlineData(typeof(TimeSpan))]
[InlineData(typeof(DateTime))] [InlineData(typeof(DateTime))]
[InlineData(typeof(DateTimeOffset))] [InlineData(typeof(DateTimeOffset))]
[InlineData(typeof(Guid))] [InlineData(typeof(Guid))]
[InlineData(typeof(IntEnum))] [InlineData(typeof(IntEnum))]
public void ConvertTo_Throws_IfValueIsNotStringData(Type destinationType) public void ConvertTo_Throws_IfValueIsNotConvertible(Type destinationType)
{ {
// Arrange // Arrange
var expectedMessage = $"The parameter conversion from type '{typeof(MyClassWithoutConverter)}' to type " +
$"'{destinationType}' failed because no type converter can convert between these types.";
// Act // Act & Assert
var ex = Assert.Throws<InvalidOperationException>( var ex = Assert.Throws<InvalidOperationException>(
() => ModelBindingHelper.ConvertTo(new MyClassWithoutConverter(), destinationType)); () => ModelBindingHelper.ConvertTo(new MyClassWithoutConverter(), destinationType));
// Assert
var expectedMessage = string.Format("The parameter conversion from type '{0}' to type '{1}' " +
"failed because no type converter can convert between these types.",
typeof(MyClassWithoutConverter), destinationType);
Assert.Equal(expectedMessage, ex.Message); Assert.Equal(expectedMessage, ex.Message);
} }
// String does not have a converter to MyClassWithoutConverter.
[Fact] [Fact]
public void ConvertTo_Throws_IfDestinationTypeIsNotConvertible() public void ConvertTo_Throws_IfDestinationTypeIsNotConvertible()
{ {
// Arrange // Arrange
var value = "Hello world"; var value = "Hello world";
var destinationType = typeof(MyClassWithoutConverter); var destinationType = typeof(MyClassWithoutConverter);
var expectedMessage = $"The parameter conversion from type '{value.GetType()}' to type " +
$"'{typeof(MyClassWithoutConverter)}' failed because no type converter can convert between these types.";
// Act // Act & Assert
var ex = Assert.Throws<InvalidOperationException>( var ex = Assert.Throws<InvalidOperationException>(
() => ModelBindingHelper.ConvertTo(value, destinationType)); () => ModelBindingHelper.ConvertTo(value, destinationType));
Assert.Equal(expectedMessage, ex.Message);
}
// Happens very rarely in practice since conversion is almost-always from strings or string arrays.
[Theory]
[InlineData(typeof(MyClassWithoutConverter))]
[InlineData(typeof(MySubClassWithoutConverter))]
public void ConvertTo_ReturnsValue_IfCompatible(Type destinationType)
{
// Arrange
var value = new MySubClassWithoutConverter();
// Act
var result = ModelBindingHelper.ConvertTo(value, destinationType);
// Assert // Assert
var expectedMessage = string.Format("The parameter conversion from type '{0}' to type '{1}' " + Assert.Same(value, result);
"failed because no type converter can convert between these types.", }
value.GetType(), typeof(MyClassWithoutConverter));
Assert.Equal(expectedMessage, ex.Message); [Theory]
[InlineData(typeof(MyClassWithoutConverter[]))]
[InlineData(typeof(MySubClassWithoutConverter[]))]
public void ConvertTo_ReusesArrayElements_IfCompatible(Type destinationType)
{
// Arrange
var value = new MyClassWithoutConverter[]
{
new MySubClassWithoutConverter(),
new MySubClassWithoutConverter(),
new MySubClassWithoutConverter(),
};
// Act
var result = ModelBindingHelper.ConvertTo(value, destinationType);
// Assert
Assert.IsType(destinationType, result);
Assert.Collection(
result as IEnumerable<MyClassWithoutConverter>,
element => { Assert.Same(value[0], element); },
element => { Assert.Same(value[1], element); },
element => { Assert.Same(value[2], element); });
} }
[Theory] [Theory]
@ -1367,10 +1386,246 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expected, outValue); Assert.Equal(expected, outValue);
} }
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(int[]))]
[InlineData(typeof(IEnumerable<int>))]
[InlineData(typeof(IReadOnlyCollection<int>))]
[InlineData(typeof(IReadOnlyList<int>))]
[InlineData(typeof(ICollection<int>))]
[InlineData(typeof(IList<int>))]
[InlineData(typeof(List<int>))]
[InlineData(typeof(Collection<int>))]
[InlineData(typeof(IntList))]
[InlineData(typeof(LinkedList<int>))]
public void CanGetCompatibleCollection_ReturnsTrue(Type destinationType)
{
// Arrange
var bindingContext = GetBindingContext(destinationType);
// Act
var result = ModelBindingHelper.CanGetCompatibleCollection<int>(bindingContext);
// Assert
Assert.True(result);
}
[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(int[]))]
[InlineData(typeof(IEnumerable<int>))]
[InlineData(typeof(IReadOnlyCollection<int>))]
[InlineData(typeof(IReadOnlyList<int>))]
[InlineData(typeof(ICollection<int>))]
[InlineData(typeof(IList<int>))]
[InlineData(typeof(List<int>))]
public void GetCompatibleCollection_ReturnsList(Type destinationType)
{
// Arrange
var bindingContext = GetBindingContext(destinationType);
// Act
var result = ModelBindingHelper.GetCompatibleCollection<int>(bindingContext);
// Assert
Assert.IsType<List<int>>(result);
}
[Theory]
[InlineData(typeof(Collection<int>))]
[InlineData(typeof(IntList))]
[InlineData(typeof(LinkedList<int>))]
public void GetCompatibleCollection_ActivatesCollection(Type destinationType)
{
// Arrange
var bindingContext = GetBindingContext(destinationType);
// Act
var result = ModelBindingHelper.GetCompatibleCollection<int>(bindingContext);
// Assert
Assert.IsType(destinationType, result);
}
[Fact]
public void GetCompatibleCollection_SetsCapacity()
{
// Arrange
var bindingContext = GetBindingContext(typeof(IList<int>));
// Act
var result = ModelBindingHelper.GetCompatibleCollection<int>(bindingContext, capacity: 23);
// Assert
var list = Assert.IsType<List<int>>(result);
Assert.Equal(23, list.Capacity);
}
[Theory]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ArrayProperty))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ArrayPropertyWithValue))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerableProperty))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerablePropertyWithArrayValue))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ListProperty))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ScalarProperty))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ScalarPropertyWithValue))]
public void CanGetCompatibleCollection_ReturnsFalse_IfReadOnly(string propertyName)
{
// Arrange
var bindingContext = GetBindingContextForProperty(propertyName);
// Act
var result = ModelBindingHelper.CanGetCompatibleCollection<int>(bindingContext);
// Assert
Assert.False(result);
}
[Theory]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerablePropertyWithArrayValueAndSetter))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerablePropertyWithListValue))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ListPropertyWithValue))]
public void CanGetCompatibleCollection_ReturnsTrue_IfCollection(string propertyName)
{
// Arrange
var bindingContext = GetBindingContextForProperty(propertyName);
// Act
var result = ModelBindingHelper.CanGetCompatibleCollection<int>(bindingContext);
// Assert
Assert.True(result);
}
[Theory]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerablePropertyWithListValue))]
[InlineData(nameof(ModelWithReadOnlyAndSpecialCaseProperties.ListPropertyWithValue))]
public void GetCompatibleCollection_ReturnsExistingCollection(string propertyName)
{
// Arrange
var bindingContext = GetBindingContextForProperty(propertyName);
// Act
var result = ModelBindingHelper.GetCompatibleCollection<int>(bindingContext);
// Assert
Assert.Same(bindingContext.Model, result);
var list = Assert.IsType<List<int>>(result);
Assert.Empty(list);
}
[Fact]
public void CanGetCompatibleCollection_ReturnsNewCollection()
{
// Arrange
var bindingContext = GetBindingContextForProperty(
nameof(ModelWithReadOnlyAndSpecialCaseProperties.EnumerablePropertyWithArrayValueAndSetter));
// Act
var result = ModelBindingHelper.GetCompatibleCollection<int>(bindingContext);
// Assert
Assert.NotSame(bindingContext.Model, result);
var list = Assert.IsType<List<int>>(result);
Assert.Empty(list);
}
[Theory]
[InlineData(typeof(Collection<string>))]
[InlineData(typeof(List<long>))]
[InlineData(typeof(MyModel))]
[InlineData(typeof(AbstractIntList))]
[InlineData(typeof(ISet<int>))]
public void CanGetCompatibleCollection_ReturnsFalse(Type destinationType)
{
// Arrange
var bindingContext = GetBindingContext(destinationType);
// Act
var result = ModelBindingHelper.CanGetCompatibleCollection<int>(bindingContext);
// Assert
Assert.False(result);
}
private static DefaultModelBindingContext GetBindingContextForProperty(string propertyName)
{
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithReadOnlyAndSpecialCaseProperties),
propertyName);
var bindingContext = GetBindingContext(metadataProvider, modelMetadata);
var container = new ModelWithReadOnlyAndSpecialCaseProperties();
bindingContext.Model = modelMetadata.PropertyGetter(container);
return bindingContext;
}
private static DefaultModelBindingContext GetBindingContext(Type modelType)
{
var metadataProvider = new EmptyModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(modelType);
return GetBindingContext(metadataProvider, metadata);
}
private static DefaultModelBindingContext GetBindingContext(
IModelMetadataProvider metadataProvider,
ModelMetadata metadata)
{
var bindingContext = new DefaultModelBindingContext
{
ModelMetadata = metadata,
OperationBindingContext = new OperationBindingContext
{
MetadataProvider = metadataProvider,
},
};
return bindingContext;
}
private class ModelWithReadOnlyAndSpecialCaseProperties
{
public int[] ArrayProperty { get; }
public int[] ArrayPropertyWithValue { get; } = new int[4];
public IEnumerable<int> EnumerableProperty { get; }
public IEnumerable<int> EnumerablePropertyWithArrayValue { get; } = new int[4];
// Special case: Value cannot be used but property can be set.
public IEnumerable<int> EnumerablePropertyWithArrayValueAndSetter { get; set; } = new int[4];
public IEnumerable<int> EnumerablePropertyWithListValue { get; } = new List<int> { 23 };
public List<int> ListProperty { get; }
public List<int> ListPropertyWithValue { get; } = new List<int> { 23 };
public int ScalarProperty { get; }
public int ScalarPropertyWithValue { get; } = 23;
}
private class MyClassWithoutConverter private class MyClassWithoutConverter
{ {
} }
private class MySubClassWithoutConverter : MyClassWithoutConverter
{
}
private abstract class AbstractIntList : List<int>
{
}
private class IntList : List<int>
{
}
private enum IntEnum private enum IntEnum
{ {
Value0 = 0, Value0 = 0,

View File

@ -77,6 +77,150 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState);
} }
private class ListContainer1
{
[ModelBinder(Name = "files")]
public List<IFormFile> ListProperty { get; set; }
}
[Fact]
public async Task BindCollectionProperty_WithData_IsBound()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(ListContainer1),
};
var data = "some data";
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => UpdateRequest(request, data, "files"));
var modelState = operationContext.ActionContext.ModelState;
// Act
var result = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert
Assert.True(result.IsModelSet);
// Model
var boundContainer = Assert.IsType<ListContainer1>(result.Model);
Assert.NotNull(boundContainer);
Assert.NotNull(boundContainer.ListProperty);
var file = Assert.Single(boundContainer.ListProperty);
Assert.Equal("form-data; name=files; filename=text.txt", file.ContentDisposition);
using (var reader = new StreamReader(file.OpenReadStream()))
{
Assert.Equal(data, reader.ReadToEnd());
}
// ModelState
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("files", kvp.Key);
var modelStateEntry = kvp.Value;
Assert.NotNull(modelStateEntry);
Assert.Empty(modelStateEntry.Errors);
Assert.Equal(ModelValidationState.Skipped, modelStateEntry.ValidationState);
Assert.Null(modelStateEntry.AttemptedValue);
Assert.Null(modelStateEntry.RawValue);
}
[Fact]
public async Task BindCollectionProperty_NoData_IsNotBound()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(ListContainer1),
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => UpdateRequest(request, data: null, name: null));
var modelState = operationContext.ActionContext.ModelState;
// Act
var result = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert
Assert.True(result.IsModelSet);
// Model (bound to an empty collection)
var boundContainer = Assert.IsType<ListContainer1>(result.Model);
Assert.NotNull(boundContainer);
Assert.Null(boundContainer.ListProperty);
// ModelState
Assert.True(modelState.IsValid);
Assert.Empty(modelState);
}
private class ListContainer2
{
[ModelBinder(Name = "files")]
public List<IFormFile> ListProperty { get; } = new List<IFormFile>
{
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file1"),
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file2"),
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file3"),
};
}
[Fact]
public async Task BindReadOnlyCollectionProperty_WithData_IsBound()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(ListContainer2),
};
var data = "some data";
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => UpdateRequest(request, data, "files"));
var modelState = operationContext.ActionContext.ModelState;
// Act
var result = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert
Assert.True(result.IsModelSet);
// Model
var boundContainer = Assert.IsType<ListContainer2>(result.Model);
Assert.NotNull(boundContainer);
Assert.NotNull(boundContainer.ListProperty);
var file = Assert.Single(boundContainer.ListProperty);
Assert.Equal("form-data; name=files; filename=text.txt", file.ContentDisposition);
using (var reader = new StreamReader(file.OpenReadStream()))
{
Assert.Equal(data, reader.ReadToEnd());
}
// ModelState
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("files", kvp.Key);
var modelStateEntry = kvp.Value;
Assert.NotNull(modelStateEntry);
Assert.Empty(modelStateEntry.Errors);
Assert.Equal(ModelValidationState.Skipped, modelStateEntry.ValidationState);
Assert.Null(modelStateEntry.AttemptedValue);
Assert.Null(modelStateEntry.RawValue);
}
[Fact] [Fact]
public async Task BindParameter_WithData_GetsBound() public async Task BindParameter_WithData_GetsBound()
{ {
@ -149,7 +293,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var modelState = operationContext.ActionContext.ModelState; var modelState = operationContext.ActionContext.ModelState;
// Act // Act
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult); var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert // Assert
Assert.Equal(default(ModelBindingResult), modelBindingResult); Assert.Equal(default(ModelBindingResult), modelBindingResult);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -81,11 +82,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
ParameterType = typeof(Person) ParameterType = typeof(Person)
}; };
// Do not add any headers. var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { request => request.Headers.Add("Header", new[] { "someValue" }));
request.Headers.Add("Header", new[] { "someValue" });
});
var modelState = operationContext.ActionContext.ModelState; var modelState = operationContext.ActionContext.ModelState;
// Act // Act
@ -154,6 +152,102 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(new string[] { "someValue" }, entry.Value.RawValue); Assert.Equal(new string[] { "someValue" }, entry.Value.RawValue);
} }
private class ListContainer1
{
[FromHeader(Name = "Header")]
public List<string> ListProperty { get; set; }
}
[Fact]
public async Task BindCollectionPropertyFromHeader_WithData_IsBound()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(ListContainer1),
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request.Headers.Add("Header", new[] { "someValue" }));
var modelState = operationContext.ActionContext.ModelState;
// Act
var result = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert
Assert.True(result.IsModelSet);
// Model
var boundContainer = Assert.IsType<ListContainer1>(result.Model);
Assert.NotNull(boundContainer);
Assert.NotNull(boundContainer.ListProperty);
var entry = Assert.Single(boundContainer.ListProperty);
Assert.Equal("someValue", entry);
// ModelState
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("Header", kvp.Key);
var modelStateEntry = kvp.Value;
Assert.NotNull(modelStateEntry);
Assert.Empty(modelStateEntry.Errors);
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
}
private class ListContainer2
{
[FromHeader(Name = "Header")]
public List<string> ListProperty { get; } = new List<string> { "One", "Two", "Three" };
}
[Fact]
public async Task BindReadOnlyCollectionPropertyFromHeader_WithData_IsBound()
{
// Arrange
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
var parameter = new ParameterDescriptor
{
Name = "Parameter1",
BindingInfo = new BindingInfo(),
ParameterType = typeof(ListContainer2),
};
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => request.Headers.Add("Header", new[] { "someValue" }));
var modelState = operationContext.ActionContext.ModelState;
// Act
var result = await argumentBinder.BindModelAsync(parameter, operationContext) ??
default(ModelBindingResult);
// Assert
Assert.True(result.IsModelSet);
// Model
var boundContainer = Assert.IsType<ListContainer2>(result.Model);
Assert.NotNull(boundContainer);
Assert.NotNull(boundContainer.ListProperty);
var entry = Assert.Single(boundContainer.ListProperty);
Assert.Equal("someValue", entry);
// ModelState
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("Header", kvp.Key);
var modelStateEntry = kvp.Value;
Assert.NotNull(modelStateEntry);
Assert.Empty(modelStateEntry.Errors);
Assert.Equal(ModelValidationState.Valid, modelStateEntry.ValidationState);
Assert.Equal("someValue", modelStateEntry.AttemptedValue);
Assert.Equal(new[] { "someValue" }, modelStateEntry.RawValue);
}
[Theory] [Theory]
[InlineData(typeof(string[]), "value1, value2, value3")] [InlineData(typeof(string[]), "value1, value2, value3")]
[InlineData(typeof(string), "value")] [InlineData(typeof(string), "value")]

View File

@ -517,17 +517,14 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var model = Assert.IsType<Order4>(modelBindingResult.Model); var model = Assert.IsType<Order4>(modelBindingResult.Model);
Assert.NotNull(model.Customer); Assert.NotNull(model.Customer);
Assert.Equal("bill", model.Customer.Name); Assert.Equal("bill", model.Customer.Name);
Assert.Empty(model.Customer.Documents); Assert.Null(model.Customer.Documents);
Assert.Equal(2, modelState.Count);
Assert.Equal(0, modelState.ErrorCount); Assert.Equal(0, modelState.ErrorCount);
Assert.True(modelState.IsValid); Assert.True(modelState.IsValid);
var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents").Value; var kvp = Assert.Single(modelState);
Assert.Null(entry.AttemptedValue); // FormFile entries don't include the model. Assert.Equal("parameter.Customer.Name", kvp.Key);
Assert.Null(entry.RawValue); var entry = kvp.Value;
entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
Assert.Equal("bill", entry.AttemptedValue); Assert.Equal("bill", entry.AttemptedValue);
Assert.Equal("bill", entry.RawValue); Assert.Equal("bill", entry.RawValue);
} }

View File

@ -3,9 +3,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features.Internal;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests namespace Microsoft.AspNetCore.Mvc.IntegrationTests
@ -959,6 +964,65 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Empty(modelState); Assert.Empty(modelState);
} }
[Fact]
public async Task TryUpdateModelAsync_TopLevelFormFileCollection_IsBound()
{
// Arrange
var data = "some data";
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
request => UpdateRequest(request, data, "files"));
var modelState = operationContext.ActionContext.ModelState;
var model = new List<IFormFile>
{
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file1"),
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file2"),
new FormFile(new MemoryStream(), baseStreamOffset: 0, length: 0, name: "file", fileName: "file3"),
};
// Act
var result = await TryUpdateModel(model, prefix: "files", operationContext: operationContext);
// Assert
Assert.True(result);
// Model
var file = Assert.Single(model);
Assert.Equal("form-data; name=files; filename=text.txt", file.ContentDisposition);
using (var reader = new StreamReader(file.OpenReadStream()))
{
Assert.Equal(data, reader.ReadToEnd());
}
// ModelState
Assert.True(modelState.IsValid);
var kvp = Assert.Single(modelState);
Assert.Equal("files", kvp.Key);
var modelStateEntry = kvp.Value;
Assert.NotNull(modelStateEntry);
Assert.Empty(modelStateEntry.Errors);
Assert.Equal(ModelValidationState.Skipped, modelStateEntry.ValidationState);
Assert.Null(modelStateEntry.AttemptedValue);
Assert.Null(modelStateEntry.RawValue);
}
private void UpdateRequest(HttpRequest request, string data, string name)
{
const string fileName = "text.txt";
var fileCollection = new FormFileCollection();
var formCollection = new FormCollection(new Dictionary<string, StringValues>(), fileCollection);
request.Form = formCollection;
request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq";
request.Headers["Content-Disposition"] = $"form-data; name={name}; filename={fileName}";
var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
fileCollection.Add(new FormFile(memoryStream, 0, data.Length, name, fileName)
{
Headers = request.Headers
});
}
private class CustomReadOnlyCollection<T> : ICollection<T> private class CustomReadOnlyCollection<T> : ICollection<T>
{ {
private ICollection<T> _original; private ICollection<T> _original;

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
@ -217,6 +218,70 @@ namespace Microsoft.AspNetCore.Mvc.Razor
Assert.IsType<ViewDataDictionary<string>>(viewContext.ViewData); Assert.IsType<ViewDataDictionary<string>>(viewContext.ViewData);
} }
[Fact]
public void Activate_Throws_WhenViewDataPropertyHasIncorrectType()
{
// Arrange
var activator = new RazorPageActivator(new EmptyModelMetadataProvider());
var instance = new HasIncorrectViewDataPropertyType();
var collection = new ServiceCollection();
collection
.AddSingleton<HtmlEncoder>(new HtmlTestEncoder())
.AddSingleton<DiagnosticSource>(new DiagnosticListener("Microsoft.AspNetCore.Mvc"));
var httpContext = new DefaultHttpContext
{
RequestServices = collection.BuildServiceProvider(),
};
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewContext = new ViewContext(
actionContext,
Mock.Of<IView>(),
new ViewDataDictionary(new EmptyModelMetadataProvider()),
Mock.Of<ITempDataDictionary>(),
TextWriter.Null,
new HtmlHelperOptions());
// Act & Assert
Assert.Throws<InvalidCastException>(() => activator.Activate(instance, viewContext));
}
[Fact]
public void Activate_CanGetUrlHelperFromDependencyInjection()
{
// Arrange
var activator = new RazorPageActivator(new EmptyModelMetadataProvider());
var instance = new HasUnusualIUrlHelperProperty();
// IUrlHelperFactory should not be used. But set it up to match a real configuration.
var collection = new ServiceCollection();
collection
.AddSingleton<IUrlHelperFactory, UrlHelperFactory>()
.AddSingleton<HtmlEncoder>(new HtmlTestEncoder())
.AddSingleton<DiagnosticSource>(new DiagnosticListener("Microsoft.AspNetCore.Mvc"))
.AddSingleton<IUrlHelperWrapper, UrlHelperWrapper>();
var httpContext = new DefaultHttpContext
{
RequestServices = collection.BuildServiceProvider(),
};
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewContext = new ViewContext(
actionContext,
Mock.Of<IView>(),
new ViewDataDictionary(new EmptyModelMetadataProvider()),
Mock.Of<ITempDataDictionary>(),
TextWriter.Null,
new HtmlHelperOptions());
// Act
activator.Activate(instance, viewContext);
// Assert
Assert.NotNull(instance.UrlHelper);
}
private abstract class TestPageBase<TModel> : RazorPage<TModel> private abstract class TestPageBase<TModel> : RazorPage<TModel>
{ {
[RazorInject] [RazorInject]
@ -258,6 +323,68 @@ namespace Microsoft.AspNetCore.Mvc.Razor
} }
} }
private class HasIncorrectViewDataPropertyType : RazorPage<MyModel>
{
[RazorInject]
public ViewDataDictionary<object> MoreViewData { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class HasUnusualIUrlHelperProperty : RazorPage<MyModel>
{
[RazorInject]
public IUrlHelperWrapper UrlHelper { get; set; }
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
}
private class UrlHelperWrapper : IUrlHelperWrapper
{
public ActionContext ActionContext
{
get
{
throw new NotImplementedException();
}
}
public string Action(UrlActionContext actionContext)
{
throw new NotImplementedException();
}
public string Content(string contentPath)
{
throw new NotImplementedException();
}
public bool IsLocalUrl(string url)
{
throw new NotImplementedException();
}
public string Link(string routeName, object values)
{
throw new NotImplementedException();
}
public string RouteUrl(UrlRouteContext routeContext)
{
throw new NotImplementedException();
}
}
private interface IUrlHelperWrapper : IUrlHelper
{
}
private class MyService : ICanHasViewContext private class MyService : ICanHasViewContext
{ {
public ViewContext ViewContext { get; private set; } public ViewContext ViewContext { get; private set; }

View File

@ -93,11 +93,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
Assert.Equal(expected, ex.Message); Assert.Equal(expected, ex.Message);
} }
[Fact] [Theory]
public void GetViewComponents_ThrowsIfInvokeReturnsATask() [InlineData(typeof(TaskReturningInvokeViewComponent))]
[InlineData(typeof(GenericTaskReturningInvokeViewComponent))]
public void GetViewComponents_ThrowsIfInvokeReturnsATask(Type type)
{ {
// Arrange // Arrange
var type = typeof(TaskReturningInvokeViewComponent);
var expected = $"Method 'Invoke' of view component '{type}' cannot return a Task."; var expected = $"Method 'Invoke' of view component '{type}' cannot return a Task.";
var provider = CreateProvider(type); var provider = CreateProvider(type);
@ -189,6 +190,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
public Task Invoke() => Task.FromResult(0); public Task Invoke() => Task.FromResult(0);
} }
public class GenericTaskReturningInvokeViewComponent
{
public Task<int> Invoke() => Task.FromResult(0);
}
public class VoidReturningInvokeViewComponent public class VoidReturningInvokeViewComponent
{ {
public void Invoke(int x) public void Invoke(int x)