// 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.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http.Formatting; using System.Runtime.CompilerServices; using System.Web.Http.Controllers; using System.Web.Http.Internal; using System.Web.Http.Metadata; using System.Web.Http.ModelBinding; namespace System.Web.Http.Validation { /// /// Recursively validate an object. /// public class DefaultBodyModelValidator : IBodyModelValidator { /// /// Determines whether the is valid and adds any validation errors to the 's /// /// The model to be validated. /// The to use for validation. /// The used to provide the model metadata. /// The within which the model is being validated. /// The to append to the key for any validation errors. /// trueif is valid, false otherwise. public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix) { if (type == null) { throw Error.ArgumentNull("type"); } if (metadataProvider == null) { throw Error.ArgumentNull("metadataProvider"); } if (actionContext == null) { throw Error.ArgumentNull("actionContext"); } if (model != null && !ShouldValidateType(model.GetType())) { return true; } ModelValidatorProvider[] validatorProviders = actionContext.GetValidatorProviders().ToArray(); // Optimization : avoid validating the object graph if there are no validator providers if (validatorProviders == null || validatorProviders.Length == 0) { return true; } ModelMetadata metadata = metadataProvider.GetMetadataForType(() => model, type); BodyModelValidatorContext validationContext = new BodyModelValidatorContext(actionContext.ModelState) { MetadataProvider = metadataProvider, ActionContext = actionContext, ValidatorCache = actionContext.GetValidatorCache(), RootPrefix = keyPrefix }; return ValidateNodeAndChildren(metadata, validationContext, container: null, validators: null); } /// /// Determines whether instances of a particular type should be validated /// /// The type to validate. /// true if the type should be validated; false otherwise public virtual bool ShouldValidateType(Type type) { return !MediaTypeFormatterCollection.IsTypeExcludedFromValidation(type); } /// /// Recursively validate the given and . /// /// The for the object to validate. /// The . /// The object containing the object to validate. /// The collection of s. /// /// if validation succeeds for the given , /// , and child nodes; otherwise. /// [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "See comment below")] protected virtual bool ValidateNodeAndChildren( ModelMetadata metadata, BodyModelValidatorContext validationContext, object container, IEnumerable validators) { // Recursion guard to avoid stack overflows RuntimeHelpers.EnsureSufficientExecutionStack(); if (metadata == null) { throw Error.ArgumentNull("metadata"); } if (validationContext == null) { throw Error.ArgumentNull("validationContext"); } object model = null; try { model = metadata.Model; } catch { // Retrieving the model failed - typically caused by a property getter throwing // Being unable to retrieve a property is not a validation error - many properties can only be retrieved if certain conditions are met // For example, Uri.AbsoluteUri throws for relative URIs but it shouldn't be considered a validation error return true; } bool isValid = true; if (validators == null) { validators = validationContext.ActionContext.GetValidators(metadata, validationContext.ValidatorCache); } // We don't need to recursively traverse the graph for null values if (model == null) { return ShallowValidate(metadata, validationContext, container, validators); } // We don't need to recursively traverse the graph for types that shouldn't be validated Type modelType = model.GetType(); if (TypeHelper.IsSimpleType(modelType) || !ShouldValidateType(modelType)) { return ShallowValidate(metadata, validationContext, container, validators); } // Check to avoid infinite recursion. This can happen with cycles in an object graph. if (validationContext.Visited.Contains(model)) { return true; } validationContext.Visited.Add(model); // Validate the children first - depth-first traversal IEnumerable enumerableModel = model as IEnumerable; if (enumerableModel == null) { isValid = ValidateProperties(metadata, validationContext); } else { isValid = ValidateElements(enumerableModel, validationContext); } if (isValid) { // Don't bother to validate this node if children failed. isValid = ShallowValidate(metadata, validationContext, container, validators); } // Pop the object so that it can be validated again in a different path validationContext.Visited.Remove(model); return isValid; } /// /// Recursively validate the properties of the given . /// /// The for the object to validate. /// The . /// /// if validation succeeds for all properties in ; /// otherwise. /// protected virtual bool ValidateProperties(ModelMetadata metadata, BodyModelValidatorContext validationContext) { if (metadata == null) { throw Error.ArgumentNull("metadata"); } if (validationContext == null) { throw Error.ArgumentNull("validationContext"); } bool isValid = true; PropertyScope propertyScope = new PropertyScope(); validationContext.KeyBuilders.Push(propertyScope); foreach (ModelMetadata childMetadata in validationContext.MetadataProvider.GetMetadataForProperties(metadata.Model, metadata.RealModelType)) { propertyScope.PropertyName = childMetadata.PropertyName; if (!ValidateNodeAndChildren(childMetadata, validationContext, metadata.Model, validators: null)) { isValid = false; } } validationContext.KeyBuilders.Pop(); return isValid; } /// /// Recursively validate the elements of the collection. /// /// The instance containing the elements to validate. /// The . /// /// if validation succeeds for all elements of ; /// otherwise. /// protected virtual bool ValidateElements(IEnumerable model, BodyModelValidatorContext validationContext) { if (model == null) { throw Error.ArgumentNull("model"); } if (validationContext == null) { throw Error.ArgumentNull("validationContext"); } bool isValid = true; Type elementType = GetElementType(model.GetType()); ModelMetadata elementMetadata = validationContext.MetadataProvider.GetMetadataForType(null, elementType); ElementScope elementScope = new ElementScope() { Index = 0 }; validationContext.KeyBuilders.Push(elementScope); IEnumerable validators = validationContext.ActionContext.GetValidators(elementMetadata, validationContext.ValidatorCache); // 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. bool anyValidatorsDefined = validators.Any(); foreach (object 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) { elementMetadata.Model = element; if (!ValidateNodeAndChildren(elementMetadata, validationContext, model, validators)) { isValid = false; } } elementScope.Index++; } validationContext.KeyBuilders.Pop(); return isValid; } /// /// Validate a single node, not including its children. /// /// The . /// The . /// The object to validate. /// The collection of s. /// /// if validation succeeds for the given and /// ; otherwise. /// protected virtual bool ShallowValidate( ModelMetadata metadata, BodyModelValidatorContext validationContext, object container, IEnumerable validators) { if (metadata == null) { throw Error.ArgumentNull("metadata"); } if (validationContext == null) { throw Error.ArgumentNull("validationContext"); } if (validators == null) { throw Error.ArgumentNull("validators"); } bool isValid = true; string modelKey = null; // 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. ICollection validatorsAsCollection = validators as ICollection; if (validatorsAsCollection != null && validatorsAsCollection.Count == 0) { return isValid; } foreach (ModelValidator validator in validators) { foreach (ModelValidationResult error in validator.Validate(metadata, container)) { if (modelKey == null) { modelKey = validationContext.RootPrefix; foreach (IBodyModelValidatorKeyBuilder keyBuilder in validationContext.KeyBuilders.Reverse()) { modelKey = keyBuilder.AppendTo(modelKey); } } string errorKey = ModelBindingHelper.CreatePropertyModelName(modelKey, error.MemberName); validationContext.ModelState.AddModelError(errorKey, error.Message); isValid = false; } } return isValid; } private static Type GetElementType(Type type) { Contract.Assert(typeof(IEnumerable).IsAssignableFrom(type)); if (type.IsArray) { return type.GetElementType(); } foreach (Type implementedInterface in type.GetInterfaces()) { if (implementedInterface.IsGenericType && implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { return implementedInterface.GetGenericArguments()[0]; } } return typeof(object); } private class PropertyScope : IBodyModelValidatorKeyBuilder { public string PropertyName { get; set; } public string AppendTo(string prefix) { return ModelBindingHelper.CreatePropertyModelName(prefix, PropertyName); } } private class ElementScope : IBodyModelValidatorKeyBuilder { public int Index { get; set; } public string AppendTo(string prefix) { return ModelBindingHelper.CreateIndexModelName(prefix, Index); } } } }