Reintroduce model binding

This commit is contained in:
Pranav K 2014-02-23 15:23:58 -08:00
parent 906e68e72e
commit b6c78de4ea
78 changed files with 5628 additions and 87 deletions

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
//}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -1,8 +0,0 @@

namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IValueProvider
{
bool ContainsPrefix(string key);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class DisplayNameAttribute : Attribute
{
public virtual string DisplayName { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,10 @@
#if K10
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ReadOnlyAttribute
{
public bool IsReadOnly { get; set; }
}
}
#endif

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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));
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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;
}
}
}

View File

@ -60,6 +60,114 @@ namespace Microsoft.AspNet.Mvc.ModelBinding {
}
}
/// <summary>
/// Looks up a localized string similar to The argument &apos;{0}&apos; 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 &apos;{0}&apos; 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 &apos;{0}&apos;..
/// </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 &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
/// </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 &apos;{0}&apos;, but this binder can only operate on models of type &apos;{1}&apos;..
/// </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 &apos;{0}&apos; to type &apos;{1}&apos; 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 &apos;{0}&apos; to type &apos;{1}&apos; 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 &apos;{0}&apos;, but this ViewData instance requires a model item of type &apos;{1}&apos;..
/// </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 &apos;{0}&apos;..
/// 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 &apos;{0}&apos;..
/// </summary>
internal static string ViewDataDictionary_ModelCannotBeNull {
get {

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IEnumerableValueProvider : IValueProvider
{
IDictionary<string, string> GetKeysFromPrefix(string prefix);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@

namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class RouteValueValueProviderFactory : IValueProviderFactory
{
public IValueProvider GetValueProvider(RequestContext requestContext)
{
return new DictionaryBasedValueProvider(requestContext.RouteValues);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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)
{

View File

@ -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": { },

View File

@ -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)

View File

@ -1,9 +0,0 @@

namespace Microsoft.AspNet.Mvc.ModelBinding
{
public interface IValueProviderFactory
{
IValueProvider CreateValueProvider(RequestContext context);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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");
//}
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
//}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
// }
//}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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"