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:
parent
ac58e8433c
commit
d3c24637b1
|
|
@ -7,9 +7,7 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
#if NETSTANDARD1_3
|
||||
using System.Reflection;
|
||||
#endif
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
|
@ -45,7 +43,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -85,7 +82,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
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.
|
||||
bindingContext.ModelState.SetModelValue(
|
||||
bindingContext.ModelName,
|
||||
|
|
@ -93,13 +90,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, model);
|
||||
return;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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>
|
||||
|
|
@ -126,15 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// <returns>An instance of <paramref name="targetType"/>.</returns>
|
||||
protected object CreateInstance(Type targetType)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Activator.CreateInstance(targetType);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Details of exception are not important.
|
||||
return null;
|
||||
}
|
||||
return Activator.CreateInstance(targetType);
|
||||
}
|
||||
|
||||
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
|
||||
|
|
|
|||
|
|
@ -113,10 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
return collection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
|
||||
var newCollection = CreateInstance(targetType);
|
||||
CopyToModel(newCollection, collection);
|
||||
|
||||
return newCollection;
|
||||
return base.ConvertToCollectionType(targetType, collection);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -128,7 +125,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
#if NETSTANDARD1_3
|
||||
|
|
@ -12,7 +13,6 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
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
|
||||
// using Task.FromResult. If you need to make changes of this nature, profile
|
||||
// allocations afterwards and look for Task<ModelBindingResult>.
|
||||
// using Task.FromResult or async state machines.
|
||||
|
||||
if (bindingContext.ModelType != typeof(IFormFile) &&
|
||||
!typeof(IEnumerable<IFormFile>).IsAssignableFrom(bindingContext.ModelType))
|
||||
var modelType = 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 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
|
||||
// and so we'll do the right thing even though we 'fell-back' to the empty prefix.
|
||||
var modelName = bindingContext.IsTopLevelObject
|
||||
? bindingContext.BinderModelName ?? bindingContext.FieldName
|
||||
: bindingContext.ModelName;
|
||||
|
||||
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
|
||||
|
||||
object value;
|
||||
if (bindingContext.ModelType == typeof(IFormFile))
|
||||
{
|
||||
var postedFiles = await GetFormFilesAsync(modelName, bindingContext);
|
||||
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()
|
||||
if (postedFiles.Count == 0)
|
||||
{
|
||||
Key = modelName,
|
||||
SuppressValidation = true
|
||||
});
|
||||
// Silently fail if the named file does not exist in the request.
|
||||
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
|
||||
return;
|
||||
}
|
||||
|
||||
bindingContext.ModelState.SetModelValue(
|
||||
modelName,
|
||||
rawValue: null,
|
||||
attemptedValue: null);
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, value);
|
||||
return;
|
||||
value = postedFiles.First();
|
||||
}
|
||||
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 postedFiles = new List<IFormFile>();
|
||||
if (request.HasFormContentType)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
#if NETSTANDARD1_3
|
||||
using System.Reflection;
|
||||
#endif
|
||||
|
|
@ -28,8 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
// 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
|
||||
// allocations afterwards and look for Task<ModelBindingResult>.
|
||||
// using Task.FromResult or async state machines.
|
||||
|
||||
var allowedBindingSource = bindingContext.BindingSource;
|
||||
if (allowedBindingSource == null ||
|
||||
|
|
@ -41,34 +38,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
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).
|
||||
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 (value != null)
|
||||
if (bindingContext.ModelType == typeof(string))
|
||||
{
|
||||
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);
|
||||
if (values.Length > 0)
|
||||
{
|
||||
model = ModelBindingHelper.ConvertValuesToCollectionType(
|
||||
bindingContext.ModelType,
|
||||
values);
|
||||
}
|
||||
// An unsupported datatype or a new collection is needed (perhaps because target type is an array) but
|
||||
// can't assign it to the property.
|
||||
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);
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -78,8 +78,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
request.Headers[headerName]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -748,46 +748,146 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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.
|
||||
//
|
||||
// For the simple cases - choose a T[] or List<T> if the destination type supports
|
||||
// it.
|
||||
//
|
||||
// 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))
|
||||
var model = bindingContext.Model;
|
||||
var modelType = bindingContext.ModelType;
|
||||
var writeable = !bindingContext.ModelMetadata.IsReadOnly;
|
||||
if (typeof(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().IsAbstract &&
|
||||
typeof(ICollection<T>).IsAssignableFrom(modelType))
|
||||
{
|
||||
var result = (ICollection<T>)Activator.CreateInstance(modelType);
|
||||
foreach (var value in values)
|
||||
{
|
||||
result.Add(value);
|
||||
}
|
||||
typeof(ICollection<T>).IsAssignableFrom(modelType);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (typeof(IEnumerable<T>).IsAssignableFrom(modelType))
|
||||
/// <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>
|
||||
/// <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>
|
||||
|
|
@ -860,7 +960,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null;
|
||||
}
|
||||
|
||||
if (value.GetType().IsAssignableFrom(type))
|
||||
if (type.IsAssignableFrom(value.GetType()))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
|
@ -917,7 +1017,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,10 +110,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
Func<ViewContext, object> valueAccessor;
|
||||
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;
|
||||
}
|
||||
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 =>
|
||||
{
|
||||
var serviceProvider = context.HttpContext.RequestServices;
|
||||
|
|
|
|||
|
|
@ -115,13 +115,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
}
|
||||
else
|
||||
{
|
||||
// Will invoke synchronously. Method must not return void, Task or Task<T>.
|
||||
if (selectedMethod.ReturnType == typeof(void))
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(
|
||||
SyncMethodName,
|
||||
componentName));
|
||||
}
|
||||
else if (selectedMethod.ReturnType.IsAssignableFrom(typeof(Task)))
|
||||
else if (typeof(Task).IsAssignableFrom(selectedMethod.ReturnType))
|
||||
{
|
||||
throw new InvalidOperationException(Resources.FormatViewComponent_SyncMethod_CannotReturnTask(
|
||||
SyncMethodName,
|
||||
|
|
|
|||
|
|
@ -317,8 +317,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{ typeof(List<int>), true },
|
||||
{ typeof(LinkedList<int>), true },
|
||||
{ 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; }
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -388,9 +388,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
{ typeof(IDictionary<int, int>), true },
|
||||
{ typeof(Dictionary<int, int>), true },
|
||||
{ typeof(SortedDictionary<int, int>), true },
|
||||
{ typeof(IList<KeyValuePair<int, int>>), false },
|
||||
{ typeof(DictionaryWithInternalConstructor<int, int>), false },
|
||||
{ typeof(DictionaryWithThrowingConstructor<int, int>), false },
|
||||
{ typeof(IList<KeyValuePair<int, int>>), true },
|
||||
{ typeof(ISet<KeyValuePair<int, int>>), false },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -554,22 +553,5 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -43,9 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("file", "file1.txt"));
|
||||
formFiles.Add(GetMockFormFile("file", "file2.txt"));
|
||||
var formFiles = GetTwoFiles();
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
|
||||
var binder = new FormFileModelBinder();
|
||||
|
|
@ -66,13 +63,38 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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]
|
||||
public async Task FormFileModelBinder_ExpectSingleFile_BindFirstFile()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("file", "file1.txt"));
|
||||
formFiles.Add(GetMockFormFile("file", "file2.txt"));
|
||||
var formFiles = GetTwoFiles();
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||
var binder = new FormFileModelBinder();
|
||||
|
|
@ -86,8 +108,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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]
|
||||
public async Task FormFileModelBinder_ReturnsNothing_WhenNoFilePosted()
|
||||
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNoFilePosted()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
|
|
@ -100,11 +140,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
// Assert
|
||||
Assert.NotEqual(default(ModelBindingResult), result);
|
||||
Assert.False(result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_ReturnsNothing_WhenNamesDontMatch()
|
||||
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
|
|
@ -118,6 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
// Assert
|
||||
Assert.NotEqual(default(ModelBindingResult), result);
|
||||
Assert.False(result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_ReturnsNothing_WithEmptyContentDisposition()
|
||||
public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
|
|
@ -165,11 +207,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
// Assert
|
||||
Assert.NotEqual(default(ModelBindingResult), result);
|
||||
Assert.False(result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_ReturnsNothing_WithNoFileNameAndZeroLength()
|
||||
public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
|
|
@ -183,15 +226,75 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
// Assert
|
||||
Assert.NotEqual(default(ModelBindingResult), result);
|
||||
Assert.False(result.IsModelSet);
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
ModelMetadata = metadataProvider.GetMetadataForType(modelType),
|
||||
ModelMetadata = metadata,
|
||||
ModelName = "file",
|
||||
ModelState = new ModelStateDictionary(),
|
||||
OperationBindingContext = new OperationBindingContext
|
||||
|
|
@ -218,6 +321,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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)
|
||||
{
|
||||
var formCollection = new Mock<IFormCollection>();
|
||||
|
|
@ -233,5 +347,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
return formFile.Object;
|
||||
}
|
||||
|
||||
private class ModelWithReadOnlyArray
|
||||
{
|
||||
public IFormFile[] ArrayProperty { get; }
|
||||
}
|
||||
|
||||
private class FileList : List<IFormFile>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Xunit;
|
||||
|
|
@ -72,6 +73,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
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]
|
||||
public async Task HeaderBinder_ReturnsNothing_ForNullBindingSource()
|
||||
{
|
||||
|
|
@ -116,11 +145,76 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
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)
|
||||
{
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Header);
|
||||
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
|
||||
{
|
||||
ModelMetadata = modelMetadata,
|
||||
|
|
@ -142,5 +236,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
|
|||
|
||||
return bindingContext;
|
||||
}
|
||||
|
||||
private class ModelWithReadOnlyArray
|
||||
{
|
||||
public string[] ArrayProperty { get; }
|
||||
}
|
||||
|
||||
private class StringList : List<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
|
|
@ -12,10 +13,8 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations;
|
|||
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Test;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -550,10 +549,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{ "ExcludedProperty", "ExcludedPropertyValue" }
|
||||
};
|
||||
|
||||
Func<ModelBindingContext, string, bool> includePredicate =
|
||||
(context, propertyName) =>
|
||||
string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
|
||||
Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) =>
|
||||
string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var valueProvider = new TestValueProvider(values);
|
||||
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
|
|
@ -658,8 +656,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
|
||||
var binder = new StubModelBinder();
|
||||
var model = new MyModel();
|
||||
Func<ModelBindingContext, string, bool> includePredicate =
|
||||
(context, propertyName) => true;
|
||||
Func<ModelBindingContext, string, bool> includePredicate = (context, propertyName) => true;
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(
|
||||
|
|
@ -1018,7 +1015,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToReturnsNullIfTrimmedValueIsEmptyString()
|
||||
public void ConvertToReturnsNull_IfConvertingNullToArrayType()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
|
|
@ -1244,21 +1241,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
() => 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]
|
||||
public void ConvertToUsesProvidedCulture()
|
||||
{
|
||||
|
|
@ -1308,43 +1290,80 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
// None of the types here have converters from MyClassWithoutConverter.
|
||||
[Theory]
|
||||
[InlineData(typeof(TimeSpan))]
|
||||
[InlineData(typeof(DateTime))]
|
||||
[InlineData(typeof(DateTimeOffset))]
|
||||
[InlineData(typeof(Guid))]
|
||||
[InlineData(typeof(IntEnum))]
|
||||
public void ConvertTo_Throws_IfValueIsNotStringData(Type destinationType)
|
||||
public void ConvertTo_Throws_IfValueIsNotConvertible(Type destinationType)
|
||||
{
|
||||
// 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>(
|
||||
() => 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);
|
||||
}
|
||||
|
||||
// String does not have a converter to MyClassWithoutConverter.
|
||||
[Fact]
|
||||
public void ConvertTo_Throws_IfDestinationTypeIsNotConvertible()
|
||||
{
|
||||
// Arrange
|
||||
var value = "Hello world";
|
||||
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>(
|
||||
() => 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
|
||||
var expectedMessage = string.Format("The parameter conversion from type '{0}' to type '{1}' " +
|
||||
"failed because no type converter can convert between these types.",
|
||||
value.GetType(), typeof(MyClassWithoutConverter));
|
||||
Assert.Equal(expectedMessage, ex.Message);
|
||||
Assert.Same(value, result);
|
||||
}
|
||||
|
||||
[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]
|
||||
|
|
@ -1367,10 +1386,246 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
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 MySubClassWithoutConverter : MyClassWithoutConverter
|
||||
{
|
||||
}
|
||||
|
||||
private abstract class AbstractIntList : List<int>
|
||||
{
|
||||
}
|
||||
|
||||
private class IntList : List<int>
|
||||
{
|
||||
}
|
||||
|
||||
private enum IntEnum
|
||||
{
|
||||
Value0 = 0,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,150 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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]
|
||||
public async Task BindParameter_WithData_GetsBound()
|
||||
{
|
||||
|
|
@ -149,7 +293,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
var modelState = operationContext.ActionContext.ModelState;
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ?? default(ModelBindingResult);
|
||||
var modelBindingResult = await argumentBinder.BindModelAsync(parameter, operationContext) ??
|
||||
default(ModelBindingResult);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(default(ModelBindingResult), modelBindingResult);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -81,11 +82,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
ParameterType = typeof(Person)
|
||||
};
|
||||
|
||||
// Do not add any headers.
|
||||
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => {
|
||||
request.Headers.Add("Header", new[] { "someValue" });
|
||||
});
|
||||
|
||||
var operationContext = ModelBindingTestHelper.GetOperationBindingContext(
|
||||
request => request.Headers.Add("Header", new[] { "someValue" }));
|
||||
var modelState = operationContext.ActionContext.ModelState;
|
||||
|
||||
// Act
|
||||
|
|
@ -154,6 +152,102 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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]
|
||||
[InlineData(typeof(string[]), "value1, value2, value3")]
|
||||
[InlineData(typeof(string), "value")]
|
||||
|
|
|
|||
|
|
@ -517,17 +517,14 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
var model = Assert.IsType<Order4>(modelBindingResult.Model);
|
||||
Assert.NotNull(model.Customer);
|
||||
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.True(modelState.IsValid);
|
||||
|
||||
var entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Documents").Value;
|
||||
Assert.Null(entry.AttemptedValue); // FormFile entries don't include the model.
|
||||
Assert.Null(entry.RawValue);
|
||||
|
||||
entry = Assert.Single(modelState, e => e.Key == "parameter.Customer.Name").Value;
|
||||
var kvp = Assert.Single(modelState);
|
||||
Assert.Equal("parameter.Customer.Name", kvp.Key);
|
||||
var entry = kvp.Value;
|
||||
Assert.Equal("bill", entry.AttemptedValue);
|
||||
Assert.Equal("bill", entry.RawValue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features.Internal;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||
|
|
@ -959,6 +964,65 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
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 ICollection<T> _original;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||
|
|
@ -217,6 +218,70 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
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>
|
||||
{
|
||||
[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
|
||||
{
|
||||
public ViewContext ViewContext { get; private set; }
|
||||
|
|
|
|||
|
|
@ -93,11 +93,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetViewComponents_ThrowsIfInvokeReturnsATask()
|
||||
[Theory]
|
||||
[InlineData(typeof(TaskReturningInvokeViewComponent))]
|
||||
[InlineData(typeof(GenericTaskReturningInvokeViewComponent))]
|
||||
public void GetViewComponents_ThrowsIfInvokeReturnsATask(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(TaskReturningInvokeViewComponent);
|
||||
var expected = $"Method 'Invoke' of view component '{type}' cannot return a Task.";
|
||||
var provider = CreateProvider(type);
|
||||
|
||||
|
|
@ -189,6 +190,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
|||
public Task Invoke() => Task.FromResult(0);
|
||||
}
|
||||
|
||||
public class GenericTaskReturningInvokeViewComponent
|
||||
{
|
||||
public Task<int> Invoke() => Task.FromResult(0);
|
||||
}
|
||||
|
||||
public class VoidReturningInvokeViewComponent
|
||||
{
|
||||
public void Invoke(int x)
|
||||
|
|
|
|||
Loading…
Reference in New Issue