From 07f5fca53d671d9999f8db9fc6add526551fecb1 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 23 Feb 2023 18:59:11 +0000 Subject: [PATCH] Add new System.ComponentModel.DataAnnotations features (#82311) * 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 --- .../ref/System.ComponentModel.Annotations.cs | 35 +++++ .../src/Resources/Strings.resx | 30 +++++ .../System.ComponentModel.Annotations.csproj | 7 +- .../DataAnnotations/AllowedValuesAttribute.cs | 57 ++++++++ .../DataAnnotations/Base64StringAttribute.cs | 64 +++++++++ .../DataAnnotations/DeniedValuesAttribute.cs | 57 ++++++++ .../DataAnnotations/LengthAttribute.cs | 99 ++++++++++++++ .../DataAnnotations/MaxLengthAttribute.cs | 7 +- .../DataAnnotations/MinLengthAttribute.cs | 7 +- .../DataAnnotations/RangeAttribute.cs | 43 +++++- .../DataAnnotations/RequiredAttribute.cs | 67 +++++++++- .../DataAnnotations/ValidationAttribute.cs | 47 +++++-- .../DataAnnotations/ValidationContext.cs | 21 +++ .../DataAnnotations/Validator.cs | 3 +- ...em.ComponentModel.Annotations.Tests.csproj | 9 +- .../AllowedValuesAttributeTests.cs | 82 ++++++++++++ .../Base64StringAttributeTests.cs | 62 +++++++++ .../DeniedValuesAttributeTests.cs | 82 ++++++++++++ .../DataAnnotations/LengthAttributeTests.cs | 122 +++++++++++++++++ .../DataAnnotations/RangeAttributeTests.cs | 124 +++++++++++++++++- .../RegularExpressionAttributeTests.Core.cs | 2 +- .../DataAnnotations/RequiredAttributeTests.cs | 119 ++++++++++++++++- 22 files changed, 1102 insertions(+), 44 deletions(-) create mode 100644 src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AllowedValuesAttribute.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Base64StringAttribute.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/DeniedValuesAttribute.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/LengthAttribute.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/AllowedValuesAttributeTests.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/Base64StringAttributeTests.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/DeniedValuesAttributeTests.cs create mode 100644 src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/LengthAttributeTests.cs diff --git a/src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs b/src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs index c56ec48cdc2e73..8658dcecfd6b76 100644 --- a/src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs +++ b/src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs @@ -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 @@ public AssociationAttribute(string name, string thisKey, string otherKey) { } public string ThisKey { get { throw null; } } public System.Collections.Generic.IEnumerable 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 @@ public DataTypeAttribute(string customDataType) { } 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 @@ public sealed partial class KeyAttribute : 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 { @@ -225,7 +257,9 @@ public RangeAttribute(int minimum, int maximum) { } 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 @@ public partial class RequiredAttribute : System.ComponentModel.DataAnnotations.V { 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)] diff --git a/src/libraries/System.ComponentModel.Annotations/src/Resources/Strings.resx b/src/libraries/System.ComponentModel.Annotations/src/Resources/Strings.resx index e20eea0c0669c5..0cac133cb27bf4 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/Resources/Strings.resx +++ b/src/libraries/System.ComponentModel.Annotations/src/Resources/Strings.resx @@ -57,6 +57,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The {0} field does not equal any of the values specified in AllowedValuesAttribute. + The argument '{0}' cannot be null, empty or contain only whitespace. @@ -66,6 +69,9 @@ The type '{0}' does not contain a public property named '{1}'. + + The {0} field is not a valid Base64 encoding. + The property {0}.{1} could not be found. @@ -105,6 +111,9 @@ The custom DataType string cannot be null or empty. + + The {0} field equals one of the values specified in DeniedValuesAttribute. + The {0} property has not been set. Use the {1} method to get the value. @@ -138,6 +147,15 @@ The field {0} must be a string or array type with a minimum length of '{1}'. + + LengthAttribute must have a MinimumLength value that is zero or greater. + + + LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength. + + + The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'. + The field of type {0} must be a string, array or ICollection type. @@ -150,6 +168,9 @@ The maximum value '{0}' must be greater than or equal to the minimum value '{1}'. + + Cannot use exclusive bounds when the maximum value is equal to the minimum value. + The minimum and maximum values must be set. @@ -159,6 +180,15 @@ The field {0} must be between {1} and {2}. + + The field {0} must be between {1} exclusive and {2}. + + + The field {0} must be between {1} and {2} exclusive. + + + The field {0} must be between {1} exclusive and {2} exclusive. + The field {0} must match the regular expression '{1}'. diff --git a/src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj b/src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj index 15afcbd56dcc51..7f273f3dc5b1f6 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj +++ b/src/libraries/System.ComponentModel.Annotations/src/System.ComponentModel.Annotations.csproj @@ -8,15 +8,18 @@ true + + + @@ -27,6 +30,7 @@ + @@ -55,8 +59,7 @@ - + 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 index 00000000000000..1e3306eb67d224 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/AllowedValuesAttribute.cs @@ -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 +{ + /// + /// Specifies a list of values that should be allowed in a property. + /// + [CLSCompliant(false)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class AllowedValuesAttribute : ValidationAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A list of values that the validated value should be equal to. + /// + public AllowedValuesAttribute(params object?[] values) + { + ArgumentNullException.ThrowIfNull(values); + Values = values; + DefaultErrorMessage = SR.AllowedValuesAttribute_Invalid; + } + + /// + /// Gets the list of values allowed by this attribute. + /// + public object?[] Values { get; } + + /// + /// Determines whether a specified object is valid. (Overrides ) + /// + /// The object to validate. + /// + /// if any of the are equal to , + /// otherwise + /// + /// + /// This method can return if the is , + /// provided that is also specified in one of the . + /// + 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 index 00000000000000..bb67765914a516 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Base64StringAttribute.cs @@ -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 +{ + /// + /// Specifies that a data field value is a well-formed Base64 string. + /// + /// + /// Recognition of valid Base64 is delegated to the class, + /// using the method. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class Base64StringAttribute : ValidationAttribute + { + /// + /// Initializes a new instance of the class. + /// + public Base64StringAttribute() + { + // Set DefaultErrorMessage not ErrorMessage, allowing user to set + // ErrorMessageResourceType and ErrorMessageResourceName to use localized messages. + DefaultErrorMessage = SR.Base64StringAttribute_Invalid; + } + + /// + /// Determines whether a specified object is valid. (Overrides ) + /// + /// The object to validate. + /// + /// if is or is a valid Base64 string, + /// otherwise + /// + public override bool IsValid(object? value) + { + if (value is null) + { + return true; + } + + if (value is not string valueAsString) + { + return false; + } + + byte[]? rentedBuffer = null; + Span destinationBuffer = valueAsString.Length < 256 + ? stackalloc byte[256] + : rentedBuffer = ArrayPool.Shared.Rent(valueAsString.Length); + + bool result = Convert.TryFromBase64String(valueAsString, destinationBuffer, out int bytesWritten); + + if (rentedBuffer != null) + { + destinationBuffer.Slice(0, bytesWritten).Clear(); + ArrayPool.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 index 00000000000000..bfc9fc2023e950 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/DeniedValuesAttribute.cs @@ -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 +{ + /// + /// Specifies a list of values that should not be allowed in a property. + /// + [CLSCompliant(false)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class DeniedValuesAttribute : ValidationAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A list of values that the validated value should not be equal to. + /// + public DeniedValuesAttribute(params object?[] values) + { + ArgumentNullException.ThrowIfNull(values); + Values = values; + DefaultErrorMessage = SR.DeniedValuesAttribute_Invalid; + } + + /// + /// Gets the list of values denied by this attribute. + /// + public object?[] Values { get; } + + /// + /// Determines whether a specified object is valid. (Overrides ) + /// + /// The object to validate. + /// + /// if none of the are equal to , + /// otherwise . + /// + /// + /// This method can return if the is , + /// provided that is not specified in any of the . + /// + 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 index 00000000000000..6bb2ceb4a2ba09 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/LengthAttribute.cs @@ -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 +{ + /// + /// Specifies the minimum and maximum length of collection/string data allowed in a property. + /// + [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; + } + + /// + /// Gets the minimum allowable length of the collection/string data. + /// + public int MinimumLength { get; } + + /// + /// Gets the maximum allowable length of the collection/string data. + /// + public int MaximumLength { get; } + + /// + /// Determines whether a specified object is valid. (Overrides ) + /// + /// + /// This method returns true if the is null. + /// It is assumed the is used if the value may not be null. + /// + /// The object to validate. + /// + /// true if the value is null or its length is between the specified minimum length and maximum length, otherwise + /// false + /// + /// + /// is less than zero or is less than . + /// + [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); + } + + /// + /// Applies formatting to a specified error message. (Overrides ) + /// + /// The name to include in the formatted string. + /// A localized string to describe the minimum acceptable length. + 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); + + /// + /// Checks that Length has a legal value. + /// + /// Length is less than zero. + private void EnsureLegalLengths() + { + if (MinimumLength < 0) + { + throw new InvalidOperationException(SR.LengthAttribute_InvalidMinLength); + } + + if (MaximumLength < MinimumLength) + { + throw new InvalidOperationException(SR.LengthAttribute_InvalidMaxLength); + } + } + } +} diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MaxLengthAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MaxLengthAttribute.cs index 483d76c4872df1..8a66591bde8abb 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MaxLengthAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MaxLengthAttribute.cs @@ -74,15 +74,12 @@ public override bool IsValid(object? value) { 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())); } diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MinLengthAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MinLengthAttribute.cs index 8171f650eae89c..4550b0f28aba30 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MinLengthAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/MinLengthAttribute.cs @@ -57,15 +57,12 @@ public override bool IsValid(object? value) { 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())); } diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RangeAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RangeAttribute.cs index 69d0b136c5637e..7798e848a2efa0 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RangeAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RangeAttribute.cs @@ -19,11 +19,12 @@ public class RangeAttribute : ValidationAttribute /// The minimum value, inclusive /// The maximum value, inclusive public RangeAttribute(int minimum, int maximum) - : base(() => SR.RangeAttribute_ValidationError) + : base(populateErrorMessageResourceAccessor: false) { Minimum = minimum; Maximum = maximum; OperandType = typeof(int); + ErrorMessageResourceAccessor = GetValidationErrorMessage; } /// @@ -32,11 +33,12 @@ public RangeAttribute(int minimum, int maximum) /// The minimum value, inclusive /// The maximum value, inclusive public RangeAttribute(double minimum, double maximum) - : base(() => SR.RangeAttribute_ValidationError) + : base(populateErrorMessageResourceAccessor: false) { Minimum = minimum; Maximum = maximum; OperandType = typeof(double); + ErrorMessageResourceAccessor = GetValidationErrorMessage; } /// @@ -51,11 +53,12 @@ public RangeAttribute( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, string minimum, string maximum) - : base(() => SR.RangeAttribute_ValidationError) + : base(populateErrorMessageResourceAccessor: false) { OperandType = type; Minimum = minimum; Maximum = maximum; + ErrorMessageResourceAccessor = GetValidationErrorMessage; } /// @@ -68,6 +71,16 @@ public RangeAttribute( /// public object Maximum { get; private set; } + /// + /// Specifies whether validation should fail for values that are equal to . + /// + public bool MinimumIsExclusive { get; set; } + + /// + /// Specifies whether validation should fail for values that are equal to . + /// + public bool MaximumIsExclusive { get; set; } + /// /// Gets the type of the and values (e.g. Int32, Double, or some custom /// type) @@ -94,10 +107,15 @@ public RangeAttribute( private void Initialize(IComparable minimum, IComparable maximum, Func 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 @@ public override bool IsValid(object? value) 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 @@ public override bool IsValid(object? value) 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); } /// @@ -234,5 +254,16 @@ private void SetupConversion() 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, + }; + } } } diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RequiredAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RequiredAttribute.cs index a5003f0d2d3188..614db4ed0c09f6 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RequiredAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/RequiredAttribute.cs @@ -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 { /// @@ -27,24 +31,77 @@ public RequiredAttribute() /// public bool AllowEmptyStrings { get; set; } + /// + /// Gets or sets a flag indicating whether the attribute should also disallow non-null default values. + /// + public bool DisallowAllDefaultValues { get; set; } + /// /// Override of /// /// The value to test /// - /// false if the is null or an empty string. If - /// - /// then false is returned only if is null. + /// Returns if the is null or an empty string. + /// If then is returned for empty strings. + /// If then is returned for values + /// that are equal to the of the declared type. /// public override bool IsValid(object? value) + => IsValidCore(value, validationContext: null); + + /// + 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; } } diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs index ae9d1c87be91dd..b35a40a0e00ba8 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttribute.cs @@ -68,6 +68,14 @@ protected ValidationAttribute(Func errorMessageAccessor) _errorMessageResourceAccessor = errorMessageAccessor; } + /// + /// Internal constructor used for delayed population of the error message delegate. + /// + private protected ValidationAttribute(bool populateErrorMessageResourceAccessor) + { + Debug.Assert(populateErrorMessageResourceAccessor is false, "Use the default constructor instead"); + } + #endregion #region Internal Properties @@ -78,9 +86,9 @@ protected ValidationAttribute(Func errorMessageAccessor) /// This property was added after the public contract for DataAnnotations was created. /// It is internal to avoid changing the DataAnnotations contract. /// - internal string? DefaultErrorMessage + private protected string? DefaultErrorMessage { - set + init { _defaultErrorMessage = value; _errorMessageResourceAccessor = null; @@ -88,6 +96,18 @@ internal string? DefaultErrorMessage } } + /// + /// Sets the delayed resource accessor in cases where we can't pass it directly to the base constructor. + /// + private protected Func 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 @@ private void SetResourceAccessorByPropertyLookup() _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 @@ public virtual bool IsValid(object? value) 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); } /// diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs index a0b8d71a6ca354..c08d3ae0294896 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationContext.cs @@ -170,6 +170,27 @@ public string DisplayName /// public IDictionary 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 diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs index 4ef742d93c85c1..3913a1df4c3fe7 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs @@ -94,7 +94,7 @@ public static bool TryValidateProperty(object? value, ValidationContext validati [RequiresUnreferencedCode(ValidationContext.InstanceTypeNotStaticallyDiscovered)] public static bool TryValidateObject( object instance, ValidationContext validationContext, ICollection? validationResults) => - TryValidateObject(instance, validationContext, validationResults, false /*validateAllProperties*/); + TryValidateObject(instance, validationContext, validationResults, validateAllProperties: false); /// /// Tests whether the given object instance is valid. @@ -522,6 +522,7 @@ private static List GetObjectPropertyValidationErrors(object in { var context = CreateValidationContext(instance, validationContext); context.MemberName = property.Name; + context.MemberType = property.PropertyType; if (_store.GetPropertyValidationAttributes(context).Any()) { diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System.ComponentModel.Annotations.Tests.csproj b/src/libraries/System.ComponentModel.Annotations/tests/System.ComponentModel.Annotations.Tests.csproj index 773170dd23181e..a7415591d315b1 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System.ComponentModel.Annotations.Tests.csproj +++ b/src/libraries/System.ComponentModel.Annotations/tests/System.ComponentModel.Annotations.Tests.csproj @@ -1,12 +1,15 @@ true - $(NetCoreAppCurrent);net48 + $(NetCoreAppCurrent) disable $(NoWarn);nullable + + + @@ -44,8 +47,6 @@ - - - + 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 index 00000000000000..73882bcbaef8d5 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/AllowedValuesAttributeTests.cs @@ -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 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 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(() => 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 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 index 00000000000000..31d0bdf10b6e0d --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/Base64StringAttributeTests.cs @@ -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 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 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 index 00000000000000..b7088d22c6f1fd --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/DeniedValuesAttributeTests.cs @@ -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 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 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(() => 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 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 index 00000000000000..a3d5a6d3a6dfa6 --- /dev/null +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/LengthAttributeTests.cs @@ -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 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 ValidValues_ICollection() + { + yield return new object[] { new LengthAttribute(0, 0), new Collection(new int[0]) }; + yield return new object[] { new LengthAttribute(12, 16), new Collection(new int[14]) }; + yield return new object[] { new LengthAttribute(16, 20), new Collection(new string[16]) }; + + yield return new object[] { new LengthAttribute(0, 2), new List(new int[0]) }; + yield return new object[] { new LengthAttribute(12, 16), new List(new int[14]) }; + yield return new object[] { new LengthAttribute(16, 16), new List(new string[16]) }; + + //ICollection but not ICollection + yield return new object[] { new LengthAttribute(0, 5), new HashSet() }; + yield return new object[] { new LengthAttribute(12, 14), new HashSet(Enumerable.Range(1, 14)) }; + yield return new object[] { new LengthAttribute(16, 20), new HashSet(Enumerable.Range(1, 16).Select(i => i.ToString())) }; + + //ICollection but not ICollection + 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 + yield return new object[] { new LengthAttribute(0, 0), new MultiCollection() }; + } + + protected override IEnumerable 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 InvalidValues_ICollection() + { + yield return new object[] { new LengthAttribute(15, 20), new Collection(new byte[14]) }; + yield return new object[] { new LengthAttribute(15, 20), new Collection(new byte[21]) }; + yield return new object[] { new LengthAttribute(15, 20), new List(new byte[14]) }; + yield return new object[] { new LengthAttribute(15, 20), new List(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(() => 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(() => attribute.GetValidationResult("Rincewind", new ValidationContext(new object()))); + } + + [Fact] + public void GetValidationResult_ValueNotStringOrICollection_ThrowsInvalidCastException() + { + Assert.Throws(() => new LengthAttribute(0, 0).GetValidationResult(new Random(), new ValidationContext(new object()))); + } + + [Fact] + public void GetValidationResult_ValueGenericIEnumerable_ThrowsInvalidCastException() + { + Assert.Throws(() => new LengthAttribute(0, 0).GetValidationResult(new GenericIEnumerableClass(), new ValidationContext(new object()))); + } + } +} diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RangeAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RangeAttributeTests.cs index 964afae32ae6a0..1ef8522f33d5b4 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RangeAttributeTests.cs +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RangeAttributeTests.cs @@ -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 @@ protected override IEnumerable ValidValues() 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 @@ protected override IEnumerable ValidValues() 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 @@ protected override IEnumerable InvalidValues() // 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 @@ protected override IEnumerable InvalidValues() // 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"); @@ -824,6 +895,32 @@ public static void Ctor_Type_String_String(Type type) Assert.Equal(type, attribute.OperandType); } + [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 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))] @@ -852,6 +949,31 @@ public static void Validate_MinimumGreaterThanMaximum_ThrowsInvalidOperationExce Assert.Throws(() => 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(() => attribute.Validate(attribute.Minimum, new ValidationContext(new object()))); + } + + public static IEnumerable 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)] diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RegularExpressionAttributeTests.Core.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RegularExpressionAttributeTests.Core.cs index 78bb2e5e028fd6..cd5490930038b2 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RegularExpressionAttributeTests.Core.cs +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RegularExpressionAttributeTests.Core.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using Xunit; -namespace System.ComponentModel.Annotations.Tests.System.ComponentModel.DataAnnotations +namespace System.ComponentModel.Annotations.Tests { public sealed partial class RegularExpressionAttributeTests { diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RequiredAttributeTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RequiredAttributeTests.cs index f06b78ccb4a14b..7abcdd0effa9cc 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RequiredAttributeTests.cs +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/RequiredAttributeTests.cs @@ -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 @@ protected override IEnumerable ValidValues() 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 InvalidValues() @@ -21,10 +51,66 @@ protected override IEnumerable 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()); + yield return new TestCase(requiredAttribute, null, CreatePropertyContext()); + yield return new TestCase(requiredAttribute, false, CreatePropertyContext()); + yield return new TestCase(requiredAttribute, 0, CreatePropertyContext()); + yield return new TestCase(requiredAttribute, 0d, CreatePropertyContext()); + yield return new TestCase(requiredAttribute, default(TimeSpan), CreatePropertyContext()); + yield return new TestCase(requiredAttribute, default(DateTime), CreatePropertyContext()); + yield return new TestCase(requiredAttribute, default(Guid), CreatePropertyContext()); + yield return new TestCase(requiredAttribute, default(ImmutableArray), CreatePropertyContext>()); + yield return new TestCase(requiredAttribute, default(StructWithTrivialEquality), CreatePropertyContext()); + // Structs that are not default but *equal* default should also fail validation. + yield return new TestCase(requiredAttribute, new StructWithTrivialEquality { Value = 42 }, CreatePropertyContext()); + } + + [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(); + attribute.Validate(defaultValue, polymorphicContext); + Assert.Equal(ValidationResult.Success, attribute.GetValidationResult(defaultValue, polymorphicContext)); + } + + public static IEnumerable 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) }; + 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 @@ public static void AllowEmptyStrings_GetSet_ReturnsExpectected() 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() + => new ValidationContext(new GenericPoco()) { MemberName = nameof(GenericPoco.Value) }; + + public class GenericPoco + { + public T Value { get; set; } + } + + /// + /// Defines a struct where all values are equal. + /// + public readonly struct StructWithTrivialEquality : IEquatable + { + public int Value { get; init; } + + public bool Equals(StructWithTrivialEquality _) => true; + public override bool Equals(object other) => other is StructWithTrivialEquality; + public override int GetHashCode() => 0; + } } }