|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace System.ComponentModel.DataAnnotations
{
/// <summary>
/// Helper class to validate objects, properties and other values using their associated
/// <see cref="ValidationAttribute" />
/// custom attributes.
/// </summary>
public static class Validator
{
private static readonly ValidationAttributeStore _store = ValidationAttributeStore.Instance;
/// <summary>
/// Tests whether the given property value is valid.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute" /> associated with the property
/// identified by <paramref name="validationContext" />. If <paramref name="validationResults" /> is non-null,
/// this method will add a <see cref="ValidationResult" /> to it for each validation failure.
/// <para>
/// If there is a <see cref="RequiredAttribute" /> found on the property, it will be evaluated before all other
/// validation attributes. If the required validator fails then validation will abort, adding that single
/// failure into the <paramref name="validationResults" /> when applicable, returning a value of <c>false</c>.
/// </para>
/// <para>
/// If <paramref name="validationResults" /> is null and there isn't a <see cref="RequiredAttribute" /> failure,
/// then all validators will be evaluated.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">
/// Describes the property member to validate and provides services and context for the
/// validators.
/// </param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <returns><c>true</c> if the value is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentException">
/// When the <see cref="ValidationContext.MemberName" /> of <paramref name="validationContext" /> is not a valid
/// property.
/// </exception>
[RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
public static bool TryValidateProperty(object? value, ValidationContext validationContext,
ICollection<ValidationResult>? validationResults)
{
// Throw if value cannot be assigned to this property. That is not a validation exception.
var propertyType = _store.GetPropertyType(validationContext);
var propertyName = validationContext.MemberName!;
EnsureValidPropertyType(propertyName, propertyType, value);
var result = true;
var breakOnFirstError = (validationResults == null);
var attributes = _store.GetPropertyValidationAttributes(validationContext);
foreach (var err in GetValidationErrors(value, validationContext, attributes, breakOnFirstError))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute" /> are set. It does not validate the
/// property values of the object.
/// <para>
/// If <paramref name="validationResults" /> is null, then execution will abort upon the first validation
/// failure. If <paramref name="validationResults" /> is non-null, then all validation attributes will be
/// evaluated.
/// </para>
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be <c>null</c>.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" />on <paramref name="validationContext" />.
/// </exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static bool TryValidateObject(
object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults) =>
TryValidateObject(instance, validationContext, validationResults, validateAllProperties: false);
/// <summary>
/// Tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute" /> are set. If
/// <paramref name="validateAllProperties" />
/// is <c>true</c>, this method will also evaluate the <see cref="ValidationAttribute" />s for all the immediate
/// properties
/// of this object. This process is not recursive.
/// <para>
/// If <paramref name="validationResults" /> is null, then execution will abort upon the first validation
/// failure. If <paramref name="validationResults" /> is non-null, then all validation attributes will be
/// evaluated.
/// </para>
/// <para>
/// For any given property, if it has a <see cref="RequiredAttribute" /> that fails validation, no other validators
/// will be evaluated for that property.
/// </para>
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="validateAllProperties">
/// If <c>true</c>, also evaluates all properties of the object (this process is not
/// recursive over properties of the properties).
/// </param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" />on <paramref name="validationContext" />.
/// </exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static bool TryValidateObject(object instance, ValidationContext validationContext,
ICollection<ValidationResult>? validationResults, bool validateAllProperties)
{
ArgumentNullException.ThrowIfNull(instance);
if (validationContext != null && instance != validationContext.ObjectInstance)
{
throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
}
var result = true;
var breakOnFirstError = (validationResults == null);
foreach (ValidationError err in GetObjectValidationErrors(instance, validationContext!, validateAllProperties, breakOnFirstError))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Tests whether the given value is valid against a specified list of <see cref="ValidationAttribute" />s.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute" />s specified. If
/// <paramref name="validationResults" /> is non-null, this method will add a <see cref="ValidationResult" />
/// to it for each validation failure.
/// <para>
/// If there is a <see cref="RequiredAttribute" /> within the <paramref name="validationAttributes" />, it will
/// be evaluated before all other validation attributes. If the required validator fails then validation will
/// abort, adding that single failure into the <paramref name="validationResults" /> when applicable, returning a
/// value of <c>false</c>.
/// </para>
/// <para>
/// If <paramref name="validationResults" /> is null and there isn't a <see cref="RequiredAttribute" /> failure,
/// then all validators will be evaluated.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">
/// Describes the object being validated and provides services and context for the
/// validators.
/// </param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="validationAttributes">
/// The list of <see cref="ValidationAttribute" />s to validate this
/// <paramref name="value" /> against.
/// </param>
/// <returns><c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
public static bool TryValidateValue(object? value, ValidationContext validationContext,
ICollection<ValidationResult>? validationResults, IEnumerable<ValidationAttribute> validationAttributes)
{
ArgumentNullException.ThrowIfNull(validationAttributes);
var result = true;
var breakOnFirstError = validationResults == null;
foreach (
var err in
GetValidationErrors(value, validationContext, validationAttributes, breakOnFirstError))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Throws a <see cref="ValidationException" /> if the given property <paramref name="value" /> is not valid.
/// </summary>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">
/// Describes the object being validated and provides services and context for the
/// validators. It cannot be <c>null</c>.
/// </param>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value" /> is invalid for this property.</exception>
[RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
public static void ValidateProperty(object? value, ValidationContext validationContext)
{
// Throw if value cannot be assigned to this property. That is not a validation exception.
var propertyType = _store.GetPropertyType(validationContext);
EnsureValidPropertyType(validationContext.MemberName!, propertyType, value);
var attributes = _store.GetPropertyValidationAttributes(validationContext);
List<ValidationError> errors = GetValidationErrors(value, validationContext, attributes, false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Throws a <see cref="ValidationException" /> if the given <paramref name="instance" /> is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object's type.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">
/// Describes the object being validated and provides services and context for the
/// validators. It cannot be <c>null</c>.
/// </param>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
/// <exception cref="ValidationException">When <paramref name="instance" /> is found to be invalid.</exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static void ValidateObject(object instance, ValidationContext validationContext)
{
ValidateObject(instance, validationContext, false /*validateAllProperties*/);
}
/// <summary>
/// Throws a <see cref="ValidationException" /> if the given object instance is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object's type.
/// If <paramref name="validateAllProperties" /> is <c>true</c> it also validates all the object's properties.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">
/// Describes the object being validated and provides services and context for the
/// validators. It cannot be <c>null</c>.
/// </param>
/// <param name="validateAllProperties">If <c>true</c>, also validates all the <paramref name="instance" />'s properties.</param>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
/// <exception cref="ValidationException">When <paramref name="instance" /> is found to be invalid.</exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static void ValidateObject(object instance, ValidationContext validationContext,
bool validateAllProperties)
{
ArgumentNullException.ThrowIfNull(instance);
ArgumentNullException.ThrowIfNull(validationContext);
if (instance != validationContext.ObjectInstance)
{
throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
}
List<ValidationError> errors = GetObjectValidationErrors(instance, validationContext, validateAllProperties, false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Throw a <see cref="ValidationException" /> if the given value is not valid for the
/// <see cref="ValidationAttribute" />s.
/// </summary>
/// <remarks>
/// This method evaluates the <see cref="ValidationAttribute" />s supplied until a validation error occurs,
/// at which time a <see cref="ValidationException" /> is thrown.
/// <para>
/// A <see cref="RequiredAttribute" /> within the <paramref name="validationAttributes" /> will always be evaluated
/// first.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the object being tested.</param>
/// <param name="validationAttributes">The list of <see cref="ValidationAttribute" />s to validate against this instance.</param>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value" /> is found to be invalid.</exception>
public static void ValidateValue(object? value, ValidationContext validationContext,
IEnumerable<ValidationAttribute> validationAttributes)
{
ArgumentNullException.ThrowIfNull(validationContext);
ArgumentNullException.ThrowIfNull(validationAttributes);
List<ValidationError> errors = GetValidationErrors(value, validationContext, validationAttributes, false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Asynchronously tests whether the given property value is valid.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute" /> associated with the property
/// identified by <paramref name="validationContext" />. If <paramref name="validationResults" /> is non-null,
/// this method will add a <see cref="ValidationResult" /> to it for each validation failure.
/// <para>
/// If there is a <see cref="RequiredAttribute" /> found on the property, it will be evaluated before all other
/// validation attributes. If the required validator fails then validation will abort, adding that single
/// failure into the <paramref name="validationResults" /> when applicable, returning a value of <c>false</c>.
/// </para>
/// <para>
/// Any <see cref="AsyncValidationAttribute" /> instances will be evaluated asynchronously after all synchronous
/// validation attributes have passed.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">
/// Describes the property member to validate and provides services and context for the validators.
/// </param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task{Boolean}" /> that is <c>true</c> if the value is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentException">
/// When the <see cref="ValidationContext.MemberName" /> of <paramref name="validationContext" /> is not a valid property.
/// </exception>
[RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
public static async Task<bool> TryValidatePropertyAsync(
object? value,
ValidationContext validationContext,
ICollection<ValidationResult>? validationResults,
CancellationToken cancellationToken = default)
{
var propertyType = _store.GetPropertyType(validationContext);
var propertyName = validationContext.MemberName!;
EnsureValidPropertyType(propertyName, propertyType, value);
var result = true;
var breakOnFirstError = (validationResults == null);
var attributes = _store.GetPropertyValidationAttributes(validationContext);
foreach (var err in await GetValidationErrorsAsync(value, validationContext, attributes, breakOnFirstError, cancellationToken).ConfigureAwait(false))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Asynchronously tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute" /> are set. It does not validate
/// the <see cref="ValidationAttribute" />s on individual properties of the object unless
/// <see cref="TryValidateObjectAsync(object, ValidationContext, ICollection{ValidationResult}?, bool, CancellationToken)" />
/// is called with <c>validateAllProperties</c> set to <see langword="true" />.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be <c>null</c>.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task{Boolean}" /> that is <c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static Task<bool> TryValidateObjectAsync(
object instance,
ValidationContext validationContext,
ICollection<ValidationResult>? validationResults,
CancellationToken cancellationToken = default) =>
TryValidateObjectAsync(instance, validationContext, validationResults, validateAllProperties: false, cancellationToken);
/// <summary>
/// Asynchronously tests whether the given object instance is valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object instance's type. It also
/// checks to ensure all properties marked with <see cref="RequiredAttribute" /> are set. If
/// <paramref name="validateAllProperties" />
/// is <c>true</c>, this method will also evaluate the <see cref="ValidationAttribute" />s for all the immediate
/// properties of this object. This process is not recursive.
/// <para>
/// When <paramref name="validateAllProperties" /> is <c>true</c>, properties are validated
/// in parallel. Within each property, synchronous attributes run first; asynchronous
/// attributes run only if all synchronous attributes pass.
/// </para>
/// <para>
/// When <paramref name="validationResults" /> is <c>null</c>, validation stops after the
/// first property error (cross-property short-circuit). Any in-flight async validators on
/// other properties are cancelled cooperatively. When <paramref name="validationResults" />
/// is non-null, all properties complete and all errors are collected.
/// </para>
/// <para>
/// Returns <see cref="Task{Boolean}" /> for interoperability with standard async
/// composition patterns such as <c>Task.WhenAll</c> and <c>Task.WhenAny</c>.
/// </para>
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object to validate and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="validateAllProperties">
/// If <c>true</c>, also evaluates all properties of the object (this process is not recursive over properties of the properties).
/// </param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task{Boolean}" /> that is <c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static async Task<bool> TryValidateObjectAsync(
object instance,
ValidationContext validationContext,
ICollection<ValidationResult>? validationResults,
bool validateAllProperties,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(instance);
if (validationContext != null && instance != validationContext.ObjectInstance)
{
throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
}
var result = true;
var breakOnFirstError = (validationResults == null);
foreach (ValidationError err in await GetObjectValidationErrorsAsync(instance, validationContext!, validateAllProperties, breakOnFirstError, cancellationToken).ConfigureAwait(false))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Asynchronously tests whether the given value is valid against a specified list of <see cref="ValidationAttribute" />s.
/// </summary>
/// <remarks>
/// This method will test each <see cref="ValidationAttribute" />s specified. If
/// <paramref name="validationResults" /> is non-null, this method will add a <see cref="ValidationResult" />
/// to it for each validation failure.
/// <para>
/// Any <see cref="AsyncValidationAttribute" /> instances will be evaluated asynchronously after all synchronous
/// validation attributes have passed.
/// </para>
/// </remarks>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators.</param>
/// <param name="validationResults">Optional collection to receive <see cref="ValidationResult" />s for the failures.</param>
/// <param name="validationAttributes">The list of <see cref="ValidationAttribute" />s to validate this <paramref name="value" /> against.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task{Boolean}" /> that is <c>true</c> if the object is valid, <c>false</c> if any validation errors are encountered.</returns>
public static async Task<bool> TryValidateValueAsync(
object? value,
ValidationContext validationContext,
ICollection<ValidationResult>? validationResults,
IEnumerable<ValidationAttribute> validationAttributes,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(validationAttributes);
var result = true;
var breakOnFirstError = validationResults == null;
foreach (var err in await GetValidationErrorsAsync(value, validationContext, validationAttributes, breakOnFirstError, cancellationToken).ConfigureAwait(false))
{
result = false;
validationResults?.Add(err.ValidationResult);
}
return result;
}
/// <summary>
/// Asynchronously throws a <see cref="ValidationException" /> if the given property <paramref name="value" /> is not valid.
/// </summary>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous validation operation.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value" /> is invalid for this property.</exception>
[RequiresUnreferencedCode("The Type of validationContext.ObjectType cannot be statically discovered.")]
public static async Task ValidatePropertyAsync(
object? value,
ValidationContext validationContext,
CancellationToken cancellationToken = default)
{
var propertyType = _store.GetPropertyType(validationContext);
EnsureValidPropertyType(validationContext.MemberName!, propertyType, value);
var attributes = _store.GetPropertyValidationAttributes(validationContext);
List<ValidationError> errors = await GetValidationErrorsAsync(value, validationContext, attributes, false, cancellationToken).ConfigureAwait(false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Asynchronously throws a <see cref="ValidationException" /> if the given <paramref name="instance" /> is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object's type.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous validation operation.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
/// <exception cref="ValidationException">When <paramref name="instance" /> is found to be invalid.</exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static Task ValidateObjectAsync(
object instance,
ValidationContext validationContext,
CancellationToken cancellationToken = default)
{
return ValidateObjectAsync(instance, validationContext, false, cancellationToken);
}
/// <summary>
/// Asynchronously throws a <see cref="ValidationException" /> if the given object instance is not valid.
/// </summary>
/// <remarks>
/// This method evaluates all <see cref="ValidationAttribute" />s attached to the object's type.
/// If <paramref name="validateAllProperties" /> is <c>true</c> it also validates all the object's properties.
/// </remarks>
/// <param name="instance">The object instance to test. It cannot be null.</param>
/// <param name="validationContext">Describes the object being validated and provides services and context for the validators.</param>
/// <param name="validateAllProperties">If <c>true</c>, also validates all the <paramref name="instance" />'s properties.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous validation operation.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
/// <exception cref="ValidationException">When <paramref name="instance" /> is found to be invalid.</exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static async Task ValidateObjectAsync(
object instance,
ValidationContext validationContext,
bool validateAllProperties,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(instance);
ArgumentNullException.ThrowIfNull(validationContext);
if (instance != validationContext.ObjectInstance)
{
throw new ArgumentException(SR.Validator_InstanceMustMatchValidationContextInstance, nameof(instance));
}
List<ValidationError> errors = await GetObjectValidationErrorsAsync(instance, validationContext, validateAllProperties, false, cancellationToken).ConfigureAwait(false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Asynchronously throws a <see cref="ValidationException" /> if the given value is not valid for the
/// <see cref="ValidationAttribute" />s.
/// </summary>
/// <param name="value">The value to test.</param>
/// <param name="validationContext">Describes the object being tested.</param>
/// <param name="validationAttributes">The list of <see cref="ValidationAttribute" />s to validate against this instance.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A <see cref="Task" /> representing the asynchronous validation operation.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ValidationException">When <paramref name="value" /> is found to be invalid.</exception>
public static async Task ValidateValueAsync(
object? value,
ValidationContext validationContext,
IEnumerable<ValidationAttribute> validationAttributes,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(validationContext);
ArgumentNullException.ThrowIfNull(validationAttributes);
List<ValidationError> errors = await GetValidationErrorsAsync(value, validationContext, validationAttributes, false, cancellationToken).ConfigureAwait(false);
if (errors.Count > 0)
{
errors[0].ThrowValidationException();
}
}
/// <summary>
/// Asynchronous version of <see cref="GetObjectValidationErrors" />.
/// Implements a 3-step pipeline: properties → type attrs → IAsyncValidatableObject or IValidatableObject.
/// </summary>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static async Task<List<ValidationError>> GetObjectValidationErrorsAsync(
object instance,
ValidationContext validationContext,
bool validateAllProperties,
bool breakOnFirstError,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(validationContext);
Debug.Assert(instance != null);
// Step 1: Validate the object properties' validation attributes
List<ValidationError> errors = await GetObjectPropertyValidationErrorsAsync(instance, validationContext, validateAllProperties, breakOnFirstError, cancellationToken).ConfigureAwait(false);
// We only proceed to Step 2 if there are no errors
if (errors.Count > 0)
{
return errors;
}
// Step 2: Validate the object's validation attributes
var attributes = _store.GetTypeValidationAttributes(validationContext);
errors.AddRange(await GetValidationErrorsAsync(instance, validationContext, attributes, breakOnFirstError, cancellationToken).ConfigureAwait(false));
// We only proceed to Step 3 if there are no errors
if (errors.Count > 0)
{
return errors;
}
// Step 3: Test for IAsyncValidatableObject implementation (preferred), fall back to IValidatableObject
if (instance is IAsyncValidatableObject asyncValidatable)
{
IAsyncEnumerable<ValidationResult>? results = asyncValidatable.ValidateAsync(validationContext, cancellationToken);
if (results is not null)
{
await foreach (ValidationResult result in results.ConfigureAwait(false))
{
if (result != ValidationResult.Success)
{
errors.Add(new ValidationError(null, instance, result));
}
}
}
}
else if (instance is IValidatableObject validatable)
{
var results = validatable.Validate(validationContext);
if (results != null)
{
foreach (ValidationResult result in results)
{
if (result != ValidationResult.Success)
{
errors.Add(new ValidationError(null, instance, result));
}
}
}
}
return errors;
}
/// <summary>
/// Asynchronous version of <see cref="GetObjectPropertyValidationErrors" />.
/// Iterates all properties and validates each using <see cref="GetValidationErrorsAsync" />.
/// </summary>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static async Task<List<ValidationError>> GetObjectPropertyValidationErrorsAsync(
object instance,
ValidationContext validationContext,
bool validateAllProperties,
bool breakOnFirstError,
CancellationToken cancellationToken)
{
var properties = GetPropertyValues(instance, validationContext);
var errors = new List<ValidationError>();
if (validateAllProperties)
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tasks = new List<Task<List<ValidationError>>>(properties.Count);
foreach (var property in properties)
{
var attributes = _store.GetPropertyValidationAttributes(property.Key);
tasks.Add(GetValidationErrorsAsync(
property.Value, property.Key, attributes,
breakOnFirstError, linkedCts.Token));
}
try
{
while (tasks.Count > 0)
{
Task<List<ValidationError>> completed = await Task.WhenAny(tasks).ConfigureAwait(false);
tasks.Remove(completed);
List<ValidationError> propertyErrors;
try
{
propertyErrors = await completed.ConfigureAwait(false);
}
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
continue;
}
if (propertyErrors.Count > 0)
{
errors.AddRange(propertyErrors);
if (breakOnFirstError)
{
linkedCts.Cancel();
break;
}
}
}
}
finally
{
// Observe any remaining in-flight tasks on every exit path
// (success short-circuit, external cancellation, or unexpected exception)
// to prevent UnobservedTaskException from the finalizer thread.
if (tasks.Count > 0)
{
linkedCts.Cancel();
foreach (Task<List<ValidationError>> remaining in tasks)
{
try { await remaining.ConfigureAwait(false); }
catch { }
}
}
}
}
else
{
foreach (var property in properties)
{
var attributes = _store.GetPropertyValidationAttributes(property.Key);
foreach (ValidationAttribute attribute in attributes)
{
if (attribute is RequiredAttribute reqAttr)
{
var validationResult = reqAttr.GetValidationResult(property.Value, property.Key);
if (validationResult != ValidationResult.Success)
{
errors.Add(new ValidationError(reqAttr, property.Value, validationResult!));
}
break;
}
}
if (breakOnFirstError && errors.Count > 0)
{
break;
}
}
}
return errors;
}
/// <summary>
/// Asynchronous version of <see cref="GetValidationErrors" />.
/// Implements two-phase validation: sync attributes first, async attributes only if no sync errors.
/// </summary>
private static async Task<List<ValidationError>> GetValidationErrorsAsync(
object? value,
ValidationContext validationContext,
IEnumerable<ValidationAttribute> attributes,
bool breakOnFirstError,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(validationContext);
var errors = new List<ValidationError>();
List<AsyncValidationAttribute>? asyncAttributes = null;
ValidationError? validationError;
// Get the required validator if there is one and test it first, aborting on failure
RequiredAttribute? required = null;
foreach (ValidationAttribute attribute in attributes)
{
required = attribute as RequiredAttribute;
if (required is not null)
{
if (!TryValidate(value, validationContext, required, out validationError))
{
errors.Add(validationError);
return errors;
}
break;
}
}
// Phase 1: Iterate through sync validators, collecting async ones for later
foreach (ValidationAttribute attr in attributes)
{
if (attr != required)
{
if (attr is AsyncValidationAttribute asyncAttr)
{
(asyncAttributes ??= new List<AsyncValidationAttribute>()).Add(asyncAttr);
}
else
{
if (!TryValidate(value, validationContext, attr, out validationError))
{
errors.Add(validationError);
if (breakOnFirstError)
{
break;
}
}
}
}
}
// Phase 2: Only run async validators if all sync validators passed — run in parallel
if (errors.Count == 0 && asyncAttributes is not null)
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tasks = new List<Task<(AsyncValidationAttribute Attr, ValidationResult? Result)>>(asyncAttributes.Count);
foreach (AsyncValidationAttribute asyncAttr in asyncAttributes)
{
tasks.Add(RunAsyncValidation(asyncAttr, value, validationContext, linkedCts.Token));
}
try
{
while (tasks.Count > 0)
{
Task<(AsyncValidationAttribute Attr, ValidationResult? Result)> completed =
await Task.WhenAny(tasks).ConfigureAwait(false);
tasks.Remove(completed);
(AsyncValidationAttribute attr, ValidationResult? result) completedResult;
try
{
completedResult = await completed.ConfigureAwait(false);
}
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
continue;
}
if (completedResult.result != ValidationResult.Success)
{
errors.Add(new ValidationError(completedResult.attr, value, completedResult.result!));
if (breakOnFirstError)
{
linkedCts.Cancel();
break;
}
}
}
}
finally
{
// Observe any remaining in-flight tasks on every exit path
// (success short-circuit, external cancellation, or unexpected exception)
// to prevent UnobservedTaskException from the finalizer thread.
if (tasks.Count > 0)
{
linkedCts.Cancel();
foreach (var remaining in tasks)
{
try { await remaining.ConfigureAwait(false); }
catch { }
}
}
}
}
return errors;
}
private static async Task<(AsyncValidationAttribute Attr, ValidationResult? Result)> RunAsyncValidation(
AsyncValidationAttribute attr,
object? value,
ValidationContext validationContext,
CancellationToken cancellationToken)
{
ValidationResult? result = await attr.GetValidationResultAsync(value, validationContext, cancellationToken).ConfigureAwait(false);
return (attr, result);
}
/// <summary>
/// Creates a new <see cref="ValidationContext" /> to use to validate the type or a member of
/// the given object instance.
/// </summary>
/// <param name="instance">The object instance to use for the context.</param>
/// <param name="validationContext">
/// An parent validation context that supplies an <see cref="IServiceProvider" />
/// and <see cref="ValidationContext.Items" />.
/// </param>
/// <returns>A new <see cref="ValidationContext" /> for the <paramref name="instance" /> provided.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static ValidationContext CreateValidationContext(object instance, ValidationContext validationContext)
{
Debug.Assert(validationContext != null);
// Create a new context using the existing ValidationContext that acts as an IServiceProvider and contains our existing items.
var context = new ValidationContext(instance, validationContext, validationContext.Items);
return context;
}
/// <summary>
/// Determine whether the given value can legally be assigned into the specified type.
/// </summary>
/// <param name="destinationType">The destination <see cref="Type" /> for the value.</param>
/// <param name="value">
/// The value to test to see if it can be assigned as the Type indicated by
/// <paramref name="destinationType" />.
/// </param>
/// <returns><c>true</c> if the assignment is legal.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="destinationType" /> is null.</exception>
private static bool CanBeAssigned(Type destinationType, object? value)
{
if (value == null)
{
// Null can be assigned only to reference types or Nullable or Nullable<>
return !destinationType.IsValueType ||
(destinationType.IsGenericType &&
destinationType.GetGenericTypeDefinition() == typeof(Nullable<>));
}
// Not null -- be sure it can be cast to the right type
return destinationType.IsInstanceOfType(value);
}
/// <summary>
/// Determines whether the given value can legally be assigned to the given property.
/// </summary>
/// <param name="propertyName">The name of the property.</param>
/// <param name="propertyType">The type of the property.</param>
/// <param name="value">The value. Null is permitted only if the property will accept it.</param>
/// <exception cref="ArgumentException"> is thrown if <paramref name="value" /> is the wrong type for this property.</exception>
private static void EnsureValidPropertyType(string propertyName, Type propertyType, object? value)
{
if (!CanBeAssigned(propertyType, value))
{
throw new ArgumentException(SR.Format(SR.Validator_Property_Value_Wrong_Type, propertyName, propertyType),
nameof(value));
}
}
/// <summary>
/// Internal iterator to enumerate all validation errors for the given object instance.
/// </summary>
/// <param name="instance">Object instance to test.</param>
/// <param name="validationContext">Describes the object type.</param>
/// <param name="validateAllProperties">if <c>true</c> also validates all properties.</param>
/// <param name="breakOnFirstError">Whether to break on the first error or validate everything.</param>
/// <returns>
/// A collection of validation errors that result from validating the <paramref name="instance" /> with
/// the given <paramref name="validationContext" />.
/// </returns>
/// <exception cref="ArgumentNullException">When <paramref name="instance" /> is null.</exception>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
/// <exception cref="ArgumentException">
/// When <paramref name="instance" /> doesn't match the
/// <see cref="ValidationContext.ObjectInstance" /> on <paramref name="validationContext" />.
/// </exception>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static List<ValidationError> GetObjectValidationErrors(object instance,
ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError)
{
ArgumentNullException.ThrowIfNull(validationContext);
Debug.Assert(instance != null);
// Step 1: Validate the object properties' validation attributes
List<ValidationError> errors = GetObjectPropertyValidationErrors(instance, validationContext, validateAllProperties, breakOnFirstError);
// We only proceed to Step 2 if there are no errors
if (errors.Count > 0)
{
return errors;
}
// Step 2: Validate the object's validation attributes
var attributes = _store.GetTypeValidationAttributes(validationContext);
errors.AddRange(GetValidationErrors(instance, validationContext, attributes, breakOnFirstError));
// We only proceed to Step 3 if there are no errors
if (errors.Count > 0)
{
return errors;
}
// Step 3: Test for IValidatableObject implementation
if (instance is IValidatableObject validatable)
{
var results = validatable.Validate(validationContext);
if (results != null)
{
foreach (ValidationResult result in results)
{
if (result != ValidationResult.Success)
{
errors.Add(new ValidationError(null, instance, result));
}
}
}
}
return errors;
}
/// <summary>
/// Internal iterator to enumerate all the validation errors for all properties of the given object instance.
/// </summary>
/// <param name="instance">Object instance to test.</param>
/// <param name="validationContext">Describes the object type.</param>
/// <param name="validateAllProperties">
/// If <c>true</c>, evaluates all the properties, otherwise just checks that
/// ones marked with <see cref="RequiredAttribute" /> are not null.
/// </param>
/// <param name="breakOnFirstError">Whether to break on the first error or validate everything.</param>
/// <returns>A list of <see cref="ValidationError" /> instances.</returns>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static List<ValidationError> GetObjectPropertyValidationErrors(object instance,
ValidationContext validationContext, bool validateAllProperties, bool breakOnFirstError)
{
var properties = GetPropertyValues(instance, validationContext);
var errors = new List<ValidationError>();
foreach (var property in properties)
{
// get list of all validation attributes for this property
var attributes = _store.GetPropertyValidationAttributes(property.Key);
if (validateAllProperties)
{
// validate all validation attributes on this property
errors.AddRange(GetValidationErrors(property.Value, property.Key, attributes, breakOnFirstError));
}
else
{
// only validate the first Required attribute
foreach (ValidationAttribute attribute in attributes)
{
if (attribute is RequiredAttribute reqAttr)
{
// Note: we let the [Required] attribute do its own null testing,
// since the user may have subclassed it and have a deeper meaning to what 'required' means
var validationResult = reqAttr.GetValidationResult(property.Value, property.Key);
if (validationResult != ValidationResult.Success)
{
errors.Add(new ValidationError(reqAttr, property.Value, validationResult!));
}
break;
}
}
}
if (breakOnFirstError && errors.Count > 0)
{
break;
}
}
return errors;
}
/// <summary>
/// Retrieves the property values for the given instance.
/// </summary>
/// <param name="instance">Instance from which to fetch the properties.</param>
/// <param name="validationContext">Describes the entity being validated.</param>
/// <returns>
/// A set of key value pairs, where the key is a validation context for the property and the value is its current
/// value.
/// </returns>
/// <remarks>Ignores indexed properties.</remarks>
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
private static List<KeyValuePair<ValidationContext, object?>> GetPropertyValues(object instance,
ValidationContext validationContext)
{
var properties = TypeDescriptor.GetProperties(instance.GetType());
var items = new List<KeyValuePair<ValidationContext, object?>>(properties.Count);
foreach (PropertyDescriptor property in properties)
{
var context = CreateValidationContext(instance, validationContext);
context.MemberName = property.Name;
if (_store.GetPropertyValidationAttributes(context).Any())
{
items.Add(new KeyValuePair<ValidationContext, object?>(context, property.GetValue(instance)));
}
}
return items;
}
/// <summary>
/// Internal iterator to enumerate all validation errors for an value.
/// </summary>
/// <remarks>
/// If a <see cref="RequiredAttribute" /> is found, it will be evaluated first, and if that fails,
/// validation will abort, regardless of the <paramref name="breakOnFirstError" /> parameter value.
/// </remarks>
/// <param name="value">The value to pass to the validation attributes.</param>
/// <param name="validationContext">Describes the type/member being evaluated.</param>
/// <param name="attributes">The validation attributes to evaluate.</param>
/// <param name="breakOnFirstError">
/// Whether or not to break on the first validation failure. A
/// <see cref="RequiredAttribute" /> failure will always abort with that sole failure.
/// </param>
/// <returns>The collection of validation errors.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
private static List<ValidationError> GetValidationErrors(object? value,
ValidationContext validationContext, IEnumerable<ValidationAttribute> attributes, bool breakOnFirstError)
{
ArgumentNullException.ThrowIfNull(validationContext);
var errors = new List<ValidationError>();
ValidationError? validationError;
// Get the required validator if there is one and test it first, aborting on failure
RequiredAttribute? required = null;
foreach (ValidationAttribute attribute in attributes)
{
required = attribute as RequiredAttribute;
if (required is not null)
{
if (!TryValidate(value, validationContext, required, out validationError))
{
errors.Add(validationError);
return errors;
}
break;
}
}
// Iterate through the rest of the validators, skipping the required validator
foreach (ValidationAttribute attr in attributes)
{
if (attr != required)
{
if (!TryValidate(value, validationContext, attr, out validationError))
{
errors.Add(validationError);
if (breakOnFirstError)
{
break;
}
}
}
}
return errors;
}
/// <summary>
/// Tests whether a value is valid against a single <see cref="ValidationAttribute" /> using the
/// <see cref="ValidationContext" />.
/// </summary>
/// <param name="value">The value to be tested for validity.</param>
/// <param name="validationContext">Describes the property member to validate.</param>
/// <param name="attribute">The validation attribute to test.</param>
/// <param name="validationError">
/// The validation error that occurs during validation. Will be <c>null</c> when the return
/// value is <c>true</c>.
/// </param>
/// <returns><c>true</c> if the value is valid.</returns>
/// <exception cref="ArgumentNullException">When <paramref name="validationContext" /> is null.</exception>
private static bool TryValidate(object? value, ValidationContext validationContext, ValidationAttribute attribute,
[NotNullWhen(false)] out ValidationError? validationError)
{
Debug.Assert(validationContext != null);
var validationResult = attribute.GetValidationResult(value, validationContext);
if (validationResult != ValidationResult.Success)
{
validationError = new ValidationError(attribute, value, validationResult!);
return false;
}
validationError = null;
return true;
}
/// <summary>
/// Private helper class to encapsulate a ValidationAttribute with the failed value and the user-visible
/// target name against which it was validated.
/// </summary>
private sealed class ValidationError
{
private readonly object? _value;
private readonly ValidationAttribute? _validationAttribute;
internal ValidationError(ValidationAttribute? validationAttribute, object? value,
ValidationResult validationResult)
{
_validationAttribute = validationAttribute;
ValidationResult = validationResult;
_value = value;
}
internal ValidationResult ValidationResult { get; }
internal void ThrowValidationException() => throw new ValidationException(ValidationResult, _validationAttribute, _value);
}
}
}
|