Add new System.ComponentModel.DataAnnotations features (#82311)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Thu, 23 Feb 2023 18:59:11 +0000 (18:59 +0000)
committerGitHub <noreply@github.com>
Thu, 23 Feb 2023 18:59:11 +0000 (18:59 +0000)
* 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

22 files changed:
src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs
src/libraries/System.ComponentModel.Annotations/src/Resources/Strings.resx
src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AllowedValuesAttribute.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Base64StringAttribute.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/DeniedValuesAttribute.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/LengthAttribute.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MaxLengthAttribute.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MinLengthAttribute.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RangeAttribute.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RequiredAttribute.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs
src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs
src/libraries/System.ComponentModel.Annotations/tests/System.ComponentModel.Annotations.Tests.csproj
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/AllowedValuesAttributeTests.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/Base64StringAttributeTests.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/DeniedValuesAttributeTests.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/LengthAttributeTests.cs [new file with mode: 0644]
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RangeAttributeTests.cs
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RegularExpressionAttributeTests.Core.cs
src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RequiredAttributeTests.cs

index c56ec48..8658dce 100644 (file)
@@ -6,6 +6,14 @@
 
 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) { }
@@ -24,6 +32,12 @@ namespace System.ComponentModel.DataAnnotations
         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
     {
@@ -87,6 +101,14 @@ namespace System.ComponentModel.DataAnnotations
         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
     {
@@ -183,6 +205,16 @@ namespace System.ComponentModel.DataAnnotations
     {
         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
     {
@@ -225,7 +257,9 @@ namespace System.ComponentModel.DataAnnotations
         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 { } }
@@ -247,6 +281,7 @@ namespace System.ComponentModel.DataAnnotations
     {
         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)]
index e20eea0..0cac133 100644 (file)
@@ -57,6 +57,9 @@
   <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>
@@ -66,6 +69,9 @@
   <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>
index 15afcbd..7f273f3 100644 (file)
@@ -8,15 +8,18 @@
     <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" />
@@ -27,6 +30,7 @@
     <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" />
@@ -55,8 +59,7 @@
     <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" />
diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AllowedValuesAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AllowedValuesAttribute.cs
new file mode 100644 (file)
index 0000000..1e3306e
--- /dev/null
@@ -0,0 +1,57 @@
+// 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;
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Base64StringAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Base64StringAttribute.cs
new file mode 100644 (file)
index 0000000..bb67765
--- /dev/null
@@ -0,0 +1,64 @@
+// 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;
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/DeniedValuesAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/DeniedValuesAttribute.cs
new file mode 100644 (file)
index 0000000..bfc9fc2
--- /dev/null
@@ -0,0 +1,57 @@
+// 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;
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/LengthAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/LengthAttribute.cs
new file mode 100644 (file)
index 0000000..6bb2ceb
--- /dev/null
@@ -0,0 +1,99 @@
+// 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);
+            }
+        }
+    }
+}
index 483d76c..8a66591 100644 (file)
@@ -74,15 +74,12 @@ namespace System.ComponentModel.DataAnnotations
             {
                 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()));
             }
index 8171f65..4550b0f 100644 (file)
@@ -57,15 +57,12 @@ namespace System.ComponentModel.DataAnnotations
             {
                 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()));
             }
index 69d0b13..7798e84 100644 (file)
@@ -19,11 +19,12 @@ namespace System.ComponentModel.DataAnnotations
         /// <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>
@@ -32,11 +33,12 @@ namespace System.ComponentModel.DataAnnotations
         /// <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>
@@ -51,11 +53,12 @@ namespace System.ComponentModel.DataAnnotations
             [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>
@@ -69,6 +72,16 @@ namespace System.ComponentModel.DataAnnotations
         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>
@@ -94,10 +107,15 @@ namespace System.ComponentModel.DataAnnotations
 
         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;
@@ -116,7 +134,7 @@ namespace System.ComponentModel.DataAnnotations
             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;
             }
@@ -142,7 +160,9 @@ namespace System.ComponentModel.DataAnnotations
 
             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>
@@ -234,5 +254,16 @@ namespace System.ComponentModel.DataAnnotations
             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,
+            };
+        }
     }
 }
index a5003f0..614db4e 100644 (file)
@@ -1,6 +1,10 @@
 // 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>
@@ -28,23 +32,76 @@ namespace System.ComponentModel.DataAnnotations
         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;
     }
 }
index ae9d1c8..b35a40a 100644 (file)
@@ -68,6 +68,14 @@ namespace System.ComponentModel.DataAnnotations
             _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
@@ -78,9 +86,9 @@ namespace System.ComponentModel.DataAnnotations
         /// 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;
@@ -88,6 +96,18 @@ namespace System.ComponentModel.DataAnnotations
             }
         }
 
+        /// <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
@@ -275,6 +295,15 @@ namespace System.ComponentModel.DataAnnotations
             _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
@@ -366,18 +395,10 @@ namespace System.ComponentModel.DataAnnotations
                     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>
index a0b8d71..c08d3ae 100644 (file)
@@ -170,6 +170,27 @@ namespace System.ComponentModel.DataAnnotations
         /// </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
index 4ef742d..3913a1d 100644 (file)
@@ -94,7 +94,7 @@ namespace System.ComponentModel.DataAnnotations
         [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.
@@ -522,6 +522,7 @@ namespace System.ComponentModel.DataAnnotations
             {
                 var context = CreateValidationContext(instance, validationContext);
                 context.MemberName = property.Name;
+                context.MemberType = property.PropertyType;
 
                 if (_store.GetPropertyValidationAttributes(context).Any())
                 {
index 773170d..a741559 100644 (file)
@@ -1,12 +1,15 @@
 <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" />
@@ -44,8 +47,6 @@
     <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>
diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/AllowedValuesAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/AllowedValuesAttributeTests.cs
new file mode 100644 (file)
index 0000000..73882bc
--- /dev/null
@@ -0,0 +1,82 @@
+// 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 } };
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/Base64StringAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/Base64StringAttributeTests.cs
new file mode 100644 (file)
index 0000000..31d0bdf
--- /dev/null
@@ -0,0 +1,62 @@
+// 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===");
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/DeniedValuesAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/DeniedValuesAttributeTests.cs
new file mode 100644 (file)
index 0000000..b7088d2
--- /dev/null
@@ -0,0 +1,82 @@
+// 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 } };
+        }
+    }
+}
diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/LengthAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/LengthAttributeTests.cs
new file mode 100644 (file)
index 0000000..a3d5a6d
--- /dev/null
@@ -0,0 +1,122 @@
+// 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())));
+        }
+    }
+}
index 964afae..1ef8522 100644 (file)
@@ -3,7 +3,6 @@
 
 using System.Collections.Generic;
 using System.Tests;
-using Microsoft.DotNet.RemoteExecutor;
 using Xunit;
 
 namespace System.ComponentModel.DataAnnotations.Tests
@@ -20,6 +19,23 @@ 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);
@@ -28,6 +44,27 @@ namespace System.ComponentModel.DataAnnotations.Tests
             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);
@@ -59,6 +96,22 @@ namespace System.ComponentModel.DataAnnotations.Tests
             // 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);
@@ -67,6 +120,24 @@ namespace System.ComponentModel.DataAnnotations.Tests
             // 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");
@@ -825,6 +896,32 @@ namespace System.ComponentModel.DataAnnotations.Tests
         }
 
         [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)
@@ -852,6 +949,31 @@ namespace System.ComponentModel.DataAnnotations.Tests
             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)]
index f06b78c..7abcdd0 100644 (file)
@@ -2,6 +2,7 @@
 // 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
@@ -14,6 +15,35 @@ 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()
@@ -21,10 +51,66 @@ namespace System.ComponentModel.DataAnnotations.Tests
             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);
@@ -33,5 +119,36 @@ namespace System.ComponentModel.DataAnnotations.Tests
             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;
+        }
     }
 }