// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Formatters; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { public static class ModelBindingHelper { /// /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// /// The type of the model object. /// The model instance to update and validate. /// The prefix to use when looking up values in the . /// /// The for the current executing request. /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. /// The used for binding. /// The used for looking up values. /// /// The set of instances for deserializing the body. /// /// The used for validating the /// bound values. /// The used for executing validation /// on the model instance. /// A that on completion returns true if the update is successful public static Task TryUpdateModelAsync( [NotNull] TModel model, [NotNull] string prefix, [NotNull] HttpContext httpContext, [NotNull] ModelStateDictionary modelState, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelBinder modelBinder, [NotNull] IValueProvider valueProvider, [NotNull] IList inputFormatters, [NotNull] IObjectModelValidator objectModelValidator, [NotNull] IModelValidatorProvider validatorProvider) where TModel : class { // Includes everything by default. return TryUpdateModelAsync( model, prefix, httpContext, modelState, metadataProvider, modelBinder, valueProvider, inputFormatters, objectModelValidator, validatorProvider, predicate: (context, propertyName) => true); } /// /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// /// The type of the model object. /// The model instance to update and validate. /// The prefix to use when looking up values in the . /// /// The for the current executing request. /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. /// The used for binding. /// The used for looking up values. /// /// The set of instances for deserializing the body. /// /// The used for validating the /// bound values. /// The used for executing validation /// on the model /// instance. /// Expression(s) which represent top level properties /// which need to be included for the current model. /// A that on completion returns true if the update is successful public static Task TryUpdateModelAsync( [NotNull] TModel model, [NotNull] string prefix, [NotNull] HttpContext httpContext, [NotNull] ModelStateDictionary modelState, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelBinder modelBinder, [NotNull] IValueProvider valueProvider, [NotNull] IList inputFormatters, [NotNull] IObjectModelValidator objectModelValidator, [NotNull] IModelValidatorProvider validatorProvider, [NotNull] params Expression>[] includeExpressions) where TModel : class { var includeExpression = GetIncludePredicateExpression(prefix, includeExpressions); var predicate = includeExpression.Compile(); return TryUpdateModelAsync( model, prefix, httpContext, modelState, metadataProvider, modelBinder, valueProvider, inputFormatters, objectModelValidator, validatorProvider, predicate: predicate); } /// /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// /// The type of the model object. /// The model instance to update and validate. /// The prefix to use when looking up values in the . /// /// The for the current executing request. /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. /// The used for binding. /// The used for looking up values. /// /// The set of instances for deserializing the body. /// /// The used for validating the /// bound values. /// The used for executing validation /// on the model instance. /// A predicate which can be used to /// filter properties(for inclusion/exclusion) at runtime. /// A that on completion returns true if the update is successful public static Task TryUpdateModelAsync( [NotNull] TModel model, [NotNull] string prefix, [NotNull] HttpContext httpContext, [NotNull] ModelStateDictionary modelState, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelBinder modelBinder, [NotNull] IValueProvider valueProvider, [NotNull] IList inputFormatters, [NotNull] IObjectModelValidator objectModelValidator, [NotNull] IModelValidatorProvider validatorProvider, [NotNull] Func predicate) where TModel : class { return TryUpdateModelAsync( model, typeof(TModel), prefix, httpContext, modelState, metadataProvider, modelBinder, valueProvider, inputFormatters, objectModelValidator, validatorProvider, predicate: predicate); } /// /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// /// The model instance to update and validate. /// The type of model instance to update and validate. /// The prefix to use when looking up values in the . /// /// The for the current executing request. /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. /// The used for binding. /// The used for looking up values. /// /// The set of instances for deserializing the body. /// /// The used for validating the /// bound values. /// The used for executing validation /// on the model instance. /// A that on completion returns true if the update is successful public static Task TryUpdateModelAsync( [NotNull] object model, [NotNull] Type modelType, [NotNull] string prefix, [NotNull] HttpContext httpContext, [NotNull] ModelStateDictionary modelState, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelBinder modelBinder, [NotNull] IValueProvider valueProvider, [NotNull] IList inputFormatters, [NotNull] IObjectModelValidator objectModelValidator, [NotNull] IModelValidatorProvider validatorProvider) { // Includes everything by default. return TryUpdateModelAsync( model, modelType, prefix, httpContext, modelState, metadataProvider, modelBinder, valueProvider, inputFormatters, objectModelValidator, validatorProvider, predicate: (context, propertyName) => true); } /// /// Updates the specified instance using the specified /// and the specified and executes validation using the specified /// . /// /// The model instance to update and validate. /// The type of model instance to update and validate. /// The prefix to use when looking up values in the . /// /// The for the current executing request. /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. /// The used for binding. /// The used for looking up values. /// /// The set of instances for deserializing the body. /// /// The used for validating the /// bound values. /// The used for executing validation /// on the model instance. /// A predicate which can be used to /// filter properties(for inclusion/exclusion) at runtime. /// A that on completion returns true if the update is successful public static async Task TryUpdateModelAsync( [NotNull] object model, [NotNull] Type modelType, [NotNull] string prefix, [NotNull] HttpContext httpContext, [NotNull] ModelStateDictionary modelState, [NotNull] IModelMetadataProvider metadataProvider, [NotNull] IModelBinder modelBinder, [NotNull] IValueProvider valueProvider, [NotNull] IList inputFormatters, [NotNull] IObjectModelValidator objectModelValidator, [NotNull] IModelValidatorProvider validatorProvider, [NotNull] Func predicate) { if (!modelType.IsAssignableFrom(model.GetType())) { var message = Resources.FormatModelType_WrongType( model.GetType().FullName, modelType.FullName); throw new ArgumentException(message, nameof(modelType)); } var modelMetadata = metadataProvider.GetMetadataForType(modelType); // Clear ModelStateDictionary entries for the model so that it will be re-validated. ClearValidationStateForModel(modelType, modelState, metadataProvider, prefix); var operationBindingContext = new OperationBindingContext { InputFormatters = inputFormatters, ModelBinder = modelBinder, ValidatorProvider = validatorProvider, MetadataProvider = metadataProvider, HttpContext = httpContext, ValueProvider = valueProvider, }; var modelBindingContext = ModelBindingContext.CreateBindingContext( operationBindingContext, modelState, modelMetadata, bindingInfo: null, modelName: prefix ?? string.Empty); modelBindingContext.Model = model; modelBindingContext.PropertyFilter = predicate; var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext); if (modelBindingResult.IsModelSet) { objectModelValidator.Validate( operationBindingContext.ValidatorProvider, modelState, modelBindingContext.ValidationState, modelBindingResult.Key, modelBindingResult.Model); return modelState.IsValid; } return false; } // Internal for tests internal static string GetPropertyName(Expression expression) { if (expression.NodeType == ExpressionType.Convert || expression.NodeType == ExpressionType.ConvertChecked) { // For Boxed Value Types expression = ((UnaryExpression)expression).Operand; } if (expression.NodeType != ExpressionType.MemberAccess) { throw new InvalidOperationException(Resources.FormatInvalid_IncludePropertyExpression( expression.NodeType)); } var memberExpression = (MemberExpression)expression; var memberInfo = memberExpression.Member as PropertyInfo; if (memberInfo != null) { if (memberExpression.Expression.NodeType != ExpressionType.Parameter) { // Chained expressions and non parameter based expressions are not supported. throw new InvalidOperationException( Resources.FormatInvalid_IncludePropertyExpression(expression.NodeType)); } return memberInfo.Name; } else { // Fields are also not supported. throw new InvalidOperationException(Resources.FormatInvalid_IncludePropertyExpression( expression.NodeType)); } } /// /// Creates an expression for a predicate to limit the set of properties used in model binding. /// /// The model type. /// The model prefix. /// Expressions identifying the properties to allow for binding. /// An expression which can be used with . public static Expression> GetIncludePredicateExpression( string prefix, Expression>[] expressions) { if (expressions.Length == 0) { // If nothing is included explcitly, treat everything as included. return (context, propertyName) => true; } var firstExpression = GetPredicateExpression(prefix, expressions[0]); var orWrapperExpression = firstExpression.Body; foreach (var expression in expressions.Skip(1)) { var predicate = GetPredicateExpression(prefix, expression); orWrapperExpression = Expression.OrElse(orWrapperExpression, Expression.Invoke(predicate, firstExpression.Parameters)); } return Expression.Lambda>( orWrapperExpression, firstExpression.Parameters); } private static Expression> GetPredicateExpression (string prefix, Expression> expression) { var propertyName = GetPropertyName(expression.Body); var property = ModelNames.CreatePropertyModelName(prefix, propertyName); return (context, modelPropertyName) => property.Equals(ModelNames.CreatePropertyModelName(context.ModelName, modelPropertyName), StringComparison.OrdinalIgnoreCase); } /// /// Clears entries for . /// /// The . /// The entry to clear. /// The . public static void ClearValidationStateForModel( [NotNull] Type modelType, [NotNull] ModelStateDictionary modelstate, [NotNull] IModelMetadataProvider metadataProvider, string modelKey) { // If modelkey is empty, we need to iterate through properties (obtained from ModelMetadata) and // clear validation state for all entries in ModelStateDictionary that start with each property name. // If modelkey is non-empty, clear validation state for all entries in ModelStateDictionary // that start with modelKey if (string.IsNullOrEmpty(modelKey)) { var modelMetadata = metadataProvider.GetMetadataForType(modelType); var elementMetadata = modelMetadata.ElementMetadata; if (elementMetadata != null) { modelMetadata = elementMetadata; } foreach (var property in modelMetadata.Properties) { var childKey = property.BinderModelName ?? property.PropertyName; modelstate.ClearValidationState(childKey); } } else { modelstate.ClearValidationState(modelKey); } } internal static void ValidateBindingContext([NotNull] ModelBindingContext bindingContext) { if (bindingContext.ModelMetadata == null) { throw new ArgumentException(Resources.ModelBinderUtil_ModelMetadataCannotBeNull, nameof(bindingContext)); } } internal static void ValidateBindingContext( ModelBindingContext bindingContext, Type requiredType, bool allowNullModel) { ValidateBindingContext(bindingContext); if (bindingContext.ModelType != requiredType) { var message = Resources.FormatModelBinderUtil_ModelTypeIsWrong(bindingContext.ModelType, requiredType); throw new ArgumentException(message, nameof(bindingContext)); } if (!allowNullModel && bindingContext.Model == null) { var message = Resources.FormatModelBinderUtil_ModelCannotBeNull(requiredType); throw new ArgumentException(message, nameof(bindingContext)); } if (bindingContext.Model != null && !bindingContext.ModelType.GetTypeInfo().IsAssignableFrom(requiredType.GetTypeInfo())) { var message = Resources.FormatModelBinderUtil_ModelInstanceIsWrong( bindingContext.Model.GetType(), requiredType); throw new ArgumentException(message, nameof(bindingContext)); } } internal static TModel CastOrDefault(object model) { return (model is TModel) ? (TModel)model : default(TModel); } public static object ConvertValuesToCollectionType(Type modelType, IList values) { // There's a limited set of collection types we can support here. // // For the simple cases - choose a T[] or List if the destination type supports // it. // // For more complex cases, if the destination type is a class and implements ICollection // then activate it and add the values. // // Otherwise just give up. if (typeof(List).IsAssignableFrom(modelType)) { return new List(values); } else if (typeof(T[]).IsAssignableFrom(modelType)) { return values.ToArray(); } else if ( modelType.GetTypeInfo().IsClass && !modelType.GetTypeInfo().IsAbstract && typeof(ICollection).IsAssignableFrom(modelType)) { var result = (ICollection)Activator.CreateInstance(modelType); foreach (var value in values) { result.Add(value); } return result; } else if (typeof(IEnumerable).IsAssignableFrom(modelType)) { return values; } else { return null; } } /// /// Converts the provided to a value of /// using the . /// /// The for conversion. /// The value to convert."/> /// /// The converted value or the default value of if the value could not be converted. /// public static T ConvertTo(object value) { return ConvertTo(value, culture: null); } /// /// Converts the provided to a value of . /// /// The for conversion. /// The value to convert."/> /// The for conversion. /// /// The converted value or the default value of if the value could not be converted. /// public static T ConvertTo(object value, CultureInfo culture) { var converted = ConvertTo(value, typeof(T), culture); return converted == null ? default(T) : (T)converted; } /// /// Converts the provided to a value of /// using the . /// /// The value to convert."/> /// The for conversion. /// /// The converted value or null if the value could not be converted. /// public static object ConvertTo(object value, [NotNull] Type type) { return ConvertTo(value, type, culture: null); } /// /// Converts the provided to a value of . /// /// The value to convert."/> /// The for conversion. /// The for conversion. /// /// The converted value or null if the value could not be converted. /// public static object ConvertTo(object value, [NotNull] Type type, CultureInfo culture) { if (value == null) { // For value types, treat null values as though they were the default value for the type. return type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : null; } if (value.GetType().IsAssignableFrom(type)) { return value; } var cultureToUse = culture ?? CultureInfo.InvariantCulture; return UnwrapPossibleArrayType(value, type, cultureToUse); } private static object UnwrapPossibleArrayType(object value, Type destinationType, CultureInfo culture) { // array conversion results in four cases, as below var valueAsArray = value as Array; if (destinationType.IsArray) { var destinationElementType = destinationType.GetElementType(); if (valueAsArray != null) { // case 1: both destination + source type are arrays, so convert each element var converted = (IList)Array.CreateInstance(destinationElementType, valueAsArray.Length); for (var i = 0; i < valueAsArray.Length; i++) { converted[i] = ConvertSimpleType(valueAsArray.GetValue(i), destinationElementType, culture); } return converted; } else { // case 2: destination type is array but source is single element, so wrap element in // array + convert var element = ConvertSimpleType(value, destinationElementType, culture); var converted = (IList)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(value, destinationType, culture); } 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(value, destinationType, culture); } private static object ConvertSimpleType(object value, Type destinationType, CultureInfo culture) { if (value == null || value.GetType().IsAssignableFrom(destinationType)) { return value; } // In case of a Nullable object, we try again with its underlying type. destinationType = UnwrapNullableType(destinationType); // if this is a user-input value but the user didn't type anything, return no value var valueAsString = value as string; if (valueAsString != null && string.IsNullOrWhiteSpace(valueAsString)) { return null; } var converter = TypeDescriptor.GetConverter(destinationType); var 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 || value is uint || value is long || value is ulong || value is short || value is ushort || value is byte || value is sbyte)) { return Enum.ToObject(destinationType, value); } throw new InvalidOperationException( Resources.FormatValueProviderResult_NoConverterExists(value.GetType(), destinationType)); } try { return canConvertFrom ? converter.ConvertFrom(null, culture, value) : converter.ConvertTo(null, culture, value, destinationType); } catch (FormatException) { throw; } catch (Exception ex) { if (ex.InnerException == null) { throw; } else { // TypeConverter throws System.Exception wrapping the FormatException, // so we throw the inner exception. ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); // This code is never reached because the previous line will always throw. throw; } } } private static Type UnwrapNullableType(Type destinationType) { return Nullable.GetUnderlyingType(destinationType) ?? destinationType; } } }