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 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)
|
||||
|
|
@ -15,9 +16,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
|||
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));
|
||||
}
|
||||
|
||||
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>
|
||||
/// 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>
|
||||
|
|
@ -70,7 +178,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding {
|
|||
}
|
||||
|
||||
/// <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>
|
||||
internal static string ViewDataDictionary_ModelCannotBeNull {
|
||||
get {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,42 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</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">
|
||||
<value>The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'.</value>
|
||||
</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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"version" : "0.1-alpha-*",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*",
|
||||
"Microsoft.AspNet.Abstractions": "0.1-alpha-*"
|
||||
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
|
||||
"Newtonsoft.Json": "5.0.8"
|
||||
},
|
||||
"configurations": {
|
||||
"net45": { },
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
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>();
|
||||
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-*",
|
||||
"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",
|
||||
"Xunit": "1.9.1",
|
||||
"Xunit.extensions": "1.9.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue