// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; using System.Runtime.CompilerServices; using Microsoft.AspNet.Mvc.ModelBinding.Internal; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { /// /// Recursively validate an object. /// public class DefaultObjectValidator : IObjectModelValidator { private readonly IList _excludeFilters; private readonly IModelMetadataProvider _modelMetadataProvider; /// /// Initializes a new instance of . /// /// s that determine /// types to exclude from validation. /// The . public DefaultObjectValidator( [NotNull] IList excludeFilters, [NotNull] IModelMetadataProvider modelMetadataProvider) { _modelMetadataProvider = modelMetadataProvider; _excludeFilters = excludeFilters; } /// public void Validate([NotNull] ModelValidationContext modelValidationContext) { var validationContext = new ValidationContext() { ModelValidationContext = modelValidationContext, Visited = new HashSet(ReferenceEqualityComparer.Instance), }; ValidateNonVisitedNodeAndChildren( modelValidationContext.RootPrefix, validationContext, validators: null); } private bool ValidateNonVisitedNodeAndChildren( string modelKey, ValidationContext validationContext, IList validators) { var modelValidationContext = validationContext.ModelValidationContext; var modelExplorer = modelValidationContext.ModelExplorer; // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); var modelState = modelValidationContext.ModelState; var bindingSource = modelValidationContext.BindingSource; if (bindingSource != null && !bindingSource.IsFromRequest) { // Short circuit if the metadata represents something that was not bound using request data. // For example model bound using [FromServices]. Treat such objects as skipped. var validationState = modelState.GetFieldValidationState(modelKey); if (validationState == ModelValidationState.Unvalidated) { modelValidationContext.ModelState.MarkFieldSkipped(modelKey); } // For validation purposes this model is valid. return true; } if (modelState.HasReachedMaxErrors) { // Short circuit if max errors have been recorded. In which case we treat this as invalid. return false; } var isValid = true; if (validators == null) { // The validators are not null in the case of validating an array. Since the validators are // the same for all the elements of the array, we do not do GetValidators for each element, // instead we just pass them over. See ValidateElements function. var validatorProvider = modelValidationContext.ValidatorProvider; var validatorProviderContext = new ModelValidatorProviderContext(modelExplorer.Metadata); validatorProvider.GetValidators(validatorProviderContext); validators = validatorProviderContext.Validators; } // We don't need to recursively traverse the graph for null values if (modelExplorer.Model == null) { return ShallowValidate(modelKey, modelExplorer, validationContext, validators); } // We don't need to recursively traverse the graph for types that shouldn't be validated var modelType = modelExplorer.Model.GetType(); if (IsTypeExcludedFromValidation(_excludeFilters, modelType)) { var result = ShallowValidate(modelKey, modelExplorer, validationContext, validators); MarkPropertiesAsSkipped(modelKey, modelExplorer.Metadata, validationContext); return result; } // Check to avoid infinite recursion. This can happen with cycles in an object graph. if (validationContext.Visited.Contains(modelExplorer.Model)) { return true; } validationContext.Visited.Add(modelExplorer.Model); // Validate the children first - depth-first traversal var enumerableModel = modelExplorer.Model as IEnumerable; if (enumerableModel == null) { isValid = ValidateProperties(modelKey, modelExplorer, validationContext); } else { isValid = ValidateElements(modelKey, enumerableModel, validationContext); } if (isValid) { // Don't bother to validate this node if children failed. isValid = ShallowValidate(modelKey, modelExplorer, validationContext, validators); } // Pop the object so that it can be validated again in a different path validationContext.Visited.Remove(modelExplorer.Model); return isValid; } private void MarkPropertiesAsSkipped(string currentModelKey, ModelMetadata metadata, ValidationContext validationContext) { var modelState = validationContext.ModelValidationContext.ModelState; var fieldValidationState = modelState.GetFieldValidationState(currentModelKey); // Since shallow validation is done, if the modelvalidation state is still marked as unvalidated, // it is because some properties in the subtree are marked as unvalidated. Mark all such properties // as skipped. Models which have their subtrees as Valid or Invalid do not need to be marked as skipped. if (fieldValidationState != ModelValidationState.Unvalidated) { return; } foreach (var childMetadata in metadata.Properties) { var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, childMetadata.PropertyName); var validationState = modelState.GetFieldValidationState(childKey); if (validationState == ModelValidationState.Unvalidated) { validationContext.ModelValidationContext.ModelState.MarkFieldSkipped(childKey); } } } private bool ValidateProperties( string currentModelKey, ModelExplorer modelExplorer, ValidationContext validationContext) { var isValid = true; foreach (var property in modelExplorer.Metadata.Properties) { var propertyExplorer = modelExplorer.GetExplorerForProperty(property.PropertyName); var propertyMetadata = propertyExplorer.Metadata; var propertyValidationContext = new ValidationContext() { ModelValidationContext = ModelValidationContext.GetChildValidationContext( validationContext.ModelValidationContext, propertyExplorer), Visited = validationContext.Visited }; var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName; var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName); if (!ValidateNonVisitedNodeAndChildren( childKey, propertyValidationContext, validators: null)) { isValid = false; } } return isValid; } private bool ValidateElements(string currentKey, IEnumerable model, ValidationContext validationContext) { var elementType = GetElementType(model.GetType()); var elementMetadata = _modelMetadataProvider.GetMetadataForType(elementType); var validatorProvider = validationContext.ModelValidationContext.ValidatorProvider; var validatorProviderContext = new ModelValidatorProviderContext(elementMetadata); validatorProvider.GetValidators(validatorProviderContext); var validators = validatorProviderContext.Validators; // If there are no validators or the object is null we bail out quickly // when there are large arrays of null, this will save a significant amount of processing // with minimal impact to other scenarios. var anyValidatorsDefined = validators.Any(); var index = 0; var isValid = true; foreach (var element in model) { // If the element is non null, the recursive calls might find more validators. // If it's null, then a shallow validation will be performed. if (element != null || anyValidatorsDefined) { var elementExplorer = new ModelExplorer(_modelMetadataProvider, elementMetadata, element); var elementKey = ModelBindingHelper.CreateIndexModelName(currentKey, index); var elementValidationContext = new ValidationContext() { ModelValidationContext = ModelValidationContext.GetChildValidationContext( validationContext.ModelValidationContext, elementExplorer), Visited = validationContext.Visited }; if (!ValidateNonVisitedNodeAndChildren(elementKey, elementValidationContext, validators)) { isValid = false; } } index++; } return isValid; } // Validates a single node (not including children) // Returns true if validation passes successfully private static bool ShallowValidate( string modelKey, ModelExplorer modelExplorer, ValidationContext validationContext, IList validators) { var isValid = true; var modelState = validationContext.ModelValidationContext.ModelState; var fieldValidationState = modelState.GetFieldValidationState(modelKey); if (fieldValidationState == ModelValidationState.Invalid) { // Even if we have no validators it's possible that model binding may have added a // validation error (conversion error, missing data). We want to still run // validators even if that's the case. isValid = false; } // When the are no validators we bail quickly. This saves a GetEnumerator allocation. // In a large array (tens of thousands or more) scenario it's very significant. if (validators == null || validators.Count > 0) { var modelValidationContext = ModelValidationContext.GetChildValidationContext( validationContext.ModelValidationContext, modelExplorer); var modelValidationState = modelState.GetValidationState(modelKey); // If either the model or its properties are unvalidated, validate them now. if (modelValidationState == ModelValidationState.Unvalidated || fieldValidationState == ModelValidationState.Unvalidated) { foreach (var validator in validators) { foreach (var error in validator.Validate(modelValidationContext)) { var errorKey = ModelBindingHelper.CreatePropertyModelName(modelKey, error.MemberName); if (!modelState.TryAddModelError(errorKey, error.Message) && modelState.GetFieldValidationState(errorKey) == ModelValidationState.Unvalidated) { // If we are not able to add a model error // for instance when the max error count is reached, mark the model as skipped. modelState.MarkFieldSkipped(errorKey); } isValid = false; } } } } if (isValid) { validationContext.ModelValidationContext.ModelState.MarkFieldValid(modelKey); } return isValid; } private bool IsTypeExcludedFromValidation(IList filters, Type type) { return filters.Any(filter => filter.IsTypeExcluded(type)); } private static Type GetElementType(Type type) { Debug.Assert(typeof(IEnumerable).IsAssignableFrom(type)); if (type.IsArray) { return type.GetElementType(); } foreach (var implementedInterface in type.GetInterfaces()) { if (implementedInterface.IsGenericType() && implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { return implementedInterface.GetGenericArguments()[0]; } } return typeof(object); } private class ValidationContext { public ModelValidationContext ModelValidationContext { get; set; } public HashSet Visited { get; set; } } } }