Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Expand data validation attributes #77402

Closed
geeknoid opened this issue Oct 24, 2022 · 51 comments · Fixed by #82311
Closed

[API Proposal]: Expand data validation attributes #77402

geeknoid opened this issue Oct 24, 2022 · 51 comments · Fixed by #82311
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.ComponentModel.DataAnnotations partner-impact This issue impacts a partner who needs to be kept updated
Milestone

Comments

@geeknoid
Copy link
Member

geeknoid commented Oct 24, 2022

Background and motivation

Our project features hundreds of different option types against which we perform data validation operations. We've introduced a number of new data validation attributes which capture essential scenarios in our code. As these are general-purpose in nature, it would be great to add those to the core set of supported attributes.

API Proposal

namespace System.ComponentModel.Annotations;

public partial class RangeAttribute : ValidationAttribute
{
+    public bool IsLowerBoundExclusive { get; set; } = false;
+    public bool IsUpperBoundExclusive { get; set; } = false;
}

public partial class RequiredAttribute : ValidationAttribute
{
     // Fail validation for structs that equal the default value for the type
+    public bool DisallowDefaultValues { get; set; } = false;
}

// Validates that the specified string uses Base64 encoding
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class Base64StringAttribute : ValidationAttribute
+{
+}

// Specifies length ranges for string/IEnumerable
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class LengthAttribute : ValidationAttribute
+{
+    public LengthAttribute(int minLength = 0, int maxLength = -1);
+
+    public int MinLength { get; }
+    public int? MaxLength { get; }
+}

// Validation using allow and deny lists
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class AllowedValuesAttribute : ValidationAttribute
+{
+    public AllowedValuesAttribute (params object[] allowedValues);
+    public object[] AllowedValues { get; }
+}
+
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class DeniedValuesAttribute : ValidationAttribute
+{
+    public DeniedValuesAttribute(params object[] deniedValues);
+    public object[] DeniedValues { get; }
+}
Original API proposal
namespace System.ComponentModel.Annotations;

/// <summary>
/// Marks a property to be validated as <see href="https://en.wikipedia.org/wiki/Base64">Base64</see> string.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class Base64StringAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides exclusive boundary validation for <see cref="long"/> or <see cref="double"/> values.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ExclusiveRangeAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum value for the range.
    /// </summary>
    public object Minimum { get; }

    /// <summary>
    /// Gets the maximum value for the range.
    /// </summary>
    public object Maximum { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(int minimum, int maximum);

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(double minimum, double maximum);
}

/// <summary>
/// Specifies the minimum length of any <see cref="IEnumerable"/> or <see cref="string"/> objects.
/// </summary>
/// <remarks>
/// The standard <see cref="MinLengthAttribute" /> supports only non generic <see cref="Array"/> or <see cref="string"/> typed objects
/// on .Net Framework, while <see cref="ICollection{T}"/> type is supported only on .Net Core.
/// See issue here <see href="https://github.com/dotnet/runtime/issues/23288"/>.
/// This attribute aims to allow validation of all these objects in a consistent manner across target frameworks.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class LengthAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum allowed length of the collection or string.
    /// </summary>
    public int MinimumLength { get; }

    /// <summary>
    /// Gets the maximum allowed length of the collection or string.
    /// </summary>
    public int? MaximumLength { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the length validation should exclude the <see cref="MinimumLength"/> and <see cref="MaximumLength"/> values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength);

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    /// <param name="maximumLength">
    /// The maximum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength, int maximumLength);
}

/// <summary>
/// Provides a way of requiring a value that is not default(T) for a property of value type.
/// </summary>
/// <remarks>Only to be used with value type properties.</remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class NonDefaultValueRequiredAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides a way of not accepting specific values for a property/field/parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class RejectedValuesAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the values which are not accepted.
    /// </summary>
    public ICollection<object> RejectedValues { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RejectedValuesAttribute"/> class.
    /// </summary>
    /// <param name="rejectedValues">Values which should not be accepted.</param>
    public RejectedValuesAttribute(params object[] rejectedValues);
}

/// <summary>
/// Provides a way of accepting only specific values for a property/field/parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class AcceptedValuesAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the values which are accepted.
    /// </summary>
    public ICollection<object> AcceptedValues { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="AcceptedValuesAttribute"/> class.
    /// </summary>
    /// <param name="acceptedValues">Values which should be accepted.</param>
    public AcceptedValuesAttribute(params object[] acceptededValues);
}

/// <summary>
/// Provides boundary validation for <see cref="TimeSpan"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class TimeSpanAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the lower bound for time span.
    /// </summary>
    public TimeSpan Minimum { get; }

    /// <summary>
    /// Gets the upper bound for time span.
    /// </summary>
    public TimeSpan? Maximum { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the time span validation should exclude the minimum and maximum values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    public TimeSpanAttribute(int minMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    /// <param name="maxMs">Maximum in milliseconds.</param>
    public TimeSpanAttribute(int minMs, int maxMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    public TimeSpanAttribute(string min);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    /// <param name="max">Maximum represented as time span string.</param>
    public TimeSpanAttribute(string min, string max);
}

API Usage

class Options
{
    // require a valid base-64 string
    [Base64String]
    public string EncodedBinaryData { get; set; } = string.Empty;

    // a value between specific exclusive boundaries
    [ExclusiveRange(0, 1.0)]
    public double BetweenZeroAndOne { get; set; } = .5;

    // 1 to 5 items
    [Length(1, 5)]
    public int[] ArrayOfStuff { get; set; }

    // anything but these
    [RejectValues("Red")]
    public string Color { get; set; }

    // 10ms to 100ms
    [TimeSpan(10, 100)]
    public TimeSpan Timeout { get; set; }
}

Alternative Designs

No response

Risks

No response

@geeknoid geeknoid added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Oct 24, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 24, 2022
@stephentoub stephentoub added this to the 8.0.0 milestone Oct 24, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Oct 24, 2022
@stephentoub stephentoub added area-System.ComponentModel untriaged New issue has not been triaged by the area owner labels Oct 24, 2022
@ghost
Copy link

ghost commented Oct 24, 2022

Tagging subscribers to this area: @dotnet/area-system-componentmodel
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

Our project features hundreds of different option types against which we perform data validation operations. We've introduced a number of new data validation attributes which capture essential scenarios in our code. As these are general-purpose in nature, it would be great to add those to the core set of supported attributes.

API Proposal

namespace System.ComponentModel.Annotations;

/// <summary>
/// Marks a property to be validated as <see href="https://en.wikipedia.org/wiki/Base64">Base64</see> string.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class Base64StringAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides exclusive boundary validation for <see cref="long"/> or <see cref="double"/> values.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ExclusiveRangeAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum value for the range.
    /// </summary>
    public object Minimum { get; }

    /// <summary>
    /// Gets the maximum value for the range.
    /// </summary>
    public object Maximum { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(int minimum, int maximum);

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(double minimum, double maximum);
}

/// <summary>
/// Specifies the minimum length of any <see cref="IEnumerable"/> or <see cref="string"/> objects.
/// </summary>
/// <remarks>
/// The standard <see cref="MinLengthAttribute" /> supports only non generic <see cref="Array"/> or <see cref="string"/> typed objects
/// on .Net Framework, while <see cref="ICollection{T}"/> type is supported only on .Net Core.
/// See issue here <see href="https://github.com/dotnet/runtime/issues/23288"/>.
/// This attribute aims to allow validation of all these objects in a consistent manner across target frameworks.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class LengthAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum allowed length of the collection or string.
    /// </summary>
    public int MinimumLength { get; }

    /// <summary>
    /// Gets the maximum allowed length of the collection or string.
    /// </summary>
    public int? MaximumLength { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the length validation should exclude the <see cref="MinimumLength"/> and <see cref="MaximumLength"/> values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength);

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    /// <param name="maximumLength">
    /// The maximum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength, int maximumLength);
}

/// <summary>
/// Provides a way of requiring a value that is not default(T) for a property of value type.
/// </summary>
/// <remarks>Only to be used with value type properties.</remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class NonDefaultValueRequiredAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides a way of not accepting specific values for a property/field/parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class RejectValuesAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the values which are not accepted.
    /// </summary>
    public ICollection<object> RejectedValues { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RejectValuesAttribute"/> class.
    /// </summary>
    /// <param name="rejectedValues">Values which should not be accepted.</param>
    public RejectValuesAttribute(params object[] rejectedValues);
}

/// <summary>
/// Provides boundary validation for <see cref="TimeSpan"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class TimeSpanAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the lower bound for time span.
    /// </summary>
    public TimeSpan Minimum { get; }

    /// <summary>
    /// Gets the upper bound for time span.
    /// </summary>
    public TimeSpan? Maximum { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the time span validation should exclude the minimum and maximum values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    public TimeSpanAttribute(int minMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    /// <param name="maxMs">Maximum in milliseconds.</param>
    public TimeSpanAttribute(int minMs, int maxMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    public TimeSpanAttribute(string min);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    /// <param name="max">Maximum represented as time span string.</param>
    public TimeSpanAttribute(string min, string max);
}

API Usage

class Options
{
    // require a valid base-64 string
    [Base64String]
    public string EncodedBinaryData { get; set; } = string.Empty;

    // a value between specific exclusive boundaries
    [ExclusiveRange(0, 1.0)]
    public double BetweenZeroAndOne { get; set; } = .5;

    // 1 to 5 items
    [Length(1, 5)]
    public int[] ArrayOfStuff { get; set; }

    // anything but these
    [RejectValues("Red")]
    public string Color { get; set; }

    // 10ms to 100ms
    [TimeSpan(10, 100)]
    public TimeSpan Timeout { get; set; }
}

Alternative Designs

No response

Risks

No response

Author: geeknoid
Assignees: -
Labels:

api-suggestion, area-System.ComponentModel, untriaged

Milestone: 8.0.0

@ghost
Copy link

ghost commented Oct 24, 2022

Tagging subscribers to this area: @ajcvickers, @bricelam, @roji
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

Our project features hundreds of different option types against which we perform data validation operations. We've introduced a number of new data validation attributes which capture essential scenarios in our code. As these are general-purpose in nature, it would be great to add those to the core set of supported attributes.

API Proposal

namespace System.ComponentModel.Annotations;

/// <summary>
/// Marks a property to be validated as <see href="https://en.wikipedia.org/wiki/Base64">Base64</see> string.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class Base64StringAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides exclusive boundary validation for <see cref="long"/> or <see cref="double"/> values.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ExclusiveRangeAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum value for the range.
    /// </summary>
    public object Minimum { get; }

    /// <summary>
    /// Gets the maximum value for the range.
    /// </summary>
    public object Maximum { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(int minimum, int maximum);

    /// <summary>
    /// Initializes a new instance of the <see cref="ExclusiveRangeAttribute"/> class.
    /// </summary>
    /// <param name="minimum">The minimum value, exclusive.</param>
    /// <param name="maximum">The maximum value, exclusive.</param>
    public ExclusiveRangeAttribute(double minimum, double maximum);
}

/// <summary>
/// Specifies the minimum length of any <see cref="IEnumerable"/> or <see cref="string"/> objects.
/// </summary>
/// <remarks>
/// The standard <see cref="MinLengthAttribute" /> supports only non generic <see cref="Array"/> or <see cref="string"/> typed objects
/// on .Net Framework, while <see cref="ICollection{T}"/> type is supported only on .Net Core.
/// See issue here <see href="https://github.com/dotnet/runtime/issues/23288"/>.
/// This attribute aims to allow validation of all these objects in a consistent manner across target frameworks.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class LengthAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the minimum allowed length of the collection or string.
    /// </summary>
    public int MinimumLength { get; }

    /// <summary>
    /// Gets the maximum allowed length of the collection or string.
    /// </summary>
    public int? MaximumLength { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the length validation should exclude the <see cref="MinimumLength"/> and <see cref="MaximumLength"/> values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength);

    /// <summary>
    /// Initializes a new instance of the <see cref="LengthAttribute"/> class.
    /// </summary>
    /// <param name="minimumLength">
    /// The minimum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    /// <param name="maximumLength">
    /// The maximum allowable length of array/string data.
    /// Value must be greater than or equal to zero.
    /// </param>
    public LengthAttribute(int minimumLength, int maximumLength);
}

/// <summary>
/// Provides a way of requiring a value that is not default(T) for a property of value type.
/// </summary>
/// <remarks>Only to be used with value type properties.</remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class NonDefaultValueRequiredAttribute : ValidationAttribute
{
}

/// <summary>
/// Provides a way of not accepting specific values for a property/field/parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class RejectValuesAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the values which are not accepted.
    /// </summary>
    public ICollection<object> RejectedValues { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RejectValuesAttribute"/> class.
    /// </summary>
    /// <param name="rejectedValues">Values which should not be accepted.</param>
    public RejectValuesAttribute(params object[] rejectedValues);
}

/// <summary>
/// Provides boundary validation for <see cref="TimeSpan"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class TimeSpanAttribute : ValidationAttribute
{
    /// <summary>
    /// Gets the lower bound for time span.
    /// </summary>
    public TimeSpan Minimum { get; }

    /// <summary>
    /// Gets the upper bound for time span.
    /// </summary>
    public TimeSpan? Maximum { get; }

    /// <summary>
    /// Gets or sets a value indicating whether the time span validation should exclude the minimum and maximum values.
    /// </summary>
    /// <remarks>
    /// By default the property is set to <c>false</c>.
    /// </remarks>
    public bool Exclusive { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    public TimeSpanAttribute(int minMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="minMs">Minimum in milliseconds.</param>
    /// <param name="maxMs">Maximum in milliseconds.</param>
    public TimeSpanAttribute(int minMs, int maxMs);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    public TimeSpanAttribute(string min);

    /// <summary>
    /// Initializes a new instance of the <see cref="TimeSpanAttribute"/> class.
    /// </summary>
    /// <param name="min">Minimum represented as time span string.</param>
    /// <param name="max">Maximum represented as time span string.</param>
    public TimeSpanAttribute(string min, string max);
}

API Usage

class Options
{
    // require a valid base-64 string
    [Base64String]
    public string EncodedBinaryData { get; set; } = string.Empty;

    // a value between specific exclusive boundaries
    [ExclusiveRange(0, 1.0)]
    public double BetweenZeroAndOne { get; set; } = .5;

    // 1 to 5 items
    [Length(1, 5)]
    public int[] ArrayOfStuff { get; set; }

    // anything but these
    [RejectValues("Red")]
    public string Color { get; set; }

    // 10ms to 100ms
    [TimeSpan(10, 100)]
    public TimeSpan Timeout { get; set; }
}

Alternative Designs

No response

Risks

No response

Author: geeknoid
Assignees: -
Labels:

api-suggestion, area-System.ComponentModel, area-System.ComponentModel.DataAnnotations, untriaged

Milestone: 8.0.0

@ericstj
Copy link
Member

ericstj commented Oct 24, 2022

@geeknoid can you include the code samples that demonstrate where validation would happen?

@rafal-mz
Copy link

@ericstj There is another issue that shows our source generated validators, which are using attributes mentioned in this issue:
#85475

It would be also helpful to create an X509Certificate2 validation attribute. We have it in our backlog but unfortunately had no capacity to create it.

@ericstj
Copy link
Member

ericstj commented Oct 24, 2022

I see, it looks like that calls back into the validation infrastructure in System.ComponentModel.Annotations - so this isn't proposing any new consumers/entry-points for this validation - just some new attributes that use the existing mechanisms.

@ajcvickers
Copy link
Member

Validation attributes are not something we plan to invest any time in going forward. It is unlikely we will accept any changes in this area.

@ajcvickers ajcvickers modified the milestones: 8.0.0, Future Oct 25, 2022
@ajcvickers ajcvickers removed the untriaged New issue has not been triaged by the area owner label Oct 25, 2022
@danmoseley
Copy link
Member

@ajcvickers can you say more -- how did we end up with that perspective, was it resources, usefulness, or lack of adoption?

@danmoseley
Copy link
Member

Also, can you say more about the cost of additions like these? What are the costs beyond the attribute itself (including the validation method)?

@ajcvickers
Copy link
Member

@danmoseley Attributes in System.ComponentModel.Annotations (aka "Data Annotations") are used in two ways:

  1. As validation attributes, through inheritance of ValidationAttribute, which, if implemented correctly can be used by any code that understands validation attributes without that code needing to be updated to consume the new attributes.
  2. As directives to consuming code as to the semantics of the annotated member. So, for example U.I. components may look for the KeyAttribute and choose to make input read-only, or look at StringLength and restrict the U.I. from allowing more characters than specified. This is independent of the attribute's use for validation and relies on the consuming code understanding the semantics of the attribute.

Use in validation has a few problems:

  1. The validation for any given attribute can never be changed without it breaking any application that uses it. So, for example, the behavior for allowing empty strings with Required is problematic in many cases, but can never be changed. Similarly, the attributes don't do recursive validation, which people often want, but can't be added by default without breaking.
  2. Validation is not performed in places where people expect it to be. For example, people expect JObject to be validated, but it never is, and we can never change that without breaking existing applications.
  3. The localization story for validation messages is not great.

The problems are worse when the attributes are consumed directly:

  1. Different consumers have different interpretations of what the attributes mean. This leads to a situation where MaxLength is applied to a property, and one consumer interprets this as number of bytes in the string, while another interprets it as number of characters. This leads to, sometimes subtle, behavioral differences in the consumers which means using an attribute for one thing can easily break code somewhere else.
  2. Some consumers respect (in whatever way they think that should be) some attributes but not others. This is again very confusing, because users can't understand why an attribute is working on one place but not another, or assume it is working somewhere it is not.
  3. After a new attribute is added, consumers must react to add support for it. Legacy code that uses these attributes will never react, and code still under active development may or may not react, further fragmenting the story for application developers.

It would be great for .NET to have a modern validation system that does not suffer from these issues. However, that has never been high enough priority to be funded, and last time I checked nobody is really interested in taking on and owning such a thing, since experience has shown the existing system to be such as can of worms. Beyond that, it doesn't seem wise to further add to the current system, beyond maintaining behavior that already exists.

@ajcvickers
Copy link
Member

Btw, the only reason my team owns this area is because we added some new attributes for legacy EF over 10 years ago. We learned our lesson and never did that again! :-)

@danmoseley
Copy link
Member

Thanks for writing all that up - I think it will be useful in future. @geeknoid I assume functionally everything works fine if you keep these attributes where you have them today.

@danmoseley
Copy link
Member

I know the libraries folks are currently adding README.md to each library indicating the kinds of changes they take. This one could probably benefit from one. @jeffhandley could you maybe point @ajcvickers at an example in the recommended format?

@geeknoid
Copy link
Member Author

@danmoseley Unfortunately, no this is somewhat of a deal-breaker for the project we're on.

As we want to move many of our components into the core libraries, these components depend on all these attributes. If the attributes don't make it to the core of the platform, then we would need to rewrite our components to not depend on the attributes, which would be a lot of busy work leading to likely lower quality experience of our customers.

@geeknoid
Copy link
Member Author

@ajcvickers So if I'm reading your response correctly, you're basically saying that the current attribute-based validation model is broken in many ways, and you don't want to invest in expanding its feature set?

For our project, we have a lot of option types, and they are validated systematically using a code generator option validator, which leverages all the validation attributes in a reliable and predictable way. It has worked very well for us, and leads to high-quality state coming into our code, and high-quality errors being presented to the user.

@ajcvickers
Copy link
Member

So if I'm reading your response correctly, you're basically saying that the current attribute-based validation model is broken in many ways, and you don't want to invest in expanding its feature set?

Yes.

For our project, we have a lot of option types, and they are validated systematically using a code generator option validator, which leverages all the validation attributes in a reliable and predictable way. It has worked very well for us, and leads to high-quality state coming into our code, and high-quality errors being presented to the user.

That's great for a system used in a well-defined way by a private group of consumers. It's a different thing to add new attributes to the BCL where they will be available to all consumers of .NET who will have many different expectations and come up with many different ways of consuming the code. And especially since there is history around how this code should be consumed that goes beyond what you are doing.

@jeffhandley
Copy link
Member

I know the libraries folks are currently adding README.md to each library indicating the kinds of changes they take. This one could probably benefit from one. @jeffhandley could you maybe point @ajcvickers at an example in the recommended format?

Yes, we've been adding them to lots of libraries the last couple weeks. The XML libraries is a good example: #77401

@jeffhandley jeffhandley added the partner-impact This issue impacts a partner who needs to be kept updated label Nov 21, 2022
@KieranDevvs
Copy link

  1. Base64StringAttribute

    • This would generally not be used for end-user UI scenarios, which makes it a bit unusual for DataAnnotations validator attributes
    • We should discuss if we want to be open to scenarios where the models being validated are not UI-oriented
    • There are plenty of other non-validation attributes in the namespace that are not UI-oriented
    • If we do open the door for those though, where will we draw the line? If we have a Base64String validator, would that inviteWellFormedJsonString or WellFormedXmlString?

IMO the validation namespace is already generic / non-UI specific and I wouldn't see any need to impose a scope limit to just UI now. I use data validation attributes in all kinds of applications (console, web desktop and mobile, services). Data validation isn't UI specific.

If we do open the door for those though, where will we draw the line? If we have a Base64String validator, would that inviteWellFormedJsonString or WellFormedXmlString?

IMO I wouldn't restrict the scope of potential validators purely based on what they logically do, but rather judge them on a case by case basis on their effort required to introduce and their level of impact to customers.

@eiriktsarpalis eiriktsarpalis added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jan 17, 2023
@eiriktsarpalis
Copy link
Member

I'm marking the issue as api-needs-work since it has come up that there is desired to use the new functionality in .NET framework. Even though DataAnnotations are not part of netstandard2.0, they do have implementations in .NET framework so adding new members to existing types becomes challenging from a type forwarding perspective.

@eiriktsarpalis eiriktsarpalis added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Jan 25, 2023
@eiriktsarpalis eiriktsarpalis added the blocking Marks issues that we want to fast track in order to unblock other important work label Feb 1, 2023
@terrajobst
Copy link
Member

@geeknoid @stephentoub

It’s marked as blocking. For tomorrow, we were planning to review interop; is this urgent? If so, I’ll update the agenda and reserve 10-15 min for it.

Otherwise it’s Tuesday next week.

@stephentoub
Copy link
Member

The sooner the better, but if we can't get to it for another week, that's ok.

@terrajobst
Copy link
Member

Then I suggest we push it to next week so we don’t have to time box too much. My gut feel is that interop stuff will take a while, probably even two rounds of reviews.

@bartonjs
Copy link
Member

bartonjs commented Feb 14, 2023

Video

  • RangeAttribute calls the current properties Minimum and Maximum, so those terms should be preferred over UpperBound and LowerBound
    • And we moved the "Is" to later in the name so that Minimum/MinimumIsExclusive are adjacent in IntelliSense and docs.
  • RequiredAttribute.DisallowDefaultValues yielded a very long discussion, and the conclusion was to insert "All": DisallowAllDefaultValues.
  • Base64StringAttribute looks good as proposed
  • For LengthAttribute we removed the defaulted paramters, both are required (if you want just one or the other use the existing attributes)
    • We changed MinLength/MaxLength to be based on long instead of int
    • And renamed MinLength/MaxLength to MinimumLength/MaximumLength
  • For AllowedValuesAttribute and DeniedValuesAttribute we renamed their properties to just be Values
  • It was asked and answered that these types are intentionally not sealed, consistency with the other data validation attributes (and the notion that these work less like attributes and more like general runtime objects)
namespace System.ComponentModel.Annotations;

public partial class RangeAttribute : ValidationAttribute
{
    public bool MinimumIsExclusive { get; set; } = false;
    public bool MaximumIsExclusive { get; set; } = false;
}

public partial class RequiredAttribute : ValidationAttribute
{
     // Fail validation for structs that equal the default value for the type
     public bool DisallowAllDefaultValues { get; set; } = false;
}

// Validates that the specified string uses Base64 encoding
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class Base64StringAttribute : ValidationAttribute
{
}

// Specifies length ranges for string/IEnumerable
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class LengthAttribute : ValidationAttribute
{
    public LengthAttribute(long minimumLength, long maximumLength);

    public long MinimumLength { get; }
    public long MaximumLength { get; }
}

// Validation using allow and deny lists
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class AllowedValuesAttribute : ValidationAttribute
{
    public AllowedValuesAttribute(params object[] values);
    public object[] Values { get; }
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class DeniedValuesAttribute : ValidationAttribute
{
    public DeniedValuesAttribute(params object[] values);
    public object[] Values { get; }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Feb 14, 2023
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Feb 17, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Feb 23, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Mar 25, 2023
@dotnet dotnet unlocked this conversation May 8, 2023
@StingyJack
Copy link

Per @eiriktsarpalis this is the better place to make the case for replacing the use of the word "Exclusive" with "Included" for the range attributes, thus finally addressing the remaining problems with the original and current names. This isnt a knock against whomever chose those names because naming can be hard. Also English is an overly complicated language with lots of odd and inconsistent rules. I am aware that c# is not English but our programs that do use English words should be borrowing ones with unambiguous meanings.

"Exclusive" is more commonly used to describe "something special, limited, or unique that anyone can get, but only from one source". Like "exclusive content" that a media company might generate, or an "exclusive story" that a journalist may write about (but that everyone can access). In this attribute you intend to mean some strict mathematical usage that is something like the opposite of that. By using "Exclusive" there is an easy point of confusion with an everyday English word.

If you dont feel there is any confusion with using any form of "Exclude" then you must recognize that it qualifies as a negative name, which does not follow long standing convention for booleans. The new names actually go farther away from those conventions than the previous choices due to the placement of the "be" word.

The intellisense argument is not logical and just satisfying someone's need to have things arranged the way they like them (I've been guilty of this in the past too). Switching the order of the "be" word to the middle makes it harder to select the correct property in intellisense, and putting four properties all starting with the same letter and being close in spelling means more keystrokes are required to select the property that I want to use when its not the first choice in the list. Instead of having two I's and two M's with one down arrow extra each at ~10 keystrokes, its now four M's and ~14 keystrokes

I'm not going to be comfortable with using this attribute with its current naming scheme. I will just avoid using it (and require any usage of it in PR's that I have to approve to follow suit) because I know these names will be problematic for someone else later and create bugs.

Copied and summarized from a few posts already made
dotnet/core#8134 (comment)
dotnet/core#8134 (comment)
dotnet/core#8134 (comment)

@eiriktsarpalis
Copy link
Member

"Exclusive" is more commonly used to describe "something special, limited, or unique that anyone can get, but only from one source". Like "exclusive content" that a media company might generate, or an "exclusive story" that a journalist may write about (but that everyone can access). In this attribute you intend to mean some strict mathematical usage that is something like the opposite of that. By using "Exclusive" there is an easy point of confusion with an everyday English word.

The terms "exclusive" and "inclusive" are fairly standard when it comes to upper bounds and ranges. A quick web search on "exclusive range" should illuminate this. The reason why we went with "exclusive" over "inclusive" is that inclusive ranges are the default for RangeAttribute.

@StingyJack
Copy link

StingyJack commented May 11, 2023

The terms "exclusive" and "inclusive" are fairly standard when it comes to upper bounds and ranges

@eiriktsarpalis - Keep in mind that the person saying this is someone who has studied mathematics extensively (at some very prestigious universities) and for whom the notions of Inclusive/Exclusive are probably something understood as naturally as breathing. There's nothing wrong with that perspective, but it is rather unique and rare. Many in our profession have a degree in a different field of study (Phd Biology, MS Communication, etc), some have taken no college classes, some didn't complete high school. We read "exclusive" and have to cognate for a few moments or stop and look it up. If the word were "Included", that impediment to reading the code would not happen.

A quick web search on "exclusive range" should illuminate this.

There are also 71 Million web search results for "exclusive range is confusing" XD

Do most English speakers recognize the word "bounds" as in "Leaps and bounds" or "out of bounds"? I dont know, but it could be either. Getting rid of the word "bounds" from the name was a good move. "Exclusive" needs the same treatment. Even changing it to "Excluded" would be better than "Exclusive". EDIT: "Inclusive" isn't really any better than "Exclusive" .

When there is a more common, more easily understood, or less ambiguous naming alterative, that should be what is used.

@eiriktsarpalis
Copy link
Member

While you're right that mathematical jargon should not necessarily always transfer to programming APIs, my point is that this is fairly established terminology from the perspective of programming APIs. A quick web search should highlight this, there are examples of this in C#, Rust, Scala, Ruby, Java and the list goes on.

@StingyJack
Copy link

there are examples of this in C#, Rust, Scala, Ruby, Java and the list goes on.

I read that and these phrases were ringing in my head...

"everyone else is doing it"

"Nobody get fired for choosing IBM"

if you do the same thing as everyone else, it's not improving or adding value. That's like repeating a pattern from existing code for the reason of consistency when you know a better pattern. You have a chance to set a better precedent, don't waste it

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented May 25, 2023

when you know a better pattern.

I don't actually. We're talking about terminology here, so what is "better" here is really a matter of established convention. I've shown beyond reasonable doubt that inclusive/exclusive is the established terminology, both in the context of mathematics and in the context of programming.

@StingyJack
Copy link

That's like

Keyword there is "like" - I'm giving a similar situation where you would likely choose the opposite. "Better" in this case has nothing to do with convention, and everything to do with readability and less defects.

If you dont want to use a more common, more easily understood, and less ambiguous naming alterative, thats up to you.

@ghost ghost locked as resolved and limited conversation to collaborators Jun 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.ComponentModel.DataAnnotations partner-impact This issue impacts a partner who needs to be kept updated
Projects
None yet
Development

Successfully merging a pull request may close this issue.