Reintroduce model binding
This commit is contained in:
parent
906e68e72e
commit
b6c78de4ea
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ArrayModelBinder<TElement> : CollectionModelBinder<TElement>
|
||||||
|
{
|
||||||
|
public override bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext.ModelMetadata.IsReadOnly)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.BindModel(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CreateOrReplaceCollection(ModelBindingContext bindingContext, IList<TElement> newCollection)
|
||||||
|
{
|
||||||
|
bindingContext.Model = newCollection.ToArray();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class CollectionModelBinder<TElement> : IModelBinder
|
||||||
|
{
|
||||||
|
public virtual bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
|
if (!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
|
List<TElement> boundCollection = (valueProviderResult != null)
|
||||||
|
? BindSimpleCollection(bindingContext, valueProviderResult.RawValue, valueProviderResult.Culture)
|
||||||
|
: BindComplexCollection(bindingContext);
|
||||||
|
|
||||||
|
bool retVal = CreateOrReplaceCollection(bindingContext, boundCollection);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this method internal
|
||||||
|
// 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[].
|
||||||
|
public List<TElement> BindSimpleCollection(ModelBindingContext bindingContext,
|
||||||
|
object rawValue,
|
||||||
|
CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (rawValue == null)
|
||||||
|
{
|
||||||
|
return null; // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TElement> boundCollection = new List<TElement>();
|
||||||
|
|
||||||
|
object[] rawValueArray = RawValueToObjectArray(rawValue);
|
||||||
|
foreach (object rawValueElement in rawValueArray)
|
||||||
|
{
|
||||||
|
ModelBindingContext innerBindingContext = new ModelBindingContext(bindingContext)
|
||||||
|
{
|
||||||
|
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)),
|
||||||
|
ModelName = bindingContext.ModelName,
|
||||||
|
ValueProvider = new CompositeValueProvider
|
||||||
|
{
|
||||||
|
// our temporary provider goes at the front of the list
|
||||||
|
new ElementalValueProvider(bindingContext.ModelName, rawValueElement, culture),
|
||||||
|
bindingContext.ValueProvider
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
object boundValue = null;
|
||||||
|
if (bindingContext.ModelBinder.BindModel(innerBindingContext))
|
||||||
|
{
|
||||||
|
boundValue = innerBindingContext.Model;
|
||||||
|
// TODO: validation
|
||||||
|
// bindingContext.ValidationNode.ChildNodes.Add(innerBindingContext.ValidationNode);
|
||||||
|
}
|
||||||
|
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used when the ValueProvider contains the collection to be bound as multiple elements, e.g. foo[0], foo[1].
|
||||||
|
private List<TElement> BindComplexCollection(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
string indexPropertyName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, "index");
|
||||||
|
ValueProviderResult valueProviderResultIndex = bindingContext.ValueProvider.GetValue(indexPropertyName);
|
||||||
|
IEnumerable<string> indexNames = CollectionModelBinderUtil.GetIndexNamesFromValueProviderResult(valueProviderResultIndex);
|
||||||
|
return BindComplexCollectionFromIndexes(bindingContext, indexNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Convert to internal
|
||||||
|
public List<TElement> BindComplexCollectionFromIndexes(ModelBindingContext bindingContext,
|
||||||
|
IEnumerable<string> indexNames)
|
||||||
|
{
|
||||||
|
bool indexNamesIsFinite;
|
||||||
|
if (indexNames != null)
|
||||||
|
{
|
||||||
|
indexNamesIsFinite = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indexNamesIsFinite = false;
|
||||||
|
indexNames = Enumerable.Range(0, Int32.MaxValue)
|
||||||
|
.Select(i => i.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TElement> boundCollection = new List<TElement>();
|
||||||
|
foreach (string indexName in indexNames)
|
||||||
|
{
|
||||||
|
string fullChildName = ModelBindingHelper.CreateIndexModelName(bindingContext.ModelName, indexName);
|
||||||
|
var childBindingContext = new ModelBindingContext(bindingContext)
|
||||||
|
{
|
||||||
|
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(null, typeof(TElement)),
|
||||||
|
ModelName = fullChildName
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didBind = false;
|
||||||
|
object boundValue = null;
|
||||||
|
|
||||||
|
Type modelType = bindingContext.ModelType;
|
||||||
|
|
||||||
|
if (bindingContext.ModelBinder.BindModel(childBindingContext))
|
||||||
|
{
|
||||||
|
didBind = true;
|
||||||
|
boundValue = childBindingContext.Model;
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
// merge validation up
|
||||||
|
// bindingContext.ValidationNode.ChildNodes.Add(childBindingContext.ValidationNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// infinite size collection stops on first bind failure
|
||||||
|
if (!didBind && !indexNamesIsFinite)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensibility point that allows the bound collection to be manipulated or transformed before
|
||||||
|
// being returned from the binder.
|
||||||
|
protected virtual bool CreateOrReplaceCollection(ModelBindingContext bindingContext, IList<TElement> newCollection)
|
||||||
|
{
|
||||||
|
CreateOrReplaceCollection(bindingContext, newCollection, () => new List<TElement>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static object[] RawValueToObjectArray(object rawValue)
|
||||||
|
{
|
||||||
|
// precondition: rawValue is not null
|
||||||
|
|
||||||
|
// Need to special-case String so it's not caught by the IEnumerable check which follows
|
||||||
|
if (rawValue is string)
|
||||||
|
{
|
||||||
|
return new[] { rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
object[] rawValueAsObjectArray = rawValue as object[];
|
||||||
|
if (rawValueAsObjectArray != null)
|
||||||
|
{
|
||||||
|
return rawValueAsObjectArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable rawValueAsEnumerable = rawValue as IEnumerable;
|
||||||
|
if (rawValueAsEnumerable != null)
|
||||||
|
{
|
||||||
|
return rawValueAsEnumerable.Cast<object>().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
return new[] { rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext,
|
||||||
|
IEnumerable<TElement> incomingElements,
|
||||||
|
Func<ICollection<TElement>> creator)
|
||||||
|
{
|
||||||
|
ICollection<TElement> collection = bindingContext.Model as ICollection<TElement>;
|
||||||
|
if (collection == null || collection.IsReadOnly)
|
||||||
|
{
|
||||||
|
collection = creator();
|
||||||
|
bindingContext.Model = collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.Clear();
|
||||||
|
foreach (TElement element in incomingElements)
|
||||||
|
{
|
||||||
|
collection.Add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
// Describes a complex model, but uses a collection rather than individual properties as the data store.
|
||||||
|
public class ComplexModelDto
|
||||||
|
{
|
||||||
|
public ComplexModelDto(ModelMetadata modelMetadata, IEnumerable<ModelMetadata> propertyMetadata)
|
||||||
|
{
|
||||||
|
if (modelMetadata == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("modelMetadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyMetadata == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("propertyMetadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelMetadata = modelMetadata;
|
||||||
|
PropertyMetadata = new Collection<ModelMetadata>(propertyMetadata.ToList());
|
||||||
|
Results = new Dictionary<ModelMetadata, ComplexModelDtoResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelMetadata ModelMetadata { get; private set; }
|
||||||
|
|
||||||
|
public Collection<ModelMetadata> PropertyMetadata { get; private set; }
|
||||||
|
|
||||||
|
// Contains entries corresponding to each property against which binding was
|
||||||
|
// attempted. If binding failed, the entry's value will be null. If binding
|
||||||
|
// was never attempted, this dictionary will not contain a corresponding
|
||||||
|
// entry.
|
||||||
|
public IDictionary<ModelMetadata, ComplexModelDtoResult> Results { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public sealed class ComplexModelDtoModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext.ModelType == typeof(ComplexModelDto))
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(ComplexModelDto), allowNullModel: false);
|
||||||
|
|
||||||
|
ComplexModelDto dto = (ComplexModelDto)bindingContext.Model;
|
||||||
|
foreach (ModelMetadata propertyMetadata in dto.PropertyMetadata)
|
||||||
|
{
|
||||||
|
ModelBindingContext propertyBindingContext = new ModelBindingContext(bindingContext)
|
||||||
|
{
|
||||||
|
ModelMetadata = propertyMetadata,
|
||||||
|
ModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName, propertyMetadata.PropertyName)
|
||||||
|
};
|
||||||
|
|
||||||
|
// bind and propagate the values
|
||||||
|
// If we can't bind, then leave the result missing (don't add a null).
|
||||||
|
|
||||||
|
if (bindingContext.ModelBinder.BindModel(propertyBindingContext))
|
||||||
|
{
|
||||||
|
dto.Results[propertyMetadata] = new ComplexModelDtoResult(propertyBindingContext.Model/*, propertyBindingContext.ValidationNode*/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public sealed class ComplexModelDtoResult
|
||||||
|
{
|
||||||
|
public ComplexModelDtoResult(object model/*, ModelValidationNode validationNode*/)
|
||||||
|
{
|
||||||
|
// TODO: Validation
|
||||||
|
//if (validationNode == null)
|
||||||
|
//{
|
||||||
|
// throw Error.ArgumentNull("validationNode");
|
||||||
|
//}
|
||||||
|
|
||||||
|
Model = model;
|
||||||
|
//ValidationNode = validationNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Model { get; private set; }
|
||||||
|
|
||||||
|
//public ModelValidationNode ValidationNode { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class is an <see cref="IModelBinder"/> that delegates to one of a collection of
|
||||||
|
/// <see cref="IModelBinder"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If no binder is available and the <see cref="ModelBindingContext"/> allows it,
|
||||||
|
/// this class tries to find a binder using an empty prefix.
|
||||||
|
/// </remarks>
|
||||||
|
public class CompositeModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public CompositeModelBinder(IEnumerable<IModelBinder> binders)
|
||||||
|
: this(binders.ToArray())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompositeModelBinder(params IModelBinder[] binders)
|
||||||
|
{
|
||||||
|
Binders = binders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IModelBinder[] Binders { get; set; }
|
||||||
|
|
||||||
|
public virtual bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingContext newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName);
|
||||||
|
|
||||||
|
bool boundSuccessfully = TryBind(newBindingContext);
|
||||||
|
if (!boundSuccessfully && !String.IsNullOrEmpty(bindingContext.ModelName)
|
||||||
|
&& bindingContext.FallbackToEmptyPrefix)
|
||||||
|
{
|
||||||
|
// fallback to empty prefix?
|
||||||
|
newBindingContext = CreateNewBindingContext(bindingContext, modelName: String.Empty);
|
||||||
|
boundSuccessfully = TryBind(newBindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boundSuccessfully)
|
||||||
|
{
|
||||||
|
return false; // something went wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
// run validation and return the model
|
||||||
|
// If we fell back to an empty prefix above and are dealing with simple types,
|
||||||
|
// propagate the non-blank model name through for user clarity in validation errors.
|
||||||
|
// Complex types will reveal their individual properties as model names and do not require this.
|
||||||
|
// TODO: Validation
|
||||||
|
//if (!newBindingContext.ModelMetadata.IsComplexType && String.IsNullOrEmpty(newBindingContext.ModelName))
|
||||||
|
//{
|
||||||
|
// newBindingContext.ValidationNode = new Validation.ModelValidationNode(newBindingContext.ModelMetadata, bindingContext.ModelName);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//newBindingContext.ValidationNode.Validate(context, null /* parentNode */);
|
||||||
|
bindingContext.Model = newBindingContext.Model;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryBind(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
// TODO: The body of this method existed as HttpActionContextExtensions.Bind. We might have to refactor it into
|
||||||
|
// something that is shared.
|
||||||
|
if (bindingContext == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("bindingContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: RuntimeHelpers.EnsureSufficientExecutionStack does not exist in the CoreCLR.
|
||||||
|
// Protects against stack overflow for deeply nested model binding
|
||||||
|
// RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||||
|
|
||||||
|
bool requiresBodyBinder = bindingContext.ModelMetadata.IsFromBody;
|
||||||
|
foreach (IModelBinder binder in Binders)
|
||||||
|
{
|
||||||
|
if (binder.BindModel(bindingContext))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either we couldn't find a binder, or the binder couldn't bind. Distinction is not important.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName)
|
||||||
|
{
|
||||||
|
var newBindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = oldBindingContext.ModelMetadata,
|
||||||
|
ModelName = modelName,
|
||||||
|
ModelState = oldBindingContext.ModelState,
|
||||||
|
ValueProvider = oldBindingContext.ValueProvider,
|
||||||
|
MetadataProvider = oldBindingContext.MetadataProvider,
|
||||||
|
ModelBinder = oldBindingContext.ModelBinder,
|
||||||
|
HttpContext = oldBindingContext.HttpContext
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
//// validation is expensive to create, so copy it over if we can
|
||||||
|
//if (Object.ReferenceEquals(modelName, oldBindingContext.ModelName))
|
||||||
|
//{
|
||||||
|
// newBindingContext.ValidationNode = oldBindingContext.ValidationNode;
|
||||||
|
//}
|
||||||
|
|
||||||
|
return newBindingContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DictionaryModelBinder<TKey, TValue> : CollectionModelBinder<KeyValuePair<TKey, TValue>>
|
||||||
|
{
|
||||||
|
protected override bool CreateOrReplaceCollection(ModelBindingContext bindingContext,
|
||||||
|
IList<KeyValuePair<TKey, TValue>> newCollection)
|
||||||
|
{
|
||||||
|
CreateOrReplaceDictionary(bindingContext, newCollection, () => new Dictionary<TKey, TValue>());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateOrReplaceDictionary(ModelBindingContext bindingContext,
|
||||||
|
IEnumerable<KeyValuePair<TKey, TValue>> incomingElements,
|
||||||
|
Func<IDictionary<TKey, TValue>> creator)
|
||||||
|
{
|
||||||
|
IDictionary<TKey, TValue> dictionary = bindingContext.Model as IDictionary<TKey, TValue>;
|
||||||
|
if (dictionary == null || dictionary.IsReadOnly)
|
||||||
|
{
|
||||||
|
dictionary = creator();
|
||||||
|
bindingContext.Model = dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary.Clear();
|
||||||
|
foreach (var element in incomingElements)
|
||||||
|
{
|
||||||
|
if (element.Key != null)
|
||||||
|
{
|
||||||
|
dictionary[element.Key] = element.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class GenericModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public GenericModelBinder(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
Type binderType = ResolveBinderType(bindingContext.ModelType);
|
||||||
|
if (binderType != null)
|
||||||
|
{
|
||||||
|
var binder = ActivatorUtilities.CreateInstance<IModelBinder>(_serviceProvider);
|
||||||
|
return binder.BindModel(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type ResolveBinderType(Type modelType)
|
||||||
|
{
|
||||||
|
return GetArrayBinder(modelType) ??
|
||||||
|
GetCollectionBinder(modelType) ??
|
||||||
|
GetDictionaryBinder(modelType) ??
|
||||||
|
GetKeyValuePairBinder(modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetArrayBinder(Type modelType)
|
||||||
|
{
|
||||||
|
if (modelType.IsArray)
|
||||||
|
{
|
||||||
|
Type elementType = modelType.GetElementType();
|
||||||
|
return typeof(ArrayModelBinder<>).MakeGenericType(elementType);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetCollectionBinder(Type modelType)
|
||||||
|
{
|
||||||
|
return GetGenericBinderType(
|
||||||
|
typeof(ICollection<>),
|
||||||
|
typeof(List<>),
|
||||||
|
typeof(CollectionModelBinder<>),
|
||||||
|
modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetDictionaryBinder(Type modelType)
|
||||||
|
{
|
||||||
|
return GetGenericBinderType(
|
||||||
|
typeof(IDictionary<,>),
|
||||||
|
typeof(Dictionary<,>),
|
||||||
|
typeof(DictionaryModelBinder<,>),
|
||||||
|
modelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetKeyValuePairBinder(Type modelType)
|
||||||
|
{
|
||||||
|
return ModelBindingHelper.GetPossibleBinderInstanceType(
|
||||||
|
closedModelType: modelType,
|
||||||
|
openModelType: typeof(KeyValuePair<,>),
|
||||||
|
openBinderType: typeof(KeyValuePairModelBinder<,>));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Example: GetGenericBinder(typeof(IList<>), typeof(List<>), typeof(ListBinder<>), ...) means that the ListBinder<T>
|
||||||
|
/// type can update models that implement IList<T>, and if for some reason the existing model instance is not
|
||||||
|
/// updatable the binder will create a List<T> object and bind to that instead. This method will return ListBinder<T>
|
||||||
|
/// or null, depending on whether the type and updatability checks succeed.
|
||||||
|
/// </remarks>
|
||||||
|
private static Type GetGenericBinderType(Type supportedInterfaceType, Type newInstanceType, Type openBinderType, Type modelType)
|
||||||
|
{
|
||||||
|
Contract.Assert(supportedInterfaceType != null);
|
||||||
|
Contract.Assert(openBinderType != null);
|
||||||
|
Contract.Assert(modelType != null);
|
||||||
|
|
||||||
|
Type[] modelTypeArguments = GetGenericBinderTypeArgs(supportedInterfaceType, modelType);
|
||||||
|
|
||||||
|
if (modelTypeArguments == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type closedNewInstanceType = newInstanceType.MakeGenericType(modelTypeArguments);
|
||||||
|
if (!modelType.GetTypeInfo().IsAssignableFrom(closedNewInstanceType.GetTypeInfo()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return openBinderType.MakeGenericType(modelTypeArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the generic arguments for the binder, based on the model type. Or null if not compatible.
|
||||||
|
private static Type[] GetGenericBinderTypeArgs(Type supportedInterfaceType, Type modelType)
|
||||||
|
{
|
||||||
|
TypeInfo modelTypeInfo = modelType.GetTypeInfo();
|
||||||
|
if (!modelTypeInfo.IsGenericType || modelTypeInfo.IsGenericTypeDefinition)
|
||||||
|
{
|
||||||
|
// not a closed generic type
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type[] modelTypeArguments = modelTypeInfo.GenericTypeArguments;
|
||||||
|
if (modelTypeArguments.Length != supportedInterfaceType.GetTypeInfo().GenericTypeParameters.Length)
|
||||||
|
{
|
||||||
|
// wrong number of generic type arguments
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelTypeArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for model binding.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModelBinder
|
||||||
|
{
|
||||||
|
bool BindModel(ModelBindingContext bindingContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public sealed class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
|
||||||
|
{
|
||||||
|
public bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext, typeof(KeyValuePair<TKey, TValue>), allowNullModel: true);
|
||||||
|
|
||||||
|
TKey key;
|
||||||
|
bool keyBindingSucceeded = TryBindStrongModel(bindingContext, "key", out key);
|
||||||
|
|
||||||
|
TValue value;
|
||||||
|
bool valueBindingSucceeded = TryBindStrongModel(bindingContext, "value", out value);
|
||||||
|
|
||||||
|
if (keyBindingSucceeded && valueBindingSucceeded)
|
||||||
|
{
|
||||||
|
bindingContext.Model = new KeyValuePair<TKey, TValue>(key, value);
|
||||||
|
}
|
||||||
|
return keyBindingSucceeded || valueBindingSucceeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this internal
|
||||||
|
public bool TryBindStrongModel<TModel>(ModelBindingContext parentBindingContext,
|
||||||
|
string propertyName,
|
||||||
|
out TModel model)
|
||||||
|
{
|
||||||
|
ModelBindingContext propertyBindingContext = new ModelBindingContext(parentBindingContext)
|
||||||
|
{
|
||||||
|
ModelMetadata = parentBindingContext.MetadataProvider.GetMetadataForType(modelAccessor: null, modelType: typeof(TModel)),
|
||||||
|
ModelName = ModelBindingHelper.CreatePropertyModelName(parentBindingContext.ModelName, propertyName)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (propertyBindingContext.ModelBinder.BindModel(propertyBindingContext))
|
||||||
|
{
|
||||||
|
object untypedModel = propertyBindingContext.Model;
|
||||||
|
model = ModelBindingHelper.CastOrDefault<TModel>(untypedModel);
|
||||||
|
// TODO: Revive once we get validation
|
||||||
|
// parentBindingContext.ValidationNode.ChildNodes.Add(propertyBindingContext.ValidationNode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
model = default(TModel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,331 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class MutableObjectModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public virtual bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
|
if (!CanBindType(bindingContext.ModelType) ||
|
||||||
|
!bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureModel(bindingContext);
|
||||||
|
IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(bindingContext);
|
||||||
|
ComplexModelDto dto = CreateAndPopulateDto(bindingContext, propertyMetadatas);
|
||||||
|
|
||||||
|
// post-processing, e.g. property setters and hooking up validation
|
||||||
|
ProcessDto(bindingContext, dto);
|
||||||
|
// TODO: Validation
|
||||||
|
// bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata)
|
||||||
|
{
|
||||||
|
return CanUpdatePropertyInternal(propertyMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanBindType(Type modelType)
|
||||||
|
{
|
||||||
|
// Simple types cannot use this binder
|
||||||
|
bool isComplexType = !modelType.HasStringConverter();
|
||||||
|
if (!isComplexType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelType == typeof(ComplexModelDto))
|
||||||
|
{
|
||||||
|
// forbidden type - will cause a stack overflow if we try binding this type
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata)
|
||||||
|
{
|
||||||
|
return !propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanUpdateReadOnlyProperty(Type propertyType)
|
||||||
|
{
|
||||||
|
// Value types have copy-by-value semantics, which prevents us from updating
|
||||||
|
// properties that are marked readonly.
|
||||||
|
if (propertyType.GetTypeInfo().IsValueType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrays are strange beasts since their contents are mutable but their sizes aren't.
|
||||||
|
// Therefore we shouldn't even try to update these. Further reading:
|
||||||
|
// http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx
|
||||||
|
if (propertyType.IsArray)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special-case known immutable reference types
|
||||||
|
if (propertyType == typeof(string))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComplexModelDto CreateAndPopulateDto(ModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas)
|
||||||
|
{
|
||||||
|
// create a DTO and call into the DTO binder
|
||||||
|
ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas);
|
||||||
|
ModelBindingContext dtoBindingContext = new ModelBindingContext(bindingContext)
|
||||||
|
{
|
||||||
|
ModelMetadata = bindingContext.MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)),
|
||||||
|
ModelName = bindingContext.ModelName
|
||||||
|
};
|
||||||
|
|
||||||
|
bindingContext.ModelBinder.BindModel(dtoBindingContext);
|
||||||
|
return (ComplexModelDto)dtoBindingContext.Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual object CreateModel(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
//// Called when the property setter null check failed, allows us to add our own error message to ModelState.
|
||||||
|
//internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelMetadata modelMetadata, object incomingValue)
|
||||||
|
//{
|
||||||
|
// return (sender, e) =>
|
||||||
|
// {
|
||||||
|
// ModelValidationNode validationNode = (ModelValidationNode)sender;
|
||||||
|
// ModelStateDictionary modelState = e.ActionContext.ModelState;
|
||||||
|
|
||||||
|
// if (modelState.IsValidField(validationNode.ModelStateKey))
|
||||||
|
// {
|
||||||
|
// string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ActionContext, modelMetadata, incomingValue);
|
||||||
|
// if (errorMessage != null)
|
||||||
|
// {
|
||||||
|
// modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//}
|
||||||
|
|
||||||
|
protected virtual void EnsureModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext.Model == null)
|
||||||
|
{
|
||||||
|
bindingContext.ModelMetadata.Model = CreateModel(bindingContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
// TODO: Revive required properties. This has a dependency on HttpBindingAttribute and DataAnnotations
|
||||||
|
// keep a set of the required properties so that we can cross-reference bound properties later
|
||||||
|
HashSet<string> requiredProperties = new HashSet<string>();
|
||||||
|
// Dictionary<string, ModelValidator> requiredValidators;
|
||||||
|
HashSet<string> skipProperties = new HashSet<string>();
|
||||||
|
// GetRequiredPropertiesCollection(bindingContext, out requiredProperties, out skipProperties);
|
||||||
|
|
||||||
|
return from propertyMetadata in bindingContext.ModelMetadata.Properties
|
||||||
|
let propertyName = propertyMetadata.PropertyName
|
||||||
|
let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName)
|
||||||
|
where shouldUpdateProperty && CanUpdateProperty(propertyMetadata)
|
||||||
|
select propertyMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetPropertyDefaultValue(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
DefaultValueAttribute attr = propertyInfo.GetCustomAttribute<DefaultValueAttribute>();
|
||||||
|
return (attr != null) ? attr.Value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//internal static void GetRequiredPropertiesCollection(ModelBindingContext bindingContext, out HashSet<string> requiredProperties, out HashSet<string> skipProperties)
|
||||||
|
//{
|
||||||
|
// requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
// // requiredValidators = new Dictionary<string, ModelValidator>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
// skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// // Use attributes on the property before attributes on the type.
|
||||||
|
// Type modelType = bindingContext.ModelType;
|
||||||
|
// ICustomTypeDescriptor modelDescriptor = new AssociatedMetadataTypeTypeDescriptionProvider(modelType)
|
||||||
|
// .GetTypeDescriptor(modelType);
|
||||||
|
|
||||||
|
// PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties();
|
||||||
|
|
||||||
|
// // TODO: Revive HttpBindingBehavior
|
||||||
|
// // HttpBindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<HttpBindingBehaviorAttribute>().SingleOrDefault();
|
||||||
|
|
||||||
|
// foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors)
|
||||||
|
// {
|
||||||
|
// string propertyName = propertyDescriptor.Name;
|
||||||
|
// ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyName];
|
||||||
|
// // ModelValidator requiredValidator = context.GetValidators(propertyMetadata).Where(v => v.IsRequired).FirstOrDefault();
|
||||||
|
// // requiredValidators[propertyName] = requiredValidator;
|
||||||
|
|
||||||
|
// HttpBindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType<HttpBindingBehaviorAttribute>().SingleOrDefault();
|
||||||
|
// HttpBindingBehaviorAttribute workingAttr = propAttr ?? typeAttr;
|
||||||
|
// if (workingAttr != null)
|
||||||
|
// {
|
||||||
|
// switch (workingAttr.Behavior)
|
||||||
|
// {
|
||||||
|
// case HttpBindingBehavior.Required:
|
||||||
|
// requiredProperties.Add(propertyName);
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// case HttpBindingBehavior.Never:
|
||||||
|
// skipProperties.Add(propertyName);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else if (requiredValidator != null)
|
||||||
|
// {
|
||||||
|
// requiredProperties.Add(propertyName);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto)
|
||||||
|
{
|
||||||
|
// TODO: Uncomment this once we revive validation
|
||||||
|
|
||||||
|
//HashSet<string> requiredProperties;
|
||||||
|
//// Dictionary<string, ModelValidator> requiredValidators;
|
||||||
|
//HashSet<string> skipProperties;
|
||||||
|
//GetRequiredPropertiesCollection(context, bindingContext, out requiredProperties, out requiredValidators, out skipProperties);
|
||||||
|
|
||||||
|
//// Eliminate provided properties from requiredProperties; leaving just *missing* required properties.
|
||||||
|
//requiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName));
|
||||||
|
|
||||||
|
//foreach (string missingRequiredProperty in requiredProperties)
|
||||||
|
//{
|
||||||
|
// string modelStateKey = ModelBindingHelper.CreatePropertyModelName(
|
||||||
|
// bindingContext.ValidationNode.ModelStateKey, missingRequiredProperty);
|
||||||
|
|
||||||
|
// // Update Model as SetProperty() would: Place null value where validator will check for non-null. This
|
||||||
|
// // ensures a failure result from a required validator (if any) even for a non-nullable property.
|
||||||
|
// // (Otherwise, propertyMetadata.Model is likely already null.)
|
||||||
|
// ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[missingRequiredProperty];
|
||||||
|
// propertyMetadata.Model = null;
|
||||||
|
|
||||||
|
// // Execute validator (if any) to get custom error message.
|
||||||
|
// ModelValidator validator = requiredValidators[missingRequiredProperty];
|
||||||
|
// bool addedError = RunValidator(validator, bindingContext, propertyMetadata, modelStateKey);
|
||||||
|
|
||||||
|
// // Fall back to default message if HttpBindingBehaviorAttribute required this property or validator
|
||||||
|
// // (oddly) succeeded.
|
||||||
|
// if (!addedError)
|
||||||
|
// {
|
||||||
|
// bindingContext.ModelState.AddModelError(modelStateKey,
|
||||||
|
// Error.Format(SRResources.MissingRequiredMember, missingRequiredProperty));
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
// for each property that was bound, call the setter, recording exceptions as necessary
|
||||||
|
foreach (var entry in dto.Results)
|
||||||
|
{
|
||||||
|
ModelMetadata propertyMetadata = entry.Key;
|
||||||
|
|
||||||
|
ComplexModelDtoResult dtoResult = entry.Value;
|
||||||
|
if (dtoResult != null)
|
||||||
|
{
|
||||||
|
SetProperty(bindingContext, propertyMetadata, dtoResult);
|
||||||
|
// TODO: Validation
|
||||||
|
// bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
|
||||||
|
protected virtual void SetProperty(ModelBindingContext bindingContext,
|
||||||
|
ModelMetadata propertyMetadata,
|
||||||
|
ComplexModelDtoResult dtoResult
|
||||||
|
/*, ModelValidator requiredValidator*/)
|
||||||
|
{
|
||||||
|
// TODO: This used TypeDescriptor which is no longer available. Lookups performed using System.ComponentModel were
|
||||||
|
// cached. To maintain parity, we'll need to cache property lookups.
|
||||||
|
PropertyInfo property = bindingContext.ModelType
|
||||||
|
.GetRuntimeProperties()
|
||||||
|
.FirstOrDefault(p => p.Name.Equals(propertyMetadata.PropertyName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (property == null || !property.CanWrite)
|
||||||
|
{
|
||||||
|
// nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object value = dtoResult.Model ?? GetPropertyDefaultValue(property);
|
||||||
|
propertyMetadata.Model = value;
|
||||||
|
|
||||||
|
|
||||||
|
//// 'Required' validators need to run first so that we can provide useful error messages if
|
||||||
|
//// the property setters throw, e.g. if we're setting entity keys to null. See comments in
|
||||||
|
//// DefaultModelBinder.SetProperty() for more information.
|
||||||
|
//if (value == null)
|
||||||
|
//{
|
||||||
|
// string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
|
||||||
|
// if (bindingContext.ModelState.IsValidField(modelStateKey))
|
||||||
|
// {
|
||||||
|
// RunValidator(requiredValidator, bindingContext, propertyMetadata, modelStateKey);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (value != null || property.PropertyType.AllowsNullValue())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
property.SetValue(bindingContext.Model, value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// don't display a duplicate error message if a binding error has already occurred for this field
|
||||||
|
//string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
|
||||||
|
//if (bindingContext.ModelState.IsValidField(modelStateKey))
|
||||||
|
//{
|
||||||
|
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// trying to set a non-nullable value type to null, need to make sure there's a message
|
||||||
|
//string modelStateKey = dtoResult.ValidationNode.ModelStateKey;
|
||||||
|
//if (bindingContext.ModelState.IsValidField(modelStateKey))
|
||||||
|
//{
|
||||||
|
// dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(propertyMetadata, value);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//// Returns true if validator execution adds a model error.
|
||||||
|
//private static bool RunValidator(ModelValidator validator, ModelBindingContext bindingContext,
|
||||||
|
// ModelMetadata propertyMetadata, string modelStateKey)
|
||||||
|
//{
|
||||||
|
// bool addedError = false;
|
||||||
|
// if (validator != null)
|
||||||
|
// {
|
||||||
|
// foreach (ModelValidationResult validationResult in validator.Validate(propertyMetadata, bindingContext.Model))
|
||||||
|
// {
|
||||||
|
// bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
|
||||||
|
// addedError = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return addedError;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public sealed class TypeConverterModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is recorded to be acted upon later.")]
|
||||||
|
[SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Http.ValueProviders.ValueProviderResult.ConvertTo(System.Type)", Justification = "The ValueProviderResult already has the necessary context to perform a culture-aware conversion.")]
|
||||||
|
public bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
|
if (!bindingContext.ModelType.HasStringConverter())
|
||||||
|
{
|
||||||
|
// this type cannot be converted
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
|
if (valueProviderResult == null)
|
||||||
|
{
|
||||||
|
return false; // no entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider should have verified this before creating
|
||||||
|
Contract.Assert(bindingContext.ModelType.HasStringConverter());
|
||||||
|
|
||||||
|
object newModel;
|
||||||
|
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newModel = valueProviderResult.ConvertTo(bindingContext.ModelType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (IsFormatException(ex))
|
||||||
|
{
|
||||||
|
// there was a type conversion failure
|
||||||
|
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref newModel);
|
||||||
|
bindingContext.Model = newModel;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFormatException(Exception ex)
|
||||||
|
{
|
||||||
|
for (; ex != null; ex = ex.InnerException)
|
||||||
|
{
|
||||||
|
if (ex is FormatException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public sealed class TypeMatchModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public bool BindModel(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ValueProviderResult valueProviderResult = GetCompatibleValueProviderResult(bindingContext);
|
||||||
|
if (valueProviderResult == null)
|
||||||
|
{
|
||||||
|
// conversion would have failed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
|
||||||
|
object model = valueProviderResult.RawValue;
|
||||||
|
ModelBindingHelper.ReplaceEmptyStringWithNull(bindingContext.ModelMetadata, ref model);
|
||||||
|
bindingContext.Model = model;
|
||||||
|
if (bindingContext.ModelMetadata.IsComplexType)
|
||||||
|
{
|
||||||
|
// TODO: Validation
|
||||||
|
//IBodyModelValidator validator = services.GetBodyModelValidator();
|
||||||
|
//ModelMetadataProvider metadataProvider = services.GetModelMetadataProvider();
|
||||||
|
//if (validator != null && metadataProvider != null)
|
||||||
|
//{
|
||||||
|
// validator.Validate(model, bindingContext.ModelType, metadataProvider, context, bindingContext.ModelName);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ValueProviderResult GetCompatibleValueProviderResult(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
ModelBindingHelper.ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
|
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
|
if (valueProviderResult == null)
|
||||||
|
{
|
||||||
|
return null; // the value doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bindingContext.ModelType.IsCompatibleWith(valueProviderResult.RawValue))
|
||||||
|
{
|
||||||
|
return null; // value is of incompatible type
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueProviderResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
|
||||||
{
|
|
||||||
public interface IValueProvider
|
|
||||||
{
|
|
||||||
bool ContainsPrefix(string key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
public static class CollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Convert an ICollection to an array, removing null values. Fast path for case where there are no null values.
|
||||||
|
/// </summary>
|
||||||
|
public static T[] ToArrayWithoutNulls<T>(this ICollection<T> collection) where T : class
|
||||||
|
{
|
||||||
|
Contract.Assert(collection != null);
|
||||||
|
|
||||||
|
T[] result = new T[collection.Count];
|
||||||
|
int count = 0;
|
||||||
|
foreach (T value in collection)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
result[count] = value;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count == collection.Count)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
T[] trimmedResult = new T[count];
|
||||||
|
Array.Copy(result, trimmedResult, count);
|
||||||
|
return trimmedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
public static class CollectionModelBinderUtil
|
||||||
|
{
|
||||||
|
public static IEnumerable<string> GetIndexNamesFromValueProviderResult(ValueProviderResult valueProviderResultIndex)
|
||||||
|
{
|
||||||
|
IEnumerable<string> indexNames = null;
|
||||||
|
if (valueProviderResultIndex != null)
|
||||||
|
{
|
||||||
|
string[] indexes = (string[])valueProviderResultIndex.ConvertTo(typeof(string[]));
|
||||||
|
if (indexes != null && indexes.Length > 0)
|
||||||
|
{
|
||||||
|
indexNames = indexes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indexNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext,
|
||||||
|
IEnumerable<TElement> incomingElements,
|
||||||
|
Func<ICollection<TElement>> creator)
|
||||||
|
{
|
||||||
|
var collection = bindingContext.Model as ICollection<TElement>;
|
||||||
|
if (collection == null || collection.IsReadOnly)
|
||||||
|
{
|
||||||
|
collection = creator();
|
||||||
|
bindingContext.Model = collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.Clear();
|
||||||
|
foreach (TElement element in incomingElements)
|
||||||
|
{
|
||||||
|
collection.Add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
internal class EfficientTypePropertyKey<T1, T2> : Tuple<T1, T2>
|
||||||
|
{
|
||||||
|
private int _hashCode;
|
||||||
|
|
||||||
|
public EfficientTypePropertyKey(T1 item1, T2 item2)
|
||||||
|
: base(item1, item2)
|
||||||
|
{
|
||||||
|
_hashCode = base.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return _hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
internal static class Error
|
||||||
|
{
|
||||||
|
internal static ArgumentException ArgumentNull(string paramName)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Exception InvalidOperation(string message, params object[] args)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, message, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Exception InvalidOperation(Exception ex, string message, params object[] args)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, message, args), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="ArgumentException"/> with the provided properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
|
||||||
|
/// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
|
||||||
|
/// <returns>The logged <see cref="Exception"/>.</returns>
|
||||||
|
internal static ArgumentException Argument(string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new ArgumentException(String.Format(CultureInfo.CurrentCulture, messageFormat, messageArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="ArgumentException"/> with the provided properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameterName">The name of the parameter that caused the current exception.</param>
|
||||||
|
/// <param name="messageFormat">A composite format string explaining the reason for the exception.</param>
|
||||||
|
/// <param name="messageArgs">An object array that contains zero or more objects to format.</param>
|
||||||
|
/// <returns>The logged <see cref="Exception"/>.</returns>
|
||||||
|
internal static ArgumentException Argument(string parameterName, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new ArgumentException(String.Format(CultureInfo.CurrentCulture, messageFormat, messageArgs), parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an <see cref="ArgumentException"/> with a default message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameterName">The name of the parameter that caused the current exception.</param>
|
||||||
|
/// <returns>The logged <see cref="Exception"/>.</returns>
|
||||||
|
internal static ArgumentException ArgumentNullOrEmpty(string parameterName)
|
||||||
|
{
|
||||||
|
return Error.Argument(parameterName, Resources.ArgumentNullOrEmpty, parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
public static class ModelBindingHelper
|
||||||
|
{
|
||||||
|
internal static TModel CastOrDefault<TModel>(object model)
|
||||||
|
{
|
||||||
|
return (model is TModel) ? (TModel)model : default(TModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string CreateIndexModelName(string parentName, int index)
|
||||||
|
{
|
||||||
|
return CreateIndexModelName(parentName, index.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string CreateIndexModelName(string parentName, string index)
|
||||||
|
{
|
||||||
|
return (parentName.Length == 0) ? "[" + index + "]" : parentName + "[" + index + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string CreatePropertyModelName(string prefix, string propertyName)
|
||||||
|
{
|
||||||
|
if (String.IsNullOrEmpty(prefix))
|
||||||
|
{
|
||||||
|
return propertyName ?? String.Empty;
|
||||||
|
}
|
||||||
|
else if (String.IsNullOrEmpty(propertyName))
|
||||||
|
{
|
||||||
|
return prefix ?? String.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return prefix + "." + propertyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Type GetPossibleBinderInstanceType(Type closedModelType, Type openModelType, Type openBinderType)
|
||||||
|
{
|
||||||
|
Type[] typeArguments = TypeExtensions.GetTypeArgumentsIfMatch(closedModelType, openModelType);
|
||||||
|
return (typeArguments != null) ? openBinderType.MakeGenericType(typeArguments) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ReplaceEmptyStringWithNull(ModelMetadata modelMetadata, ref object model)
|
||||||
|
{
|
||||||
|
if (model is string &&
|
||||||
|
modelMetadata.ConvertEmptyStringToNull &&
|
||||||
|
String.IsNullOrWhiteSpace(model as string))
|
||||||
|
{
|
||||||
|
model = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ValidateBindingContext(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("bindingContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingContext.ModelMetadata == null)
|
||||||
|
{
|
||||||
|
throw Error.Argument("bindingContext", Resources.ModelBinderUtil_ModelMetadataCannotBeNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ValidateBindingContext(ModelBindingContext bindingContext, Type requiredType, bool allowNullModel)
|
||||||
|
{
|
||||||
|
ValidateBindingContext(bindingContext);
|
||||||
|
|
||||||
|
if (bindingContext.ModelType != requiredType)
|
||||||
|
{
|
||||||
|
throw Error.Argument("bindingContext", Resources.ModelBinderUtil_ModelTypeIsWrong, bindingContext.ModelType, requiredType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowNullModel && bindingContext.Model == null)
|
||||||
|
{
|
||||||
|
throw Error.Argument("bindingContext", Resources.ModelBinderUtil_ModelCannotBeNull, requiredType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingContext.Model != null && !bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo()))
|
||||||
|
{
|
||||||
|
throw Error.Argument("bindingContext", Resources.ModelBinderUtil_ModelInstanceIsWrong, bindingContext.Model.GetType(), requiredType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is a container for prefix values. It normalizes all the values into dotted-form and then stores
|
||||||
|
/// them in a sorted array. All queries for prefixes are also normalized to dotted-form, and searches
|
||||||
|
/// for ContainsPrefix are done with a binary search.
|
||||||
|
/// </summary>
|
||||||
|
public class PrefixContainer
|
||||||
|
{
|
||||||
|
private readonly ICollection<string> _originalValues;
|
||||||
|
private readonly string[] _sortedValues;
|
||||||
|
|
||||||
|
internal PrefixContainer(ICollection<string> values)
|
||||||
|
{
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("values");
|
||||||
|
}
|
||||||
|
|
||||||
|
_originalValues = values;
|
||||||
|
_sortedValues = _originalValues.ToArrayWithoutNulls();
|
||||||
|
Array.Sort(_sortedValues, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
if (prefix == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix.Length == 0)
|
||||||
|
{
|
||||||
|
return _sortedValues.Length > 0; // only match empty string when we have some value
|
||||||
|
}
|
||||||
|
|
||||||
|
PrefixComparer prefixComparer = new PrefixComparer(prefix);
|
||||||
|
bool containsPrefix = Array.BinarySearch(_sortedValues, prefix, prefixComparer) > -1;
|
||||||
|
if (!containsPrefix)
|
||||||
|
{
|
||||||
|
// If there's something in the search boundary that starts with the same name
|
||||||
|
// as the collection prefix that we're trying to find, the binary search would actually fail.
|
||||||
|
// For example, let's say we have foo.a, foo.bE and foo.b[0]. Calling Array.BinarySearch
|
||||||
|
// will fail to find foo.b because it will land on foo.bE, then look at foo.a and finally
|
||||||
|
// failing to find the prefix which is actually present in the container (foo.b[0]).
|
||||||
|
// Here we're doing another pass looking specifically for collection prefix.
|
||||||
|
containsPrefix = Array.BinarySearch(_sortedValues, prefix + "[", prefixComparer) > -1;
|
||||||
|
}
|
||||||
|
return containsPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given "foo.bar", "foo.hello", "something.other", foo[abc].baz and asking for prefix "foo" will return:
|
||||||
|
// - "bar"/"foo.bar"
|
||||||
|
// - "hello"/"foo.hello"
|
||||||
|
// - "abc"/"foo[abc]"
|
||||||
|
internal IDictionary<string, string> GetKeysFromPrefix(string prefix)
|
||||||
|
{
|
||||||
|
IDictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var entry in _originalValues)
|
||||||
|
{
|
||||||
|
if (entry != null)
|
||||||
|
{
|
||||||
|
if (entry.Length == prefix.Length)
|
||||||
|
{
|
||||||
|
// No key in this entry
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix.Length == 0)
|
||||||
|
{
|
||||||
|
GetKeyFromEmptyPrefix(entry, result);
|
||||||
|
}
|
||||||
|
else if (entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
GetKeyFromNonEmptyPrefix(prefix, entry, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GetKeyFromEmptyPrefix(string entry, IDictionary<string, string> results)
|
||||||
|
{
|
||||||
|
string key;
|
||||||
|
int dotPosition = entry.IndexOf('.');
|
||||||
|
int bracketPosition = entry.IndexOf('[');
|
||||||
|
int delimiterPosition = -1;
|
||||||
|
|
||||||
|
if (dotPosition == -1)
|
||||||
|
{
|
||||||
|
if (bracketPosition != -1)
|
||||||
|
{
|
||||||
|
delimiterPosition = bracketPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (bracketPosition == -1)
|
||||||
|
{
|
||||||
|
delimiterPosition = dotPosition;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delimiterPosition = Math.Min(dotPosition, bracketPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition);
|
||||||
|
results[key] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GetKeyFromNonEmptyPrefix(string prefix, string entry, IDictionary<string, string> results)
|
||||||
|
{
|
||||||
|
string key = null;
|
||||||
|
string fullName = null;
|
||||||
|
int keyPosition = prefix.Length + 1;
|
||||||
|
|
||||||
|
switch (entry[prefix.Length])
|
||||||
|
{
|
||||||
|
case '.':
|
||||||
|
int dotPosition = entry.IndexOf('.', keyPosition);
|
||||||
|
if (dotPosition == -1)
|
||||||
|
{
|
||||||
|
dotPosition = entry.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = entry.Substring(keyPosition, dotPosition - keyPosition);
|
||||||
|
fullName = entry.Substring(0, dotPosition);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
int bracketPosition = entry.IndexOf(']', keyPosition);
|
||||||
|
if (bracketPosition == -1)
|
||||||
|
{
|
||||||
|
// Malformed for dictionary
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = entry.Substring(keyPosition, bracketPosition - keyPosition);
|
||||||
|
fullName = entry.Substring(0, bracketPosition + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.ContainsKey(key))
|
||||||
|
{
|
||||||
|
results.Add(key, fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsPrefixMatch(string prefix, string testString)
|
||||||
|
{
|
||||||
|
if (testString == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix.Length == 0)
|
||||||
|
{
|
||||||
|
return true; // shortcut - non-null testString matches empty prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix.Length > testString.Length)
|
||||||
|
{
|
||||||
|
return false; // not long enough
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!testString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false; // prefix doesn't match
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testString.Length == prefix.Length)
|
||||||
|
{
|
||||||
|
return true; // exact match
|
||||||
|
}
|
||||||
|
|
||||||
|
// invariant: testString.Length > prefix.Length
|
||||||
|
switch (testString[prefix.Length])
|
||||||
|
{
|
||||||
|
case '.':
|
||||||
|
case '[':
|
||||||
|
return true; // known delimiters
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false; // not known delimiter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PrefixComparer : IComparer<String>
|
||||||
|
{
|
||||||
|
private string _prefix;
|
||||||
|
|
||||||
|
public PrefixComparer(string prefix)
|
||||||
|
{
|
||||||
|
_prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(string x, string y)
|
||||||
|
{
|
||||||
|
string testString = Object.ReferenceEquals(x, _prefix) ? y : x;
|
||||||
|
if (IsPrefixMatch(_prefix, testString))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringComparer.OrdinalIgnoreCase.Compare(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
{
|
{
|
||||||
public static class TypeExtensions
|
public static class TypeExtensions
|
||||||
{
|
{
|
||||||
public static bool IsCompatibleObject<T>(this object value)
|
public static bool IsCompatibleWith(this Type type, object value)
|
||||||
{
|
{
|
||||||
return (value is T || (value == null && TypeAllowsNullValue(typeof(T))));
|
return (value == null && AllowsNullValue(type)) ||
|
||||||
|
type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsNullableValueType(this Type type)
|
public static bool IsNullableValueType(this Type type)
|
||||||
|
|
@ -15,9 +16,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
return Nullable.GetUnderlyingType(type) != null;
|
return Nullable.GetUnderlyingType(type) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TypeAllowsNullValue(this Type type)
|
public static bool AllowsNullValue(this Type type)
|
||||||
{
|
{
|
||||||
return (!type.GetTypeInfo().IsValueType || IsNullableValueType(type));
|
return (!type.GetTypeInfo().IsValueType || IsNullableValueType(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool HasStringConverter(this Type type)
|
||||||
|
{
|
||||||
|
// TODO: This depends on TypeConverter which does not exist in the CoreCLR.
|
||||||
|
// return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
|
||||||
|
TypeInfo typeInfo = type.GetTypeInfo();
|
||||||
|
if (typeInfo.IsPrimitive || type == typeof(string))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (IsNullableValueType(type) && HasStringConverter(type.GenericTypeArguments[0]))
|
||||||
|
{
|
||||||
|
// Nullable<T> where T is a primitive type or has a type converter
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type[] GetTypeArgumentsIfMatch(Type closedType, Type matchingOpenType)
|
||||||
|
{
|
||||||
|
TypeInfo closedTypeInfo = closedType.GetTypeInfo();
|
||||||
|
if (!closedTypeInfo.IsGenericType)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type openType = closedType.GetGenericTypeDefinition();
|
||||||
|
return (matchingOpenType == openType) ? closedTypeInfo.GenericTypeArguments : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public abstract class AssociatedMetadataProvider<TModelMetadata> : IModelMetadataProvider
|
||||||
|
where TModelMetadata : ModelMetadata
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<Type, TypeInformation> _typeInfoCache = new ConcurrentDictionary<Type, TypeInformation>();
|
||||||
|
|
||||||
|
public IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
|
||||||
|
{
|
||||||
|
if (containerType == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("containerType");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetMetadataForPropertiesCore(container, containerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
|
||||||
|
{
|
||||||
|
if (containerType == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("containerType");
|
||||||
|
}
|
||||||
|
if (String.IsNullOrEmpty(propertyName))
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNullOrEmpty("propertyName");
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeInformation typeInfo = GetTypeInformation(containerType);
|
||||||
|
PropertyInformation propertyInfo;
|
||||||
|
if (!typeInfo.Properties.TryGetValue(propertyName, out propertyInfo))
|
||||||
|
{
|
||||||
|
throw Error.Argument("propertyName", Resources.Common_PropertyNotFound, containerType, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
|
||||||
|
{
|
||||||
|
if (modelType == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("modelType");
|
||||||
|
}
|
||||||
|
|
||||||
|
TModelMetadata prototype = GetTypeInformation(modelType).Prototype;
|
||||||
|
return CreateMetadataFromPrototype(prototype, modelAccessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelMetadata GetMetadataForParameter(ParameterInfo parameter)
|
||||||
|
{
|
||||||
|
if (parameter == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
TModelMetadata prototype = GetTypeInformation(parameter.ParameterType, parameter.GetCustomAttributes()).Prototype;
|
||||||
|
return CreateMetadataFromPrototype(prototype, modelAccessor: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override for creating the prototype metadata (without the accessor)
|
||||||
|
protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes,
|
||||||
|
Type containerType,
|
||||||
|
Type modelType,
|
||||||
|
string propertyName);
|
||||||
|
|
||||||
|
// Override for applying the prototype + modelAccess to yield the final metadata
|
||||||
|
protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype,
|
||||||
|
Func<object> modelAccessor);
|
||||||
|
|
||||||
|
private IEnumerable<ModelMetadata> GetMetadataForPropertiesCore(object container, Type containerType)
|
||||||
|
{
|
||||||
|
TypeInformation typeInfo = GetTypeInformation(containerType);
|
||||||
|
foreach (KeyValuePair<string, PropertyInformation> kvp in typeInfo.Properties)
|
||||||
|
{
|
||||||
|
PropertyInformation propertyInfo = kvp.Value;
|
||||||
|
Func<object> modelAccessor = null;
|
||||||
|
if (container != null)
|
||||||
|
{
|
||||||
|
Func<object, object> propertyGetter = propertyInfo.ValueAccessor;
|
||||||
|
modelAccessor = () => propertyGetter(container);
|
||||||
|
}
|
||||||
|
yield return CreateMetadataFromPrototype(propertyInfo.Prototype, modelAccessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeInformation GetTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes = null)
|
||||||
|
{
|
||||||
|
// This retrieval is implemented as a TryGetValue/TryAdd instead of a GetOrAdd to avoid the performance cost of creating instance delegates
|
||||||
|
TypeInformation typeInfo;
|
||||||
|
if (!_typeInfoCache.TryGetValue(type, out typeInfo))
|
||||||
|
{
|
||||||
|
typeInfo = CreateTypeInformation(type, associatedAttributes);
|
||||||
|
_typeInfoCache.TryAdd(type, typeInfo);
|
||||||
|
}
|
||||||
|
return typeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeInformation CreateTypeInformation(Type type, IEnumerable<Attribute> associatedAttributes)
|
||||||
|
{
|
||||||
|
TypeInfo typeInfo = type.GetTypeInfo();
|
||||||
|
IEnumerable<Attribute> attributes = typeInfo.GetCustomAttributes();
|
||||||
|
if (associatedAttributes != null)
|
||||||
|
{
|
||||||
|
attributes = attributes.Concat(associatedAttributes);
|
||||||
|
}
|
||||||
|
TypeInformation info = new TypeInformation
|
||||||
|
{
|
||||||
|
Prototype = CreateMetadataPrototype(attributes, containerType: null, modelType: type, propertyName: null)
|
||||||
|
};
|
||||||
|
// TODO: Determine if we need this. TypeDescriptor does not exist in CoreCLR.
|
||||||
|
//ICustomTypeDescriptor typeDescriptor = TypeDescriptorHelper.Get(type);
|
||||||
|
//info.TypeDescriptor = typeDescriptor;
|
||||||
|
|
||||||
|
Dictionary<string, PropertyInformation> properties = new Dictionary<string, PropertyInformation>();
|
||||||
|
|
||||||
|
// TODO: Figure out if there's a better way to identify public non-static properties
|
||||||
|
foreach (PropertyInfo property in type.GetRuntimeProperties().Where(p => p.GetMethod.IsPublic && !p.GetMethod.IsStatic))
|
||||||
|
{
|
||||||
|
// Avoid re-generating a property descriptor if one has already been generated for the property name
|
||||||
|
if (!properties.ContainsKey(property.Name))
|
||||||
|
{
|
||||||
|
properties.Add(property.Name, CreatePropertyInformation(type, property));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Properties = properties;
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PropertyInformation CreatePropertyInformation(Type containerType, PropertyInfo property)
|
||||||
|
{
|
||||||
|
PropertyInformation info = new PropertyInformation();
|
||||||
|
info.ValueAccessor = CreatePropertyValueAccessor(property);
|
||||||
|
info.Prototype = CreateMetadataPrototype(property.GetCustomAttributes().Cast<Attribute>(), containerType, property.PropertyType, property.Name);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<object, object> CreatePropertyValueAccessor(PropertyInfo property)
|
||||||
|
{
|
||||||
|
Type declaringType = property.DeclaringType;
|
||||||
|
TypeInfo declaringTypeInfo = declaringType.GetTypeInfo();
|
||||||
|
if (declaringTypeInfo.IsVisible)
|
||||||
|
{
|
||||||
|
if (property.CanRead)
|
||||||
|
{
|
||||||
|
MethodInfo getMethodInfo = property.GetMethod;
|
||||||
|
if (getMethodInfo != null)
|
||||||
|
{
|
||||||
|
return CreateDynamicValueAccessor(getMethodInfo, declaringType, property.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If either the type isn't public or we can't find a public getter, use the slow Reflection path
|
||||||
|
return container => property.GetValue(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses Lightweight Code Gen to generate a tiny delegate that gets the property value
|
||||||
|
// This is an optimization to avoid having to go through the much slower System.Reflection APIs
|
||||||
|
// e.g. generates (object o) => (Person)o.Id
|
||||||
|
private static Func<object, object> CreateDynamicValueAccessor(MethodInfo getMethodInfo, Type declaringType, string propertyName)
|
||||||
|
{
|
||||||
|
Contract.Assert(getMethodInfo != null && getMethodInfo.IsPublic && !getMethodInfo.IsStatic);
|
||||||
|
|
||||||
|
TypeInfo declaringTypeInfo = declaringType.GetTypeInfo();
|
||||||
|
Type propertyType = getMethodInfo.ReturnType;
|
||||||
|
DynamicMethod dynamicMethod = new DynamicMethod("Get" + propertyName + "From" + declaringType.Name, typeof(object), new Type[] { typeof(object) });
|
||||||
|
ILGenerator ilg = dynamicMethod.GetILGenerator();
|
||||||
|
|
||||||
|
// Load the container onto the stack, convert from object => declaring type for the property
|
||||||
|
ilg.Emit(OpCodes.Ldarg_0);
|
||||||
|
if (declaringTypeInfo.IsValueType)
|
||||||
|
{
|
||||||
|
ilg.Emit(OpCodes.Unbox, declaringType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ilg.Emit(OpCodes.Castclass, declaringType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if declaring type is value type, we use Call : structs don't have inheritance
|
||||||
|
// if get method is sealed or isn't virtual, we use Call : it can't be overridden
|
||||||
|
if (declaringTypeInfo.IsValueType || !getMethodInfo.IsVirtual || getMethodInfo.IsFinal)
|
||||||
|
{
|
||||||
|
ilg.Emit(OpCodes.Call, getMethodInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ilg.Emit(OpCodes.Callvirt, getMethodInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box if the property type is a value type, so it can be returned as an object
|
||||||
|
if (propertyType.GetTypeInfo().IsValueType)
|
||||||
|
{
|
||||||
|
ilg.Emit(OpCodes.Box, propertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return property value
|
||||||
|
ilg.Emit(OpCodes.Ret);
|
||||||
|
|
||||||
|
return (Func<object, object>)dynamicMethod.CreateDelegate(typeof(Func<object, object>));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TypeInformation
|
||||||
|
{
|
||||||
|
public TModelMetadata Prototype { get; set; }
|
||||||
|
public Dictionary<string, PropertyInformation> Properties { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PropertyInformation
|
||||||
|
{
|
||||||
|
public Func<object, object> ValueAccessor { get; set; }
|
||||||
|
public TModelMetadata Prototype { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class CachedDataAnnotationsMetadataAttributes
|
||||||
|
{
|
||||||
|
public CachedDataAnnotationsMetadataAttributes(IEnumerable<Attribute> attributes)
|
||||||
|
{
|
||||||
|
Display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
|
||||||
|
DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
|
||||||
|
DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
|
||||||
|
Editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
|
||||||
|
ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DisplayAttribute Display { get; protected set; }
|
||||||
|
|
||||||
|
public DisplayNameAttribute DisplayName { get; protected set; }
|
||||||
|
|
||||||
|
public DisplayFormatAttribute DisplayFormat { get; protected set; }
|
||||||
|
|
||||||
|
public EditableAttribute Editable { get; protected set; }
|
||||||
|
|
||||||
|
public ReadOnlyAttribute ReadOnly { get; protected set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
|
||||||
|
{
|
||||||
|
public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype,
|
||||||
|
Func<object> modelAccessor)
|
||||||
|
: base(prototype, modelAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CachedDataAnnotationsModelMetadata(DataAnnotationsModelMetadataProvider provider,
|
||||||
|
Type containerType,
|
||||||
|
Type modelType,
|
||||||
|
string propertyName,
|
||||||
|
IEnumerable<Attribute> attributes)
|
||||||
|
: base(provider,
|
||||||
|
containerType,
|
||||||
|
modelType,
|
||||||
|
propertyName,
|
||||||
|
new CachedDataAnnotationsMetadataAttributes(attributes))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ComputeConvertEmptyStringToNull()
|
||||||
|
{
|
||||||
|
return PrototypeCache.DisplayFormat != null
|
||||||
|
? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull
|
||||||
|
: base.ComputeConvertEmptyStringToNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string ComputeDescription()
|
||||||
|
{
|
||||||
|
return PrototypeCache.Display != null
|
||||||
|
? PrototypeCache.Display.GetDescription()
|
||||||
|
: base.ComputeDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ComputeIsReadOnly()
|
||||||
|
{
|
||||||
|
if (PrototypeCache.Editable != null)
|
||||||
|
{
|
||||||
|
return !PrototypeCache.Editable.AllowEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PrototypeCache.ReadOnly != null)
|
||||||
|
{
|
||||||
|
return PrototypeCache.ReadOnly.IsReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ComputeIsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string GetDisplayName()
|
||||||
|
{
|
||||||
|
// DisplayName could be provided by either the DisplayAttribute, or DisplayNameAttribute. If neither of
|
||||||
|
// those supply a name, then we fall back to the property name (in base.GetDisplayName()).
|
||||||
|
//
|
||||||
|
// DisplayName has lower precedence than Display.Name, for consistency with MVC.
|
||||||
|
|
||||||
|
// DisplayAttribute doesn't require you to set a name, so this could be null.
|
||||||
|
if (PrototypeCache.Display != null)
|
||||||
|
{
|
||||||
|
string name = PrototypeCache.Display.GetName();
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's also possible for DisplayNameAttribute to be used without setting a name. If a user does that, then DisplayName will
|
||||||
|
// return the empty string - but for consistency with MVC we allow it. We do fallback to the property name in the (unlikely)
|
||||||
|
// scenario that the user sets null as the DisplayName, again, for consistency with MVC.
|
||||||
|
if (PrototypeCache.DisplayName != null)
|
||||||
|
{
|
||||||
|
string name = PrototypeCache.DisplayName.DisplayName;
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither attribute specifies a name, we'll fall back to the property name.
|
||||||
|
return base.GetDisplayName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
// This class assumes that model metadata is expensive to create, and allows the user to
|
||||||
|
// stash a cache object that can be copied around as a prototype to make creation and
|
||||||
|
// computation quicker. It delegates the retrieval of values to getter methods, the results
|
||||||
|
// of which are cached on a per-metadata-instance basis.
|
||||||
|
//
|
||||||
|
// This allows flexible caching strategies: either caching the source of information across
|
||||||
|
// instances or caching of the actual information itself, depending on what the developer
|
||||||
|
// decides to put into the prototype cache.
|
||||||
|
public abstract class CachedModelMetadata<TPrototypeCache> : ModelMetadata
|
||||||
|
{
|
||||||
|
private bool _convertEmptyStringToNull;
|
||||||
|
private string _description;
|
||||||
|
private bool _isReadOnly;
|
||||||
|
private bool _isComplexType;
|
||||||
|
|
||||||
|
private bool _convertEmptyStringToNullComputed;
|
||||||
|
private bool _descriptionComputed;
|
||||||
|
private bool _isReadOnlyComputed;
|
||||||
|
private bool _isComplexTypeComputed;
|
||||||
|
|
||||||
|
// Constructor for creating real instances of the metadata class based on a prototype
|
||||||
|
protected CachedModelMetadata(CachedModelMetadata<TPrototypeCache> prototype, Func<object> modelAccessor)
|
||||||
|
: base(prototype.Provider, prototype.ContainerType, modelAccessor, prototype.ModelType, prototype.PropertyName)
|
||||||
|
{
|
||||||
|
CacheKey = prototype.CacheKey;
|
||||||
|
PrototypeCache = prototype.PrototypeCache;
|
||||||
|
|
||||||
|
_isComplexType = prototype.IsComplexType;
|
||||||
|
_isComplexTypeComputed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for creating the prototype instances of the metadata class
|
||||||
|
protected CachedModelMetadata(DataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, TPrototypeCache prototypeCache)
|
||||||
|
: base(provider, containerType, null /* modelAccessor */, modelType, propertyName)
|
||||||
|
{
|
||||||
|
PrototypeCache = prototypeCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override bool ConvertEmptyStringToNull
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_convertEmptyStringToNullComputed)
|
||||||
|
{
|
||||||
|
_convertEmptyStringToNull = ComputeConvertEmptyStringToNull();
|
||||||
|
_convertEmptyStringToNullComputed = true;
|
||||||
|
}
|
||||||
|
return _convertEmptyStringToNull;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_convertEmptyStringToNull = value;
|
||||||
|
_convertEmptyStringToNullComputed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override string Description
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_descriptionComputed)
|
||||||
|
{
|
||||||
|
_description = ComputeDescription();
|
||||||
|
_descriptionComputed = true;
|
||||||
|
}
|
||||||
|
return _description;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_description = value;
|
||||||
|
_descriptionComputed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override bool IsReadOnly
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_isReadOnlyComputed)
|
||||||
|
{
|
||||||
|
_isReadOnly = ComputeIsReadOnly();
|
||||||
|
_isReadOnlyComputed = true;
|
||||||
|
}
|
||||||
|
return _isReadOnly;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isReadOnly = value;
|
||||||
|
_isReadOnlyComputed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override bool IsComplexType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_isComplexTypeComputed)
|
||||||
|
{
|
||||||
|
_isComplexType = ComputeIsComplexType();
|
||||||
|
_isComplexTypeComputed = true;
|
||||||
|
}
|
||||||
|
return _isComplexType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TPrototypeCache PrototypeCache { get; set; }
|
||||||
|
|
||||||
|
protected virtual bool ComputeConvertEmptyStringToNull()
|
||||||
|
{
|
||||||
|
return base.ConvertEmptyStringToNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual string ComputeDescription()
|
||||||
|
{
|
||||||
|
return base.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool ComputeIsReadOnly()
|
||||||
|
{
|
||||||
|
return base.IsReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool ComputeIsComplexType()
|
||||||
|
{
|
||||||
|
return base.IsComplexType;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool ComputeIsFromBody()
|
||||||
|
{
|
||||||
|
return base.IsFromBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DataAnnotationsModelMetadataProvider : AssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>
|
||||||
|
{
|
||||||
|
protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(
|
||||||
|
IEnumerable<Attribute> attributes,
|
||||||
|
Type containerType,
|
||||||
|
Type modelType,
|
||||||
|
string propertyName)
|
||||||
|
{
|
||||||
|
return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(
|
||||||
|
CachedDataAnnotationsModelMetadata prototype,
|
||||||
|
Func<object> modelAccessor)
|
||||||
|
{
|
||||||
|
return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DisplayAttribute : Attribute
|
||||||
|
{
|
||||||
|
internal string GetDescription()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string GetName()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DisplayFormatAttribute : Attribute
|
||||||
|
{
|
||||||
|
public bool ConvertEmptyStringToNull { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DisplayNameAttribute : Attribute
|
||||||
|
{
|
||||||
|
public virtual string DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class EditableAttribute : Attribute
|
||||||
|
{
|
||||||
|
public bool AllowEdit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class EmptyModelMetadataProvider : AssociatedMetadataProvider<ModelMetadata>
|
||||||
|
{
|
||||||
|
protected override ModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes,
|
||||||
|
Type containerType,
|
||||||
|
Type modelType,
|
||||||
|
string propertyName)
|
||||||
|
{
|
||||||
|
return new ModelMetadata(this, containerType, null, modelType, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ModelMetadata CreateMetadataFromPrototype(ModelMetadata prototype, Func<object> modelAccessor)
|
||||||
|
{
|
||||||
|
return new ModelMetadata(this, prototype.ContainerType, modelAccessor, prototype.ModelType, prototype.PropertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public interface IModelMetadataProvider
|
||||||
|
{
|
||||||
|
IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType);
|
||||||
|
|
||||||
|
ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName);
|
||||||
|
|
||||||
|
ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType);
|
||||||
|
|
||||||
|
ModelMetadata GetMetadataForParameter(ParameterInfo parameterInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelMetadata
|
||||||
|
{
|
||||||
|
private readonly Type _containerType;
|
||||||
|
private readonly Type _modelType;
|
||||||
|
private readonly string _propertyName;
|
||||||
|
private EfficientTypePropertyKey<Type, string> _cacheKey;
|
||||||
|
|
||||||
|
private bool _convertEmptyStringToNull = true;
|
||||||
|
private object _model;
|
||||||
|
private Func<object> _modelAccessor;
|
||||||
|
private IEnumerable<ModelMetadata> _properties;
|
||||||
|
private Type _realModelType;
|
||||||
|
|
||||||
|
public ModelMetadata(IModelMetadataProvider provider,
|
||||||
|
Type containerType,
|
||||||
|
Func<object> modelAccessor,
|
||||||
|
Type modelType,
|
||||||
|
string propertyName)
|
||||||
|
{
|
||||||
|
if (provider == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("provider");
|
||||||
|
}
|
||||||
|
if (modelType == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("modelType");
|
||||||
|
}
|
||||||
|
|
||||||
|
Provider = provider;
|
||||||
|
|
||||||
|
_containerType = containerType;
|
||||||
|
_modelAccessor = modelAccessor;
|
||||||
|
_modelType = modelType;
|
||||||
|
_propertyName = propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ContainerType
|
||||||
|
{
|
||||||
|
get { return _containerType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ConvertEmptyStringToNull
|
||||||
|
{
|
||||||
|
get { return _convertEmptyStringToNull; }
|
||||||
|
set { _convertEmptyStringToNull = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the model needs to be consumed from the body.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool IsFromBody { get; set; }
|
||||||
|
|
||||||
|
public virtual bool IsComplexType
|
||||||
|
{
|
||||||
|
get { return !ModelType.HasStringConverter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsNullableValueType
|
||||||
|
{
|
||||||
|
get { return ModelType.IsNullableValueType(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool IsReadOnly { get; set; }
|
||||||
|
|
||||||
|
public object Model
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_modelAccessor != null)
|
||||||
|
{
|
||||||
|
_model = _modelAccessor();
|
||||||
|
_modelAccessor = null;
|
||||||
|
}
|
||||||
|
return _model;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_model = value;
|
||||||
|
_modelAccessor = null;
|
||||||
|
_properties = null;
|
||||||
|
_realModelType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ModelType
|
||||||
|
{
|
||||||
|
get { return _modelType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IEnumerable<ModelMetadata> Properties
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_properties == null)
|
||||||
|
{
|
||||||
|
_properties = Provider.GetMetadataForProperties(Model, RealModelType);
|
||||||
|
}
|
||||||
|
return _properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PropertyName
|
||||||
|
{
|
||||||
|
get { return _propertyName; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IModelMetadataProvider Provider { get; set; }
|
||||||
|
|
||||||
|
/// <returns>
|
||||||
|
/// Gets TModel if ModelType is Nullable(TModel), ModelType otherwise.
|
||||||
|
/// </returns>
|
||||||
|
internal Type RealModelType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_realModelType == null)
|
||||||
|
{
|
||||||
|
_realModelType = ModelType;
|
||||||
|
|
||||||
|
// Don't call GetType() if the model is Nullable<T>, because it will
|
||||||
|
// turn Nullable<T> into T for non-null values
|
||||||
|
if (Model != null && !ModelType.IsNullableValueType())
|
||||||
|
{
|
||||||
|
_realModelType = Model.GetType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _realModelType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal EfficientTypePropertyKey<Type, string> CacheKey
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cacheKey == null)
|
||||||
|
{
|
||||||
|
_cacheKey = CreateCacheKey(ContainerType, ModelType, PropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _cacheKey;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_cacheKey = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "The method is a delegating helper to choose among multiple property values")]
|
||||||
|
public virtual string GetDisplayName()
|
||||||
|
{
|
||||||
|
return PropertyName ?? ModelType.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Revive ModelValidators
|
||||||
|
//public virtual IEnumerable<ModelValidator> GetValidators(IEnumerable<ModelValidatorProvider> validatorProviders)
|
||||||
|
//{
|
||||||
|
// if (validatorProviders == null)
|
||||||
|
// {
|
||||||
|
// throw Error.ArgumentNull("validatorProviders");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return validatorProviders.SelectMany(provider => provider.GetValidators(this, validatorProviders));
|
||||||
|
//}
|
||||||
|
|
||||||
|
private static EfficientTypePropertyKey<Type, string> CreateCacheKey(Type containerType, Type modelType, string propertyName)
|
||||||
|
{
|
||||||
|
// If metadata is for a property then containerType != null && propertyName != null
|
||||||
|
// If metadata is for a type then containerType == null && propertyName == null, so we have to use modelType for the cache key.
|
||||||
|
return new EfficientTypePropertyKey<Type, string>(containerType ?? modelType, propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
#if K10
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ReadOnlyAttribute
|
||||||
|
{
|
||||||
|
public bool IsReadOnly { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelBindingContext
|
||||||
|
{
|
||||||
|
private string _modelName;
|
||||||
|
private ModelStateDictionary _modelState;
|
||||||
|
|
||||||
|
public ModelBindingContext()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// copies certain values that won't change between parent and child objects,
|
||||||
|
// e.g. ValueProvider, ModelState
|
||||||
|
public ModelBindingContext(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (bindingContext != null)
|
||||||
|
{
|
||||||
|
ModelState = bindingContext.ModelState;
|
||||||
|
ValueProvider = bindingContext.ValueProvider;
|
||||||
|
MetadataProvider = bindingContext.MetadataProvider;
|
||||||
|
ModelBinder = bindingContext.ModelBinder;
|
||||||
|
HttpContext = bindingContext.HttpContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Model
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureModelMetadata();
|
||||||
|
return ModelMetadata.Model;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
EnsureModelMetadata();
|
||||||
|
ModelMetadata.Model = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelMetadata ModelMetadata { get; set; }
|
||||||
|
|
||||||
|
public string ModelName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_modelName == null)
|
||||||
|
{
|
||||||
|
_modelName = String.Empty;
|
||||||
|
}
|
||||||
|
return _modelName;
|
||||||
|
}
|
||||||
|
set { _modelName = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is writeable to support unit testing")]
|
||||||
|
public ModelStateDictionary ModelState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_modelState == null)
|
||||||
|
{
|
||||||
|
_modelState = new ModelStateDictionary();
|
||||||
|
}
|
||||||
|
return _modelState;
|
||||||
|
}
|
||||||
|
set { _modelState = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ModelType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureModelMetadata();
|
||||||
|
return ModelMetadata.ModelType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FallbackToEmptyPrefix { get; set; }
|
||||||
|
|
||||||
|
public HttpContext HttpContext { get; set; }
|
||||||
|
|
||||||
|
public IValueProvider ValueProvider
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IModelBinder ModelBinder
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IModelMetadataProvider MetadataProvider
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureModelMetadata()
|
||||||
|
{
|
||||||
|
if (ModelMetadata == null)
|
||||||
|
{
|
||||||
|
throw Error.InvalidOperation(Resources.ModelBindingContext_ModelMetadataMustBeSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelError
|
||||||
|
{
|
||||||
|
public ModelError(Exception exception)
|
||||||
|
: this(exception, errorMessage: null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelError(Exception exception, string errorMessage)
|
||||||
|
: this(errorMessage)
|
||||||
|
{
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelError(string errorMessage)
|
||||||
|
{
|
||||||
|
ErrorMessage = errorMessage ?? String.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception Exception { get; private set; }
|
||||||
|
|
||||||
|
public string ErrorMessage { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelErrorCollection : Collection<ModelError>
|
||||||
|
{
|
||||||
|
public void Add(Exception exception)
|
||||||
|
{
|
||||||
|
Add(new ModelError(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string errorMessage)
|
||||||
|
{
|
||||||
|
Add(new ModelError(errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelState
|
||||||
|
{
|
||||||
|
private readonly ModelErrorCollection _errors = new ModelErrorCollection();
|
||||||
|
|
||||||
|
public ValueProviderResult Value { get; set; }
|
||||||
|
|
||||||
|
public ModelErrorCollection Errors
|
||||||
|
{
|
||||||
|
get { return _errors; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ModelStateDictionary : Dictionary<string, ModelState>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ModelState> _innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public ModelStateDictionary()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModelStateDictionary(ModelStateDictionary dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("dictionary");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in dictionary)
|
||||||
|
{
|
||||||
|
_innerDictionary.Add(entry.Key, entry.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid
|
||||||
|
{
|
||||||
|
get { return Values.All(modelState => modelState.Errors.Count == 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddModelError(string key, Exception exception)
|
||||||
|
{
|
||||||
|
GetModelStateForKey(key).Errors.Add(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddModelError(string key, string errorMessage)
|
||||||
|
{
|
||||||
|
GetModelStateForKey(key).Errors.Add(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelState GetModelStateForKey(string key)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("key");
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelState modelState;
|
||||||
|
if (!TryGetValue(key, out modelState))
|
||||||
|
{
|
||||||
|
modelState = new ModelState();
|
||||||
|
this[key] = modelState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelState;
|
||||||
|
}
|
||||||
|
public void SetModelValue(string key, ValueProviderResult value)
|
||||||
|
{
|
||||||
|
GetModelStateForKey(key).Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,114 @@ namespace Microsoft.AspNet.Mvc.ModelBinding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The argument '{0}' is null or empty..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ArgumentNullOrEmpty {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ArgumentNullOrEmpty", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The property {0}.{1} could not be found..
|
||||||
|
/// </summary>
|
||||||
|
internal static string Common_PropertyNotFound {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Common_PropertyNotFound", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The key is invalid JQuery syntax because it is missing a closing bracket..
|
||||||
|
/// </summary>
|
||||||
|
internal static string JQuerySyntaxMissingClosingBracket {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("JQuerySyntaxMissingClosingBracket", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The value '{0}' is not valid for {1}..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderConfig_ValueInvalid {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderConfig_ValueInvalid", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to A value is required..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderConfig_ValueRequired {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderConfig_ValueRequired", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The binding context has a null Model, but this binder requires a non-null model of type '{0}'..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderUtil_ModelCannotBeNull {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderUtil_ModelCannotBeNull", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderUtil_ModelInstanceIsWrong {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderUtil_ModelInstanceIsWrong", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The binding context cannot have a null ModelMetadata..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderUtil_ModelMetadataCannotBeNull {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderUtil_ModelMetadataCannotBeNull", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBinderUtil_ModelTypeIsWrong {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBinderUtil_ModelTypeIsWrong", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The ModelMetadata property must be set before accessing this property..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ModelBindingContext_ModelMetadataMustBeSet {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ModelBindingContext_ModelMetadataMustBeSet", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ValueProviderResult_ConversionThrew {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ValueProviderResult_ConversionThrew", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types..
|
||||||
|
/// </summary>
|
||||||
|
internal static string ValueProviderResult_NoConverterExists {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ValueProviderResult_NoConverterExists", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'..
|
/// Looks up a localized string similar to The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -70,7 +178,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to The model item passed into the ViewData is null, but this ViewData instance requires a non-null model item of type '{0}'..
|
/// Looks up a localized string similar to The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string ViewDataDictionary_ModelCannotBeNull {
|
internal static string ViewDataDictionary_ModelCannotBeNull {
|
||||||
get {
|
get {
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,42 @@
|
||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
|
<data name="ArgumentNullOrEmpty" xml:space="preserve">
|
||||||
|
<value>The argument '{0}' is null or empty.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Common_PropertyNotFound" xml:space="preserve">
|
||||||
|
<value>The property {0}.{1} could not be found.</value>
|
||||||
|
</data>
|
||||||
|
<data name="JQuerySyntaxMissingClosingBracket" xml:space="preserve">
|
||||||
|
<value>The key is invalid JQuery syntax because it is missing a closing bracket.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderConfig_ValueInvalid" xml:space="preserve">
|
||||||
|
<value>The value '{0}' is not valid for {1}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderConfig_ValueRequired" xml:space="preserve">
|
||||||
|
<value>A value is required.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderUtil_ModelCannotBeNull" xml:space="preserve">
|
||||||
|
<value>The binding context has a null Model, but this binder requires a non-null model of type '{0}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderUtil_ModelInstanceIsWrong" xml:space="preserve">
|
||||||
|
<value>The binding context has a Model of type '{0}', but this binder can only operate on models of type '{1}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderUtil_ModelMetadataCannotBeNull" xml:space="preserve">
|
||||||
|
<value>The binding context cannot have a null ModelMetadata.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBinderUtil_ModelTypeIsWrong" xml:space="preserve">
|
||||||
|
<value>The binding context has a ModelType of '{0}', but this binder can only operate on models of type '{1}'.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ModelBindingContext_ModelMetadataMustBeSet" xml:space="preserve">
|
||||||
|
<value>The ModelMetadata property must be set before accessing this property.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ValueProviderResult_ConversionThrew" xml:space="preserve">
|
||||||
|
<value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ValueProviderResult_NoConverterExists" xml:space="preserve">
|
||||||
|
<value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDataDictionary_ModelCannotBeNull" xml:space="preserve">
|
<data name="ViewDataDictionary_ModelCannotBeNull" xml:space="preserve">
|
||||||
<value>The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'.</value>
|
<value>The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "It is more fundamentally a value provider than a collection")]
|
||||||
|
public class CompositeValueProvider : Collection<IValueProvider>, IEnumerableValueProvider
|
||||||
|
{
|
||||||
|
public CompositeValueProvider()
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompositeValueProvider(IList<IValueProvider> list)
|
||||||
|
: base(list)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
if (this[i].ContainsPrefix(prefix))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
// Performance-sensitive
|
||||||
|
// Caching the count is faster for IList<T>
|
||||||
|
int itemCount = Items.Count;
|
||||||
|
for (int i = 0; i < itemCount; i++)
|
||||||
|
{
|
||||||
|
IValueProvider vp = Items[i];
|
||||||
|
ValueProviderResult result = vp.GetValue(key);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
|
||||||
|
{
|
||||||
|
foreach (IValueProvider vp in this)
|
||||||
|
{
|
||||||
|
IDictionary<string, string> result = GetKeysFromPrefixFromProvider(vp, prefix);
|
||||||
|
if (result != null && result.Count > 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IDictionary<string, string> GetKeysFromPrefixFromProvider(IValueProvider provider, string prefix)
|
||||||
|
{
|
||||||
|
IEnumerableValueProvider enumeratedProvider = provider as IEnumerableValueProvider;
|
||||||
|
return (enumeratedProvider != null) ? enumeratedProvider.GetKeysFromPrefix(prefix) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InsertItem(int index, IValueProvider item)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("item");
|
||||||
|
}
|
||||||
|
base.InsertItem(index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetItem(int index, IValueProvider item)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("item");
|
||||||
|
}
|
||||||
|
base.SetItem(index, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DictionaryBasedValueProvider : IValueProvider
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, object> _values;
|
||||||
|
|
||||||
|
public DictionaryBasedValueProvider(IDictionary<string, object> values)
|
||||||
|
{
|
||||||
|
_values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsPrefix(string key)
|
||||||
|
{
|
||||||
|
return _values.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("key");
|
||||||
|
}
|
||||||
|
|
||||||
|
object value;
|
||||||
|
if (_values.TryGetValue(key, out value))
|
||||||
|
{
|
||||||
|
return new ValueProviderResult(value, value.ToString(), CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
// Represents a value provider that contains a single value.
|
||||||
|
internal sealed class ElementalValueProvider : IValueProvider
|
||||||
|
{
|
||||||
|
public ElementalValueProvider(string name, object rawValue, CultureInfo culture)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
RawValue = rawValue;
|
||||||
|
Culture = culture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CultureInfo Culture { get; private set; }
|
||||||
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
public object RawValue { get; private set; }
|
||||||
|
|
||||||
|
public bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
return PrefixContainer.IsPrefixMatch(Name, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
return String.Equals(key, Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? new ValueProviderResult(RawValue, Convert.ToString(RawValue, Culture), Culture)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public interface IEnumerableValueProvider : IValueProvider
|
||||||
|
{
|
||||||
|
IDictionary<string, string> GetKeysFromPrefix(string prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public interface IValueProviderFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a value provider with values from the given <paramref name="requestContext"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestContext">RequestContext that value provider will populate from</param>
|
||||||
|
/// <returns>a value provider instance or null</returns>
|
||||||
|
IValueProvider GetValueProvider(RequestContext requestContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the methods that are required for a value provider.
|
||||||
|
/// </summary>
|
||||||
|
public interface IValueProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the collection contains the specified prefix.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prefix">The prefix to search for.</param>
|
||||||
|
/// <returns>true if the collection contains the specified prefix; otherwise, false.</returns>
|
||||||
|
bool ContainsPrefix(string prefix);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a value object using the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key of the value object to retrieve.</param>
|
||||||
|
/// <returns>The value object for the specified key. If the exact key is not found, null.</returns>
|
||||||
|
ValueProviderResult GetValue(string key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class NameValuePairsValueProvider : IEnumerableValueProvider
|
||||||
|
{
|
||||||
|
private readonly CultureInfo _culture;
|
||||||
|
private PrefixContainer _prefixContainer;
|
||||||
|
private readonly IReadableStringCollection _values;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a NameValuePairsProvider wrapping an existing set of key value pairs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The key value pairs to wrap.</param>
|
||||||
|
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
|
||||||
|
public NameValuePairsValueProvider(IReadableStringCollection values, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("values");
|
||||||
|
}
|
||||||
|
|
||||||
|
_values = values;
|
||||||
|
_culture = culture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CultureInfo Culture
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _culture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrefixContainer PrefixContainer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_prefixContainer == null)
|
||||||
|
{
|
||||||
|
// Initialization race is OK providing data remains read-only and object identity is not significant
|
||||||
|
// TODO: Figure out if we can have IReadableStringCollection expose Keys, Count etc
|
||||||
|
|
||||||
|
_prefixContainer = new PrefixContainer(_values.Select(v => v.Key).ToArray());
|
||||||
|
}
|
||||||
|
return _prefixContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
return PrefixContainer.ContainsPrefix(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix)
|
||||||
|
{
|
||||||
|
if (prefix == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
return PrefixContainer.GetKeysFromPrefix(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("key");
|
||||||
|
}
|
||||||
|
|
||||||
|
IList<string> values = _values.GetValues(key);
|
||||||
|
if (values == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (values.Count == 1)
|
||||||
|
{
|
||||||
|
var value = (string)values[0];
|
||||||
|
return new ValueProviderResult(value, value, _culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValueProviderResult(values, _values.Get(key), _culture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class QueryStringValueProvider : NameValuePairsValueProvider
|
||||||
|
{
|
||||||
|
public QueryStringValueProvider(HttpContext context, CultureInfo culture)
|
||||||
|
: base(context.Request.Query, culture)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class QueryStringValueProviderFactory : IValueProviderFactory
|
||||||
|
{
|
||||||
|
private static readonly object _cacheKey = new object();
|
||||||
|
|
||||||
|
public IValueProvider GetValueProvider(RequestContext requestContext)
|
||||||
|
{
|
||||||
|
if (requestContext == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("requestContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the query string once-per request.
|
||||||
|
IDictionary<object, object> storage = requestContext.HttpContext.Items;
|
||||||
|
object value;
|
||||||
|
if (!storage.TryGetValue(_cacheKey, out value))
|
||||||
|
{
|
||||||
|
var provider = new QueryStringValueProvider(requestContext.HttpContext, CultureInfo.InvariantCulture);
|
||||||
|
storage[_cacheKey] = provider;
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (QueryStringValueProvider)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class RouteValueValueProviderFactory : IValueProviderFactory
|
||||||
|
{
|
||||||
|
public IValueProvider GetValueProvider(RequestContext requestContext)
|
||||||
|
{
|
||||||
|
return new DictionaryBasedValueProvider(requestContext.RouteValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ValueProviderResult
|
||||||
|
{
|
||||||
|
private static readonly CultureInfo _staticCulture = CultureInfo.InvariantCulture;
|
||||||
|
private CultureInfo _instanceCulture;
|
||||||
|
|
||||||
|
// default constructor so that subclassed types can set the properties themselves
|
||||||
|
protected ValueProviderResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult(object rawValue, string attemptedValue, CultureInfo culture)
|
||||||
|
{
|
||||||
|
RawValue = rawValue;
|
||||||
|
AttemptedValue = attemptedValue;
|
||||||
|
Culture = culture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AttemptedValue { get; protected set; }
|
||||||
|
|
||||||
|
public CultureInfo Culture
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instanceCulture == null)
|
||||||
|
{
|
||||||
|
_instanceCulture = _staticCulture;
|
||||||
|
}
|
||||||
|
return _instanceCulture;
|
||||||
|
}
|
||||||
|
protected set { _instanceCulture = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public object RawValue { get; protected set; }
|
||||||
|
|
||||||
|
public object ConvertTo(Type type)
|
||||||
|
{
|
||||||
|
return ConvertTo(type, culture: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual object ConvertTo(Type type, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
throw Error.ArgumentNull("type");
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeInfo typeInfo = type.GetTypeInfo();
|
||||||
|
object value = RawValue;
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
// treat null route parameters as though they were the default value for the type
|
||||||
|
return typeInfo.IsValueType ? Activator.CreateInstance(type) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.GetType().GetTypeInfo().IsAssignableFrom(typeInfo))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
CultureInfo cultureToUse = culture ?? Culture;
|
||||||
|
return UnwrapPossibleArrayType(cultureToUse, value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ConvertSimpleType(CultureInfo culture, object value, TypeInfo destinationType)
|
||||||
|
{
|
||||||
|
if (value == null || value.GetType().GetTypeInfo().IsAssignableFrom(destinationType))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a user-input value but the user didn't type anything, return no value
|
||||||
|
string valueAsString = value as string;
|
||||||
|
|
||||||
|
if (valueAsString != null && String.IsNullOrWhiteSpace(valueAsString))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationType == typeof(int).GetTypeInfo())
|
||||||
|
{
|
||||||
|
return Convert.ToInt32(value);
|
||||||
|
}
|
||||||
|
else if (destinationType == typeof(bool).GetTypeInfo())
|
||||||
|
{
|
||||||
|
return Boolean.Parse(value.ToString());
|
||||||
|
}
|
||||||
|
else if (destinationType == typeof(string).GetTypeInfo())
|
||||||
|
{
|
||||||
|
return Convert.ToString(value);
|
||||||
|
}
|
||||||
|
throw Error.InvalidOperation(Resources.ValueProviderResult_NoConverterExists, value.GetType(), destinationType);
|
||||||
|
|
||||||
|
// TODO: Revive once we get TypeConverters
|
||||||
|
//TypeConverter converter = TypeDescriptor.GetConverter(destinationType);
|
||||||
|
//bool canConvertFrom = converter.CanConvertFrom(value.GetType());
|
||||||
|
//if (!canConvertFrom)
|
||||||
|
//{
|
||||||
|
// converter = TypeDescriptor.GetConverter(value.GetType());
|
||||||
|
//}
|
||||||
|
//if (!(canConvertFrom || converter.CanConvertTo(destinationType)))
|
||||||
|
//{
|
||||||
|
// // EnumConverter cannot convert integer, so we verify manually
|
||||||
|
// if (destinationType.GetTypeInfo().IsEnum && value is int)
|
||||||
|
// {
|
||||||
|
// return Enum.ToObject(destinationType, (int)value);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // In case of a Nullable object, we try again with its underlying type.
|
||||||
|
// Type underlyingType = Nullable.GetUnderlyingType(destinationType);
|
||||||
|
// if (underlyingType != null)
|
||||||
|
// {
|
||||||
|
// return ConvertSimpleType(culture, value, underlyingType);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// throw Error.InvalidOperation(Resources.ValueProviderResult_NoConverterExists, value.GetType(), destinationType);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//try
|
||||||
|
//{
|
||||||
|
// return canConvertFrom
|
||||||
|
// ? converter.ConvertFrom(null, culture, value)
|
||||||
|
// : converter.ConvertTo(null, culture, value, destinationType);
|
||||||
|
//}
|
||||||
|
//catch (Exception ex)
|
||||||
|
//{
|
||||||
|
// throw Error.InvalidOperation(ex, Resources.ValueProviderResult_ConversionThrew, value.GetType(), destinationType);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
|
||||||
|
{
|
||||||
|
// array conversion results in four cases, as below
|
||||||
|
Array valueAsArray = value as Array;
|
||||||
|
if (destinationType.IsArray)
|
||||||
|
{
|
||||||
|
Type destinationElementType = destinationType.GetElementType();
|
||||||
|
TypeInfo destElementTypeInfo = destinationElementType.GetTypeInfo();
|
||||||
|
if (valueAsArray != null)
|
||||||
|
{
|
||||||
|
// case 1: both destination + source type are arrays, so convert each element
|
||||||
|
IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
|
||||||
|
for (int i = 0; i < valueAsArray.Length; i++)
|
||||||
|
{
|
||||||
|
converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destElementTypeInfo);
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// case 2: destination type is array but source is single element, so wrap element in array + convert
|
||||||
|
object element = ConvertSimpleType(culture, value, destElementTypeInfo);
|
||||||
|
IList converted = Array.CreateInstance(destinationElementType, 1);
|
||||||
|
converted[0] = element;
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (valueAsArray != null)
|
||||||
|
{
|
||||||
|
// case 3: destination type is single element but source is array, so extract first element + convert
|
||||||
|
if (valueAsArray.Length > 0)
|
||||||
|
{
|
||||||
|
value = valueAsArray.GetValue(0);
|
||||||
|
return ConvertSimpleType(culture, value, destinationType.GetTypeInfo());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// case 3(a): source is empty array, so can't perform conversion
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// case 4: both destination + source type are single elements, so convert
|
||||||
|
return ConvertSimpleType(culture, value, destinationType.GetTypeInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
protected override void SetModel(object value)
|
protected override void SetModel(object value)
|
||||||
{
|
{
|
||||||
// IsCompatibleObject verifies if the value is either an instance of TModel or if value happens to be null that TModel is nullable type.
|
// IsCompatibleObject verifies if the value is either an instance of TModel or if value happens to be null that TModel is nullable type.
|
||||||
bool castWillSucceed = value.IsCompatibleObject<TModel>();
|
bool castWillSucceed = typeof(TModel).IsCompatibleWith(value);
|
||||||
|
|
||||||
if (castWillSucceed)
|
if (castWillSucceed)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"version" : "0.1-alpha-*",
|
"version" : "0.1-alpha-*",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*",
|
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Abstractions": "0.1-alpha-*"
|
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
|
||||||
|
"Newtonsoft.Json": "5.0.8"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"net45": { },
|
"net45": { },
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
protected virtual ActionDescriptor SelectBestCandidate(RequestContext context, List<ActionDescriptor> candidates)
|
protected virtual ActionDescriptor SelectBestCandidate(RequestContext context, List<ActionDescriptor> candidates)
|
||||||
{
|
{
|
||||||
var valueProviders = _valueProviderFactory.Select(vpf => vpf.CreateValueProvider(context)).ToArray();
|
var valueProviders = _valueProviderFactory.Select(vpf => vpf.GetValueProvider(context)).ToArray();
|
||||||
|
|
||||||
var applicableCandiates = new List<ActionDescriptorCandidate>();
|
var applicableCandiates = new List<ActionDescriptorCandidate>();
|
||||||
foreach (var action in candidates)
|
foreach (var action in candidates)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
|
||||||
{
|
|
||||||
public interface IValueProviderFactory
|
|
||||||
{
|
|
||||||
IValueProvider CreateValueProvider(RequestContext context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
|
|
||||||
using Microsoft.AspNet.Abstractions;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
|
||||||
{
|
|
||||||
// This is a temporary placeholder
|
|
||||||
public class QueryStringValueProviderFactory : IValueProviderFactory
|
|
||||||
{
|
|
||||||
public IValueProvider CreateValueProvider(RequestContext context)
|
|
||||||
{
|
|
||||||
return new QueryStringValueProvider(context.HttpContext.Request.Query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QueryStringValueProvider : IValueProvider
|
|
||||||
{
|
|
||||||
private readonly IReadableStringCollection _values;
|
|
||||||
|
|
||||||
public QueryStringValueProvider(IReadableStringCollection values)
|
|
||||||
{
|
|
||||||
_values = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ContainsPrefix(string key)
|
|
||||||
{
|
|
||||||
return _values.Get(key) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
|
||||||
{
|
|
||||||
// This is a temporary placeholder
|
|
||||||
public class RouteValueValueProviderFactory : IValueProviderFactory
|
|
||||||
{
|
|
||||||
public IValueProvider CreateValueProvider(RequestContext context)
|
|
||||||
{
|
|
||||||
return new ValueProvider(context.RouteValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
|
||||||
{
|
|
||||||
// This is a temporary placeholder
|
|
||||||
public class ValueProvider : IValueProvider
|
|
||||||
{
|
|
||||||
private readonly IDictionary<string, object> _values;
|
|
||||||
|
|
||||||
public ValueProvider(IDictionary<string, object> values)
|
|
||||||
{
|
|
||||||
_values = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ContainsPrefix(string key)
|
|
||||||
{
|
|
||||||
return _values.ContainsKey(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ArrayModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BindModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someName[0]", "42" },
|
||||||
|
{ "someName[1]", "84" }
|
||||||
|
};
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(valueProvider);
|
||||||
|
var binder = new ArrayModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
|
||||||
|
int[] array = bindingContext.Model as int[];
|
||||||
|
Assert.Equal(new[] { 42, 84 }, array);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBinder_ValueProviderDoesNotContainPrefix_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(new SimpleHttpValueProvider());
|
||||||
|
var binder = new ArrayModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool bound = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBinder_ModelMetadataReturnsReadOnly_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "foo[0]", "42" },
|
||||||
|
};
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(valueProvider);
|
||||||
|
bindingContext.ModelMetadata.IsReadOnly = true;
|
||||||
|
var binder = new ArrayModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool bound = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(bound);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateIntBinder()
|
||||||
|
{
|
||||||
|
var mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
var value = mbc.ValueProvider.GetValue(mbc.ModelName);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
mbc.Model = value.ConvertTo(mbc.ModelType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return mockIntBinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider)
|
||||||
|
{
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(int[])),
|
||||||
|
ModelName = "someName",
|
||||||
|
ValueProvider = valueProvider,
|
||||||
|
ModelBinder = CreateIntBinder(),
|
||||||
|
MetadataProvider = metadataProvider
|
||||||
|
};
|
||||||
|
return bindingContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class CollectionModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BindComplexCollectionFromIndexes_FiniteIndexes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someName[foo]", "42" },
|
||||||
|
{ "someName[baz]", "200" }
|
||||||
|
};
|
||||||
|
var bindingContext = GetModelBindingContext(valueProvider);
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<int> boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, new[] { "foo", "bar", "baz" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { 42, 0, 200 }, boundCollection.ToArray());
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Equal(new[] { "someName[foo]", "someName[baz]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindComplexCollectionFromIndexes_InfiniteIndexes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someName[0]", "42" },
|
||||||
|
{ "someName[1]", "100" },
|
||||||
|
{ "someName[3]", "400" }
|
||||||
|
};
|
||||||
|
var bindingContext = GetModelBindingContext(valueProvider);
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<int> boundCollection = binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { 42, 100 }, boundCollection.ToArray());
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Equal(new[] { "someName[0]", "someName[1]" }, bindingContext.ValidationNode.ChildNodes.Select(o => o.ModelStateKey).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_ComplexCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someName.index", new[] { "foo", "bar", "baz" } },
|
||||||
|
{ "someName[foo]", "42" },
|
||||||
|
{ "someName[bar]", "100" },
|
||||||
|
{ "someName[baz]", "200" }
|
||||||
|
};
|
||||||
|
var bindingContext = GetModelBindingContext(valueProvider);
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_SimpleCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someName", new[] { "42", "100", "200" } }
|
||||||
|
};
|
||||||
|
var bindingContext = GetModelBindingContext(valueProvider);
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Equal(new[] { 42, 100, 200 }, ((List<int>)bindingContext.Model).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindSimpleCollection_RawValueIsEmptyCollection_ReturnsEmptyList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<int> boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: new object[0], culture: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(boundCollection);
|
||||||
|
Assert.Empty(boundCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindSimpleCollection_RawValueIsNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var binder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<int> boundCollection = binder.BindSimpleCollection(bindingContext: null, rawValue: null, culture: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(boundCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindSimpleCollection_SubBindingSucceeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
var bindingContext = GetModelBindingContext(new SimpleHttpValueProvider());
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
// ModelValidationNode childValidationNode = null;
|
||||||
|
Mock.Get<IModelBinder>(bindingContext.ModelBinder)
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
Assert.Equal("someName", mbc.ModelName);
|
||||||
|
// childValidationNode = mbc.ValidationNode;
|
||||||
|
mbc.Model = 42;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var modelBinder = new CollectionModelBinder<int>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<int> boundCollection = modelBinder.BindSimpleCollection(bindingContext, new int[1], culture);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { 42 }, boundCollection.ToArray());
|
||||||
|
// Assert.Equal(new[] { childValidationNode }, bindingContext.ValidationNode.ChildNodes.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelBindingContext GetModelBindingContext(IValueProvider valueProvider)
|
||||||
|
{
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = metadataProvider.GetMetadataForType(null, typeof(int)),
|
||||||
|
ModelName = "someName",
|
||||||
|
ValueProvider = valueProvider,
|
||||||
|
ModelBinder = CreateIntBinder(),
|
||||||
|
MetadataProvider = metadataProvider
|
||||||
|
};
|
||||||
|
return bindingContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateIntBinder()
|
||||||
|
{
|
||||||
|
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
var value = mbc.ValueProvider.GetValue(mbc.ModelName);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
mbc.Model = value.ConvertTo(mbc.ModelType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return mockIntBinder.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ComplexModelDtoResultTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ThrowsIfValidationNodeIsNull()
|
||||||
|
{
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new ComplexModelDtoResult("some string"),
|
||||||
|
"validationNode");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
//[Fact]
|
||||||
|
//public void Constructor_SetsProperties()
|
||||||
|
//{
|
||||||
|
// // Arrange
|
||||||
|
// ModelValidationNode validationNode = GetValidationNode();
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// ComplexModelDtoResult result = new ComplexModelDtoResult("some string", validationNode);
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.Equal("some string", result.Model);
|
||||||
|
// Assert.Equal(validationNode, result.ValidationNode);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//private static ModelValidationNode GetValidationNode()
|
||||||
|
//{
|
||||||
|
// EmptyModelMetadataProvider provider = new EmptyModelMetadataProvider();
|
||||||
|
// ModelMetadata metadata = provider.GetMetadataForType(null, typeof(object));
|
||||||
|
// return new ModelValidationNode(metadata, "someKey");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ComplexModelDtoTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorThrowsIfModelMetadataIsNull()
|
||||||
|
{
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new ComplexModelDto(null, Enumerable.Empty<ModelMetadata>()),
|
||||||
|
"modelMetadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorThrowsIfPropertyMetadataIsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelMetadata modelMetadata = GetModelMetadata();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new ComplexModelDto(modelMetadata, null),
|
||||||
|
"propertyMetadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorSetsProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelMetadata modelMetadata = GetModelMetadata();
|
||||||
|
ModelMetadata[] propertyMetadata = new ModelMetadata[0];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ComplexModelDto dto = new ComplexModelDto(modelMetadata, propertyMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(modelMetadata, dto.ModelMetadata);
|
||||||
|
Assert.Equal(propertyMetadata, dto.PropertyMetadata.ToArray());
|
||||||
|
Assert.Empty(dto.Results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelMetadata GetModelMetadata()
|
||||||
|
{
|
||||||
|
return new ModelMetadata(new EmptyModelMetadataProvider(), typeof(object), null, typeof(object), "PropertyName");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class CompositeModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
bool validationCalled = false;
|
||||||
|
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = true,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
|
||||||
|
ModelName = "someName",
|
||||||
|
//ModelState = executionContext.Controller.ViewData.ModelState,
|
||||||
|
//PropertyFilter = _ => true,
|
||||||
|
ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "someName", "dummyValue" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns(
|
||||||
|
delegate(ModelBindingContext context)
|
||||||
|
{
|
||||||
|
Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata);
|
||||||
|
Assert.Equal("someName", context.ModelName);
|
||||||
|
Assert.Same(bindingContext.ValueProvider, context.ValueProvider);
|
||||||
|
|
||||||
|
context.Model = 42;
|
||||||
|
// TODO: Validation
|
||||||
|
// mbc.ValidationNode.Validating += delegate { validationCalled = true; };
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
//binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */);
|
||||||
|
IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool isBound = shimBinder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isBound);
|
||||||
|
Assert.Equal(42, bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.True(validationCalled);
|
||||||
|
Assert.True(bindingContext.ModelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
bool validationCalled = false;
|
||||||
|
List<int> expectedModel = new List<int> { 1, 2, 3, 4, 5 };
|
||||||
|
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = true,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
|
||||||
|
ModelName = "someName",
|
||||||
|
//ModelState = executionContext.Controller.ViewData.ModelState,
|
||||||
|
//PropertyFilter = _ => true,
|
||||||
|
ValueProvider = new SimpleValueProvider
|
||||||
|
{
|
||||||
|
{ "someOtherName", "dummyValue" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns(
|
||||||
|
delegate(ModelBindingContext mbc)
|
||||||
|
{
|
||||||
|
if (!String.IsNullOrEmpty(mbc.ModelName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata);
|
||||||
|
Assert.Equal("", mbc.ModelName);
|
||||||
|
Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider);
|
||||||
|
|
||||||
|
mbc.Model = expectedModel;
|
||||||
|
// TODO: Validation
|
||||||
|
// mbc.ValidationNode.Validating += delegate { validationCalled = true; };
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
//binderProviders.RegisterBinderForType(typeof(List<int>), mockIntBinder.Object, false /* suppressPrefixCheck */);
|
||||||
|
IModelBinder shimBinder = new CompositeModelBinder(mockIntBinder.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool isBound = shimBinder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isBound);
|
||||||
|
Assert.Equal(expectedModel, bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.True(validationCalled);
|
||||||
|
// Assert.True(bindingContext.ModelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelBinder> mockListBinder = new Mock<IModelBinder>();
|
||||||
|
mockListBinder.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns(false)
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
IModelBinder shimBinder = (IModelBinder)mockListBinder.Object;
|
||||||
|
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = false,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List<int>)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool isBound = shimBinder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isBound);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.True(bindingContext.ModelState.IsValid);
|
||||||
|
mockListBinder.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var innerBinder = Mock.Of<IModelBinder>();
|
||||||
|
CompositeModelBinder shimBinder = new CompositeModelBinder(innerBinder);
|
||||||
|
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = true,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)),
|
||||||
|
//ModelState = executionContext.Controller.ViewData.ModelState
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool isBound = shimBinder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isBound);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SimpleModel
|
||||||
|
{
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
public string LastName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SimpleValueProvider : Dictionary<string, object>, IValueProvider
|
||||||
|
{
|
||||||
|
private readonly CultureInfo _culture;
|
||||||
|
|
||||||
|
public SimpleValueProvider()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleValueProvider(CultureInfo culture)
|
||||||
|
: base(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
_culture = culture ?? CultureInfo.InvariantCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from ValueProviderUtil
|
||||||
|
public bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
foreach (string key in Keys)
|
||||||
|
{
|
||||||
|
if (key != null)
|
||||||
|
{
|
||||||
|
if (prefix.Length == 0)
|
||||||
|
{
|
||||||
|
return true; // shortcut - non-null key matches empty prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (key.Length == prefix.Length)
|
||||||
|
{
|
||||||
|
return true; // exact match
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (key[prefix.Length])
|
||||||
|
{
|
||||||
|
case '.': // known separator characters
|
||||||
|
case '[':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // nothing found
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
object rawValue;
|
||||||
|
if (TryGetValue(key, out rawValue))
|
||||||
|
{
|
||||||
|
return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// value not found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class DictionaryModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BindModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = metadataProvider.GetMetadataForType(null, 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") }
|
||||||
|
},
|
||||||
|
ModelBinder = CreateKvpBinder(),
|
||||||
|
MetadataProvider = metadataProvider
|
||||||
|
};
|
||||||
|
var binder = new DictionaryModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
|
||||||
|
var dictionary = Assert.IsAssignableFrom<IDictionary<int, string>>(bindingContext.Model);
|
||||||
|
Assert.NotNull(dictionary);
|
||||||
|
Assert.Equal(2, dictionary.Count);
|
||||||
|
Assert.Equal("forty-two", dictionary[42]);
|
||||||
|
Assert.Equal("eighty-four", dictionary[84]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateKvpBinder()
|
||||||
|
{
|
||||||
|
Mock<IModelBinder> mockKvpBinder = new Mock<IModelBinder>();
|
||||||
|
mockKvpBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
var value = mbc.ValueProvider.GetValue(mbc.ModelName);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
mbc.Model = value.ConvertTo(mbc.ModelType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return mockKvpBinder.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class KeyValuePairModelBinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_MissingKey_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider();
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(valueProvider, Mock.Of<IModelBinder>());
|
||||||
|
var binder = new KeyValuePairModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(retVal);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Empty(bindingContext.ValidationNode.ChildNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_MissingValue_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new SimpleHttpValueProvider();
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(valueProvider);
|
||||||
|
var binder = new KeyValuePairModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Equal(new[] { "someName.key" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_SubBindingSucceeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
IModelBinder innerBinder = new CompositeModelBinder(CreateStringBinder(), CreateIntBinder());
|
||||||
|
var valueProvider = new SimpleHttpValueProvider();
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(valueProvider, innerBinder);
|
||||||
|
|
||||||
|
var binder = new KeyValuePairModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Equal(new KeyValuePair<int, string>(42, "some-value"), bindingContext.Model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Equal(new[] { "someName.key", "someName.value" }, bindingContext.ValidationNode.ChildNodes.Select(n => n.ModelStateKey).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryBindStrongModel_BinderExists_BinderReturnsCorrectlyTypedObject_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(new SimpleHttpValueProvider());
|
||||||
|
var binder = new KeyValuePairModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
int model;
|
||||||
|
bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Equal(42, model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Single(bindingContext.ValidationNode.ChildNodes);
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryBindStrongModel_BinderExists_BinderReturnsIncorrectlyTypedObject_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var innerBinder = new Mock<IModelBinder>();
|
||||||
|
innerBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
Assert.Equal("someName.key", mbc.ModelName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
var bindingContext = GetBindingContext(new SimpleHttpValueProvider(), innerBinder.Object);
|
||||||
|
|
||||||
|
|
||||||
|
var binder = new KeyValuePairModelBinder<int, string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
int model;
|
||||||
|
bool retVal = binder.TryBindStrongModel(bindingContext, "key", out model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Equal(default(int), model);
|
||||||
|
// TODO: Validation
|
||||||
|
// Assert.Single(bindingContext.ValidationNode.ChildNodes);
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, IModelBinder innerBinder = null)
|
||||||
|
{
|
||||||
|
var metataProvider = new EmptyModelMetadataProvider();
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = metataProvider.GetMetadataForType(null, typeof(KeyValuePair<int, string>)),
|
||||||
|
ModelName = "someName",
|
||||||
|
ValueProvider = valueProvider,
|
||||||
|
ModelBinder = innerBinder ?? CreateIntBinder(),
|
||||||
|
MetadataProvider = metataProvider
|
||||||
|
};
|
||||||
|
return bindingContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateIntBinder()
|
||||||
|
{
|
||||||
|
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
if (mbc.ModelType == typeof(int))
|
||||||
|
{
|
||||||
|
mbc.Model = 42;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return mockIntBinder.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IModelBinder CreateStringBinder()
|
||||||
|
{
|
||||||
|
Mock<IModelBinder> mockIntBinder = new Mock<IModelBinder>();
|
||||||
|
mockIntBinder
|
||||||
|
.Setup(o => o.BindModel(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Returns((ModelBindingContext mbc) =>
|
||||||
|
{
|
||||||
|
if (mbc.ModelType == typeof(string))
|
||||||
|
{
|
||||||
|
mbc.Model = "some-value";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return mockIntBinder.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ModelBindingContextTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CopyConstructor()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext originalBindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
|
||||||
|
ModelName = "theName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleHttpValueProvider()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(newBindingContext.ModelMetadata);
|
||||||
|
Assert.Equal("", newBindingContext.ModelName);
|
||||||
|
Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
|
||||||
|
Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.Throws<InvalidOperationException>(
|
||||||
|
() => bindingContext.Model = null,
|
||||||
|
"The ModelMetadata property must be set before accessing this property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ModelAndModelTypeAreFedFromModelMetadata()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(42, bindingContext.Model);
|
||||||
|
Assert.Equal(typeof(int), bindingContext.ModelType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
//[Fact]
|
||||||
|
//public void ValidationNodeProperty()
|
||||||
|
//{
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName"));
|
||||||
|
//}
|
||||||
|
|
||||||
|
// TODO: Validation
|
||||||
|
//[Fact]
|
||||||
|
//public void ValidationNodeProperty_DefaultValues()
|
||||||
|
//{
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
|
||||||
|
// ModelName = "theInt"
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// ModelValidationNode validationNode = bindingContext.ValidationNode;
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.NotNull(validationNode);
|
||||||
|
// Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
|
||||||
|
// Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class TypeConverterModelBinderTest
|
||||||
|
{
|
||||||
|
// private static readonly ModelBinderErrorMessageProvider = (modelMetadata, incomingValue) => null;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(typeof(int));
|
||||||
|
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "not an integer" }
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(retVal);
|
||||||
|
Assert.Equal("The value 'not an integer' is not valid for Int32.", bindingContext.ModelState["theModelName"].Errors[0].ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState_ErrorNotAddedIfCallbackReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(typeof(int));
|
||||||
|
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "not an integer" }
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(retVal);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: TypeConverter
|
||||||
|
//[Fact]
|
||||||
|
//public void BindModel_Error_GeneralExceptionsSavedInModelState()
|
||||||
|
//{
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext bindingContext = GetBindingContext(typeof(Dummy));
|
||||||
|
// bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
|
// {
|
||||||
|
// { "theModelName", "foo" }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.False(retVal);
|
||||||
|
// Assert.Null(bindingContext.Model);
|
||||||
|
// Assert.Equal("The parameter conversion from type 'System.String' to type 'Microsoft.AspNet.Mvc.ModelBinding.Test.TypeConverterModelBinderTest+Dummy' failed. See the inner exception for more information.", bindingContext.ModelState["theModelName"].Errors[0].Exception.Message);
|
||||||
|
// Assert.Equal("From DummyTypeConverter: foo", bindingContext.ModelState["theModelName"].Errors[0].Exception.InnerException.Message);
|
||||||
|
//}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_NullValueProviderResult_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(typeof(int));
|
||||||
|
|
||||||
|
TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(retVal, "BindModel should have returned null.");
|
||||||
|
Assert.Empty(bindingContext.ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_ValidValueProviderResult_ConvertEmptyStringsToNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(typeof(string));
|
||||||
|
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "" }
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Null(bindingContext.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BindModel_ValidValueProviderResult_ReturnsModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ModelBindingContext bindingContext = GetBindingContext(typeof(int));
|
||||||
|
bindingContext.ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "theModelName", "42" }
|
||||||
|
};
|
||||||
|
|
||||||
|
TypeConverterModelBinder binder = new TypeConverterModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool retVal = binder.BindModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(retVal);
|
||||||
|
Assert.Equal(42, bindingContext.Model);
|
||||||
|
Assert.True(bindingContext.ModelState.ContainsKey("theModelName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelBindingContext GetBindingContext(Type modelType)
|
||||||
|
{
|
||||||
|
return new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, modelType),
|
||||||
|
ModelName = "theModelName",
|
||||||
|
ValueProvider = new SimpleHttpValueProvider() // empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: TypeConverter
|
||||||
|
//[TypeConverter(typeof(DummyTypeConverter))]
|
||||||
|
//private struct Dummy
|
||||||
|
//{
|
||||||
|
//}
|
||||||
|
|
||||||
|
private sealed class DummyTypeConverter : TypeConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
|
{
|
||||||
|
return (sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(String.Format("From DummyTypeConverter: {0}", value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class CultureReplacer : IDisposable
|
||||||
|
{
|
||||||
|
private const string _defaultCultureName = "en-GB";
|
||||||
|
private const string _defaultUICultureName = "en-US";
|
||||||
|
private static readonly CultureInfo _defaultCulture = CultureInfo.GetCultureInfo(_defaultCultureName);
|
||||||
|
private readonly CultureInfo _originalCulture;
|
||||||
|
private readonly CultureInfo _originalUICulture;
|
||||||
|
private readonly long _threadId;
|
||||||
|
|
||||||
|
// Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture
|
||||||
|
// We want to be able to find issues where the InvariantCulture is used, but a specific culture should be.
|
||||||
|
//
|
||||||
|
// UICulture => Language
|
||||||
|
public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName)
|
||||||
|
{
|
||||||
|
_originalCulture = Thread.CurrentThread.CurrentCulture;
|
||||||
|
_originalUICulture = Thread.CurrentThread.CurrentUICulture;
|
||||||
|
_threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
|
||||||
|
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(uiCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used.
|
||||||
|
/// </summary>
|
||||||
|
public static string DefaultCultureName
|
||||||
|
{
|
||||||
|
get { return _defaultCultureName; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the culture that is used as the default value for Thread.UICurrentCulture when CultureReplacer is used.
|
||||||
|
/// </summary>
|
||||||
|
public static string DefaultUICultureName
|
||||||
|
{
|
||||||
|
get { return _defaultUICultureName; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used.
|
||||||
|
/// </summary>
|
||||||
|
public static CultureInfo DefaultCulture
|
||||||
|
{
|
||||||
|
get { return _defaultCulture; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
Assert.True(Thread.CurrentThread.ManagedThreadId == _threadId, "The current thread is not the same as the thread invoking the constructor. This should never happen.");
|
||||||
|
Thread.CurrentThread.CurrentCulture = _originalCulture;
|
||||||
|
Thread.CurrentThread.CurrentUICulture = _originalUICulture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public static class ExceptionAssert
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an exception of the given type (or optionally a derived type) is thrown.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static TException Throws<TException>(Action testCode)
|
||||||
|
where TException : Exception
|
||||||
|
{
|
||||||
|
Type exceptionType = typeof(TException);
|
||||||
|
Exception exception = RecordException(testCode);
|
||||||
|
|
||||||
|
TargetInvocationException tie = exception as TargetInvocationException;
|
||||||
|
if (tie != null)
|
||||||
|
{
|
||||||
|
exception = tie.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotNull(exception);
|
||||||
|
return Assert.IsAssignableFrom<TException>(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an exception of the given type (or optionally a derived type) is thrown.
|
||||||
|
/// Also verifies that the exception message matches.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static TException Throws<TException>(Action testCode, string exceptionMessage)
|
||||||
|
where TException : Exception
|
||||||
|
{
|
||||||
|
var ex = Throws<TException>(testCode);
|
||||||
|
VerifyExceptionMessage(ex, exceptionMessage);
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an exception of the given type (or optionally a derived type) is thrown.
|
||||||
|
/// Also verified that the exception message matches.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TException">The type of the exception expected to be thrown</typeparam>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static TException Throws<TException>(Func<object> testCode, string exceptionMessage)
|
||||||
|
where TException : Exception
|
||||||
|
{
|
||||||
|
return Throws<TException>(() => { testCode(); }, exceptionMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the code throws an <see cref="ArgumentException"/> (or optionally any exception which derives from it).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||||
|
/// <param name="exceptionMessage">The exception message to verify</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage)
|
||||||
|
{
|
||||||
|
var ex = Throws<ArgumentException>(testCode);
|
||||||
|
|
||||||
|
if (paramName != null)
|
||||||
|
{
|
||||||
|
Assert.Equal(paramName, ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true);
|
||||||
|
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName)
|
||||||
|
{
|
||||||
|
var ex = Throws<ArgumentNullException>(testCode);
|
||||||
|
|
||||||
|
if (paramName != null)
|
||||||
|
{
|
||||||
|
Assert.Equal(paramName, ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot
|
||||||
|
/// be null or empty.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="testCode">A delegate to the code to be tested</param>
|
||||||
|
/// <param name="paramName">The name of the parameter that should throw the exception</param>
|
||||||
|
/// <returns>The exception that was thrown, when successful</returns>
|
||||||
|
/// <exception cref="ThrowsException">Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown</exception>
|
||||||
|
public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName)
|
||||||
|
{
|
||||||
|
return Throws<ArgumentException>(testCode, "Value cannot be null or empty.\r\nParameter name: " + paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've re-implemented all the xUnit.net Throws code so that we can get this
|
||||||
|
// updated implementation of RecordException which silently unwraps any instances
|
||||||
|
// of AggregateException. In addition to unwrapping exceptions, this method ensures
|
||||||
|
// that tests are executed in with a known set of Culture and UICulture. This prevents
|
||||||
|
// tests from failing when executed on a non-English machine.
|
||||||
|
private static Exception RecordException(Action testCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (new CultureReplacer())
|
||||||
|
{
|
||||||
|
testCode();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return UnwrapException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception UnwrapException(Exception exception)
|
||||||
|
{
|
||||||
|
AggregateException aggEx = exception as AggregateException;
|
||||||
|
if (aggEx != null)
|
||||||
|
{
|
||||||
|
return aggEx.GetBaseException();
|
||||||
|
}
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyException(Type exceptionType, Exception exception)
|
||||||
|
{
|
||||||
|
Assert.NotNull(exception);
|
||||||
|
Assert.IsAssignableFrom(exceptionType, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false)
|
||||||
|
{
|
||||||
|
if (expectedMessage != null)
|
||||||
|
{
|
||||||
|
if (!partialMatch)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Contains(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test.Binders
|
||||||
|
{
|
||||||
|
public class AssociatedMetadataProviderTest
|
||||||
|
{
|
||||||
|
// GetMetadataForProperties
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertiesNullContainerTypeThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => provider.GetMetadataForProperties(new Object(), containerType: null),
|
||||||
|
"containerType");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertiesCreatesMetadataForAllPropertiesOnModelWithPropertyValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
PropertyModel model = new PropertyModel { LocalAttributes = 42, MetadataAttributes = "hello", MixedAttributes = 21.12 };
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.GetMetadataForProperties(model, typeof(PropertyModel)).ToList(); // Call ToList() to force the lazy evaluation to evaluate
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
CreateMetadataPrototypeParams local =
|
||||||
|
provider.CreateMetadataPrototypeLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
|
||||||
|
m.PropertyName == "LocalAttributes");
|
||||||
|
Assert.Equal(typeof(int), local.ModelType);
|
||||||
|
Assert.True(local.Attributes.Any(a => a is RequiredAttribute));
|
||||||
|
|
||||||
|
CreateMetadataPrototypeParams metadata =
|
||||||
|
provider.CreateMetadataPrototypeLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
|
||||||
|
m.PropertyName == "MetadataAttributes");
|
||||||
|
Assert.Equal(typeof(string), metadata.ModelType);
|
||||||
|
Assert.True(metadata.Attributes.Any(a => a is RangeAttribute));
|
||||||
|
|
||||||
|
CreateMetadataPrototypeParams mixed =
|
||||||
|
provider.CreateMetadataPrototypeLog.Single(m => m.ContainerType == typeof(PropertyModel) &&
|
||||||
|
m.PropertyName == "MixedAttributes");
|
||||||
|
Assert.Equal(typeof(double), mixed.ModelType);
|
||||||
|
Assert.True(mixed.Attributes.Any(a => a is RequiredAttribute));
|
||||||
|
Assert.True(mixed.Attributes.Any(a => a is RangeAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyWithNullContainerReturnsMetadataWithNullValuesForProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.GetMetadataForProperties(null, typeof(PropertyModel)).ToList(); // Call ToList() to force the lazy evaluation to evaluate
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(provider.CreateMetadataFromPrototypeLog.Any());
|
||||||
|
foreach (var parms in provider.CreateMetadataFromPrototypeLog)
|
||||||
|
{
|
||||||
|
Assert.Null(parms.Model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataForProperty
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyNullContainerTypeThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => provider.GetMetadataForProperty(modelAccessor: null, containerType: null, propertyName: "propertyName"),
|
||||||
|
"containerType");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyNullOrEmptyPropertyNameThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgument(
|
||||||
|
() => provider.GetMetadataForProperty(modelAccessor: null, containerType: typeof(object), propertyName: null),
|
||||||
|
"propertyName",
|
||||||
|
"The argument 'propertyName' is null or empty.");
|
||||||
|
ExceptionAssert.ThrowsArgument(
|
||||||
|
() => provider.GetMetadataForProperty(modelAccessor: null, containerType: typeof(object), propertyName: String.Empty),
|
||||||
|
"propertyName",
|
||||||
|
"The argument 'propertyName' is null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyInvalidPropertyNameThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgument(
|
||||||
|
() => provider.GetMetadataForProperty(modelAccessor: null, containerType: typeof(object), propertyName: "BadPropertyName"),
|
||||||
|
"propertyName",
|
||||||
|
"The property System.Object.BadPropertyName could not be found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyWithLocalAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(int), "LocalAttributes");
|
||||||
|
provider.CreateMetadataFromPrototypeReturnValue = metadata;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "LocalAttributes");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(metadata, result);
|
||||||
|
Assert.True(provider.CreateMetadataPrototypeLog.Single(parameters => parameters.PropertyName == "LocalAttributes").Attributes.Any(a => a is RequiredAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyWithMetadataAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(string), "MetadataAttributes");
|
||||||
|
provider.CreateMetadataFromPrototypeReturnValue = metadata;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "MetadataAttributes");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(metadata, result);
|
||||||
|
CreateMetadataPrototypeParams parms = provider.CreateMetadataPrototypeLog.Single(p => p.PropertyName == "MetadataAttributes");
|
||||||
|
Assert.True(parms.Attributes.Any(a => a is RangeAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForPropertyWithMixedAttributes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider, typeof(PropertyModel), null, typeof(double), "MixedAttributes");
|
||||||
|
provider.CreateMetadataFromPrototypeReturnValue = metadata;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata result = provider.GetMetadataForProperty(null, typeof(PropertyModel), "MixedAttributes");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(metadata, result);
|
||||||
|
CreateMetadataPrototypeParams parms = provider.CreateMetadataPrototypeLog.Single(p => p.PropertyName == "MixedAttributes");
|
||||||
|
Assert.True(parms.Attributes.Any(a => a is RequiredAttribute));
|
||||||
|
Assert.True(parms.Attributes.Any(a => a is RangeAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataForType
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForTypeNullModelTypeThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => provider.GetMetadataForType(() => new Object(), modelType: null),
|
||||||
|
"modelType");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMetadataForTypeIncludesAttributesOnType()
|
||||||
|
{
|
||||||
|
TestableAssociatedMetadataProvider provider = new TestableAssociatedMetadataProvider();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider, null, null, typeof(TypeModel), null);
|
||||||
|
provider.CreateMetadataFromPrototypeReturnValue = metadata;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata result = provider.GetMetadataForType(null, typeof(TypeModel));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(metadata, result);
|
||||||
|
CreateMetadataPrototypeParams parms = provider.CreateMetadataPrototypeLog.Single(p => p.ModelType == typeof(TypeModel));
|
||||||
|
Assert.True(parms.Attributes.Any(a => a is ReadOnlyAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
// TODO: This type used System.ComponentModel.MetadataType to separate attribute declaration from property
|
||||||
|
// declaration. Need to figure out if this is still relevant since the type does not exist in CoreCLR.
|
||||||
|
private class PropertyModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int LocalAttributes { get; set; }
|
||||||
|
|
||||||
|
[Range(10, 100)]
|
||||||
|
public string MetadataAttributes { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(10, 100)]
|
||||||
|
public double MixedAttributes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RequiredAttribute : Attribute
|
||||||
|
{ }
|
||||||
|
|
||||||
|
private sealed class RangeAttribute : Attribute
|
||||||
|
{
|
||||||
|
public RangeAttribute(int min, int max)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ModelWithReadOnlyProperty
|
||||||
|
{
|
||||||
|
public int ReadOnlyProperty { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ReadOnly(true)]
|
||||||
|
private class TypeModel
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestableAssociatedMetadataProvider : AssociatedMetadataProvider<ModelMetadata>
|
||||||
|
{
|
||||||
|
public List<CreateMetadataPrototypeParams> CreateMetadataPrototypeLog = new List<CreateMetadataPrototypeParams>();
|
||||||
|
public List<CreateMetadataFromPrototypeParams> CreateMetadataFromPrototypeLog = new List<CreateMetadataFromPrototypeParams>();
|
||||||
|
public ModelMetadata CreateMetadataPrototypeReturnValue = null;
|
||||||
|
public ModelMetadata CreateMetadataFromPrototypeReturnValue = null;
|
||||||
|
|
||||||
|
protected override ModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
|
||||||
|
{
|
||||||
|
CreateMetadataPrototypeLog.Add(new CreateMetadataPrototypeParams
|
||||||
|
{
|
||||||
|
Attributes = attributes,
|
||||||
|
ContainerType = containerType,
|
||||||
|
ModelType = modelType,
|
||||||
|
PropertyName = propertyName
|
||||||
|
});
|
||||||
|
|
||||||
|
return CreateMetadataPrototypeReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ModelMetadata CreateMetadataFromPrototype(ModelMetadata prototype, Func<object> modelAccessor)
|
||||||
|
{
|
||||||
|
CreateMetadataFromPrototypeLog.Add(new CreateMetadataFromPrototypeParams
|
||||||
|
{
|
||||||
|
Prototype = prototype,
|
||||||
|
Model = modelAccessor == null ? null : modelAccessor()
|
||||||
|
});
|
||||||
|
|
||||||
|
return CreateMetadataFromPrototypeReturnValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateMetadataPrototypeParams
|
||||||
|
{
|
||||||
|
public IEnumerable<Attribute> Attributes { get; set; }
|
||||||
|
public Type ContainerType { get; set; }
|
||||||
|
public Type ModelType { get; set; }
|
||||||
|
public string PropertyName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateMetadataFromPrototypeParams
|
||||||
|
{
|
||||||
|
public ModelMetadata Prototype { get; set; }
|
||||||
|
public object Model { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ModelMetadataTest
|
||||||
|
{
|
||||||
|
// Guard clauses
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullProviderThrows()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new ModelMetadata(provider: null, containerType: null, modelAccessor: null, modelType: typeof(object), propertyName: null),
|
||||||
|
"provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullTypeThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new ModelMetadata(provider: provider.Object, containerType: null, modelAccessor: null, modelType: null, propertyName: null),
|
||||||
|
"modelType");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider.Object, typeof(Exception), () => "model", typeof(string), "propertyName");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(typeof(Exception), metadata.ContainerType);
|
||||||
|
Assert.True(metadata.ConvertEmptyStringToNull);
|
||||||
|
Assert.Null(metadata.Description);
|
||||||
|
Assert.Equal("model", metadata.Model);
|
||||||
|
Assert.Equal(typeof(string), metadata.ModelType);
|
||||||
|
Assert.Equal("propertyName", metadata.PropertyName);
|
||||||
|
Assert.False(metadata.IsReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComplexType
|
||||||
|
|
||||||
|
struct IsComplexTypeModel
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(string))]
|
||||||
|
[InlineData(typeof(Nullable<int>))]
|
||||||
|
[InlineData(typeof(int))]
|
||||||
|
public void IsComplexTypeTestsReturnsFalseForSimpleTypes(Type type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelMetadata = new ModelMetadata(provider.Object, null, null, type, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(modelMetadata.IsComplexType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(object))]
|
||||||
|
[InlineData(typeof(IDisposable))]
|
||||||
|
[InlineData(typeof(IsComplexTypeModel))]
|
||||||
|
[InlineData(typeof(Nullable<IsComplexTypeModel>))]
|
||||||
|
public void IsComplexTypeTestsReturnsTrueForComplexTypes(Type type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelMetadata = new ModelMetadata(provider.Object, null, null, type, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelMetadata.IsComplexType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNullableValueType
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsNullableValueTypeTests()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.False(new ModelMetadata(provider.Object, null, null, typeof(string), null).IsNullableValueType);
|
||||||
|
Assert.False(new ModelMetadata(provider.Object, null, null, typeof(IDisposable), null).IsNullableValueType);
|
||||||
|
Assert.True(new ModelMetadata(provider.Object, null, null, typeof(Nullable<int>), null).IsNullableValueType);
|
||||||
|
Assert.False(new ModelMetadata(provider.Object, null, null, typeof(int), null).IsNullableValueType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesCallsProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Type modelType = typeof(string);
|
||||||
|
List<ModelMetadata> propertyMetadata = new List<ModelMetadata>();
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, modelType, null);
|
||||||
|
provider.Setup(p => p.GetMetadataForProperties(null, modelType))
|
||||||
|
.Returns(propertyMetadata)
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IEnumerable<ModelMetadata> result = metadata.Properties;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(propertyMetadata, result.ToList());
|
||||||
|
provider.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesListGetsResetWhenModelGetsReset()
|
||||||
|
{
|
||||||
|
// Dev10 Bug #923263
|
||||||
|
// Arrange
|
||||||
|
IModelMetadataProvider provider = new EmptyModelMetadataProvider();
|
||||||
|
var metadata = new ModelMetadata(provider, null, () => new Class1(), typeof(Class1), null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelMetadata[] originalProps = metadata.Properties.ToArray();
|
||||||
|
metadata.Model = new Class2();
|
||||||
|
ModelMetadata[] newProps = metadata.Properties.ToArray();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
ModelMetadata originalProp = Assert.Single(originalProps);
|
||||||
|
Assert.Equal(typeof(string), originalProp.ModelType);
|
||||||
|
Assert.Equal("Prop1", originalProp.PropertyName);
|
||||||
|
ModelMetadata newProp = Assert.Single(newProps);
|
||||||
|
Assert.Equal(typeof(int), newProp.ModelType);
|
||||||
|
Assert.Equal("Prop2", newProp.PropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Class1
|
||||||
|
{
|
||||||
|
public string Prop1 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Class2
|
||||||
|
{
|
||||||
|
public int Prop2 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplayName()
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnsPropertyNameWhenSetAndDisplayNameIsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), "PropertyName");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string result = metadata.GetDisplayName();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("PropertyName", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReturnsTypeNameWhenPropertyNameAndDisplayNameAreNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Mock<IModelMetadataProvider> provider = new Mock<IModelMetadataProvider>();
|
||||||
|
ModelMetadata metadata = new ModelMetadata(provider.Object, null, null, typeof(object), null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string result = metadata.GetDisplayName();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Object", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
private class DummyContactModel
|
||||||
|
{
|
||||||
|
public int IntField = 0;
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
public string LastName { get; set; }
|
||||||
|
public Nullable<int> NullableIntValue { get; set; }
|
||||||
|
public int[] Array { get; set; }
|
||||||
|
|
||||||
|
public string this[int index]
|
||||||
|
{
|
||||||
|
get { return "Indexed into " + index; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DummyModelContainer
|
||||||
|
{
|
||||||
|
public DummyContactModel Model { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
//public class ModelBindingContextTest
|
||||||
|
//{
|
||||||
|
// [Fact]
|
||||||
|
// public void CopyConstructor()
|
||||||
|
// {
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext originalBindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object)),
|
||||||
|
// ModelName = "theName",
|
||||||
|
// ModelState = new ModelStateDictionary(),
|
||||||
|
// ValueProvider = new SimpleHttpValueProvider()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext newBindingContext = new ModelBindingContext(originalBindingContext);
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.Null(newBindingContext.ModelMetadata);
|
||||||
|
// Assert.Equal("", newBindingContext.ModelName);
|
||||||
|
// Assert.Equal(originalBindingContext.ModelState, newBindingContext.ModelState);
|
||||||
|
// Assert.Equal(originalBindingContext.ValueProvider, newBindingContext.ValueProvider);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ModelProperty()
|
||||||
|
// {
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int))
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// MemberHelper.TestPropertyValue(bindingContext, "Model", 42);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ModelProperty_ThrowsIfModelMetadataDoesNotExist()
|
||||||
|
// {
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext();
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// Assert.Throws<InvalidOperationException>(
|
||||||
|
// () => bindingContext.Model = null,
|
||||||
|
// "The ModelMetadata property must be set before accessing this property.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ModelNameProperty()
|
||||||
|
// {
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext();
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// Assert.Reflection.StringProperty(bindingContext, bc => bc.ModelName, String.Empty);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ModelStateProperty()
|
||||||
|
// {
|
||||||
|
// // Arrange
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext();
|
||||||
|
// ModelStateDictionary modelState = new ModelStateDictionary();
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ModelState", modelState);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ModelAndModelTypeAreFedFromModelMetadata()
|
||||||
|
// {
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.Equal(42, bindingContext.Model);
|
||||||
|
// Assert.Equal(typeof(int), bindingContext.ModelType);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ValidationNodeProperty()
|
||||||
|
// {
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int))
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act & assert
|
||||||
|
// MemberHelper.TestPropertyWithDefaultInstance(bindingContext, "ValidationNode", new ModelValidationNode(bindingContext.ModelMetadata, "someName"));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Fact]
|
||||||
|
// public void ValidationNodeProperty_DefaultValues()
|
||||||
|
// {
|
||||||
|
// // Act
|
||||||
|
// ModelBindingContext bindingContext = new ModelBindingContext
|
||||||
|
// {
|
||||||
|
// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(() => 42, typeof(int)),
|
||||||
|
// ModelName = "theInt"
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// ModelValidationNode validationNode = bindingContext.ValidationNode;
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.NotNull(validationNode);
|
||||||
|
// Assert.Equal(bindingContext.ModelMetadata, validationNode.ModelMetadata);
|
||||||
|
// Assert.Equal(bindingContext.ModelName, validationNode.ModelStateKey);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public sealed class SimpleHttpValueProvider : Dictionary<string, object>, IValueProvider
|
||||||
|
{
|
||||||
|
private readonly CultureInfo _culture;
|
||||||
|
|
||||||
|
public SimpleHttpValueProvider()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleHttpValueProvider(CultureInfo culture)
|
||||||
|
: base(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
_culture = culture ?? CultureInfo.InvariantCulture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from ValueProviderUtil
|
||||||
|
public bool ContainsPrefix(string prefix)
|
||||||
|
{
|
||||||
|
foreach (string key in Keys)
|
||||||
|
{
|
||||||
|
if (key != null)
|
||||||
|
{
|
||||||
|
if (prefix.Length == 0)
|
||||||
|
{
|
||||||
|
return true; // shortcut - non-null key matches empty prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (key.Length == prefix.Length)
|
||||||
|
{
|
||||||
|
return true; // exact match
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (key[prefix.Length])
|
||||||
|
{
|
||||||
|
case '.': // known separator characters
|
||||||
|
case '[':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // nothing found
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueProviderResult GetValue(string key)
|
||||||
|
{
|
||||||
|
object rawValue;
|
||||||
|
if (TryGetValue(key, out rawValue))
|
||||||
|
{
|
||||||
|
return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// value not found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Microsoft.AspNet.PipelineCore.Collections;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class NameValuePairsValueProviderTest
|
||||||
|
{
|
||||||
|
private static readonly IReadableStringCollection _backingStore = new ReadableStringCollection(
|
||||||
|
new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
{"foo", new[] { "fooValue1", "fooValue2"} },
|
||||||
|
{"bar.baz", new[] {"someOtherValue" }},
|
||||||
|
{"null_value", null},
|
||||||
|
{"prefix.null_value", null}
|
||||||
|
});
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_GuardClauses()
|
||||||
|
{
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => new NameValuePairsValueProvider(values: null, culture: CultureInfo.InvariantCulture),
|
||||||
|
"values");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsPrefix_GuardClauses()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => valueProvider.ContainsPrefix(null),
|
||||||
|
"prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsPrefix_WithEmptyCollection_ReturnsFalseForEmptyPrefix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var backingStore = new ReadableStringCollection(new Dictionary<string, string[]>());
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool result = valueProvider.ContainsPrefix("");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForEmptyPrefix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool result = valueProvider.ContainsPrefix("");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsPrefix_WithNonEmptyCollection_ReturnsTrueForKnownPrefixes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.True(valueProvider.ContainsPrefix("foo"));
|
||||||
|
Assert.True(valueProvider.ContainsPrefix("bar"));
|
||||||
|
Assert.True(valueProvider.ContainsPrefix("bar.baz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainsPrefix_WithNonEmptyCollection_ReturnsFalseForUnknownPrefix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
bool result = valueProvider.ContainsPrefix("biff");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetKeysFromPrefix_GuardClauses()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => valueProvider.GetKeysFromPrefix(null),
|
||||||
|
"prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetKeysFromPrefix_EmptyPrefix_ReturnsAllPrefixes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal<KeyValuePair<string, string>>(
|
||||||
|
result.OrderBy(kvp => kvp.Key),
|
||||||
|
new Dictionary<string, string> { { "bar", "bar" }, { "foo", "foo" }, { "null_value", "null_value" }, { "prefix", "prefix" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetKeysFromPrefix_UnknownPrefix_ReturnsEmptyDictionary()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("abc");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetKeysFromPrefix_KnownPrefix_ReturnsMatchingItems()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IDictionary<string, string> result = valueProvider.GetKeysFromPrefix("bar");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
KeyValuePair<string, string> kvp = Assert.Single(result);
|
||||||
|
Assert.Equal("baz", kvp.Key);
|
||||||
|
Assert.Equal("bar.baz", kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValue_GuardClauses()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(
|
||||||
|
() => valueProvider.GetValue(null),
|
||||||
|
"key");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValue_SingleValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, culture);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ValueProviderResult vpResult = valueProvider.GetValue("bar.baz");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(vpResult);
|
||||||
|
Assert.Equal("someOtherValue", vpResult.RawValue);
|
||||||
|
Assert.Equal("someOtherValue", vpResult.AttemptedValue);
|
||||||
|
Assert.Equal(culture, vpResult.Culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValue_MultiValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, culture);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ValueProviderResult vpResult = valueProvider.GetValue("foo");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(vpResult);
|
||||||
|
Assert.Equal(new [] { "fooValue1", "fooValue2" }, (IList<string>)vpResult.RawValue);
|
||||||
|
Assert.Equal("fooValue1,fooValue2", vpResult.AttemptedValue);
|
||||||
|
Assert.Equal(culture, vpResult.Culture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Determine if this is still relevant. Right now the lookup returns null while
|
||||||
|
// we expect a ValueProviderResult that wraps a null value.
|
||||||
|
//[Theory]
|
||||||
|
//[InlineData("null_value")]
|
||||||
|
//[InlineData("prefix.null_value")]
|
||||||
|
//public void GetValue_NullValue(string key)
|
||||||
|
//{
|
||||||
|
// // Arrange
|
||||||
|
// var culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
// var valueProvider = new NameValuePairsValueProvider(_backingStore, culture);
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
// ValueProviderResult vpResult = valueProvider.GetValue(key);
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
// Assert.NotNull(vpResult);
|
||||||
|
// Assert.Equal(null, vpResult.RawValue);
|
||||||
|
// Assert.Equal(null, vpResult.AttemptedValue);
|
||||||
|
// Assert.Equal(culture, vpResult.Culture);
|
||||||
|
//}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValue_NullMultipleValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var backingStore = new ReadableStringCollection(
|
||||||
|
new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
{ "key", new string[] { null, null, "value" } }
|
||||||
|
});
|
||||||
|
var culture = CultureInfo.GetCultureInfo("fr-FR");
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(backingStore, culture);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ValueProviderResult vpResult = valueProvider.GetValue("key");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new[] { null, null, "value" }, vpResult.RawValue as IEnumerable<string>);
|
||||||
|
Assert.Equal(",,value", vpResult.AttemptedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValue_ReturnsNullIfKeyNotFound()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var valueProvider = new NameValuePairsValueProvider(_backingStore, null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ValueProviderResult vpResult = valueProvider.GetValue("bar");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(vpResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class QueryStringValueProviderFactoryTest
|
||||||
|
{
|
||||||
|
private readonly QueryStringValueProviderFactory _factory = new QueryStringValueProviderFactory();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValueProvider_WhenrequestContextParameterIsNull_Throws()
|
||||||
|
{
|
||||||
|
// Act and Assert
|
||||||
|
ExceptionAssert.ThrowsArgumentNull(() => _factory.GetValueProvider(requestContext: null), "requestContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetValueProvider_ReturnsQueryStringValueProviderInstaceWithInvariantCulture()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new Mock<HttpRequest>();
|
||||||
|
request.SetupGet(f => f.Query).Returns(Mock.Of<IReadableStringCollection>());
|
||||||
|
var context = new Mock<HttpContext>();
|
||||||
|
context.SetupGet(c => c.Items).Returns(new Dictionary<object, object>());
|
||||||
|
context.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
var requestContext = new RequestContext(context.Object, new Dictionary<string, object>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IValueProvider result = _factory.GetValueProvider(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var valueProvider = Assert.IsType<QueryStringValueProvider>(result);
|
||||||
|
Assert.Equal(CultureInfo.InvariantCulture, valueProvider.Culture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
|
{
|
||||||
|
public class ValueProviderResultTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ConvertTo_ReturnsNullForReferenceTypes_WhenValueIsNull()
|
||||||
|
{
|
||||||
|
var valueProviderResult = new ValueProviderResult(null, null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var convertedValue = valueProviderResult.ConvertTo(typeof(string));
|
||||||
|
|
||||||
|
Assert.Equal(null, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertTo_ReturnsDefaultForValueTypes_WhenValueIsNull()
|
||||||
|
{
|
||||||
|
var valueProviderResult = new ValueProviderResult(null, null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var convertedValue = valueProviderResult.ConvertTo(typeof(int));
|
||||||
|
|
||||||
|
Assert.Equal(0, convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"version" : "0.1-alpha-*",
|
"version" : "0.1-alpha-*",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNet.Mvc.ModelBinding" : "0.1-alpha-*",
|
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
|
||||||
|
"Microsoft.AspNet.PipelineCore": "0.1-alpha-*",
|
||||||
|
"Microsoft.AspNet.Mvc.ModelBinding" : "",
|
||||||
"Moq": "4.0.10827",
|
"Moq": "4.0.10827",
|
||||||
"Xunit": "1.9.1",
|
"Xunit": "1.9.1",
|
||||||
"Xunit.extensions": "1.9.1"
|
"Xunit.extensions": "1.9.1"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue