Bind to readonly non-`null` collections
- part 1/2 of #2294 - handle readonly non-`null` collections in relevant binders - `CollectionModelBinder.CopyToModel()` and `MutableObjectModelBinder.AddToProperty()` methods - handle read-only controller properties in `DefaultControllerActionArgumentBinder` - do not copy into arrays e.g. add `CopyToModel()` override in `ArrayModelBinder` - remove ability to set a private controller property - confirm `SetMethod.IsPublic` in `DefaultControllerActionArgumentBinder` - avoid NREs in `GetModel()` overrides Test handling of readonly collections - previous tests barely touched this scenario - also add more tests setting controller properties nits: - add missing `[NotNull]` attributes - add missing doc comments - consolidate a few `[Fact]`s into `[Theory]`s - simplify some wrapping; shorten a few lines - remove dead code in `DefaultControllerActionArgumentBinder` and `ControllerActionArgumentBinderTests`
This commit is contained in:
parent
a80a333fea
commit
3fd4991959
|
|
@ -4,12 +4,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.Core;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Framework.Internal;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
@ -19,6 +19,10 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
public class DefaultControllerActionArgumentBinder : IControllerActionArgumentBinder
|
||||
{
|
||||
private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod =
|
||||
typeof(DefaultControllerActionArgumentBinder).GetTypeInfo().GetDeclaredMethod(
|
||||
nameof(CallPropertyAddRange));
|
||||
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly IObjectModelValidator _validator;
|
||||
|
||||
|
|
@ -64,12 +68,11 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
|
||||
public async Task<ModelBindingResult> BindModelAsync(
|
||||
ParameterDescriptor parameter,
|
||||
ModelStateDictionary modelState,
|
||||
OperationBindingContext operationContext)
|
||||
[NotNull] ParameterDescriptor parameter,
|
||||
[NotNull] ModelStateDictionary modelState,
|
||||
[NotNull] OperationBindingContext operationContext)
|
||||
{
|
||||
var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
|
||||
var parameterType = parameter.ParameterType;
|
||||
var modelBindingContext = GetModelBindingContext(
|
||||
parameter.Name,
|
||||
metadata,
|
||||
|
|
@ -98,6 +101,21 @@ namespace Microsoft.AspNet.Mvc
|
|||
return modelBindingResult;
|
||||
}
|
||||
|
||||
// Called via reflection.
|
||||
private static void CallPropertyAddRange<TElement>(object target, object source)
|
||||
{
|
||||
var targetCollection = (ICollection<TElement>)target;
|
||||
var sourceCollection = source as IEnumerable<TElement>;
|
||||
if (sourceCollection != null && !targetCollection.IsReadOnly)
|
||||
{
|
||||
targetCollection.Clear();
|
||||
foreach (var item in sourceCollection)
|
||||
{
|
||||
targetCollection.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateProperties(object controller, Type containerType, Dictionary<string, object> properties)
|
||||
{
|
||||
var propertyHelpers = PropertyHelper.GetProperties(controller);
|
||||
|
|
@ -105,17 +123,47 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
var propertyHelper = propertyHelpers.First(helper =>
|
||||
string.Equals(helper.Name, property.Key, StringComparison.Ordinal));
|
||||
if (propertyHelper.Property == null || !propertyHelper.Property.CanWrite)
|
||||
var propertyType = propertyHelper.Property.PropertyType;
|
||||
var source = property.Value;
|
||||
if (propertyHelper.Property.CanWrite && propertyHelper.Property.SetMethod?.IsPublic == true)
|
||||
{
|
||||
// nothing to do
|
||||
return;
|
||||
// Handle settable property. Do not set the property if the type is a non-nullable type.
|
||||
if (source != null || propertyType.AllowsNullValue())
|
||||
{
|
||||
propertyHelper.SetValue(controller, source);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not set the property if the type is a non nullable type.
|
||||
if (property.Value != null || propertyHelper.Property.PropertyType.AllowsNullValue())
|
||||
if (propertyType.IsArray)
|
||||
{
|
||||
propertyHelper.SetValue(controller, property.Value);
|
||||
// Do not attempt to copy values into an array because an array's length is immutable. This choice
|
||||
// is also consistent with MutableObjectModelBinder's handling of a read-only array property.
|
||||
continue;
|
||||
}
|
||||
|
||||
var target = propertyHelper.GetValue(controller);
|
||||
if (source == null || target == null)
|
||||
{
|
||||
// Nothing to do when source or target is null.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine T if this is an ICollection<T> property.
|
||||
var collectionTypeArguments = propertyType
|
||||
.ExtractGenericInterface(typeof(ICollection<>))
|
||||
?.GenericTypeArguments;
|
||||
if (collectionTypeArguments == null)
|
||||
{
|
||||
// Not a collection model.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle a read-only collection property.
|
||||
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(
|
||||
collectionTypeArguments);
|
||||
propertyAddRange.Invoke(obj: null, parameters: new[] { target, source });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,18 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IModelBinder"/> implementation for binding array values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">Type of elements in the array.</typeparam>
|
||||
public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
|
||||
{
|
||||
public override Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
|
||||
/// <inheritdoc />
|
||||
public override Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext.ModelMetadata.IsReadOnly)
|
||||
{
|
||||
|
|
@ -19,9 +25,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
return base.BindModelAsync(bindingContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetModel(IEnumerable<TElement> newCollection)
|
||||
{
|
||||
return newCollection.ToArray();
|
||||
return newCollection?.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void CopyToModel([NotNull] object target, IEnumerable<TElement> sourceCollection)
|
||||
{
|
||||
// Do not attempt to copy values into an array because an array's length is immutable. This choice is also
|
||||
// consistent with MutableObjectModelBinder's handling of a read-only array property.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,23 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IModelBinder"/> implementation for binding collection values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">Type of elements in the collection.</typeparam>
|
||||
public class CollectionModelBinder<TElement> : IModelBinder
|
||||
{
|
||||
public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||
|
||||
|
|
@ -23,19 +30,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
|
||||
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
|
||||
var bindCollectionTask = valueProviderResult != null ?
|
||||
BindSimpleCollection(bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture) :
|
||||
BindComplexCollection(bindingContext);
|
||||
var boundCollection = await bindCollectionTask;
|
||||
var model = GetModel(boundCollection);
|
||||
|
||||
IEnumerable<TElement> boundCollection;
|
||||
if (valueProviderResult == null)
|
||||
{
|
||||
boundCollection = await BindComplexCollection(bindingContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
boundCollection = await BindSimpleCollection(
|
||||
bindingContext,
|
||||
valueProviderResult.RawValue,
|
||||
valueProviderResult.Culture);
|
||||
}
|
||||
|
||||
var model = bindingContext.Model;
|
||||
if (model == null)
|
||||
{
|
||||
model = GetModel(boundCollection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Special case for TryUpdateModelAsync(collection, ...) scenarios. Model is null in all other cases.
|
||||
CopyToModel(model, boundCollection);
|
||||
}
|
||||
|
||||
return new ModelBindingResult(model, bindingContext.ModelName, isModelSet: true);
|
||||
}
|
||||
|
||||
// Used when the ValueProvider contains the collection to be bound as a single element, e.g. the raw value
|
||||
// is [ "1", "2" ] and needs to be converted to an int[].
|
||||
internal async Task<IEnumerable<TElement>> BindSimpleCollection(ModelBindingContext bindingContext,
|
||||
object rawValue,
|
||||
CultureInfo culture)
|
||||
internal async Task<IEnumerable<TElement>> BindSimpleCollection(
|
||||
ModelBindingContext bindingContext,
|
||||
object rawValue,
|
||||
CultureInfo culture)
|
||||
{
|
||||
if (rawValue == null)
|
||||
{
|
||||
|
|
@ -62,7 +90,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
};
|
||||
|
||||
object boundValue = null;
|
||||
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
|
||||
var result =
|
||||
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(innerBindingContext);
|
||||
if (result != null)
|
||||
{
|
||||
boundValue = result.Model;
|
||||
|
|
@ -79,11 +108,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
var indexPropertyName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "index");
|
||||
var valueProviderResultIndex = await bindingContext.ValueProvider.GetValueAsync(indexPropertyName);
|
||||
var indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(valueProviderResultIndex);
|
||||
|
||||
return await BindComplexCollectionFromIndexes(bindingContext, indexNames);
|
||||
}
|
||||
|
||||
internal async Task<IEnumerable<TElement>> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext,
|
||||
IEnumerable<string> indexNames)
|
||||
internal async Task<IEnumerable<TElement>> BindComplexCollectionFromIndexes(
|
||||
ModelBindingContext bindingContext,
|
||||
IEnumerable<string> indexNames)
|
||||
{
|
||||
bool indexNamesIsFinite;
|
||||
if (indexNames != null)
|
||||
|
|
@ -114,7 +145,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
var modelType = bindingContext.ModelType;
|
||||
|
||||
var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
|
||||
var result =
|
||||
await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childBindingContext);
|
||||
if (result != null)
|
||||
{
|
||||
didBind = true;
|
||||
|
|
@ -133,13 +165,50 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
return boundCollection;
|
||||
}
|
||||
|
||||
// Extensibility point that allows the bound collection to be manipulated or transformed before
|
||||
// being returned from the binder.
|
||||
/// <summary>
|
||||
/// Gets an <see cref="object"/> assignable to the collection property.
|
||||
/// </summary>
|
||||
/// <param name="newCollection">
|
||||
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see cref="object"/> assignable to the collection property. Or <c>null</c> if nothing was bound.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Extensibility point that allows the bound collection to be manipulated or transformed before being
|
||||
/// returned from the binder.
|
||||
/// </remarks>
|
||||
protected virtual object GetModel(IEnumerable<TElement> newCollection)
|
||||
{
|
||||
// Depends on fact BindSimpleCollection() and BindComplexCollection() always return a List<TElement>
|
||||
// instance or null. In addition GenericModelBinder confirms a List<TElement> is assignable to the
|
||||
// property prior to instantiating this binder and subclass binders do not call this method.
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds values from <paramref name="sourceCollection"/> to given <paramref name="target"/>.
|
||||
/// </summary>
|
||||
/// <param name="target"><see cref="object"/> into which values are copied.</param>
|
||||
/// <param name="sourceCollection">
|
||||
/// Collection of values retrieved from value providers. Or <c>null</c> if nothing was bound.
|
||||
/// </param>
|
||||
/// <remarks>Called only in TryUpdateModelAsync(collection, ...) scenarios.</remarks>
|
||||
protected virtual void CopyToModel([NotNull] object target, IEnumerable<TElement> sourceCollection)
|
||||
{
|
||||
var targetCollection = target as ICollection<TElement>;
|
||||
Debug.Assert(targetCollection != null); // This binder is instantiated only for ICollection model types.
|
||||
|
||||
if (sourceCollection != null && targetCollection != null && !targetCollection.IsReadOnly)
|
||||
{
|
||||
targetCollection.Clear();
|
||||
foreach (var element in sourceCollection)
|
||||
{
|
||||
targetCollection.Add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static object[] RawValueToObjectArray(object rawValue)
|
||||
{
|
||||
// precondition: rawValue is not null
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IModelBinder"/> implementation for binding dictionary values.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">Type of keys in the dictionary.</typeparam>
|
||||
/// <typeparam name="TValue">Type of values in the dictionary.</typeparam>
|
||||
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override object GetModel(IEnumerable<KeyValuePair<TKey, TValue>> newCollection)
|
||||
{
|
||||
return newCollection.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return newCollection?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,20 @@ using System.Reflection;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IModelBinder"/> implementation for binding complex values.
|
||||
/// </summary>
|
||||
public class MutableObjectModelBinder : IModelBinder
|
||||
{
|
||||
public virtual async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
|
||||
private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod =
|
||||
typeof(MutableObjectModelBinder).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyAddRange));
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||
if (!CanBindType(bindingContext.ModelMetadata))
|
||||
|
|
@ -44,7 +52,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
isModelSet: true);
|
||||
}
|
||||
|
||||
protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
|
||||
/// <summary>
|
||||
/// Gets an indication whether a property with the given <paramref name="propertyMetadata"/> can be updated.
|
||||
/// </summary>
|
||||
/// <param name="propertyMetadata"><see cref="ModelMetadata"/> for the property of interest.</param>
|
||||
/// <returns><c>true</c> if the property can be updated; <c>false</c> otherwise.</returns>
|
||||
/// <remarks>Should return <c>true</c> only for properties <see cref="SetProperty"/> can update.</remarks>
|
||||
protected virtual bool CanUpdateProperty([NotNull] ModelMetadata propertyMetadata)
|
||||
{
|
||||
return CanUpdatePropertyInternal(propertyMetadata);
|
||||
}
|
||||
|
|
@ -255,14 +269,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childContext);
|
||||
}
|
||||
|
||||
protected virtual object CreateModel(ModelBindingContext bindingContext)
|
||||
/// <summary>
|
||||
/// Creates suitable <see cref="object"/> for given <paramref name="bindingContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
|
||||
/// <returns>An <see cref="object"/> compatible with <see cref="ModelBindingContext.ModelType"/>.</returns>
|
||||
protected virtual object CreateModel([NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
// If the Activator throws an exception, we want to propagate it back up the call stack, since the
|
||||
// application developer should know that this was an invalid type to try to bind to.
|
||||
return Activator.CreateInstance(bindingContext.ModelType);
|
||||
}
|
||||
|
||||
protected virtual void EnsureModel(ModelBindingContext bindingContext)
|
||||
/// <summary>
|
||||
/// Ensures <see cref="ModelBindingContext.Model"/> is not <c>null</c> in given
|
||||
/// <paramref name="bindingContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
|
||||
protected virtual void EnsureModel([NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
if (bindingContext.Model == null)
|
||||
{
|
||||
|
|
@ -270,7 +294,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
|
||||
/// <summary>
|
||||
/// Gets the collection of <see cref="ModelMetadata"/> for properties this binder should update.
|
||||
/// </summary>
|
||||
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
|
||||
/// <returns>Collection of <see cref="ModelMetadata"/> for properties this binder should update.</returns>
|
||||
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(
|
||||
[NotNull] ModelBindingContext bindingContext)
|
||||
{
|
||||
var validationInfo = GetPropertyValidationInfo(bindingContext);
|
||||
var newPropertyFilter = GetPropertyFilter();
|
||||
|
|
@ -404,11 +434,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a property in the current <see cref="ModelBindingContext.Model"/>.
|
||||
/// </summary>
|
||||
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
|
||||
/// <param name="modelExplorer">
|
||||
/// The <see cref="ModelExplorer"/> for the model containing property to set.
|
||||
/// </param>
|
||||
/// <param name="propertyMetadata">The <see cref="ModelMetadata"/> for the property to set.</param>
|
||||
/// <param name="dtoResult">The <see cref="ModelBindingResult"/> for the property's new value.</param>
|
||||
/// <param name="requiredValidator">
|
||||
/// The <see cref="IModelValidator"/> which ensures the value is not <c>null</c>. Or <c>null</c> if no such
|
||||
/// requirement exists.
|
||||
/// </param>
|
||||
/// <remarks>Should succeed in all cases that <see cref="CanUpdateProperty"/> returns <c>true</c>.</remarks>
|
||||
protected virtual void SetProperty(
|
||||
ModelBindingContext bindingContext,
|
||||
ModelExplorer modelExplorer,
|
||||
ModelMetadata propertyMetadata,
|
||||
ModelBindingResult dtoResult,
|
||||
[NotNull] ModelBindingContext bindingContext,
|
||||
[NotNull] ModelExplorer modelExplorer,
|
||||
[NotNull] ModelMetadata propertyMetadata,
|
||||
[NotNull] ModelBindingResult dtoResult,
|
||||
IModelValidator requiredValidator)
|
||||
{
|
||||
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
|
||||
|
|
@ -416,9 +460,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
propertyMetadata.PropertyName,
|
||||
bindingFlags);
|
||||
|
||||
if (property == null || !property.CanWrite)
|
||||
if (property == null)
|
||||
{
|
||||
// nothing to do
|
||||
// Nothing to do if property does not exist.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!property.CanWrite)
|
||||
{
|
||||
// Try to handle as a collection if property exists but is not settable.
|
||||
AddToProperty(bindingContext, modelExplorer, property, dtoResult);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -466,21 +517,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
{
|
||||
propertyMetadata.PropertySetter(bindingContext.Model, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception exception)
|
||||
{
|
||||
// don't display a duplicate error message if a binding error has already occurred for this field
|
||||
var targetInvocationException = ex as TargetInvocationException;
|
||||
if (targetInvocationException != null &&
|
||||
targetInvocationException.InnerException != null)
|
||||
{
|
||||
ex = targetInvocationException.InnerException;
|
||||
}
|
||||
var modelStateKey = dtoResult.Key;
|
||||
var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey);
|
||||
if (validationState == ModelValidationState.Unvalidated)
|
||||
{
|
||||
bindingContext.ModelState.AddModelError(modelStateKey, ex);
|
||||
}
|
||||
AddModelError(exception, bindingContext, dtoResult);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -496,6 +535,82 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
// Neither [DefaultValue] nor [Required] is relevant for a non-settable collection.
|
||||
private void AddToProperty(
|
||||
ModelBindingContext bindingContext,
|
||||
ModelExplorer modelExplorer,
|
||||
PropertyInfo property,
|
||||
ModelBindingResult dtoResult)
|
||||
{
|
||||
var propertyExplorer = modelExplorer.GetExplorerForProperty(property.Name);
|
||||
|
||||
var target = propertyExplorer.Model;
|
||||
var source = dtoResult.Model;
|
||||
if (!dtoResult.IsModelSet || target == null || source == null)
|
||||
{
|
||||
// Cannot copy to or from a null collection.
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine T if this is an ICollection<T> property. No need for a T[] case because CanUpdateProperty()
|
||||
// ensures property is either settable or not an array. Underlying assumption is that CanUpdateProperty()
|
||||
// and SetProperty() are overridden together.
|
||||
var collectionTypeArguments = propertyExplorer.ModelType
|
||||
.ExtractGenericInterface(typeof(ICollection<>))
|
||||
?.GenericTypeArguments;
|
||||
if (collectionTypeArguments == null)
|
||||
{
|
||||
// Not a collection model.
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(collectionTypeArguments);
|
||||
try
|
||||
{
|
||||
propertyAddRange.Invoke(obj: null, parameters: new[] { target, source });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
AddModelError(exception, bindingContext, dtoResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Called via reflection.
|
||||
private static void CallPropertyAddRange<TElement>(object target, object source)
|
||||
{
|
||||
var targetCollection = (ICollection<TElement>)target;
|
||||
var sourceCollection = source as IEnumerable<TElement>;
|
||||
if (sourceCollection != null && !targetCollection.IsReadOnly)
|
||||
{
|
||||
targetCollection.Clear();
|
||||
foreach (var item in sourceCollection)
|
||||
{
|
||||
targetCollection.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddModelError(
|
||||
Exception exception,
|
||||
ModelBindingContext bindingContext,
|
||||
ModelBindingResult dtoResult)
|
||||
{
|
||||
var targetInvocationException = exception as TargetInvocationException;
|
||||
if (targetInvocationException != null && targetInvocationException.InnerException != null)
|
||||
{
|
||||
exception = targetInvocationException.InnerException;
|
||||
}
|
||||
|
||||
// Do not add an error message if a binding error has already occurred for this property.
|
||||
var modelState = bindingContext.ModelState;
|
||||
var modelStateKey = dtoResult.Key;
|
||||
var validationState = modelState.GetFieldValidationState(modelStateKey);
|
||||
if (validationState == ModelValidationState.Unvalidated)
|
||||
{
|
||||
modelState.AddModelError(modelStateKey, exception);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if validator execution adds a model error.
|
||||
private static bool RunValidator(
|
||||
IModelValidator validator,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
|
|
@ -18,39 +16,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
{
|
||||
public class ControllerActionArgumentBinderTests
|
||||
{
|
||||
public class MySimpleModel
|
||||
{
|
||||
}
|
||||
|
||||
[Bind(Prefix = "TypePrefix")]
|
||||
public class MySimpleModelWithTypeBasedBind
|
||||
{
|
||||
}
|
||||
|
||||
public void ParameterWithNoBindAttribute(MySimpleModelWithTypeBasedBind parameter)
|
||||
{
|
||||
}
|
||||
|
||||
public void ParameterHasFieldPrefix([Bind(Prefix = "simpleModelPrefix")] string parameter)
|
||||
{
|
||||
}
|
||||
|
||||
public void ParameterHasEmptyFieldPrefix([Bind(Prefix = "")] MySimpleModel parameter,
|
||||
[Bind(Prefix = "")] MySimpleModelWithTypeBasedBind parameter1)
|
||||
{
|
||||
}
|
||||
|
||||
public void ParameterHasPrefixAndComplexType(
|
||||
[Bind(Prefix = "simpleModelPrefix")] MySimpleModel parameter,
|
||||
[Bind(Prefix = "simpleModelPrefix")] MySimpleModelWithTypeBasedBind parameter1)
|
||||
{
|
||||
}
|
||||
|
||||
public void ParameterHasEmptyBindAttribute([Bind] MySimpleModel parameter,
|
||||
[Bind] MySimpleModelWithTypeBasedBind parameter1)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindActionArgumentsAsync_DoesNotAddActionArguments_IfBinderReturnsFalse()
|
||||
{
|
||||
|
|
@ -76,7 +41,6 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
};
|
||||
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
|
||||
var argumentBinder = GetArgumentBinder();
|
||||
|
||||
// Act
|
||||
|
|
@ -253,7 +217,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "ValueBinderMarkedProperty",
|
||||
Name = nameof(TestController.StringProperty),
|
||||
ParameterType = typeof(string),
|
||||
});
|
||||
|
||||
|
|
@ -284,7 +248,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "ValueBinderMarkedProperty",
|
||||
Name = nameof(TestController.StringProperty),
|
||||
ParameterType = typeof(string),
|
||||
});
|
||||
|
||||
|
|
@ -325,8 +289,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "ValueBinderMarkedProperty",
|
||||
BindingInfo = new BindingInfo(),
|
||||
Name = nameof(TestController.StringProperty),
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(string)
|
||||
});
|
||||
|
||||
|
|
@ -339,8 +303,37 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello", controller.ValueBinderMarkedProperty);
|
||||
Assert.Null(controller.UnmarkedProperty);
|
||||
Assert.Equal("Hello", controller.StringProperty);
|
||||
Assert.Equal(new List<string> { "goodbye" }, controller.CollectionProperty);
|
||||
Assert.Null(controller.UntouchedProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindActionArgumentsAsync_AddsToCollectionControllerProperties()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetActionDescriptor();
|
||||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = nameof(TestController.CollectionProperty),
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(ICollection<string>),
|
||||
});
|
||||
|
||||
var expected = new List<string> { "Hello", "World", "!!" };
|
||||
var actionContext = GetActionContext(actionDescriptor);
|
||||
var actionBindingContext = GetActionBindingContext(model: expected);
|
||||
var argumentBinder = GetArgumentBinder();
|
||||
var controller = new TestController();
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, controller.CollectionProperty);
|
||||
Assert.Null(controller.StringProperty);
|
||||
Assert.Null(controller.UntouchedProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -351,7 +344,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = "NotNullableProperty",
|
||||
Name = nameof(TestController.NonNullableProperty),
|
||||
BindingInfo = new BindingInfo() { BindingSource = BindingSource.Custom },
|
||||
ParameterType = typeof(int)
|
||||
});
|
||||
|
|
@ -372,13 +365,13 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
var controller = new TestController();
|
||||
|
||||
// Some non default value.
|
||||
controller.NotNullableProperty = -1;
|
||||
controller.NonNullableProperty = -1;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(-1, controller.NotNullableProperty);
|
||||
Assert.Equal(-1, controller.NonNullableProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -419,6 +412,148 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
Assert.Null(controller.NullableProperty);
|
||||
}
|
||||
|
||||
// property name, property type, property accessor, input value, expected value
|
||||
public static TheoryData<string, Type, Func<object, object>, object, object> SkippedPropertyData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, Type, Func<object, object>, object, object>
|
||||
{
|
||||
{
|
||||
nameof(TestController.ArrayProperty),
|
||||
typeof(string[]),
|
||||
controller => ((TestController)controller).ArrayProperty,
|
||||
new string[] { "hello", "world" },
|
||||
new string[] { "goodbye" }
|
||||
},
|
||||
{
|
||||
nameof(TestController.CollectionProperty),
|
||||
typeof(ICollection<string>),
|
||||
controller => ((TestController)controller).CollectionProperty,
|
||||
null,
|
||||
new List<string> { "goodbye" }
|
||||
},
|
||||
{
|
||||
nameof(TestController.NonCollectionProperty),
|
||||
typeof(Person),
|
||||
controller => ((TestController)controller).NonCollectionProperty,
|
||||
new Person { Name = "Fred" },
|
||||
new Person { Name = "Ginger" }
|
||||
},
|
||||
{
|
||||
nameof(TestController.NullCollectionProperty),
|
||||
typeof(ICollection<string>),
|
||||
controller => ((TestController)controller).NullCollectionProperty,
|
||||
new List<string> { "hello", "world" },
|
||||
null
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SkippedPropertyData))]
|
||||
public async Task BindActionArgumentsAsync_SkipsReadOnlyControllerProperties(
|
||||
string propertyName,
|
||||
Type propertyType,
|
||||
Func<object, object> propertyAccessor,
|
||||
object inputValue,
|
||||
object expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetActionDescriptor();
|
||||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = propertyName,
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = propertyType,
|
||||
});
|
||||
|
||||
var actionContext = GetActionContext(actionDescriptor);
|
||||
var actionBindingContext = GetActionBindingContext(model: inputValue);
|
||||
var argumentBinder = GetArgumentBinder();
|
||||
var controller = new TestController();
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, propertyAccessor(controller));
|
||||
Assert.Null(controller.StringProperty);
|
||||
Assert.Null(controller.UntouchedProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindActionArgumentsAsync_SetsMultipleControllerProperties()
|
||||
{
|
||||
// Arrange
|
||||
var boundPropertyTypes = new Dictionary<string, Type>
|
||||
{
|
||||
{ nameof(TestController.ArrayProperty), typeof(string[]) }, // Skipped
|
||||
{ nameof(TestController.CollectionProperty), typeof(List<string>) },
|
||||
{ nameof(TestController.NonCollectionProperty), typeof(Person) }, // Skipped
|
||||
{ nameof(TestController.NullCollectionProperty), typeof(List<string>) }, // Skipped
|
||||
{ nameof(TestController.StringProperty), typeof(string) },
|
||||
};
|
||||
var inputPropertyValues = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(TestController.ArrayProperty), new string[] { "hello", "world" } },
|
||||
{ nameof(TestController.CollectionProperty), new List<string> { "hello", "world" } },
|
||||
{ nameof(TestController.NonCollectionProperty), new Person { Name = "Fred" } },
|
||||
{ nameof(TestController.NullCollectionProperty), new List<string> { "hello", "world" } },
|
||||
{ nameof(TestController.StringProperty), "Hello" },
|
||||
};
|
||||
var expectedPropertyValues = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(TestController.ArrayProperty), new string[] { "goodbye" } },
|
||||
{ nameof(TestController.CollectionProperty), new List<string> { "hello", "world" } },
|
||||
{ nameof(TestController.NonCollectionProperty), new Person { Name = "Ginger" } },
|
||||
{ nameof(TestController.NullCollectionProperty), null },
|
||||
{ nameof(TestController.StringProperty), "Hello" },
|
||||
};
|
||||
|
||||
var actionDescriptor = GetActionDescriptor();
|
||||
foreach (var keyValuePair in boundPropertyTypes)
|
||||
{
|
||||
actionDescriptor.BoundProperties.Add(
|
||||
new ParameterDescriptor
|
||||
{
|
||||
Name = keyValuePair.Key,
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = keyValuePair.Value,
|
||||
});
|
||||
}
|
||||
|
||||
var actionContext = GetActionContext(actionDescriptor);
|
||||
var argumentBinder = GetArgumentBinder();
|
||||
var controller = new TestController();
|
||||
var binder = new Mock<IModelBinder>();
|
||||
binder
|
||||
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
|
||||
.Returns<ModelBindingContext>(bindingContext =>
|
||||
{
|
||||
object model;
|
||||
var isModelSet = inputPropertyValues.TryGetValue(bindingContext.ModelName, out model);
|
||||
return Task.FromResult(new ModelBindingResult(model, bindingContext.ModelName, isModelSet));
|
||||
});
|
||||
var actionBindingContext = new ActionBindingContext
|
||||
{
|
||||
ModelBinder = binder.Object,
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindActionArgumentsAsync(actionContext, actionBindingContext, controller);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new string[] { "goodbye" }, controller.ArrayProperty); // Skipped
|
||||
Assert.Equal(new List<string> { "hello", "world" }, controller.CollectionProperty);
|
||||
Assert.Equal(new Person { Name = "Ginger" }, controller.NonCollectionProperty); // Skipped
|
||||
Assert.Null(controller.NullCollectionProperty); // Skipped
|
||||
Assert.Null(controller.UntouchedProperty); // Not bound
|
||||
Assert.Equal("Hello", controller.StringProperty);
|
||||
}
|
||||
|
||||
private static ActionContext GetActionContext(ActionDescriptor descriptor = null)
|
||||
{
|
||||
return new ActionContext(
|
||||
|
|
@ -440,12 +575,17 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
}
|
||||
|
||||
private static ActionBindingContext GetActionBindingContext()
|
||||
{
|
||||
return GetActionBindingContext("Hello");
|
||||
}
|
||||
|
||||
private static ActionBindingContext GetActionBindingContext(object model)
|
||||
{
|
||||
var binder = new Mock<IModelBinder>();
|
||||
binder
|
||||
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
|
||||
.Returns(Task.FromResult(
|
||||
result: new ModelBindingResult(model: "Hello", key: string.Empty, isModelSet: true)));
|
||||
result: new ModelBindingResult(model: model, key: string.Empty, isModelSet: true)));
|
||||
return new ActionBindingContext()
|
||||
{
|
||||
ModelBinder = binder.Object,
|
||||
|
|
@ -466,41 +606,40 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
validator);
|
||||
}
|
||||
|
||||
// No need for bind-related attributes on properties in this controller class. Properties are added directly
|
||||
// to the BoundProperties collection, bypassing usual requirements.
|
||||
private class TestController
|
||||
{
|
||||
public string UnmarkedProperty { get; set; }
|
||||
public string UntouchedProperty { get; set; }
|
||||
|
||||
[NonValueProviderBinderMetadata]
|
||||
public string NonValueBinderMarkedProperty { get; set; }
|
||||
public string[] ArrayProperty { get; } = new string[] { "goodbye" };
|
||||
|
||||
[ValueProviderMetadata]
|
||||
public string ValueBinderMarkedProperty { get; set; }
|
||||
public ICollection<string> CollectionProperty { get; } = new List<string> { "goodbye" };
|
||||
|
||||
[CustomBindingSource]
|
||||
public int NotNullableProperty { get; set; }
|
||||
public Person NonCollectionProperty { get; } = new Person { Name = "Ginger" };
|
||||
|
||||
public ICollection<string> NullCollectionProperty { get; private set; }
|
||||
|
||||
public string StringProperty { get; set; }
|
||||
|
||||
public int NonNullableProperty { get; set; }
|
||||
|
||||
[CustomBindingSource]
|
||||
public int? NullableProperty { get; set; }
|
||||
|
||||
public Person ActionWithBodyParam([FromBody] Person bodyParam)
|
||||
{
|
||||
return bodyParam;
|
||||
}
|
||||
|
||||
public Person ActionWithTwoBodyParam([FromBody] Person bodyParam, [FromBody] Person bodyParam1)
|
||||
{
|
||||
return bodyParam;
|
||||
}
|
||||
}
|
||||
|
||||
private class Person
|
||||
private class Person : IEquatable<Person>, IEquatable<object>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class NonValueProviderBinderMetadataAttribute : Attribute, IBindingSourceMetadata
|
||||
{
|
||||
public BindingSource BindingSource { get { return BindingSource.Body; } }
|
||||
public bool Equals(Person other)
|
||||
{
|
||||
return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
bool IEquatable<object>.Equals(object obj)
|
||||
{
|
||||
return Equals(obj as Person);
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomBindingSourceAttribute : Attribute, IBindingSourceMetadata
|
||||
|
|
@ -512,17 +651,5 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
|||
{
|
||||
public BindingSource BindingSource { get { return BindingSource.Query; } }
|
||||
}
|
||||
|
||||
[Bind(new string[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })]
|
||||
private class TypeWithIncludedPropertiesUsingBindAttribute
|
||||
{
|
||||
public int ExcludedByDefault1 { get; set; }
|
||||
|
||||
public int ExcludedByDefault2 { get; set; }
|
||||
|
||||
public int IncludedExplicitly1 { get; set; }
|
||||
|
||||
public int IncludedExplicitly2 { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,57 +11,110 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
public class ArrayModelBinderTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task BindModel()
|
||||
public async Task BindModelAsync_ValueProviderContainPrefix_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName[0]", "42" },
|
||||
{ "someName[1]", "84" }
|
||||
{ "someName[1]", "84" },
|
||||
};
|
||||
var bindingContext = GetBindingContext(valueProvider);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var binder = new ArrayModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var retVal = await binder.BindModelAsync(bindingContext);
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(retVal);
|
||||
Assert.NotNull(result);
|
||||
|
||||
int[] array = retVal.Model as int[];
|
||||
var array = Assert.IsType<int[]>(result.Model);
|
||||
Assert.Equal(new[] { 42, 84 }, array);
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
|
||||
public async Task BindModelAsync_ValueProviderDoesNotContainPrefix_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = GetBindingContext(new SimpleHttpValueProvider());
|
||||
var binder = new ArrayModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var bound = await binder.BindModelAsync(bindingContext);
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(bound);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBinder_ModelMetadataReturnsReadOnly_ReturnsNull()
|
||||
public static TheoryData<int[]> ArrayModelData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<int[]>
|
||||
{
|
||||
new int[0],
|
||||
new [] { 357 },
|
||||
new [] { 357, 357 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[MemberData(nameof(ArrayModelData))]
|
||||
public async Task BindModelAsync_ModelMetadataReadOnly_ReturnsNull(int[] model)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "foo[0]", "42" },
|
||||
{ "someName[0]", "42" },
|
||||
{ "someName[1]", "84" },
|
||||
};
|
||||
var bindingContext = GetBindingContext(valueProvider, isReadOnly: true);
|
||||
bindingContext.Model = model;
|
||||
var binder = new ArrayModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var bound = await binder.BindModelAsync(bindingContext);
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(bound);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// Here "fails silently" means the call does not update the array but also does not throw or set an error.
|
||||
[Theory]
|
||||
[MemberData(nameof(ArrayModelData))]
|
||||
public async Task BindModelAsync_ModelMetadataNotReadOnly_ModelNonNull_FailsSilently(int[] model)
|
||||
{
|
||||
// Arrange
|
||||
var arrayLength = model.Length;
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName[0]", "42" },
|
||||
{ "someName[1]", "84" },
|
||||
};
|
||||
|
||||
var bindingContext = GetBindingContext(valueProvider, isReadOnly: false);
|
||||
var modelState = bindingContext.ModelState;
|
||||
bindingContext.Model = model;
|
||||
var binder = new ArrayModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
Assert.Same(model, result.Model);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
for (var i = 0; i < arrayLength; i++)
|
||||
{
|
||||
// Array should be unchanged.
|
||||
Assert.Equal(357, model[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static IModelBinder CreateIntBinder()
|
||||
|
|
|
|||
|
|
@ -56,8 +56,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindModel_ComplexCollection()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_ComplexCollection_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
|
|
@ -67,33 +69,109 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
{ "someName[bar]", "100" },
|
||||
{ "someName[baz]", "200" }
|
||||
};
|
||||
var bindingContext = GetModelBindingContext(valueProvider);
|
||||
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var binder = new CollectionModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var retVal = await binder.BindModelAsync(bindingContext);
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
|
||||
var list = Assert.IsAssignableFrom<IList<int>>(result.Model);
|
||||
Assert.Equal(new[] { 42, 100, 200 }, list.ToArray());
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindModel_SimpleCollection()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_ComplexCollection_BindingContextModelNonNull_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName.index", new[] { "foo", "bar", "baz" } },
|
||||
{ "someName[foo]", "42" },
|
||||
{ "someName[bar]", "100" },
|
||||
{ "someName[baz]", "200" }
|
||||
};
|
||||
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var list = new List<int>();
|
||||
bindingContext.Model = list;
|
||||
var binder = new CollectionModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
|
||||
Assert.Same(list, result.Model);
|
||||
Assert.Equal(new[] { 42, 100, 200 }, list.ToArray());
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_SimpleCollection_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName", new[] { "42", "100", "200" } }
|
||||
};
|
||||
var bindingContext = GetModelBindingContext(valueProvider);
|
||||
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var binder = new CollectionModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var retVal = await binder.BindModelAsync(bindingContext);
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(retVal);
|
||||
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)retVal.Model).ToArray());
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
|
||||
var list = Assert.IsAssignableFrom<IList<int>>(result.Model);
|
||||
Assert.Equal(new[] { 42, 100, 200 }, list.ToArray());
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_SimpleCollection_BindingContextModelNonNull_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName", new[] { "42", "100", "200" } }
|
||||
};
|
||||
var bindingContext = GetModelBindingContext(valueProvider, isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var list = new List<int>();
|
||||
bindingContext.Model = list;
|
||||
var binder = new CollectionModelBinder<int>();
|
||||
|
||||
// Act
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
|
||||
Assert.Same(list, result.Model);
|
||||
Assert.Equal(new[] { 42, 100, 200 }, list.ToArray());
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
@ -156,9 +234,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
Assert.Equal(new[] { 42 }, boundCollection.ToArray());
|
||||
}
|
||||
|
||||
private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider)
|
||||
private static ModelBindingContext GetModelBindingContext(
|
||||
IValueProvider valueProvider,
|
||||
bool isReadOnly = false)
|
||||
{
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
metadataProvider.ForType<IList<int>>().BindingDetails(bd => bd.IsReadOnly = isReadOnly);
|
||||
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = metadataProvider.GetMetadataForType(typeof(int)),
|
||||
|
|
@ -170,6 +252,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
MetadataProvider = metadataProvider
|
||||
}
|
||||
};
|
||||
|
||||
return bindingContext;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,39 +11,81 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
{
|
||||
public class DictionaryModelBinderTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task BindModel()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
ModelBindingContext bindingContext = new ModelBindingContext
|
||||
var bindingContext = GetModelBindingContext(isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var binder = new DictionaryModelBinder<int, string>();
|
||||
|
||||
// Act
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(result.Model);
|
||||
Assert.True(modelState.IsValid);
|
||||
|
||||
Assert.NotNull(dictionary);
|
||||
Assert.Equal(2, dictionary.Count);
|
||||
Assert.Equal("forty-two", dictionary[42]);
|
||||
Assert.Equal("eighty-four", dictionary[84]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task BindModel_BindingContextModelNonNull_Succeeds(bool isReadOnly)
|
||||
{
|
||||
// Arrange
|
||||
var bindingContext = GetModelBindingContext(isReadOnly);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var dictionary = new Dictionary<int, string>();
|
||||
bindingContext.Model = dictionary;
|
||||
var binder = new DictionaryModelBinder<int, string>();
|
||||
|
||||
// Act
|
||||
var result = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsModelSet);
|
||||
Assert.Same(dictionary, result.Model);
|
||||
Assert.True(modelState.IsValid);
|
||||
|
||||
Assert.NotNull(dictionary);
|
||||
Assert.Equal(2, dictionary.Count);
|
||||
Assert.Equal("forty-two", dictionary[42]);
|
||||
Assert.Equal("eighty-four", dictionary[84]);
|
||||
}
|
||||
|
||||
private static ModelBindingContext GetModelBindingContext(bool isReadOnly)
|
||||
{
|
||||
var metadataProvider = new TestModelMetadataProvider();
|
||||
metadataProvider.ForType<List<int>>().BindingDetails(bd => bd.IsReadOnly = isReadOnly);
|
||||
var valueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
|
||||
{ "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") },
|
||||
};
|
||||
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
ModelMetadata = metadataProvider.GetMetadataForType(typeof(IDictionary<int, string>)),
|
||||
ModelName = "someName",
|
||||
ValueProvider = new SimpleHttpValueProvider
|
||||
{
|
||||
{ "someName[0]", new KeyValuePair<int, string>(42, "forty-two") },
|
||||
{ "someName[1]", new KeyValuePair<int, string>(84, "eighty-four") }
|
||||
},
|
||||
ValueProvider = valueProvider,
|
||||
OperationBindingContext = new OperationBindingContext
|
||||
{
|
||||
ModelBinder = CreateKvpBinder(),
|
||||
MetadataProvider = metadataProvider
|
||||
}
|
||||
};
|
||||
var binder = new DictionaryModelBinder<int, string>();
|
||||
|
||||
// Act
|
||||
var retVal = await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(retVal);
|
||||
|
||||
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(retVal.Model);
|
||||
Assert.NotNull(dictionary);
|
||||
Assert.Equal(2, dictionary.Count);
|
||||
Assert.Equal("forty-two", dictionary[42]);
|
||||
Assert.Equal("eighty-four", dictionary[84]);
|
||||
return bindingContext;
|
||||
}
|
||||
|
||||
private static IModelBinder CreateKvpBinder()
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
var modelMetadata = GetMetadataForType(modelType);
|
||||
var bindingContext = new MutableObjectBinderContext
|
||||
{
|
||||
|
|
@ -486,69 +486,43 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
testableBinder.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_HasPublicSetter_ReturnsTrue()
|
||||
[Theory]
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyArray), false)]
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyInt), false)] // read-only value type
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), true)]
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlySimple), true)]
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyString), false)]
|
||||
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadWriteString), true)]
|
||||
public void CanUpdateProperty_ReturnsExpectedValue(string propertyName, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadWriteString");
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty(propertyName);
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.True(canUpdate);
|
||||
Assert.Equal(expected, canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyArray_ReturnsFalse()
|
||||
[Theory]
|
||||
[InlineData(nameof(CollectionContainer.ReadOnlyArray), false)]
|
||||
[InlineData(nameof(CollectionContainer.ReadOnlyDictionary), true)]
|
||||
[InlineData(nameof(CollectionContainer.ReadOnlyList), true)]
|
||||
[InlineData(nameof(CollectionContainer.SettableArray), true)]
|
||||
[InlineData(nameof(CollectionContainer.SettableDictionary), true)]
|
||||
[InlineData(nameof(CollectionContainer.SettableList), true)]
|
||||
public void CanUpdateProperty_CollectionProperty_FalseOnlyForArray(string propertyName, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyArray");
|
||||
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var metadata = metadataProvider.GetMetadataForProperty(typeof(CollectionContainer), propertyName);
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(metadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyReferenceTypeNotBlacklisted_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyObject");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.True(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyString_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyString");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUpdateProperty_ReadOnlyValueType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var propertyMetadata = GetMetadataForCanUpdateProperty("ReadOnlyInt");
|
||||
|
||||
// Act
|
||||
var canUpdate = MutableObjectModelBinder.CanUpdatePropertyInternal(propertyMetadata);
|
||||
|
||||
// Assert
|
||||
Assert.False(canUpdate);
|
||||
Assert.Equal(expected, canUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1364,6 +1338,142 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
// If didn't throw, success!
|
||||
}
|
||||
|
||||
// Property name, property accessor
|
||||
public static TheoryData<string, Func<object, object>> MyCanUpdateButCannotSetPropertyData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, Func<object, object>>
|
||||
{
|
||||
{
|
||||
nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject),
|
||||
model => ((Simple)((MyModelTestingCanUpdateProperty)model).ReadOnlyObject).Name
|
||||
},
|
||||
{
|
||||
nameof(MyModelTestingCanUpdateProperty.ReadOnlySimple),
|
||||
model => ((MyModelTestingCanUpdateProperty)model).ReadOnlySimple.Name
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Reviewers: Is this inconsistency with CanUpdateProperty() an issue we should be tracking?
|
||||
[Theory]
|
||||
[MemberData(nameof(MyCanUpdateButCannotSetPropertyData))]
|
||||
public void SetProperty_ValueProvidedAndCanUpdatePropertyTrue_DoesNothing(
|
||||
string propertyName,
|
||||
Func<object, object> propertAccessor)
|
||||
{
|
||||
// Arrange
|
||||
var model = new MyModelTestingCanUpdateProperty();
|
||||
var type = model.GetType();
|
||||
var bindingContext = CreateContext(GetMetadataForType(type), model);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
|
||||
var modelExplorer = metadataProvider.GetModelExplorerForType(type, model);
|
||||
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName];
|
||||
var dtoResult = new ModelBindingResult(
|
||||
model: new Simple { Name = "Hanna" },
|
||||
isModelSet: true,
|
||||
key: propertyName);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetProperty(
|
||||
bindingContext,
|
||||
modelExplorer,
|
||||
propertyMetadata,
|
||||
dtoResult,
|
||||
requiredValidator: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Joe", propertAccessor(model));
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Empty(modelState);
|
||||
}
|
||||
|
||||
// Property name, property accessor, collection.
|
||||
public static TheoryData<string, Func<object, object>, object> CollectionPropertyData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, Func<object, object>, object>
|
||||
{
|
||||
{
|
||||
nameof(CollectionContainer.ReadOnlyDictionary),
|
||||
model => ((CollectionContainer)model).ReadOnlyDictionary,
|
||||
new Dictionary<int, string>
|
||||
{
|
||||
{ 1, "one" },
|
||||
{ 2, "two" },
|
||||
{ 3, "three" },
|
||||
}
|
||||
},
|
||||
{
|
||||
nameof(CollectionContainer.ReadOnlyList),
|
||||
model => ((CollectionContainer)model).ReadOnlyList,
|
||||
new List<int> { 1, 2, 3, 4 }
|
||||
},
|
||||
{
|
||||
nameof(CollectionContainer.SettableArray),
|
||||
model => ((CollectionContainer)model).SettableArray,
|
||||
new int[] { 1, 2, 3, 4 }
|
||||
},
|
||||
{
|
||||
nameof(CollectionContainer.SettableDictionary),
|
||||
model => ((CollectionContainer)model).SettableDictionary,
|
||||
new Dictionary<int, string>
|
||||
{
|
||||
{ 1, "one" },
|
||||
{ 2, "two" },
|
||||
{ 3, "three" },
|
||||
}
|
||||
},
|
||||
{
|
||||
nameof(CollectionContainer.SettableList),
|
||||
model => ((CollectionContainer)model).SettableList,
|
||||
new List<int> { 1, 2, 3, 4 }
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CollectionPropertyData))]
|
||||
public void SetProperty_CollectionProperty_UpdatesModel(
|
||||
string propertyName,
|
||||
Func<object, object> propertyAccessor,
|
||||
object collection)
|
||||
{
|
||||
// Arrange
|
||||
var model = new CollectionContainer();
|
||||
var type = model.GetType();
|
||||
var bindingContext = CreateContext(GetMetadataForType(type), model);
|
||||
var modelState = bindingContext.ModelState;
|
||||
var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider;
|
||||
var modelExplorer = metadataProvider.GetModelExplorerForType(type, model);
|
||||
|
||||
var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName];
|
||||
var dtoResult = new ModelBindingResult(model: collection, isModelSet: true, key: propertyName);
|
||||
|
||||
var testableBinder = new TestableMutableObjectModelBinder();
|
||||
|
||||
// Act
|
||||
testableBinder.SetProperty(
|
||||
bindingContext,
|
||||
modelExplorer,
|
||||
propertyMetadata,
|
||||
dtoResult,
|
||||
requiredValidator: null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(collection, propertyAccessor(model));
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Empty(modelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetProperty_PropertyIsSettable_CallsSetter()
|
||||
{
|
||||
|
|
@ -1715,8 +1825,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
public int ReadOnlyInt { get; private set; }
|
||||
public string ReadOnlyString { get; private set; }
|
||||
public string[] ReadOnlyArray { get; private set; }
|
||||
public object ReadOnlyObject { get; private set; }
|
||||
public object ReadOnlyObject { get; } = new Simple { Name = "Joe" };
|
||||
public string ReadWriteString { get; set; }
|
||||
public Simple ReadOnlySimple { get; } = new Simple { Name = "Joe" };
|
||||
}
|
||||
|
||||
private sealed class ModelWhosePropertySetterThrows
|
||||
|
|
@ -1788,7 +1899,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
public int IncludedByDefault2 { get; set; }
|
||||
}
|
||||
|
||||
public class Document
|
||||
private class Document
|
||||
{
|
||||
[NonValueBinderMetadata]
|
||||
public string Version { get; set; }
|
||||
|
|
@ -1807,7 +1918,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
public BindingSource BindingSource { get { return BindingSource.Query; } }
|
||||
}
|
||||
|
||||
public class ExcludedProvider : IPropertyBindingPredicateProvider
|
||||
private class ExcludedProvider : IPropertyBindingPredicateProvider
|
||||
{
|
||||
public Func<ModelBindingContext, string, bool> PropertyFilter
|
||||
{
|
||||
|
|
@ -1820,16 +1931,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
public class SimpleContainer
|
||||
private class SimpleContainer
|
||||
{
|
||||
public Simple Simple { get; set; }
|
||||
}
|
||||
|
||||
public class Simple
|
||||
private class Simple
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class CollectionContainer
|
||||
{
|
||||
public int[] ReadOnlyArray { get; } = new int[4];
|
||||
|
||||
// Read-only collections get added values.
|
||||
public IDictionary<int, string> ReadOnlyDictionary { get; } = new Dictionary<int, string>();
|
||||
|
||||
public IList<int> ReadOnlyList { get; } = new List<int>();
|
||||
|
||||
// Settable values are overwritten.
|
||||
public int[] SettableArray { get; set; } = new int[] { 0, 1 };
|
||||
|
||||
public IDictionary<int, string> SettableDictionary { get; set; } = new Dictionary<int, string>
|
||||
{
|
||||
{ 0, "zero" },
|
||||
{ 25, "twenty-five" },
|
||||
};
|
||||
|
||||
public IList<int> SettableList { get; set; } = new List<int> { 3, 9, 0 };
|
||||
}
|
||||
|
||||
private IServiceProvider CreateServices()
|
||||
{
|
||||
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
|
||||
|
|
|
|||
Loading…
Reference in New Issue