diff --git a/src/MiniValidation/MiniValidator.cs b/src/MiniValidation/MiniValidator.cs index 91d00c4..0b0758a 100644 --- a/src/MiniValidation/MiniValidator.cs +++ b/src/MiniValidation/MiniValidator.cs @@ -15,668 +15,660 @@ namespace MiniValidation; /// public static class MiniValidator { - private static readonly TypeDetailsCache _typeDetailsCache = new(); - private static readonly IDictionary _emptyErrors = new ReadOnlyDictionary(new Dictionary()); - - /// - /// Gets or sets the maximum depth allowed when validating an object with recursion enabled. - /// Defaults to 32. - /// - public static int MaxDepth { get; set; } = 32; - - /// - /// Determines if the specified has anything to validate. - /// - /// - /// Objects of types with nothing to validate will always return true when passed to . - /// - /// The . - /// true to recursively check descendant types; if false only simple values directly on the target type are checked. - /// true if has anything to validate, false if not. - /// Thrown when is null. - public static bool RequiresValidation(Type targetType, bool recurse = true) - { - if (targetType is null) - { - throw new ArgumentNullException(nameof(targetType)); - } - - return typeof(IValidatableObject).IsAssignableFrom(targetType) - || typeof(IAsyncValidatableObject).IsAssignableFrom(targetType) - || (recurse && typeof(IEnumerable).IsAssignableFrom(targetType)) - || _typeDetailsCache.Get(targetType).Properties.Any(p => p.HasValidationAttributes || recurse); - } - - /// - /// Determines whether the specific object is valid. This method recursively validates descendant objects. - /// - /// The object to validate. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - public static bool TryValidate(TTarget target, out IDictionary errors) - { - return TryValidateImpl(target, null, recurse: true, allowAsync: false, out errors); - } - - /// - /// Determines whether the specific object is valid. This method recursively validates descendant objects. - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - public static bool TryValidate(TTarget target, IServiceProvider serviceProvider, out IDictionary errors) - { - if (serviceProvider is null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - return TryValidateImpl(target, serviceProvider, recurse: true, allowAsync: false, out errors); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The type of the target of validation. - /// The object to validate. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - public static bool TryValidate(TTarget target, bool recurse, out IDictionary errors) - { - return TryValidateImpl(target, null, recurse, allowAsync: false, out errors); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The type of the target of validation. - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - public static bool TryValidate(TTarget target, IServiceProvider serviceProvider, bool recurse, out IDictionary errors) - { - if (serviceProvider is null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - return TryValidateImpl(target, serviceProvider, recurse, allowAsync: false, out errors); - } - - /// - /// Determines whether the specific object is valid. - /// - /// - /// The object to validate. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true to allow asynchronous validation if an object in the graph requires it. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - /// Throw when requires async validation and is false. - public static bool TryValidate(TTarget target, bool recurse, bool allowAsync, out IDictionary errors) - { - return TryValidateImpl(target, null, recurse, allowAsync, out errors); - } - - /// - /// Determines whether the specific object is valid. - /// - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true to allow asynchronous validation if an object in the graph requires it. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - /// Throw when requires async validation and is false. - public static bool TryValidate(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary errors) - { - return TryValidateImpl(target, serviceProvider, recurse, allowAsync, out errors); - } - - /// - /// Determines whether the specific object is valid. - /// - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true to allow asynchronous validation if an object in the graph requires it. - /// A dictionary that contains details of each failed validation. - /// true if is valid; otherwise false. - /// Thrown when is null. - /// Throw when requires async validation and is false. - private static bool TryValidateImpl(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary errors) - { - if (target is null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (!RequiresValidation(target.GetType(), recurse)) - { - errors = _emptyErrors; - - // Return true for types with nothing to validate - return true; - } - - if (_typeDetailsCache.Get(target.GetType()).RequiresAsync && !allowAsync) - { - throw new ArgumentException($"The target type {target.GetType().Name} requires async validation. Call the '{nameof(TryValidateAsync)}' method instead.", nameof(target)); - } - - var validatedObjects = new Dictionary(); - var workingErrors = new Dictionary>(); - - var validateTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects); - - bool isValid; - - if (validateTask.IsCompleted) - { - isValid = validateTask.GetAwaiter().GetResult(); - } - else - { - // This is a backstop check as TryValidateImpl and the methods it calls should all be doing this check as the object - // graph is walked during validation. - try - { - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - } - catch (Exception) - { + private static readonly TypeDetailsCache _typeDetailsCache = new(); + private static readonly IDictionary _emptyErrors = new ReadOnlyDictionary(new Dictionary()); + + /// + /// Gets or sets the maximum depth allowed when validating an object with recursion enabled. + /// Defaults to 32. + /// + public static int MaxDepth { get; set; } = 32; + + /// + /// Determines if the specified has anything to validate. + /// + /// + /// Objects of types with nothing to validate will always return true when passed to . + /// + /// The . + /// true to recursively check descendant types; if false only simple values directly on the target type are checked. + /// true if has anything to validate, false if not. + /// Thrown when is null. + public static bool RequiresValidation(Type targetType, bool recurse = true) + { + if (targetType is null) + { + throw new ArgumentNullException(nameof(targetType)); + } + + return typeof(IValidatableObject).IsAssignableFrom(targetType) + || typeof(IAsyncValidatableObject).IsAssignableFrom(targetType) + || (recurse && typeof(IEnumerable).IsAssignableFrom(targetType)) + || _typeDetailsCache.Get(targetType).Properties.Any(p => p.HasValidationAttributes || recurse); + } + + /// + /// Determines whether the specific object is valid. This method recursively validates descendant objects. + /// + /// The object to validate. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + public static bool TryValidate(TTarget target, out IDictionary errors) + { + return TryValidateImpl(target, null, recurse: true, allowAsync: false, out errors); + } + + /// + /// Determines whether the specific object is valid. This method recursively validates descendant objects. + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + public static bool TryValidate(TTarget target, IServiceProvider serviceProvider, out IDictionary errors) + { + if (serviceProvider is null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + return TryValidateImpl(target, serviceProvider, recurse: true, allowAsync: false, out errors); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The type of the target of validation. + /// The object to validate. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + public static bool TryValidate(TTarget target, bool recurse, out IDictionary errors) + { + return TryValidateImpl(target, null, recurse, allowAsync: false, out errors); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The type of the target of validation. + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + public static bool TryValidate(TTarget target, IServiceProvider serviceProvider, bool recurse, out IDictionary errors) + { + if (serviceProvider is null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + return TryValidateImpl(target, serviceProvider, recurse, allowAsync: false, out errors); + } + + /// + /// Determines whether the specific object is valid. + /// + /// + /// The object to validate. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true to allow asynchronous validation if an object in the graph requires it. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + /// Throw when requires async validation and is false. + public static bool TryValidate(TTarget target, bool recurse, bool allowAsync, out IDictionary errors) + { + return TryValidateImpl(target, null, recurse, allowAsync, out errors); + } + + /// + /// Determines whether the specific object is valid. + /// + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true to allow asynchronous validation if an object in the graph requires it. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + /// Throw when requires async validation and is false. + public static bool TryValidate(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary errors) + { + return TryValidateImpl(target, serviceProvider, recurse, allowAsync, out errors); + } + + /// + /// Determines whether the specific object is valid. + /// + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true to allow asynchronous validation if an object in the graph requires it. + /// A dictionary that contains details of each failed validation. + /// true if is valid; otherwise false. + /// Thrown when is null. + /// Throw when requires async validation and is false. + private static bool TryValidateImpl(TTarget target, IServiceProvider? serviceProvider, bool recurse, bool allowAsync, out IDictionary errors) + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (!RequiresValidation(target.GetType(), recurse)) + { + errors = _emptyErrors; + + // Return true for types with nothing to validate + return true; + } + + if (_typeDetailsCache.Get(target.GetType()).RequiresAsync && !allowAsync) + { + throw new ArgumentException($"The target type {target.GetType().Name} requires async validation. Call the '{nameof(TryValidateAsync)}' method instead.", nameof(target)); + } + + var validatedObjects = new Dictionary(); + var workingErrors = new Dictionary>(); + + var validateTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects); + + bool isValid; + + if (validateTask.IsCompleted) + { + isValid = validateTask.GetAwaiter().GetResult(); + } + else + { + // This is a backstop check as TryValidateImpl and the methods it calls should all be doing this check as the object + // graph is walked during validation. + try + { + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + } + catch (Exception) + { #if NET6_0_OR_GREATER - // Always observe the ValueTask - _ = validateTask.AsTask().GetAwaiter().GetResult(); + // Always observe the ValueTask + _ = validateTask.AsTask().GetAwaiter().GetResult(); #else _ = validateTask.GetAwaiter().GetResult(); #endif - throw; - } + throw; + } #if NET6_0_OR_GREATER - isValid = validateTask.AsTask().GetAwaiter().GetResult(); + isValid = validateTask.AsTask().GetAwaiter().GetResult(); #else isValid = validateTask.GetAwaiter().GetResult(); #endif - } + } - errors = MapToFinalErrorsResult(workingErrors); + errors = MapToFinalErrorsResult(workingErrors); - return isValid; - } + return isValid; + } - /// - /// Determines whether the specific object is valid. - /// - /// The object to validate. - /// Thrown when is null. + /// + /// Determines whether the specific object is valid. + /// + /// The object to validate. + /// Thrown when is null. #if NET6_0_OR_GREATER - public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target) + public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target) #else public static Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target) #endif - { - return TryValidateImplAsync(target, null, recurse: true); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// Thrown when is null. + { + return TryValidateImplAsync(target, null, recurse: true); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// Thrown when is null. #if NET6_0_OR_GREATER - public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider serviceProvider) + public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider serviceProvider) #else public static Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider serviceProvider) #endif - { - if (serviceProvider is null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - return TryValidateImplAsync(target, serviceProvider, recurse: true); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The object to validate. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true if is valid; otherwise false and the validation errors. - /// + { + if (serviceProvider is null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + return TryValidateImplAsync(target, serviceProvider, recurse: true); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The object to validate. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true if is valid; otherwise false and the validation errors. + /// #if NET6_0_OR_GREATER - public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse) + public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse) #else public static Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, bool recurse) #endif - { - return TryValidateImplAsync(target, null, recurse); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true if is valid; otherwise false and the validation errors. - /// + { + return TryValidateImplAsync(target, null, recurse); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true if is valid; otherwise false and the validation errors. + /// #if NET6_0_OR_GREATER - public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) + public static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) #else public static Task<(bool IsValid, IDictionary Errors)> TryValidateAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) #endif - { - return TryValidateImplAsync(target, serviceProvider, recurse); - } - - /// - /// Determines whether the specific object is valid. - /// - /// The object to validate. - /// The service provider to use when creating ValidationContext. - /// true to recursively validate descendant objects; if false only simple values directly on are validated. - /// true if is valid; otherwise false and the validation errors. - /// + { + return TryValidateImplAsync(target, serviceProvider, recurse); + } + + /// + /// Determines whether the specific object is valid. + /// + /// The object to validate. + /// The service provider to use when creating ValidationContext. + /// true to recursively validate descendant objects; if false only simple values directly on are validated. + /// true if is valid; otherwise false and the validation errors. + /// #if NET6_0_OR_GREATER - private static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateImplAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) + private static ValueTask<(bool IsValid, IDictionary Errors)> TryValidateImplAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) #else private static Task<(bool IsValid, IDictionary Errors)> TryValidateImplAsync(TTarget target, IServiceProvider? serviceProvider, bool recurse) #endif - { - if (target is null) - { - throw new ArgumentNullException(nameof(target)); - } + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } - IDictionary? errors; + IDictionary? errors; - if (!RequiresValidation(target.GetType(), recurse)) - { - errors = _emptyErrors; + if (!RequiresValidation(target.GetType(), recurse)) + { + errors = _emptyErrors; - // Return true for types with nothing to validate + // Return true for types with nothing to validate #if NET6_0_OR_GREATER - return ValueTask.FromResult((true, errors)); + return ValueTask.FromResult((true, errors)); #else return Task.FromResult((true, errors)); #endif - } + } - var validatedObjects = new Dictionary(); - var workingErrors = new Dictionary>(); - var validationTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync: true, workingErrors, validatedObjects); + var validatedObjects = new Dictionary(); + var workingErrors = new Dictionary>(); + var validationTask = TryValidateImpl(target, serviceProvider, recurse, allowAsync: true, workingErrors, validatedObjects); - if (validationTask.IsCompleted) - { - var isValid = validationTask.GetAwaiter().GetResult(); - errors = MapToFinalErrorsResult(workingErrors); + if (validationTask.IsCompleted) + { + var isValid = validationTask.GetAwaiter().GetResult(); + errors = MapToFinalErrorsResult(workingErrors); #if NET6_0_OR_GREATER - return ValueTask.FromResult((isValid, errors)); + return ValueTask.FromResult((isValid, errors)); #else return Task.FromResult((isValid, errors)); #endif - } + } - // Handle async completion - return HandleTryValidateAsyncResult(validationTask, workingErrors); - } + // Handle async completion + return HandleTryValidateAsyncResult(validationTask, workingErrors); + } #if NET6_0_OR_GREATER - private static async ValueTask<(bool IsValid, IDictionary Errors)> HandleTryValidateAsyncResult(ValueTask validationTask, Dictionary> workingErrors) + private static async ValueTask<(bool IsValid, IDictionary Errors)> HandleTryValidateAsyncResult(ValueTask validationTask, Dictionary> workingErrors) #else private static async Task<(bool IsValid, IDictionary Errors)> HandleTryValidateAsyncResult(Task validationTask, Dictionary> workingErrors) #endif - { - var isValid = await validationTask.ConfigureAwait(false); + { + var isValid = await validationTask.ConfigureAwait(false); - var errors = MapToFinalErrorsResult(workingErrors); + var errors = MapToFinalErrorsResult(workingErrors); - return (isValid, errors); - } + return (isValid, errors); + } #if NET6_0_OR_GREATER - private static async ValueTask TryValidateImpl( + private static async ValueTask TryValidateImpl( #else private static async Task TryValidateImpl( #endif - object target, - IServiceProvider? serviceProvider, - bool recurse, - bool allowAsync, - Dictionary> workingErrors, - Dictionary validatedObjects, - List? validationResults = null, - string? prefix = null, - int currentDepth = 0) - { - if (target is null) - { - throw new ArgumentNullException(nameof(target)); - } - - // Once we get to this point we have to box the target in order to track whether we've validated it or not - if (validatedObjects.ContainsKey(target)) - { - var result = validatedObjects[target]; - // If there's a null result it means this object is the one currently being validated - // so just skip this reference to it by returning true. If there is a result it means - // we already validated this object as part of this validation operation. - return !result.HasValue || result == true; - } - - // Add current target to tracking dictionary in null (validating) state - validatedObjects.Add(target, null); - - var targetType = target.GetType(); - var (typeProperties, _) = _typeDetailsCache.Get(targetType); - - var isValid = true; - var propertiesToRecurse = recurse ? new Dictionary() : null; - var validationContext = new ValidationContext(target, serviceProvider: serviceProvider, items: null); - - foreach (var property in typeProperties) - { - // Skip properties that don't have validation attributes if we're not recursing - if (!(property.HasValidationAttributes || recurse)) - { - continue; - } - - var propertyValue = property.GetValue(target); - var propertyValueType = propertyValue?.GetType(); - var (properties, _) = _typeDetailsCache.Get(propertyValueType); - - if (property.HasValidationAttributes) - { - validationContext.MemberName = property.Name; - validationContext.DisplayName = GetDisplayName(property); - validationResults ??= new(); - var propertyIsValid = Validator.TryValidateValue(propertyValue!, validationContext, validationResults, property.ValidationAttributes); - - if (!propertyIsValid) - { - ProcessValidationResults(property.Name, validationResults, workingErrors, prefix); - isValid = false; - } - } - - if (recurse && propertyValue is not null && - (property.Recurse - || typeof(IValidatableObject).IsAssignableFrom(propertyValueType) - || typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType) - || properties.Any(p => p.Recurse))) - { - propertiesToRecurse!.Add(property, propertyValue); - } - } - - if (recurse && currentDepth <= MaxDepth) - { - // Validate IEnumerable - if (target is IEnumerable) - { - RuntimeHelpers.EnsureSufficientExecutionStack(); - - var validateTask = TryValidateEnumerable(target, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, prefix, currentDepth); - - try - { - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - } - catch (Exception) - { - // Always observe the ValueTask - _ = await validateTask.ConfigureAwait(false); - throw; - } - - isValid = await validateTask.ConfigureAwait(false) && isValid; - } - - // Validate complex properties - if (propertiesToRecurse!.Count > 0) - { - foreach (var property in propertiesToRecurse) - { - var propertyDetails = property.Key; - var propertyValue = property.Value; - - if (propertyValue != null) - { - RuntimeHelpers.EnsureSufficientExecutionStack(); - - if (propertyDetails.IsEnumerable) - { - var thePrefix = $"{prefix}{propertyDetails.Name}"; - - var validateTask = TryValidateEnumerable(propertyValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth); - try - { - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - } - catch (Exception) - { - // Always observe the ValueTask - _ = await validateTask.ConfigureAwait(false); - throw; - } - - isValid = await validateTask.ConfigureAwait(false) && isValid; - } - else - { - var thePrefix = $"{prefix}{propertyDetails.Name}."; // <-- Note trailing '.' here - - var validateTask = TryValidateImpl(propertyValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1); - try - { - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - } - catch (Exception) - { - // Always observe the ValueTask - _ = await validateTask.ConfigureAwait(false); - throw; - } - - isValid = await validateTask.ConfigureAwait(false) && isValid; - } - } - } - } - } - - if (isValid && typeof(IValidatableObject).IsAssignableFrom(targetType)) - { - var validatable = (IValidatableObject)target; - - // Reset validation context - validationContext.MemberName = null; - validationContext.DisplayName = validationContext.ObjectType.Name; - - var validatableResults = validatable.Validate(validationContext); - if (validatableResults is not null) - { - ProcessValidationResults(validatableResults, workingErrors, prefix); - isValid = workingErrors.Count == 0 && isValid; - } - } - - if (isValid && typeof(IAsyncValidatableObject).IsAssignableFrom(targetType)) - { - var validatable = (IAsyncValidatableObject)target; - - // Reset validation context - validationContext.MemberName = null; - validationContext.DisplayName = validationContext.ObjectType.Name; - - var validateTask = validatable.ValidateAsync(validationContext); - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - - var validatableResults = await validateTask.ConfigureAwait(false); - if (validatableResults is not null) - { - ProcessValidationResults(validatableResults, workingErrors, prefix); - isValid = workingErrors.Count == 0 && isValid; - } - } - - // Update state of target in tracking dictionary - validatedObjects[target] = isValid; - - return isValid; - - static string GetDisplayName(PropertyDetails property) - { - return property.DisplayAttribute?.GetName() ?? property.Name; - } - } - - private static void ThrowIfAsyncNotAllowed(bool taskCompleted, bool allowAsync) - { - if (!allowAsync & !taskCompleted) - { - throw new InvalidOperationException($"An object in the validation graph requires async validation. Call the '{nameof(TryValidateAsync)}' method instead."); - } - } + object target, + IServiceProvider? serviceProvider, + bool recurse, + bool allowAsync, + Dictionary> workingErrors, + Dictionary validatedObjects, + List? validationResults = null, + string? prefix = null, + int currentDepth = 0) + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + // Once we get to this point we have to box the target in order to track whether we've validated it or not + if (validatedObjects.TryGetValue(target, out var result)) + { + // If there's a null result it means this object is the one currently being validated + // so just skip this reference to it by returning true. If there is a result it means + // we already validated this object as part of this validation operation. + return !result.HasValue || result == true; + } + + // Add current target to tracking dictionary in null (validating) state + validatedObjects.Add(target, null); + + var targetType = target.GetType(); + var (typeProperties, _) = _typeDetailsCache.Get(targetType); + + var isValid = true; + var propertiesToRecurse = recurse ? new Dictionary() : null; + var validationContext = new ValidationContext(target, serviceProvider: serviceProvider, items: null); + + foreach (var property in typeProperties) + { + // Skip properties that don't have validation attributes if we're not recursing + if (!(property.HasValidationAttributes || recurse)) + { + continue; + } + + var propertyValue = property.GetValue(target); + var propertyValueType = propertyValue?.GetType(); + var (properties, _) = _typeDetailsCache.Get(propertyValueType); + + if (property.HasValidationAttributes) + { + validationContext.MemberName = property.Name; + validationContext.DisplayName = GetDisplayName(property); + validationResults ??= new(); + var propertyIsValid = Validator.TryValidateValue(propertyValue!, validationContext, validationResults, property.ValidationAttributes); + + if (!propertyIsValid) + { + ProcessValidationResults(property.Name, validationResults, workingErrors, prefix); + isValid = false; + } + } + + if (recurse && propertyValue is not null && + (property.Recurse + || typeof(IValidatableObject).IsAssignableFrom(propertyValueType) + || typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType) + || properties.Any(p => p.Recurse))) + { + propertiesToRecurse!.Add(property, propertyValue); + } + } + + if (recurse && currentDepth <= MaxDepth) + { + // Validate IEnumerable + if (target is IEnumerable targets) + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + + var validateTask = TryValidateEnumerable(targets, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, prefix, currentDepth); + + try + { + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + } + catch (Exception) + { + // Always observe the ValueTask + _ = await validateTask.ConfigureAwait(false); + throw; + } + + isValid = await validateTask.ConfigureAwait(false) && isValid; + } + + // Validate complex properties + if (propertiesToRecurse!.Count > 0) + { + foreach (var property in propertiesToRecurse) + { + var propertyDetails = property.Key; + var propertyValue = property.Value; + + if (propertyValue != null) + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + + if (propertyDetails.IsEnumerable && propertyValue is IEnumerable propertyValues) + { + var thePrefix = $"{prefix}{propertyDetails.Name}"; + + var validateTask = TryValidateEnumerable(propertyValues, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth); + try + { + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + } + catch (Exception) + { + // Always observe the ValueTask + _ = await validateTask.ConfigureAwait(false); + throw; + } + + isValid = await validateTask.ConfigureAwait(false) && isValid; + } + else + { + var thePrefix = $"{prefix}{propertyDetails.Name}."; // <-- Note trailing '.' here + + var validateTask = TryValidateImpl(propertyValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1); + try + { + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + } + catch (Exception) + { + // Always observe the ValueTask + _ = await validateTask.ConfigureAwait(false); + throw; + } + + isValid = await validateTask.ConfigureAwait(false) && isValid; + } + } + } + } + } + + if (isValid && target is IValidatableObject validatable) + { + // Reset validation context + validationContext.MemberName = null; + validationContext.DisplayName = validationContext.ObjectType.Name; + + var validatableResults = validatable.Validate(validationContext); + if (validatableResults is not null) + { + ProcessValidationResults(validatableResults, workingErrors, prefix); + isValid = workingErrors.Count == 0 && isValid; + } + } + + if (isValid && target is IAsyncValidatableObject asyncValidatable) + { + // Reset validation context + validationContext.MemberName = null; + validationContext.DisplayName = validationContext.ObjectType.Name; + + var validateTask = asyncValidatable.ValidateAsync(validationContext); + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + + var validatableResults = await validateTask.ConfigureAwait(false); + if (validatableResults is not null) + { + ProcessValidationResults(validatableResults, workingErrors, prefix); + isValid = workingErrors.Count == 0 && isValid; + } + } + + // Update state of target in tracking dictionary + validatedObjects[target] = isValid; + + return isValid; + + static string GetDisplayName(PropertyDetails property) + { + return property.DisplayAttribute?.GetName() ?? property.Name; + } + } + + private static void ThrowIfAsyncNotAllowed(bool taskCompleted, bool allowAsync) + { + if (!allowAsync & !taskCompleted) + { + throw new InvalidOperationException($"An object in the validation graph requires async validation. Call the '{nameof(TryValidateAsync)}' method instead."); + } + } #if NET6_0_OR_GREATER - private static async ValueTask TryValidateEnumerable( + private static async ValueTask TryValidateEnumerable( #else private static async Task TryValidateEnumerable( #endif - object target, - IServiceProvider? serviceProvider, - bool recurse, - bool allowAsync, - Dictionary> workingErrors, - Dictionary validatedObjects, - List? validationResults, - string? prefix = null, - int currentDepth = 0) - { - var isValid = true; - if (target is IEnumerable items) - { - // Validate each instance in the collection - var index = 0; - foreach (var item in items) - { - if (item is null) - { - continue; - } - - var itemPrefix = $"{prefix}[{index}]."; - - var validateTask = TryValidateImpl(item, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, itemPrefix, currentDepth + 1); - try - { - ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); - } - catch (Exception) - { - // Always observe the ValueTask - _ = await validateTask.ConfigureAwait(false); - throw; - } - - isValid = await validateTask.ConfigureAwait(false); - - if (!isValid) - { - break; - } - index++; - } - } - return isValid; - } - - private static IDictionary MapToFinalErrorsResult(Dictionary> workingErrors) - { + IEnumerable items, + IServiceProvider? serviceProvider, + bool recurse, + bool allowAsync, + Dictionary> workingErrors, + Dictionary validatedObjects, + List? validationResults, + string? prefix = null, + int currentDepth = 0) + { + var isValid = true; + // Validate each instance in the collection + var index = 0; + foreach (var item in items) + { + if (item is null) + { + continue; + } + + var itemPrefix = $"{prefix}[{index}]."; + + var validateTask = TryValidateImpl(item, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, itemPrefix, currentDepth + 1); + try + { + ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync); + } + catch (Exception) + { + // Always observe the ValueTask + _ = await validateTask.ConfigureAwait(false); + throw; + } + + isValid = await validateTask.ConfigureAwait(false); + + if (!isValid) + { + break; + } + index++; + } + return isValid; + } + + private static IDictionary MapToFinalErrorsResult(Dictionary> workingErrors) + { #if NET6_0_OR_GREATER - var result = new AdaptiveCapacityDictionary(workingErrors.Count); + var result = new AdaptiveCapacityDictionary(workingErrors.Count); #else var result = new Dictionary(workingErrors.Count); #endif - foreach (var fieldError in workingErrors) - { - if (!result.ContainsKey(fieldError.Key)) - { - result.Add(fieldError.Key, fieldError.Value.ToArray()); - } - else - { - var existingFieldErrors = result[fieldError.Key]; - result[fieldError.Key] = existingFieldErrors.Concat(fieldError.Value).ToArray(); - } - } - - return result; - } - - private static void ProcessValidationResults(IEnumerable validationResults, Dictionary> errors, string? prefix) - { - foreach (var result in validationResults) - { - var hasMemberNames = false; - foreach (var memberName in result.MemberNames) - { - var key = $"{prefix}{memberName}"; - if (!errors.ContainsKey(key)) - { - errors.Add(key, new()); - } - errors[key].Add(result.ErrorMessage ?? ""); - hasMemberNames = true; - } - - if (!hasMemberNames) - { - // Class level error message - var key = ""; - if (!errors.ContainsKey(key)) - { - errors.Add(key, new()); - } - errors[key].Add(result.ErrorMessage ?? ""); - } - } - } - - private static void ProcessValidationResults(string propertyName, ICollection validationResults, Dictionary> errors, string? prefix) - { - if (validationResults.Count == 0) - { - return; - } - - var errorsList = new List(validationResults.Count); - - foreach (var result in validationResults) - { - errorsList.Add(result.ErrorMessage ?? ""); - } - - errors.Add($"{prefix}{propertyName}", errorsList); - validationResults.Clear(); - } + foreach (var fieldError in workingErrors) + { + if (!result.ContainsKey(fieldError.Key)) + { + result.Add(fieldError.Key, fieldError.Value.ToArray()); + } + else + { + var existingFieldErrors = result[fieldError.Key]; + result[fieldError.Key] = existingFieldErrors.Concat(fieldError.Value).ToArray(); + } + } + + return result; + } + + private static void ProcessValidationResults(IEnumerable validationResults, Dictionary> errors, string? prefix) + { + foreach (var result in validationResults) + { + var hasMemberNames = false; + foreach (var memberName in result.MemberNames) + { + var key = $"{prefix}{memberName}"; + if (!errors.ContainsKey(key)) + { + errors.Add(key, new()); + } + errors[key].Add(result.ErrorMessage ?? ""); + hasMemberNames = true; + } + + if (!hasMemberNames) + { + // Class level error message + var key = ""; + if (!errors.ContainsKey(key)) + { + errors.Add(key, new()); + } + errors[key].Add(result.ErrorMessage ?? ""); + } + } + } + + private static void ProcessValidationResults(string propertyName, ICollection validationResults, Dictionary> errors, string? prefix) + { + if (validationResults.Count == 0) + { + return; + } + + var errorsList = new List(validationResults.Count); + + foreach (var result in validationResults) + { + errorsList.Add(result.ErrorMessage ?? ""); + } + + errors.Add($"{prefix}{propertyName}", errorsList); + validationResults.Clear(); + } }