// 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.Diagnostics; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc { 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 }; var modelBindingContext = new ModelBindingContext { Model = model, ModelMetadata = modelMetadata, ModelName = prefix, ModelState = modelState, ValueProvider = valueProvider, FallbackToEmptyPrefix = true, OperationBindingContext = operationBindingContext, PropertyFilter = predicate }; var modelBindingResult = await modelBinder.BindModelAsync(modelBindingContext); if (modelBindingResult != null) { var modelExplorer = new ModelExplorer(metadataProvider, modelMetadata, modelBindingResult.Model); var modelValidationContext = new ModelValidationContext(modelBindingContext, modelExplorer); objectModelValidator.Validate( modelValidationContext, new ModelValidationNode(prefix, modelBindingContext.ModelMetadata, modelBindingResult.Model) { ValidateAllProperties = true }); 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); } internal static void ReplaceEmptyStringWithNull(ModelMetadata modelMetadata, ref object model) { if (model is string && modelMetadata.ConvertEmptyStringToNull && string.IsNullOrWhiteSpace(model as string)) { model = null; } } 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; } } } }