Skip to content

[API Proposal]: Allow supplying a Regex object to a RegularExpressionAttribute #101965

Open
@mrudat

Description

@mrudat

Background and motivation

It would be useful to supply a Regex object to a RegularExpressionAttribute so that you can use a compile-time Regex for data validation.

API Proposal

+ using System.Diagnostics.CodeAnalysis;
  namespace System.ComponentModel.DataAnnotations;

  public class RegularExpressionAttribute : ValidationAttribute
  {
        /// <summary>
        ///     Constructor that accepts the regular expression pattern
        /// </summary>
        /// <param name="pattern">The regular expression to use.  It cannot be null.</param>
        public RegularExpressionAttribute([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Regex")] string pattern) { }

+       /// <summary>
+       /// Create a <see cref="RegularExpressionAttribute"/> using a <see cref="Regex"/> returned from the specified type and method name.
+       /// </summary>
+       /// <param name="regexType">The type that contains the method returning a <see cref="Regex"/>.</param>
+       /// <param name="regexMethodName">The method name that returns the <see cref="Regex"/>. The method must be static and accept no arguments.</param>
+       /// <exception cref="ArgumentNullException">When the <paramref name="regexType"/> is <c>null</c>.</exception>
+       /// <exception cref="ArgumentNullException">When the <paramref name="regexMethodName"/> is <c>null</c>.</exception>
+       /// <exception cref="ArgumentException">When the <paramref name="regexMethodName"/> is empty or consists only of white-space characters.</exception>
+       public RegularExpressionAttribute([DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes.AllMethods)] System.Type regexType, string regexMethodName) { }
  }

API Usage

using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

var model = new Model {  MustTypeAgree = "agree" };
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model), results, true);

Console.WriteLine($"MustTypeAgree: {model.MustTypeAgree}");
Console.WriteLine($"IsValid: {isValid}");

// MustTypeAgree: agree
// IsValid: true

public partial class Model
{
    [Required]
    [RegularExpression(typeof(Model), "GetAgreeRegex")]
    public string? MustTypeAgree { get; set; }

    [GeneratedRegex("AGREE", RegexOptions.IgnoreCase)]
    public static partial Regex GetAgreeRegex();
}

Alternative Designs

  1. Skip integrating [GeneratedRegex] with RegularExpressionAttribute and instead use a source generator to create custom regex validation attribute class.
  2. Implement a separate ValidationAttribute or perhaps derive from RegularExpressionAttribute to handle this scenario separately.
  3. Instead of adding a constructor overload, add a Type? RegexType { get; init; } property that would pair with the existing Pattern such that when it's set, we would enter into the new behavior.

Risks

  1. For sake of design-time scenarios, we try to avoid throwing exceptions (other than argument validation exceptions) from attribute constructors. With this in mind, we need defensive code in the new attribute initialization.
  2. Any validation systems that have special awareness of RegularExpressionAttribute to perform special-purpose logic might not take advantage of the new behavior and end up reconstructing the Regex instance from only the pattern, ignoring any RegexOptions specified on the [GeneratedRegex].
    • For instance, Swashbuckle.AspNetCore extracts the Pattern from the attribute and embeds it into the OpenApiSchema. This will not respect RegexOptions.IgnoreCase or other options.
  3. GeneratedRegexAttribute applies a default timeout of Timeout.Infinite while RegularExpressionAttribute applies a default of 2000ms. While we should likely respect a non-infinite timeout specified on [GeneratedRegex], we need to make sure the 2000ms default for the attribute is still applied instead of Timeout.Infinite.
Original Proposal

API Proposal

namespace System.ComponentModel.DataAnnotations;

public class RegularExpressionAttribute : ValidationAttribute
{
    /// <summary>
    /// Initializes a new instance of the System.ComponentModel.DataAnnotations.RegularExpressionAttribute class.
    /// </summary>
    /// <param name="pattern">The <see cref="Regex"/> that is used to validate the data field value.</param>
    /// <exception cref="System.ArgumentNullException">pattern is null</exception>
    public RegularExpressionAttribute(Regex pattern);
}

API Usage

public partial class CannedRegularExpressionAttribute() : RegularExpressionAttribute(TheRegex())
{
    [GeneratedRegex(@"<Insert some complicated regular expression here>")]
    private static partial Regex TheRegex();
}
public partial record ARecord {
    [RegularExpressionAttribute(TheRegex())]
    public string NeedsComplexValidation;

    [GeneratedRegex(@"<Insert some complicated regular expression here>")]
    private static partial Regex TheRegex();
}

Alternative Designs

Perhaps there would be extra performance gains from generating a RegularExpressionAttribute's Validation method directly?

Risks

No API (that I can spot) on Regex allows specifying matchTimeout for an existing Regex object.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions