* Add RangeAttribute.Minimum/MaximumIsExclusive properties.
* Add RequiredAttribute.DisallowAllDefaultValues.
* Add LengthAttribute implementation & tests.
* Add AllowedValuesAttribute & DeniedValuesAttribute
* Add Base64StringAttribute.
* Address feedback
* Address feedback.
* Reinstate culture-insensitive parsing
namespace System.ComponentModel.DataAnnotations
{
+ [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple = false)]
+ [System.CLSCompliant(false)]
+ public partial class AllowedValuesAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute
+ {
+ public AllowedValuesAttribute(params object?[] values) { }
+ public object?[] Values { get { throw null; } }
+ public override bool IsValid(object? value) { throw null; }
+ }
public partial class AssociatedMetadataTypeTypeDescriptionProvider : System.ComponentModel.TypeDescriptionProvider
{
public AssociatedMetadataTypeTypeDescriptionProvider(System.Type type) { }
public string ThisKey { get { throw null; } }
public System.Collections.Generic.IEnumerable<string> ThisKeyMembers { get { throw null; } }
}
+ [System.AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+ public class Base64StringAttribute : ValidationAttribute
+ {
+ public Base64StringAttribute() { }
+ public override bool IsValid(object? value) { throw null; }
+ }
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
public partial class CompareAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute
{
public virtual string GetDataTypeName() { throw null; }
public override bool IsValid(object? value) { throw null; }
}
+ [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple = false)]
+ [System.CLSCompliant(false)]
+ public partial class DeniedValuesAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute
+ {
+ public DeniedValuesAttribute(params object?[] values) { }
+ public object?[] Values { get { throw null; } }
+ public override bool IsValid(object? value) { throw null; }
+ }
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Field | System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false)]
public sealed partial class DisplayAttribute : System.Attribute
{
{
public KeyAttribute() { }
}
+ [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple = false)]
+ public partial class LengthAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute
+ {
+ [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Uses reflection to get the 'Count' property on types that don't implement ICollection. This 'Count' property may be trimmed. Ensure it is preserved.")]
+ public LengthAttribute(int minimumLength, int maximumLength) { }
+ public int MinimumLength { get { throw null; } }
+ public int MaximumLength { get { throw null; } }
+ public override string FormatErrorMessage(string name) { throw null; }
+ public override bool IsValid(object? value) { throw null; }
+ }
[System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false)]
public partial class MaxLengthAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute
{
public RangeAttribute([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, string minimum, string maximum) { }
public bool ConvertValueInInvariantCulture { get { throw null; } set { } }
public object Maximum { get { throw null; } }
+ public bool MaximumIsExclusive { get { throw null; } set { } }
public object Minimum { get { throw null; } }
+ public bool MinimumIsExclusive { get { throw null; } set { } }
[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)]
public System.Type OperandType { get { throw null; } }
public bool ParseLimitsInInvariantCulture { get { throw null; } set { } }
{
public RequiredAttribute() { }
public bool AllowEmptyStrings { get { throw null; } set { } }
+ public bool DisallowAllDefaultValues { get { throw null; } set { } }
public override bool IsValid(object? value) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)]
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
+ <data name="AllowedValuesAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field does not equal any of the values specified in AllowedValuesAttribute.</value>
+ </data>
<data name="ArgumentIsNullOrWhitespace" xml:space="preserve">
<value>The argument '{0}' cannot be null, empty or contain only whitespace.</value>
</data>
<data name="AttributeStore_Unknown_Property" xml:space="preserve">
<value>The type '{0}' does not contain a public property named '{1}'.</value>
</data>
+ <data name="Base64StringAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field is not a valid Base64 encoding.</value>
+ </data>
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>
<data name="DataTypeAttribute_EmptyDataTypeString" xml:space="preserve">
<value>The custom DataType string cannot be null or empty.</value>
</data>
+ <data name="DeniedValuesAttribute_Invalid" xml:space="preserve">
+ <value>The {0} field equals one of the values specified in DeniedValuesAttribute.</value>
+ </data>
<data name="DisplayAttribute_PropertyNotSet" xml:space="preserve">
<value>The {0} property has not been set. Use the {1} method to get the value.</value>
</data>
<data name="MinLengthAttribute_ValidationError" xml:space="preserve">
<value>The field {0} must be a string or array type with a minimum length of '{1}'.</value>
</data>
+ <data name="LengthAttribute_InvalidMinLength" xml:space="preserve">
+ <value>LengthAttribute must have a MinimumLength value that is zero or greater.</value>
+ </data>
+ <data name="LengthAttribute_InvalidMaxLength" xml:space="preserve">
+ <value>LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength.</value>
+ </data>
+ <data name="LengthAttribute_ValidationError" xml:space="preserve">
+ <value>The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'.</value>
+ </data>
<data name="LengthAttribute_InvalidValueType" xml:space="preserve">
<value>The field of type {0} must be a string, array or ICollection type.</value>
</data>
<data name="RangeAttribute_MinGreaterThanMax" xml:space="preserve">
<value>The maximum value '{0}' must be greater than or equal to the minimum value '{1}'.</value>
</data>
+ <data name="RangeAttribute_CannotUseExclusiveBoundsWhenTheyAreEqual" xml:space="preserve">
+ <value>Cannot use exclusive bounds when the maximum value is equal to the minimum value.</value>
+ </data>
<data name="RangeAttribute_Must_Set_Min_And_Max" xml:space="preserve">
<value>The minimum and maximum values must be set.</value>
</data>
<data name="RangeAttribute_ValidationError" xml:space="preserve">
<value>The field {0} must be between {1} and {2}.</value>
</data>
+ <data name="RangeAttribute_ValidationError_MinExclusive" xml:space="preserve">
+ <value>The field {0} must be between {1} exclusive and {2}.</value>
+ </data>
+ <data name="RangeAttribute_ValidationError_MaxExclusive" xml:space="preserve">
+ <value>The field {0} must be between {1} and {2} exclusive.</value>
+ </data>
+ <data name="RangeAttribute_ValidationError_MinExclusive_MaxExclusive" xml:space="preserve">
+ <value>The field {0} must be between {1} exclusive and {2} exclusive.</value>
+ </data>
<data name="RegexAttribute_ValidationError" xml:space="preserve">
<value>The field {0} must match the regular expression '{1}'.</value>
</data>
<GenerateResxSourceIncludeDefaultValues>true</GenerateResxSourceIncludeDefaultValues>
</PropertyGroup>
<ItemGroup>
+ <Compile Include="System\ComponentModel\DataAnnotations\AllowedValuesAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\AssociatedMetadataTypeTypeDescriptor.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\AssociatedMetadataTypeTypeDescriptionProvider.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\AssociationAttribute.cs" />
+ <Compile Include="System\ComponentModel\DataAnnotations\Base64StringAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\CompareAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ConcurrencyCheckAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\CreditCardAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\CustomValidationAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DataType.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DataTypeAttribute.cs" />
+ <Compile Include="System\ComponentModel\DataAnnotations\DeniedValuesAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DisplayAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DisplayColumnAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DisplayFormatAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\FilterUIHintAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\IValidatableObject.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\KeyAttribute.cs" />
+ <Compile Include="System\ComponentModel\DataAnnotations\LengthAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\LocalizableString.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\MaxLengthAttribute.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\MetadataPropertyDescriptorWrapper.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ValidationException.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ValidationResult.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\Validator.cs" />
- <Compile Include="$(CommonPath)System\NotImplemented.cs"
- Link="Common\System\NotImplemented.cs" />
+ <Compile Include="$(CommonPath)System\NotImplemented.cs" Link="Common\System\NotImplemented.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Collections" />
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.ComponentModel.DataAnnotations
+{
+ /// <summary>
+ /// Specifies a list of values that should be allowed in a property.
+ /// </summary>
+ [CLSCompliant(false)]
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
+ AllowMultiple = false)]
+ public class AllowedValuesAttribute : ValidationAttribute
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AllowedValuesAttribute"/> class.
+ /// </summary>
+ /// <param name="values">
+ /// A list of values that the validated value should be equal to.
+ /// </param>
+ public AllowedValuesAttribute(params object?[] values)
+ {
+ ArgumentNullException.ThrowIfNull(values);
+ Values = values;
+ DefaultErrorMessage = SR.AllowedValuesAttribute_Invalid;
+ }
+
+ /// <summary>
+ /// Gets the list of values allowed by this attribute.
+ /// </summary>
+ public object?[] Values { get; }
+
+ /// <summary>
+ /// Determines whether a specified object is valid. (Overrides <see cref="ValidationAttribute.IsValid(object)" />)
+ /// </summary>
+ /// <param name="value">The object to validate.</param>
+ /// <returns>
+ /// <see langword="true" /> if any of the <see cref="Values"/> are equal to <paramref name="value"/>,
+ /// otherwise <see langword="false" />
+ /// </returns>
+ /// <remarks>
+ /// This method can return <see langword="true"/> if the <paramref name="value" /> is <see langword="null"/>,
+ /// provided that <see langword="null"/> is also specified in one of the <see cref="Values"/>.
+ /// </remarks>
+ public override bool IsValid(object? value)
+ {
+ foreach (object? allowed in Values)
+ {
+ if (allowed is null ? value is null : allowed.Equals(value))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+
+namespace System.ComponentModel.DataAnnotations
+{
+ /// <summary>
+ /// Specifies that a data field value is a well-formed Base64 string.
+ /// </summary>
+ /// <remarks>
+ /// Recognition of valid Base64 is delegated to the <see cref="Convert"/> class,
+ /// using the <see cref="Convert.TryFromBase64String(string, Span{byte}, out int)"/> method.
+ /// </remarks>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+ public class Base64StringAttribute : ValidationAttribute
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Base64StringAttribute"/> class.
+ /// </summary>
+ public Base64StringAttribute()
+ {
+ // Set DefaultErrorMessage not ErrorMessage, allowing user to set
+ // ErrorMessageResourceType and ErrorMessageResourceName to use localized messages.
+ DefaultErrorMessage = SR.Base64StringAttribute_Invalid;
+ }
+
+ /// <summary>
+ /// Determines whether a specified object is valid. (Overrides <see cref="ValidationAttribute.IsValid(object)" />)
+ /// </summary>
+ /// <param name="value">The object to validate.</param>
+ /// <returns>
+ /// <see langword="true" /> if <paramref name="value"/> is <see langword="null"/> or is a valid Base64 string,
+ /// otherwise <see langword="false" />
+ /// </returns>
+ public override bool IsValid(object? value)
+ {
+ if (value is null)
+ {
+ return true;
+ }
+
+ if (value is not string valueAsString)
+ {
+ return false;
+ }
+
+ byte[]? rentedBuffer = null;
+ Span<byte> destinationBuffer = valueAsString.Length < 256
+ ? stackalloc byte[256]
+ : rentedBuffer = ArrayPool<byte>.Shared.Rent(valueAsString.Length);
+
+ bool result = Convert.TryFromBase64String(valueAsString, destinationBuffer, out int bytesWritten);
+
+ if (rentedBuffer != null)
+ {
+ destinationBuffer.Slice(0, bytesWritten).Clear();
+ ArrayPool<byte>.Shared.Return(rentedBuffer);
+ }
+
+ return result;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.ComponentModel.DataAnnotations
+{
+ /// <summary>
+ /// Specifies a list of values that should not be allowed in a property.
+ /// </summary>
+ [CLSCompliant(false)]
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
+ AllowMultiple = false)]
+ public class DeniedValuesAttribute : ValidationAttribute
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeniedValuesAttribute"/> class.
+ /// </summary>
+ /// <param name="values">
+ /// A list of values that the validated value should not be equal to.
+ /// </param>
+ public DeniedValuesAttribute(params object?[] values)
+ {
+ ArgumentNullException.ThrowIfNull(values);
+ Values = values;
+ DefaultErrorMessage = SR.DeniedValuesAttribute_Invalid;
+ }
+
+ /// <summary>
+ /// Gets the list of values denied by this attribute.
+ /// </summary>
+ public object?[] Values { get; }
+
+ /// <summary>
+ /// Determines whether a specified object is valid. (Overrides <see cref="ValidationAttribute.IsValid(object)" />)
+ /// </summary>
+ /// <param name="value">The object to validate.</param>
+ /// <returns>
+ /// <see langword="true" /> if none of the <see cref="Values"/> are equal to <paramref name="value"/>,
+ /// otherwise <see langword="false" />.
+ /// </returns>
+ /// <remarks>
+ /// This method can return <see langword="true"/> if the <paramref name="value" /> is <see langword="null"/>,
+ /// provided that <see langword="null"/> is not specified in any of the <see cref="Values"/>.
+ /// </remarks>
+ public override bool IsValid(object? value)
+ {
+ foreach (object? allowed in Values)
+ {
+ if (allowed is null ? value is null : allowed.Equals(value))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace System.ComponentModel.DataAnnotations
+{
+ /// <summary>
+ /// Specifies the minimum and maximum length of collection/string data allowed in a property.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+ public class LengthAttribute : ValidationAttribute
+ {
+ [RequiresUnreferencedCode(CountPropertyHelper.RequiresUnreferencedCodeMessage)]
+ public LengthAttribute(int minimumLength, int maximumLength)
+ : base(SR.LengthAttribute_ValidationError)
+ {
+ MinimumLength = minimumLength;
+ MaximumLength = maximumLength;
+ }
+
+ /// <summary>
+ /// Gets the minimum allowable length of the collection/string data.
+ /// </summary>
+ public int MinimumLength { get; }
+
+ /// <summary>
+ /// Gets the maximum allowable length of the collection/string data.
+ /// </summary>
+ public int MaximumLength { get; }
+
+ /// <summary>
+ /// Determines whether a specified object is valid. (Overrides <see cref="ValidationAttribute.IsValid(object)" />)
+ /// </summary>
+ /// <remarks>
+ /// This method returns <c>true</c> if the <paramref name="value" /> is null.
+ /// It is assumed the <see cref="RequiredAttribute" /> is used if the value may not be null.
+ /// </remarks>
+ /// <param name="value">The object to validate.</param>
+ /// <returns>
+ /// <c>true</c> if the value is null or its length is between the specified minimum length and maximum length, otherwise
+ /// <c>false</c>
+ /// </returns>
+ /// <exception cref="InvalidOperationException">
+ /// <see cref="MinimumLength"/> is less than zero or <see cref="MaximumLength"/> is less than <see cref="MinimumLength"/>.
+ /// </exception>
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "The ctor is marked with RequiresUnreferencedCode.")]
+ public override bool IsValid(object? value)
+ {
+ // Check the lengths for legality
+ EnsureLegalLengths();
+
+ int length;
+ // Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
+ if (value is null)
+ {
+ return true;
+ }
+
+ if (value is string str)
+ {
+ length = str.Length;
+ }
+ else if (!CountPropertyHelper.TryGetCount(value, out length))
+ {
+ throw new InvalidCastException(SR.Format(SR.LengthAttribute_InvalidValueType, value.GetType()));
+ }
+
+ return (uint)(length - MinimumLength) <= (uint)(MaximumLength - MinimumLength);
+ }
+
+ /// <summary>
+ /// Applies formatting to a specified error message. (Overrides <see cref="ValidationAttribute.FormatErrorMessage" />)
+ /// </summary>
+ /// <param name="name">The name to include in the formatted string.</param>
+ /// <returns>A localized string to describe the minimum acceptable length.</returns>
+ public override string FormatErrorMessage(string name) =>
+ // An error occurred, so we know the value is less than the minimum
+ string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, MinimumLength, MaximumLength);
+
+ /// <summary>
+ /// Checks that Length has a legal value.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Length is less than zero.</exception>
+ private void EnsureLegalLengths()
+ {
+ if (MinimumLength < 0)
+ {
+ throw new InvalidOperationException(SR.LengthAttribute_InvalidMinLength);
+ }
+
+ if (MaximumLength < MinimumLength)
+ {
+ throw new InvalidOperationException(SR.LengthAttribute_InvalidMaxLength);
+ }
+ }
+ }
+}
{
return true;
}
+
if (value is string str)
{
length = str.Length;
}
- else if (CountPropertyHelper.TryGetCount(value, out var count))
- {
- length = count;
- }
- else
+ else if (!CountPropertyHelper.TryGetCount(value, out length))
{
throw new InvalidCastException(SR.Format(SR.LengthAttribute_InvalidValueType, value.GetType()));
}
{
return true;
}
+
if (value is string str)
{
length = str.Length;
}
- else if (CountPropertyHelper.TryGetCount(value, out var count))
- {
- length = count;
- }
- else
+ else if (!CountPropertyHelper.TryGetCount(value, out length))
{
throw new InvalidCastException(SR.Format(SR.LengthAttribute_InvalidValueType, value.GetType()));
}
/// <param name="minimum">The minimum value, inclusive</param>
/// <param name="maximum">The maximum value, inclusive</param>
public RangeAttribute(int minimum, int maximum)
- : base(() => SR.RangeAttribute_ValidationError)
+ : base(populateErrorMessageResourceAccessor: false)
{
Minimum = minimum;
Maximum = maximum;
OperandType = typeof(int);
+ ErrorMessageResourceAccessor = GetValidationErrorMessage;
}
/// <summary>
/// <param name="minimum">The minimum value, inclusive</param>
/// <param name="maximum">The maximum value, inclusive</param>
public RangeAttribute(double minimum, double maximum)
- : base(() => SR.RangeAttribute_ValidationError)
+ : base(populateErrorMessageResourceAccessor: false)
{
Minimum = minimum;
Maximum = maximum;
OperandType = typeof(double);
+ ErrorMessageResourceAccessor = GetValidationErrorMessage;
}
/// <summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
string minimum,
string maximum)
- : base(() => SR.RangeAttribute_ValidationError)
+ : base(populateErrorMessageResourceAccessor: false)
{
OperandType = type;
Minimum = minimum;
Maximum = maximum;
+ ErrorMessageResourceAccessor = GetValidationErrorMessage;
}
/// <summary>
public object Maximum { get; private set; }
/// <summary>
+ /// Specifies whether validation should fail for values that are equal to <see cref="Minimum"/>.
+ /// </summary>
+ public bool MinimumIsExclusive { get; set; }
+
+ /// <summary>
+ /// Specifies whether validation should fail for values that are equal to <see cref="Maximum"/>.
+ /// </summary>
+ public bool MaximumIsExclusive { get; set; }
+
+ /// <summary>
/// Gets the type of the <see cref="Minimum" /> and <see cref="Maximum" /> values (e.g. Int32, Double, or some custom
/// type)
/// </summary>
private void Initialize(IComparable minimum, IComparable maximum, Func<object, object?> conversion)
{
- if (minimum.CompareTo(maximum) > 0)
+ int cmp = minimum.CompareTo(maximum);
+ if (cmp > 0)
{
throw new InvalidOperationException(SR.Format(SR.RangeAttribute_MinGreaterThanMax, maximum, minimum));
}
+ else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive))
+ {
+ throw new InvalidOperationException(SR.RangeAttribute_CannotUseExclusiveBoundsWhenTheyAreEqual);
+ }
Minimum = minimum;
Maximum = maximum;
SetupConversion();
// Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty.
- if (value == null || (value as string)?.Length == 0)
+ if (value is null or string { Length: 0 })
{
return true;
}
var min = (IComparable)Minimum;
var max = (IComparable)Maximum;
- return min.CompareTo(convertedValue) <= 0 && max.CompareTo(convertedValue) >= 0;
+ return
+ (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) &&
+ (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0);
}
/// <summary>
Justification = "The ctor that allows this code to be called is marked with RequiresUnreferencedCode.")]
private TypeConverter GetOperandTypeConverter() =>
TypeDescriptor.GetConverter(OperandType);
+
+ private string GetValidationErrorMessage()
+ {
+ return (MinimumIsExclusive, MaximumIsExclusive) switch
+ {
+ (false, false) => SR.RangeAttribute_ValidationError,
+ (true, false) => SR.RangeAttribute_ValidationError_MinExclusive,
+ (false, true) => SR.RangeAttribute_ValidationError_MaxExclusive,
+ (true, true) => SR.RangeAttribute_ValidationError_MinExclusive_MaxExclusive,
+ };
+ }
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
namespace System.ComponentModel.DataAnnotations
{
/// <summary>
public bool AllowEmptyStrings { get; set; }
/// <summary>
+ /// Gets or sets a flag indicating whether the attribute should also disallow non-null default values.
+ /// </summary>
+ public bool DisallowAllDefaultValues { get; set; }
+
+ /// <summary>
/// Override of <see cref="ValidationAttribute.IsValid(object)" />
/// </summary>
/// <param name="value">The value to test</param>
/// <returns>
- /// <c>false</c> if the <paramref name="value" /> is null or an empty string. If
- /// <see cref="RequiredAttribute.AllowEmptyStrings" />
- /// then <c>false</c> is returned only if <paramref name="value" /> is null.
+ /// Returns <see langword="false" /> if the <paramref name="value" /> is null or an empty string.
+ /// If <see cref="AllowEmptyStrings" /> then <see langword="true" /> is returned for empty strings.
+ /// If <see cref="DisallowAllDefaultValues"/> then <see langword="false" /> is returned for values
+ /// that are equal to the <see langword="default" /> of the declared type.
/// </returns>
public override bool IsValid(object? value)
+ => IsValidCore(value, validationContext: null);
+
+ /// <inheritdoc />
+ protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
+ {
+ return IsValidCore(value, validationContext)
+ ? ValidationResult.Success
+ : CreateFailedValidationResult(validationContext);
+ }
+
+ private bool IsValidCore(object? value, ValidationContext? validationContext)
{
- if (value == null)
+ if (value is null)
{
return false;
}
+ if (DisallowAllDefaultValues)
+ {
+ // To determine the default value of non-nullable types we need the declaring type of the value.
+ // This is the property type in a validation context falling back to the runtime type for root values.
+ Type declaringType = validationContext?.MemberType ?? value.GetType();
+ if (GetDefaultValueForNonNullableValueType(declaringType) is object defaultValue)
+ {
+ return !defaultValue.Equals(value);
+ }
+ }
+
// only check string length if empty strings are not allowed
- return AllowEmptyStrings || !(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue);
+ return AllowEmptyStrings || value is not string stringValue || !string.IsNullOrWhiteSpace(stringValue);
+ }
+
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern",
+ Justification = "GetUninitializedObject is only called struct types. You can always create an instance of a struct.")]
+ private object? GetDefaultValueForNonNullableValueType(Type type)
+ {
+ object? defaultValue = _defaultValueCache;
+
+ if (defaultValue != null && defaultValue.GetType() == type)
+ {
+ Debug.Assert(type.IsValueType && Nullable.GetUnderlyingType(type) is null);
+ }
+ else if (type.IsValueType && Nullable.GetUnderlyingType(type) is null)
+ {
+ _defaultValueCache = defaultValue = RuntimeHelpers.GetUninitializedObject(type);
+ }
+ else
+ {
+ defaultValue = null;
+ }
+
+ return defaultValue;
}
+
+ private object? _defaultValueCache;
}
}
_errorMessageResourceAccessor = errorMessageAccessor;
}
+ /// <summary>
+ /// Internal constructor used for delayed population of the error message delegate.
+ /// </summary>
+ private protected ValidationAttribute(bool populateErrorMessageResourceAccessor)
+ {
+ Debug.Assert(populateErrorMessageResourceAccessor is false, "Use the default constructor instead");
+ }
+
#endregion
#region Internal Properties
/// This property was added after the public contract for DataAnnotations was created.
/// It is internal to avoid changing the DataAnnotations contract.
/// </summary>
- internal string? DefaultErrorMessage
+ private protected string? DefaultErrorMessage
{
- set
+ init
{
_defaultErrorMessage = value;
_errorMessageResourceAccessor = null;
}
}
+ /// <summary>
+ /// Sets the delayed resource accessor in cases where we can't pass it directly to the base constructor.
+ /// </summary>
+ private protected Func<string> ErrorMessageResourceAccessor
+ {
+ init
+ {
+ Debug.Assert(_defaultErrorMessage is null && _errorMessageResourceName is null && _errorMessage is null && _errorMessageResourceType is null);
+ _errorMessageResourceAccessor = value;
+ }
+ }
+
#endregion
#region Protected Properties
_errorMessageResourceAccessor = () => (string)property.GetValue(null, null)!;
}
+ private protected ValidationResult CreateFailedValidationResult(ValidationContext validationContext)
+ {
+ string[]? memberNames = validationContext.MemberName is { } memberName
+ ? new[] { memberName }
+ : null;
+
+ return new ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames);
+ }
+
#endregion
#region Protected & Public Methods
SR.ValidationAttribute_IsValid_NotImplemented);
}
- var result = ValidationResult.Success;
-
// call overridden method.
- if (!IsValid(value))
- {
- string[]? memberNames = validationContext.MemberName is { } memberName
- ? new[] { memberName }
- : null;
- result = new ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames);
- }
-
- return result;
+ return IsValid(value)
+ ? ValidationResult.Success
+ : CreateFailedValidationResult(validationContext);
}
/// <summary>
/// </value>
public IDictionary<object, object?> Items => _items;
+ internal Type? MemberType
+ {
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
+ Justification = "The ctors are marked with RequiresUnreferencedCode.")]
+ get
+ {
+ Type? propertyType = _propertyType;
+
+ if (propertyType is null && MemberName != null)
+ {
+ _propertyType = propertyType = ValidationAttributeStore.Instance.GetPropertyType(this);
+ }
+
+ return propertyType;
+ }
+
+ set => _propertyType = value;
+ }
+
+ private Type? _propertyType;
+
#endregion
#region Methods
[RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)]
public static bool TryValidateObject(
object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults) =>
- TryValidateObject(instance, validationContext, validationResults, false /*validateAllProperties*/);
+ TryValidateObject(instance, validationContext, validationResults, validateAllProperties: false);
/// <summary>
/// Tests whether the given object instance is valid.
{
var context = CreateValidationContext(instance, validationContext);
context.MemberName = property.Name;
+ context.MemberType = property.PropertyType;
if (_store.GetPropertyValidationAttributes(context).Any())
{
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
- <TargetFrameworks>$(NetCoreAppCurrent);net48</TargetFrameworks>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<Nullable>disable</Nullable> <!-- Disable nullable attributes as some tests depend on them not being present. -->
<NoWarn>$(NoWarn);nullable</NoWarn>
</PropertyGroup>
<ItemGroup>
+ <Compile Include="System\ComponentModel\DataAnnotations\AllowedValuesAttributeTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\AssociatedMetadataTypeTypeDescriptionProviderTests.cs" />
+ <Compile Include="System\ComponentModel\DataAnnotations\Base64StringAttributeTests.cs" />
+ <Compile Include="System\ComponentModel\DataAnnotations\LengthAttributeTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\UIHintAttributeTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\FilterUIHintAttributeTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\DisplayAttributeTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ValidationExceptionTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ValidationResultTests.cs" />
<Compile Include="System\ComponentModel\DataAnnotations\ValidatorTests.cs" />
- </ItemGroup>
- <ItemGroup Condition="'$(TargetFramework)' == 'net48'">
- <Reference Include="System.ComponentModel.DataAnnotations" />
+ <Compile Include="System\ComponentModel\DataAnnotations\DeniedValuesAttributeTests.cs" />
</ItemGroup>
</Project>
--- /dev/null
+// 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 Xunit;
+
+namespace System.ComponentModel.DataAnnotations.Tests
+{
+ public class AllowedValuesAttributeTests : ValidationAttributeTestBase
+ {
+ protected override IEnumerable<TestCase> ValidValues()
+ {
+ var allowAttr = new AllowedValuesAttribute("apple", "banana", "cherry");
+ yield return new TestCase(allowAttr, "apple");
+ yield return new TestCase(allowAttr, "banana");
+ yield return new TestCase(allowAttr, "cherry");
+
+ allowAttr = new AllowedValuesAttribute(0, 1, 1, 2, 3, 5, 8, 13);
+ yield return new TestCase(allowAttr, 0);
+ yield return new TestCase(allowAttr, 1);
+ yield return new TestCase(allowAttr, 3);
+ yield return new TestCase(allowAttr, 5);
+ yield return new TestCase(allowAttr, 8);
+ yield return new TestCase(allowAttr, 13);
+
+ allowAttr = new AllowedValuesAttribute(-1, false, 3.1, "str", null, new object(), new byte[] { 0xff });
+ foreach (object? value in allowAttr.Values)
+ yield return new TestCase(allowAttr, value);
+
+ foreach (object? value in allowAttr.Values)
+ yield return new TestCase(new AllowedValuesAttribute(value), value);
+
+ }
+
+ protected override IEnumerable<TestCase> InvalidValues()
+ {
+ var allowAttr = new AllowedValuesAttribute("apple", "banana", "cherry");
+ yield return new TestCase(allowAttr, null);
+ yield return new TestCase(allowAttr, "mango");
+ yield return new TestCase(allowAttr, 13);
+ yield return new TestCase(allowAttr, false);
+
+ allowAttr = new AllowedValuesAttribute(0, 1, 1, 2, 3, 5, 8, 13);
+ yield return new TestCase(allowAttr, -1);
+ yield return new TestCase(allowAttr, 4);
+ yield return new TestCase(allowAttr, 7);
+ yield return new TestCase(allowAttr, 10);
+ yield return new TestCase(allowAttr, "mango");
+ yield return new TestCase(allowAttr, false);
+
+ allowAttr = new AllowedValuesAttribute(-1, false, 3.1, "str", null, new object(), new byte[] { 0xff });
+ yield return new TestCase(allowAttr, 0);
+ yield return new TestCase(allowAttr, true);
+ yield return new TestCase(allowAttr, 3.11);
+ yield return new TestCase(allowAttr, "str'");
+ yield return new TestCase(allowAttr, new object()); // reference equality
+ yield return new TestCase(allowAttr, new byte[] { 0xff }); // reference equality
+ }
+
+ [Fact]
+ public void Ctor_NullParameter_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => new AllowedValuesAttribute(values: null));
+ }
+
+ [Theory]
+ [MemberData(nameof(Get_Ctor_ValuesPropertyReturnsTheSameArray))]
+ public void Ctor_ValuesPropertyReturnsTheSameArray(object?[] inputs)
+ {
+ var attr = new AllowedValuesAttribute(values: inputs);
+ Assert.Same(inputs, attr.Values);
+ }
+
+ public static IEnumerable<object[]> Get_Ctor_ValuesPropertyReturnsTheSameArray()
+ {
+ yield return new object?[][] { new object?[] { null } };
+ yield return new object?[][] { new object?[] { 1, 2, 3 } };
+ yield return new object?[][] { new object?[] { "apple", "banana", "mango", null } };
+ yield return new object?[][] { new object?[] { null, false, 0, -0d, 1.1 } };
+ }
+ }
+}
--- /dev/null
+// 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.Text;
+
+namespace System.ComponentModel.DataAnnotations.Tests
+{
+ public class Base64StringAttributeTests : ValidationAttributeTestBase
+ {
+ protected override IEnumerable<TestCase> ValidValues()
+ {
+ var attribute = new Base64StringAttribute();
+ yield return new TestCase(attribute, "abc=");
+ yield return new TestCase(attribute, "BQYHCA==");
+ yield return new TestCase(attribute, "abc= \t\n\t\r ");
+ yield return new TestCase(attribute, "abc \r\n\t = \t\n\t\r ");
+ yield return new TestCase(attribute, "\t\tabc=\t\t");
+ yield return new TestCase(attribute, "\r\nabc=\r\n");
+ yield return new TestCase(attribute, Text2Base64(""));
+ yield return new TestCase(attribute, Text2Base64("hello, world!"));
+ yield return new TestCase(attribute, Text2Base64("hello, world!"));
+ yield return new TestCase(attribute, Text2Base64(new string('x', 2048)));
+
+ static string Text2Base64(string text) => Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
+ }
+
+ protected override IEnumerable<TestCase> InvalidValues()
+ {
+ var attribute = new Base64StringAttribute();
+ yield return new TestCase(attribute, "@");
+ yield return new TestCase(attribute, "^!");
+ yield return new TestCase(attribute, "hello, world!");
+ yield return new TestCase(attribute, new string('@', 2048));
+
+ // Input must be at least 4 characters long
+ yield return new TestCase(attribute, "No");
+
+ // Length of input must be a multiple of 4
+ yield return new TestCase(attribute, "NoMore");
+
+ // Input must not contain invalid characters
+ yield return new TestCase(attribute, "2-34");
+
+ // Input must not contain 3 or more padding characters in a row
+ yield return new TestCase(attribute, "a===");
+ yield return new TestCase(attribute, "abc=====");
+ yield return new TestCase(attribute, "a===\r \t \n");
+
+ // Input must not contain padding characters in the middle of the string
+ yield return new TestCase(attribute, "No=n");
+ yield return new TestCase(attribute, "abcdabc=abcd");
+ yield return new TestCase(attribute, "abcdab==abcd");
+ yield return new TestCase(attribute, "abcda===abcd");
+ yield return new TestCase(attribute, "abcd====abcd");
+
+ // Input must not contain extra trailing padding characters
+ yield return new TestCase(attribute, "=");
+ yield return new TestCase(attribute, "abc===");
+ }
+ }
+}
--- /dev/null
+// 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 Xunit;
+
+namespace System.ComponentModel.DataAnnotations.Tests
+{
+ public class DeniedValuesAttributeTests : ValidationAttributeTestBase
+ {
+ protected override IEnumerable<TestCase> InvalidValues()
+ {
+ var denyAttr = new DeniedValuesAttribute("apple", "banana", "cherry");
+ yield return new TestCase(denyAttr, "apple");
+ yield return new TestCase(denyAttr, "banana");
+ yield return new TestCase(denyAttr, "cherry");
+
+ denyAttr = new DeniedValuesAttribute(0, 1, 1, 2, 3, 5, 8, 13);
+ yield return new TestCase(denyAttr, 0);
+ yield return new TestCase(denyAttr, 1);
+ yield return new TestCase(denyAttr, 3);
+ yield return new TestCase(denyAttr, 5);
+ yield return new TestCase(denyAttr, 8);
+ yield return new TestCase(denyAttr, 13);
+
+ denyAttr = new DeniedValuesAttribute(-1, false, 3.1, "str", null, new object(), new byte[] { 0xff });
+ foreach (object? value in denyAttr.Values)
+ yield return new TestCase(denyAttr, value);
+
+ foreach (object? value in denyAttr.Values)
+ yield return new TestCase(new DeniedValuesAttribute(value), value);
+
+ }
+
+ protected override IEnumerable<TestCase> ValidValues()
+ {
+ var denyAttr = new DeniedValuesAttribute("apple", "banana", "cherry");
+ yield return new TestCase(denyAttr, null);
+ yield return new TestCase(denyAttr, "mango");
+ yield return new TestCase(denyAttr, 13);
+ yield return new TestCase(denyAttr, false);
+
+ denyAttr = new DeniedValuesAttribute(0, 1, 1, 2, 3, 5, 8, 13);
+ yield return new TestCase(denyAttr, -1);
+ yield return new TestCase(denyAttr, 4);
+ yield return new TestCase(denyAttr, 7);
+ yield return new TestCase(denyAttr, 10);
+ yield return new TestCase(denyAttr, "mango");
+ yield return new TestCase(denyAttr, false);
+
+ denyAttr = new DeniedValuesAttribute(-1, false, 3.1, "str", null, new object(), new byte[] { 0xff });
+ yield return new TestCase(denyAttr, 0);
+ yield return new TestCase(denyAttr, true);
+ yield return new TestCase(denyAttr, 3.11);
+ yield return new TestCase(denyAttr, "str'");
+ yield return new TestCase(denyAttr, new object()); // reference equality
+ yield return new TestCase(denyAttr, new byte[] { 0xff }); // reference equality
+ }
+
+ [Fact]
+ public void Ctor_NullParameter_ThrowsArgumentNullException()
+ {
+ Assert.Throws<ArgumentNullException>(() => new DeniedValuesAttribute(values: null));
+ }
+
+ [Theory]
+ [MemberData(nameof(Get_Ctor_ValuesPropertyReturnsTheSameArray))]
+ public void Ctor_ValuesPropertyReturnsTheSameArray(object?[] inputs)
+ {
+ var attr = new DeniedValuesAttribute(values: inputs);
+ Assert.Same(inputs, attr.Values);
+ }
+
+ public static IEnumerable<object[]> Get_Ctor_ValuesPropertyReturnsTheSameArray()
+ {
+ yield return new object?[][] { new object?[] { null } };
+ yield return new object?[][] { new object?[] { 1, 2, 3 } };
+ yield return new object?[][] { new object?[] { "apple", "banana", "mango", null } };
+ yield return new object?[][] { new object?[] { null, false, 0, -0d, 1.1 } };
+ }
+ }
+}
--- /dev/null
+// 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;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Xunit;
+
+namespace System.ComponentModel.DataAnnotations.Tests
+{
+ public class LengthAttributeTests : ValidationAttributeTestBase
+ {
+ protected override IEnumerable<TestCase> ValidValues()
+ {
+ yield return new TestCase(new LengthAttribute(10, 20), null);
+ yield return new TestCase(new LengthAttribute(0, 0), "");
+ yield return new TestCase(new LengthAttribute(12, 20), "OverMinLength");
+ yield return new TestCase(new LengthAttribute(16, 16), "EqualToMinLength");
+ yield return new TestCase(new LengthAttribute(12, 16), "EqualToMaxLength");
+
+ yield return new TestCase(new LengthAttribute(0, 0), new int[0]);
+ yield return new TestCase(new LengthAttribute(12, 16), new int[14]);
+ yield return new TestCase(new LengthAttribute(16, 20), new string[16]);
+ }
+
+ public static IEnumerable<object[]> ValidValues_ICollection()
+ {
+ yield return new object[] { new LengthAttribute(0, 0), new Collection<int>(new int[0]) };
+ yield return new object[] { new LengthAttribute(12, 16), new Collection<int>(new int[14]) };
+ yield return new object[] { new LengthAttribute(16, 20), new Collection<string>(new string[16]) };
+
+ yield return new object[] { new LengthAttribute(0, 2), new List<int>(new int[0]) };
+ yield return new object[] { new LengthAttribute(12, 16), new List<int>(new int[14]) };
+ yield return new object[] { new LengthAttribute(16, 16), new List<string>(new string[16]) };
+
+ //ICollection<T> but not ICollection
+ yield return new object[] { new LengthAttribute(0, 5), new HashSet<int>() };
+ yield return new object[] { new LengthAttribute(12, 14), new HashSet<int>(Enumerable.Range(1, 14)) };
+ yield return new object[] { new LengthAttribute(16, 20), new HashSet<string>(Enumerable.Range(1, 16).Select(i => i.ToString())) };
+
+ //ICollection but not ICollection<T>
+ yield return new object[] { new LengthAttribute(0, 1), new ArrayList(new int[0]) };
+ yield return new object[] { new LengthAttribute(12, 16), new ArrayList(new int[14]) };
+ yield return new object[] { new LengthAttribute(16, 16), new ArrayList(new string[16]) };
+
+ //Multi ICollection<T>
+ yield return new object[] { new LengthAttribute(0, 0), new MultiCollection() };
+ }
+
+ protected override IEnumerable<TestCase> InvalidValues()
+ {
+ yield return new TestCase(new LengthAttribute(15, 20), "UnderMinLength");
+ yield return new TestCase(new LengthAttribute(10, 12), "OverMaxLength");
+ yield return new TestCase(new LengthAttribute(15, 20), new byte[14]);
+ yield return new TestCase(new LengthAttribute(15, 20), new byte[21]);
+
+ yield return new TestCase(new LengthAttribute(12, 20), new int[3, 3]);
+ yield return new TestCase(new LengthAttribute(12, 20), new int[3, 7]);
+ }
+
+ public static IEnumerable<object[]> InvalidValues_ICollection()
+ {
+ yield return new object[] { new LengthAttribute(15, 20), new Collection<byte>(new byte[14]) };
+ yield return new object[] { new LengthAttribute(15, 20), new Collection<byte>(new byte[21]) };
+ yield return new object[] { new LengthAttribute(15, 20), new List<byte>(new byte[14]) };
+ yield return new object[] { new LengthAttribute(15, 20), new List<byte>(new byte[21]) };
+ }
+
+ [Theory]
+ [InlineData(-2, -3)]
+ [InlineData(21, 1)]
+ [InlineData(128, -1)]
+ [InlineData(-1, 12)]
+ [InlineData(0, 0)]
+ [InlineData(0, 10)]
+ public void Ctor(int minimumLength, int maximumLength)
+ {
+ var attr = new LengthAttribute(minimumLength, maximumLength);
+ Assert.Equal(minimumLength, attr.MinimumLength);
+ Assert.Equal(maximumLength, attr.MaximumLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(ValidValues_ICollection))]
+ public void Validate_ICollection_Valid(LengthAttribute attribute, object value)
+ {
+ attribute.Validate(value, new ValidationContext(new object()));
+ Assert.True(attribute.IsValid(value));
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidValues_ICollection))]
+ public void Validate_ICollection_Invalid(LengthAttribute attribute, object value)
+ {
+ Assert.Throws<ValidationException>(() => attribute.Validate(value, new ValidationContext(new object())));
+ Assert.False(attribute.IsValid(value));
+ }
+
+ [Theory]
+ [InlineData(-1, 0)]
+ [InlineData(0, -1)]
+ [InlineData(10, 5)]
+ public void GetValidationResult_InvalidLength_ThrowsInvalidOperationException(int minimumLength, int maximumLength)
+ {
+ var attribute = new LengthAttribute(minimumLength, maximumLength);
+ Assert.Throws<InvalidOperationException>(() => attribute.GetValidationResult("Rincewind", new ValidationContext(new object())));
+ }
+
+ [Fact]
+ public void GetValidationResult_ValueNotStringOrICollection_ThrowsInvalidCastException()
+ {
+ Assert.Throws<InvalidCastException>(() => new LengthAttribute(0, 0).GetValidationResult(new Random(), new ValidationContext(new object())));
+ }
+
+ [Fact]
+ public void GetValidationResult_ValueGenericIEnumerable_ThrowsInvalidCastException()
+ {
+ Assert.Throws<InvalidCastException>(() => new LengthAttribute(0, 0).GetValidationResult(new GenericIEnumerableClass(), new ValidationContext(new object())));
+ }
+ }
+}
using System.Collections.Generic;
using System.Tests;
-using Microsoft.DotNet.RemoteExecutor;
using Xunit;
namespace System.ComponentModel.DataAnnotations.Tests
yield return new TestCase(intRange, 3);
yield return new TestCase(new RangeAttribute(1, 1), 1);
+ intRange = new RangeAttribute(0, 10) { MinimumIsExclusive = true };
+ yield return new TestCase(intRange, 1);
+ yield return new TestCase(intRange, 2);
+ yield return new TestCase(intRange, 9);
+ yield return new TestCase(intRange, 10);
+
+ intRange = new RangeAttribute(0, 10) { MaximumIsExclusive = true };
+ yield return new TestCase(intRange, 0);
+ yield return new TestCase(intRange, 1);
+ yield return new TestCase(intRange, 9);
+
+ intRange = new RangeAttribute(0, 10) { MinimumIsExclusive = true, MaximumIsExclusive = true };
+ yield return new TestCase(intRange, 1);
+ yield return new TestCase(intRange, 2);
+ yield return new TestCase(intRange, 8);
+ yield return new TestCase(intRange, 9);
+
RangeAttribute doubleRange = new RangeAttribute(1.0, 3.0);
yield return new TestCase(doubleRange, null);
yield return new TestCase(doubleRange, string.Empty);
yield return new TestCase(doubleRange, 3.0);
yield return new TestCase(new RangeAttribute(1.0, 1.0), 1);
+ doubleRange = new RangeAttribute(0d, 1d) { MinimumIsExclusive = true };
+ yield return new TestCase(doubleRange, double.Epsilon);
+ yield return new TestCase(doubleRange, 1e-100);
+ yield return new TestCase(doubleRange, 0.00000001);
+ yield return new TestCase(doubleRange, 0.99999999);
+ yield return new TestCase(doubleRange, 1d);
+
+ doubleRange = new RangeAttribute(0d, 1d) { MaximumIsExclusive = true };
+ yield return new TestCase(doubleRange, -0d);
+ yield return new TestCase(doubleRange, 0d);
+ yield return new TestCase(doubleRange, double.Epsilon);
+ yield return new TestCase(doubleRange, 1e-100);
+ yield return new TestCase(doubleRange, 0.00000001);
+ yield return new TestCase(doubleRange, 0.99999999);
+
+ doubleRange = new RangeAttribute(0d, 1d) { MinimumIsExclusive = true, MaximumIsExclusive = true };
+ yield return new TestCase(doubleRange, double.Epsilon);
+ yield return new TestCase(doubleRange, 1e-100);
+ yield return new TestCase(doubleRange, 0.00000001);
+ yield return new TestCase(doubleRange, 0.99999999);
+
RangeAttribute stringIntRange = new RangeAttribute(typeof(int), "1", "3");
yield return new TestCase(stringIntRange, null);
yield return new TestCase(stringIntRange, string.Empty);
// Implements IConvertible (throws NotSupportedException - is caught)
yield return new TestCase(intRange, new IConvertibleImplementor() { IntThrow = new NotSupportedException() });
+ intRange = new RangeAttribute(0, 10) { MinimumIsExclusive = true };
+ yield return new TestCase(intRange, -1);
+ yield return new TestCase(intRange, 0);
+ yield return new TestCase(intRange, 11);
+
+ intRange = new RangeAttribute(0, 10) { MaximumIsExclusive = true };
+ yield return new TestCase(intRange, -1);
+ yield return new TestCase(intRange, 10);
+ yield return new TestCase(intRange, 11);
+
+ intRange = new RangeAttribute(0, 10) { MinimumIsExclusive = true, MaximumIsExclusive = true };
+ yield return new TestCase(intRange, -1);
+ yield return new TestCase(intRange, 0);
+ yield return new TestCase(intRange, 10);
+ yield return new TestCase(intRange, 11);
+
RangeAttribute doubleRange = new RangeAttribute(1.0, 3.0);
yield return new TestCase(doubleRange, 0.9999999);
yield return new TestCase(doubleRange, 3.0000001);
// Implements IConvertible (throws NotSupportedException - is caught)
yield return new TestCase(doubleRange, new IConvertibleImplementor() { DoubleThrow = new NotSupportedException() });
+ doubleRange = new RangeAttribute(0d, 1d) { MinimumIsExclusive = true };
+ yield return new TestCase(doubleRange, -0.1);
+ yield return new TestCase(doubleRange, -0d);
+ yield return new TestCase(doubleRange, 0d);
+ yield return new TestCase(doubleRange, 1.00000001);
+
+ doubleRange = new RangeAttribute(0d, 1d) { MaximumIsExclusive = true };
+ yield return new TestCase(doubleRange, -0.1);
+ yield return new TestCase(doubleRange, 1d);
+ yield return new TestCase(doubleRange, 1.00000001);
+
+ doubleRange = new RangeAttribute(0d, 1d) { MinimumIsExclusive = true, MaximumIsExclusive = true };
+ yield return new TestCase(doubleRange, -0.1);
+ yield return new TestCase(doubleRange, -0d);
+ yield return new TestCase(doubleRange, 0d);
+ yield return new TestCase(doubleRange, 1d);
+ yield return new TestCase(doubleRange, 1.00000001);
+
RangeAttribute stringIntRange = new RangeAttribute(typeof(int), "1", "3");
yield return new TestCase(stringIntRange, 0);
yield return new TestCase(stringIntRange, "0");
}
[Theory]
+ [MemberData(nameof(GetRangeAttributeConstructorResults))]
+ public static void ExclusiveBoundProperties_DefaultToFalse(RangeAttribute attribute)
+ {
+ Assert.False(attribute.MinimumIsExclusive);
+ Assert.False(attribute.MaximumIsExclusive);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetRangeAttributeConstructorResults))]
+ public static void ExclusiveBoundProperties_CanBeSet(RangeAttribute attribute)
+ {
+ attribute.MinimumIsExclusive = true;
+ Assert.True(attribute.MinimumIsExclusive);
+
+ attribute.MaximumIsExclusive = true;
+ Assert.True(attribute.MaximumIsExclusive);
+ }
+
+ public static IEnumerable<object[]> GetRangeAttributeConstructorResults()
+ {
+ yield return new[] { new RangeAttribute(0, 1) };
+ yield return new[] { new RangeAttribute(0d, 1d) };
+ yield return new[] { new RangeAttribute(typeof(double), "0.0", "0.1") };
+ }
+
+ [Theory]
[InlineData(null)]
[InlineData(typeof(object))]
public static void Validate_InvalidOperandType_ThrowsInvalidOperationException(Type type)
Assert.Throws<InvalidOperationException>(() => attribute.Validate("Any", new ValidationContext(new object())));
}
+
+ [Theory]
+ [MemberData(nameof(GetRangeAttributesWithExclusiveEqualBounds))]
+ public static void Validate_ExclusiveEqualBounds_ThrowsInvalidOperationException(RangeAttribute attribute)
+ {
+ // sanity check
+ Assert.Equal(attribute.Minimum, attribute.Maximum);
+ Assert.True(attribute.MinimumIsExclusive || attribute.MaximumIsExclusive);
+ // Validate SUT
+ Assert.Throws<InvalidOperationException>(() => attribute.Validate(attribute.Minimum, new ValidationContext(new object())));
+ }
+
+ public static IEnumerable<object[]> GetRangeAttributesWithExclusiveEqualBounds()
+ {
+ yield return new[] { new RangeAttribute(0, 0) { MinimumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(0, 0) { MaximumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(0, 0) { MinimumIsExclusive = true, MaximumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(1.1, 1.1) { MinimumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(1.1, 1.1) { MaximumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(1.1, 1.1) { MinimumIsExclusive = true, MaximumIsExclusive = true } };
+ yield return new[] { new RangeAttribute(typeof(double), "0.0", "0.0") { MinimumIsExclusive = true, ParseLimitsInInvariantCulture = true } };
+ yield return new[] { new RangeAttribute(typeof(double), "0.0", "0.0") { MaximumIsExclusive = true, ParseLimitsInInvariantCulture = true } };
+ yield return new[] { new RangeAttribute(typeof(double), "0.0", "0.0") { MinimumIsExclusive = true, MaximumIsExclusive = true, ParseLimitsInInvariantCulture = true, } };
+ }
+
[Theory]
[InlineData(null, "3")]
[InlineData("3", null)]
using System.ComponentModel.DataAnnotations;
using Xunit;
-namespace System.ComponentModel.Annotations.Tests.System.ComponentModel.DataAnnotations
+namespace System.ComponentModel.Annotations.Tests
{
public sealed partial class RegularExpressionAttributeTests
{
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
+using System.Collections.Immutable;
using Xunit;
namespace System.ComponentModel.DataAnnotations.Tests
yield return new TestCase(new RequiredAttribute() { AllowEmptyStrings = true }, string.Empty);
yield return new TestCase(new RequiredAttribute() { AllowEmptyStrings = true }, " \t \r \n ");
yield return new TestCase(new RequiredAttribute(), new object());
+
+ // default value types with DisallowAllDefaultValues turned off
+ var requiredAttribute = new RequiredAttribute();
+ yield return new TestCase(requiredAttribute, false);
+ yield return new TestCase(requiredAttribute, 0);
+ yield return new TestCase(requiredAttribute, 0d);
+ yield return new TestCase(requiredAttribute, default(TimeSpan));
+ yield return new TestCase(requiredAttribute, default(DateTime));
+ yield return new TestCase(requiredAttribute, default(Guid));
+
+ // non-default value types with DisallowAllDefaultValues turned on
+ requiredAttribute = new RequiredAttribute { DisallowAllDefaultValues = true };
+ yield return new TestCase(requiredAttribute, true);
+ yield return new TestCase(requiredAttribute, 1);
+ yield return new TestCase(requiredAttribute, 0.1);
+ yield return new TestCase(requiredAttribute, TimeSpan.MaxValue);
+ yield return new TestCase(requiredAttribute, DateTime.MaxValue);
+ yield return new TestCase(requiredAttribute, Guid.Parse("c3436566-4083-4bbe-8b56-f9c278162c4b"));
+
+ // reference types with DisallowAllDefaultValues turned on
+ requiredAttribute = new RequiredAttribute { DisallowAllDefaultValues = true };
+ yield return new TestCase(requiredAttribute, "SomeString");
+ yield return new TestCase(requiredAttribute, new object());
+
+ // reference types with DisallowAllDefaultValues and AllowEmptyStrings turned on
+ requiredAttribute = new RequiredAttribute { DisallowAllDefaultValues = true, AllowEmptyStrings = true };
+ yield return new TestCase(requiredAttribute, "SomeString");
+ yield return new TestCase(requiredAttribute, string.Empty);
+ yield return new TestCase(requiredAttribute, new object());
}
protected override IEnumerable<TestCase> InvalidValues()
yield return new TestCase(new RequiredAttribute(), null);
yield return new TestCase(new RequiredAttribute() { AllowEmptyStrings = false }, string.Empty);
yield return new TestCase(new RequiredAttribute() { AllowEmptyStrings = false }, " \t \r \n ");
+
+ // default values with DisallowAllDefaultValues turned on
+ var requiredAttribute = new RequiredAttribute { DisallowAllDefaultValues = true };
+ yield return new TestCase(requiredAttribute, null);
+ yield return new TestCase(requiredAttribute, false);
+ yield return new TestCase(requiredAttribute, 0);
+ yield return new TestCase(requiredAttribute, 0d);
+ yield return new TestCase(requiredAttribute, default(TimeSpan));
+ yield return new TestCase(requiredAttribute, default(DateTime));
+ yield return new TestCase(requiredAttribute, default(Guid));
+ yield return new TestCase(requiredAttribute, default(StructWithTrivialEquality));
+ // Structs that are not default but *equal* default should also fail validation.
+ yield return new TestCase(requiredAttribute, new StructWithTrivialEquality { Value = 42 });
+
+ // default value properties with DisallowDefaultValues turned on
+ requiredAttribute = new RequiredAttribute { DisallowAllDefaultValues = true };
+ yield return new TestCase(requiredAttribute, null, CreatePropertyContext<object?>());
+ yield return new TestCase(requiredAttribute, null, CreatePropertyContext<int?>());
+ yield return new TestCase(requiredAttribute, false, CreatePropertyContext<bool>());
+ yield return new TestCase(requiredAttribute, 0, CreatePropertyContext<int>());
+ yield return new TestCase(requiredAttribute, 0d, CreatePropertyContext<double>());
+ yield return new TestCase(requiredAttribute, default(TimeSpan), CreatePropertyContext<TimeSpan>());
+ yield return new TestCase(requiredAttribute, default(DateTime), CreatePropertyContext<DateTime>());
+ yield return new TestCase(requiredAttribute, default(Guid), CreatePropertyContext<Guid>());
+ yield return new TestCase(requiredAttribute, default(ImmutableArray<int>), CreatePropertyContext<ImmutableArray<int>>());
+ yield return new TestCase(requiredAttribute, default(StructWithTrivialEquality), CreatePropertyContext<StructWithTrivialEquality>());
+ // Structs that are not default but *equal* default should also fail validation.
+ yield return new TestCase(requiredAttribute, new StructWithTrivialEquality { Value = 42 }, CreatePropertyContext<StructWithTrivialEquality>());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetNonNullDefaultValues))]
+ public void DefaultValueTypes_OnPolymorphicProperties_SucceedValidation(object defaultValue)
+ {
+ var attribute = new RequiredAttribute { DisallowAllDefaultValues = true };
+ Assert.False(attribute.IsValid(defaultValue)); // Fails validation when no contexts present
+
+ // Polymorphic contexts should succeed validation
+ var polymorphicContext = CreatePropertyContext<object>();
+ attribute.Validate(defaultValue, polymorphicContext);
+ Assert.Equal(ValidationResult.Success, attribute.GetValidationResult(defaultValue, polymorphicContext));
+ }
+
+ public static IEnumerable<object[]> GetNonNullDefaultValues()
+ {
+ // default value types on polymorphic properties with DisallowDefaultValues turned on
+
+ yield return new object[] { false };
+ yield return new object[] { 0 };
+ yield return new object[] { 0d };
+ yield return new object[] { default(TimeSpan) };
+ yield return new object[] { default(DateTime) };
+ yield return new object[] { default(Guid) };
+ yield return new object[] { default(ImmutableArray<int>) };
+ yield return new object[] { default(StructWithTrivialEquality) };
+ yield return new object[] { new StructWithTrivialEquality { Value = 42 } };
}
[Fact]
- public static void AllowEmptyStrings_GetSet_ReturnsExpectected()
+ public void AllowEmptyStrings_GetSet_ReturnsExpectected()
{
var attribute = new RequiredAttribute();
Assert.False(attribute.AllowEmptyStrings);
attribute.AllowEmptyStrings = false;
Assert.False(attribute.AllowEmptyStrings);
}
+
+ [Fact]
+ public void DisallowAllowAllDefaultValues_GetSet_ReturnsExpectected()
+ {
+ var attribute = new RequiredAttribute();
+ Assert.False(attribute.DisallowAllDefaultValues);
+ attribute.DisallowAllDefaultValues = true;
+ Assert.True(attribute.DisallowAllDefaultValues);
+ attribute.DisallowAllDefaultValues = false;
+ Assert.False(attribute.DisallowAllDefaultValues);
+ }
+
+ private static ValidationContext CreatePropertyContext<T>()
+ => new ValidationContext(new GenericPoco<T>()) { MemberName = nameof(GenericPoco<T>.Value) };
+
+ public class GenericPoco<T>
+ {
+ public T Value { get; set; }
+ }
+
+ /// <summary>
+ /// Defines a struct where all values are equal.
+ /// </summary>
+ public readonly struct StructWithTrivialEquality : IEquatable<StructWithTrivialEquality>
+ {
+ public int Value { get; init; }
+
+ public bool Equals(StructWithTrivialEquality _) => true;
+ public override bool Equals(object other) => other is StructWithTrivialEquality;
+ public override int GetHashCode() => 0;
+ }
}
}