Skip to content

[API Proposal]: Introduce a code generator to handle option validation #85475

Closed

Description

@geeknoid

Background and motivation

In my current project, we have many option types which need to be validated on startup. To reduce startup overhead and improve validation feature set, we've implemented a source code generator that implements the validation logic. This is a general-purpose mechanism which could benefit the broader community.

API Proposal

namespace Microsoft.Extensions.Options;

/// <summary>
/// Triggers the automatic generation of the implementation of <see cref="Microsoft.Extensions.Options.IValidateOptions{T}" /> at compile time.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
[Conditional("CODE_GENERATION_ATTRIBUTES")]
public sealed class OptionsValidatorAttribute : Attribute
{
}

/// <summary>
/// Marks a field or property to be enumerated, and each enumerated object to be validated.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
[Conditional("CODE_GENERATION_ATTRIBUTES")]
public sealed class ValidateEnumerableAttribute :  ValidationAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ValidateEnumerableAttribute"/> class.
    /// </summary>
    /// <remarks>
    /// Using this constructor for a field/property tells the code generator to
    /// generate validation for the individual members of the enumerable's type.
    /// </remarks>
    public ValidateEnumerableAttribute();

    /// <summary>
    /// Initializes a new instance of the <see cref="ValidateEnumerableAttribute"/> class.
    /// </summary>
    /// <param name="validator">A type that implements <see cref="IValidateOptions{T}" /> for the enumerable's type.</param>
    /// <remarks>
    /// Using this constructor for a field/property tells the code generator to use the given type to validate
    /// the object held by the enumerable.
    /// </remarks>
    public ValidateEnumerableAttribute(Type validator);

    /// <summary>
    /// Gets the type to use to validate the enumerable's objects.
    /// </summary>
    public Type? Validator { get; }
}

/// <summary>
/// Marks a field or property to be validated transitively.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
[Conditional("CODE_GENERATION_ATTRIBUTES")]
public sealed class ValidateTransitivelyAttribute :  ValidationAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ValidateTransitivelyAttribute"/> class.
    /// </summary>
    /// <remarks>
    /// Using this constructor for a field/property tells the code generator to
    /// generate validation for the individual members of the field/property's type.
    /// </remarks>
    public ValidateTransitivelyAttribute();

    /// <summary>
    /// Initializes a new instance of the <see cref="ValidateTransitivelyAttribute"/> class.
    /// </summary>
    /// <param name="validator">A type that implements <see cref="IValidateOptions{T}" /> for the field/property's type.</param>
    /// <remarks>
    /// Using this constructor for a field/property tells the code generator to use the given type to validate
    /// the object held by the field/property.
    /// </remarks>
    public ValidateTransitivelyAttribute(Type validator);

    /// <summary>
    /// Gets the type to use to validate a field or property.
    /// </summary>
    public Type? Validator { get; }
}

API Usage

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
using Microsoft.R9.Extensions.Options.Validation;

namespace Foo;

public class MyOptions
{
    [Required]
    public string Name { get; set; } = string.Empty;

    [ValidateTransitively]
    public NestedOptions? Nested { get; set; }

    [ValidateEnumerable]
    public IList<AccountOptions> Accounts { get; set; }
}

public class NestedOptions
{
    [Range(0, 10)]
    public int Value { get; set; }
}

public class AccountOptions
{
    [Required]
    public string Username { get; set; }
}

[OptionsValidator]
internal sealed partial class MyOptionsValidator : IValidateOptions<MyOptions>
{
    // the implementation of this class is generated
}

[OptionsValidator]
internal sealed partial class NestedOptionsValidator : IValidateOptions<NestedOptions>
{
    // the implementation of this class is generated
}

[OptionsValidator]
internal sealed partial class AccountOptionsValidator : IValidateOptions<AccountOptions>
{
    // the implementation of this class is generated
}

//
// The app will need to register the IValidateOptions<MyOptions> interface to the DI to enable the generated validation. Here is some example 
//

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyOptions>(builder.Configuration.GetSection(...));

builder.Services.AddSingleton<IValidateOptions<MyOptions>, MyOptionsValidator>();

With the above, the generated validator ensures that Name is specified, will perform transitive validation of the NestedOptions value, and will perform validation on all AccountOptions instances. Here's an example of the code that generator might produce:

// <auto-generated/>
#nullable enable
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
#pragma warning disable CS0618 // Type or member is obsolete
namespace Foo
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
    internal sealed partial class __AccountOptionsValidator__
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Foo.AccountOptions options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "AccountOptions" : name) + ".";
            var builder = global::Microsoft.Extensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "Username";
            context.DisplayName = baseName + "Username";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.Username, context));

            return builder.Build();
        }
    }
}
#pragma warning disable CS0618 // Type or member is obsolete
namespace Foo
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
    internal sealed partial class __NestedOptionsValidator__
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Foo.NestedOptions options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "NestedOptions" : name) + ".";
            var builder = global::Microsoft.Extensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "Value";
            context.DisplayName = baseName + "Value";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A2.GetValidationResult(options.Value, context));

            return builder.Build();
        }
    }
}
#pragma warning disable CS0618 // Type or member is obsolete
namespace Foo
{
    partial class AccountOptionsValidator
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Foo.AccountOptions options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "AccountOptions" : name) + ".";
            var builder = global::Microsoft.xtensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "Username";
            context.DisplayName = baseName + "Username";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.Username, context));

            return builder.Build();
        }
    }
}
#pragma warning disable CS0618 // Type or member is obsolete
namespace Foo
{
    partial class MyOptionsValidator
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Foo.MyOptions options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "MyOptions" : name) + ".";
            var builder = global::Microsoft.Extensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "Name";
            context.DisplayName = baseName + "Name";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.Name, context));

            if (options.Nested != null)
            {
                builder.AddErrors(global::Foo.__NestedOptionsValidator__.Validate(baseName + "Nested", options.Nested));
            }

            if (options.Accounts != null)
            {
                var count = 0;
                foreach (var o in options.Accounts)
                {
                    if (o is not null)
                    {
                        builder.AddErrors(global::Foo.__AccountOptionsValidator__.Validate(baseName + $"Accounts[{count++}]", o));
                    }
                    else
                    {
                        builder.AddError(baseName + $"Accounts[{count++}] is null");
                    }
                }
            }

            return builder.Build();
        }
    }
}
#pragma warning disable CS0618 // Type or member is obsolete
namespace Foo
{
    partial class NestedOptionsValidator
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Foo.NestedOptions options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "NestedOptions" : name) + ".";
            var builder = global::Microsoft.Extensions.Options.Extensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "Value";
            context.DisplayName = baseName + "Value";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A2.GetValidationResult(options.Value, context));

            return builder.Build();
        }
    }
}
#pragma warning disable CS0618 // Type or member is obsolete
namespace Microsoft.Extensions.Options.ClusterMetadata.ServiceFabric
{
    partial class ServiceFabricMetadataValidator
    {
        /// <summary>
        /// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
        /// </summary>
        /// <param name="name">The name of the options instance being validated.</param>
        /// <param name="options">The options instance.</param>
        /// <returns>Validation result.</returns>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
        public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Microsoft.Extensions.Options.ClusterMetadata.ServiceFabric.ServiceFabricMetadata options)
        {
            var baseName = (string.IsNullOrEmpty(name) ? "ServiceFabricMetadata" : name) + ".";
            var builder = global::Microsoft.Extensions.Options.Validation.ValidateOptionsResultBuilder.Create();
            var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);

            context.MemberName = "ServiceName";
            context.DisplayName = baseName + "ServiceName";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.ServiceName, context));

            context.MemberName = "ReplicaOrInstanceId";
            context.DisplayName = baseName + "ReplicaOrInstanceId";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A3.GetValidationResult(options.ReplicaOrInstanceId, context));

            context.MemberName = "ApplicationName";
            context.DisplayName = baseName + "ApplicationName";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.ApplicationName, context));

            context.MemberName = "NodeName";
            context.DisplayName = baseName + "NodeName";
            builder.AddError(global::__OptionValidationStaticInstances.__Attributes.A1.GetValidationResult(options.NodeName, context));

            return builder.Build();
        }
    }
}
namespace __OptionValidationStaticInstances
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
    internal static class __Attributes
    {
        internal static readonly global::System.ComponentModel.DataAnnotations.RequiredAttribute A1 = new global::System.ComponentModel.DataAnnotations.RequiredAttribute();

        internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A2 = new global::System.ComponentModel.DataAnnotations.RangeAttribute(
            (int)0,
            (int)10);

        internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A3 = new global::System.ComponentModel.DataAnnotations.RangeAttribute(
            (double)0,
            (double)9.223372036854776E+18);
    }
}
namespace __OptionValidationStaticInstances
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.Generators", "1.0.0.0")]
    internal static class __Validators
    {
    }
}

Note that the generated code shown here depends on #77404

Alternative Designs

No response

Risks

No response

Activity

  1. eerhardt commented on Jan 10, 2023

    @eerhardt
    Member

    Would it make sense to define this source generator at the System.ComponentModel.DataAnnotations library level? That way other libraries using DataAnnotations (e.g. ASP.NET model validation) can take advantage of it as well.

  2. tarekgh commented on Jan 10, 2023

    @tarekgh
    Member

    Would that need to introduce a dependency on Microsoft.Extensions.Options from System.ComponentModel.DataAnnotations?

  3. eerhardt commented on Jan 10, 2023

    @eerhardt
    Member

    Would that need to introduce a dependency on Microsoft.Extensions.Options from System.ComponentModel.DataAnnotations?

    No, that isn't the right layering. There is an Microsoft.Extensions.Options.DataAnnotations library that has a dependency on System.ComponentModel.DataAnnotations.

    My thinking is that the core of this validation source generator would be written in terms of System.ComponentModel.DataAnnotations, then each API that uses DataAnnotations underneath (MS.Ext.Options, ASP.NET, etc.) could reuse this core validation source generator logic.

  4. self-assigned this
    on Mar 21, 2023
  5. DamianEdwards commented on Apr 4, 2023

    @DamianEdwards
    Member

    We should consider having the generator emit code that uses the Validator.TryValidateValues method, rather than using the ValidationAttribute.GetValidationResult method on each relevant attribute. This is to ensure the two-phase validation logic that first checks for RequiredAttribute is always run (which is contained in Validator).

    Additionally, we should ensure the generated code is linker and native AOT friendly. This could be as simple as suppressing the warning from ValidationContext(object instance) as the generated code will root the instance type anyway, and the conditional reflection that both ValidationAttribute.GetValidationResult and Validator.TryValidateValues can end up performing to discover DisplayAttribute on validated members if ValidationContext.MemberName is set will be avoided as ValidationContext.DisplayName is being set as well which disables that path.

  6. tarekgh commented on Apr 6, 2023

    @tarekgh
    Member

    @geeknoid

    [OptionsValidator]
    internal sealed partial class MyOptionsValidator : IValidateOptions<MyOptions>
    {
        // the implementation of this class is generated
    }

    Is it anticipated that users will ever provide an implementation within classes designated with the OptionsValidator attribute? I ask because it seems peculiar to require users to provide an empty class every time just to generate the implementation code for the IValidateOptions interface.

  7. geeknoid commented on Apr 7, 2023

    @geeknoid
    MemberAuthor

    @tarekgh No, in general there isn't anything particularly useful you can put as implementation logic in those classes. I surveyed our source base and out of 163 uses of the attribute, only one had a single constant defined in the class, which is used elsewhere in the project.

    When originally designing this stuff, I had it so it was possible to provide some hand-written validation logic that the generator code would invoke. But then it turned out to be easier to just capture that in completely different validator types, so I took that feature out of the generator.

    The generator needs four bits of state:

    • The type to validate
    • The name of the generated type
    • The namespace and/or parent type of the generated type
    • The visibility modifier of the generated type

    You can imagine this attribute pattern instead of the current approach:

    [assembly: CreateOptionsValidator(typeof(MyOptions), Name = "Namespace1.Namespace2.MyParentType+MyOptionsValidator", Visibility="internal")]

    Not great, but it could get better with some reasonable defaults:

    • The generated type will default to the same namespace, parent type, and name as the type being validated, with a Validator suffix tacked on.
    • The generated type will default to "internal" visibility.
    • We use generic attributes.

    Then you'd get:

    [assembly: CreateOptionsValidator<MyOptions>]

    which is indeed concise. If the validator is defined in the same assembly as the option type being validated (the 99.99% case), then you could also support annotating the option type itself:

    [CreateOptionsValidator]
    public class MyOptions
    {
    }

    Which is even more concise.

  8. tarekgh commented on Apr 9, 2023

    @tarekgh
    Member

    @geeknoid Having both the assembly generic attribute and an attribute that can be directly applied to the Options type is great because it offers simplicity and flexibility in usage. Could you please update the proposal to reflect that?

  9. tarekgh commented on Apr 9, 2023

    @tarekgh
    Member

    One more note, I am not good in naming in general, the design guidelines suggest not naming the classes as verbs.

    ✔️ DO name classes and structs with nouns or noun phrases, using PascalCasing.
    

    Would keeping the name as OptionsValidatorAttribute or something like that would be better? The same applies to ValidateTransitively and ValidateEnumerableAttribute. (Suggestions like DependenciesValidatiorAttribute and EnumerableValidationAttribute).
    I see we already have CompareAttribute though :-(

  10. DamianEdwards commented on Apr 9, 2023

    @DamianEdwards
    Member

    The ValidateTransitely attribute shouldn't be limited to options validation. Using a custom ValidationAttribute to implement recursive validation has been a common technique documented online for many years now. If we're going to introduce support for recursive validation via an attribute I suggest we do so by following that established pattern, so something specific to the options validator source generator.

    WRT to the design of the generator attributes, we seemingly have three established patterns for using source generators in the framework today:

    1. Require user code to declare a partial class from an in-framework base class and decorate that class with an attribute to opt into the source generator, e.g. the System.Text.Json source generation pattern
    2. Require user code to declare a partial method of a specific signature and decorate that method with an attribute to opt into the source generator, e.g. the Regex source generation pattern
    3. Use call-site replacement techniques (global extension methods or pending language feature) to replace original call with generated code, reusing parameter values from user code invocation, e.g. the ASP.NET Core Request Delegate Generator and Microsoft.Extentions.Configuration binding pattern

    The original proposed design for the options validation source generator seems to match the first pattern and leaves the user in control of aspects including the namespace, etc., but the subsequent proposals deviate from this and any other established pattern. Are we suggesting introducing a new BCL source generator pattern?

    @stephentoub @eerhardt

  11. tarekgh commented on Apr 9, 2023

    @tarekgh
    Member

    The original proposed design for the options validation source generator seems to match the first pattern and leaves the user in control of aspects including the namespace, etc., but the subsequent proposals deviate from this and any other established pattern. Are we suggesting introducing a new BCL source generator pattern?

    I am not sure we have to restrict ourselves to the options we have. We should do the right things regardless. Do you see any issue with the modified proposal? Do you see this can create any confusion? I am thinking more about the dev experience here.

  12. tarekgh commented on Apr 9, 2023

    @tarekgh
    Member

    Using a custom ValidationAttribute to implement recursive validation has been a common technique documented online for many years now. If we're going to introduce support for recursive validation via an attribute I suggest we do so by following that established pattern, so something specific to the options validator source generator.

    This is a good point. I think the pattern is to extend ValidationAttribute and we can name the new attribute specific to Options. @jeffhandley do you have any feedback on this part? do we have patterns for IEnumerable attribute too?

  13. jeffhandley commented on Apr 11, 2023

    @jeffhandley
    Member

    Great points on both topics, @DamianEdwards.

    Source Generator Trigger

    I was leaning toward the attribution on the options class, but heeding the point of not wanting to inadvertently introduce another way to trigger source generators, I lean back to the user declaring the partial class and annotating it as shown here.

    That approach avoids challenges around naming collisions, access modifiers, and anything else where we then need to introduce a meta-model for giving us details for the type's definition. And it also leaves open the extensibility point (thus far unused, mind you) for user code to be incorporated into the generated code, perhaps overriding what the generator would do.

    Transitive Validation

    I think the topics that we would need to address for transitive validation to be handled in a general-purpose manner are:

    1. Member Name syntax. By keeping the transitive validation operation as an app-level or app-framework-level concern, we've avoided needing to define a general-purpose syntax for nested member names in validation results. For child properties, it's easy to assume dot-notation. But for enumerables, the syntax is less obvious.
    2. Graph Walk Algorithm. Different scenarios require different graph walk approaches. When should short-circuiting occur? Is there a depth limit? What happens when we detect cycles (and already-validated objects have validation results)?

    For these reasons, I've shied away from trying to provide a general-purpose transitive validator. But with either a general-purpose or options-special-purpose transitive validator, I would prefer the approach of it being defined as a derivative of ValidationAttribute.

  14. 12 remaining items

  15. assigned and unassigned on Apr 27, 2023
  16. added this to the 8.0.0 milestone on Apr 27, 2023
  17. added
    partner-impactThis issue impacts a partner who needs to be kept updated
    blockingMarks issues that we want to fast track in order to unblock other important work
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Apr 27, 2023
  18. terrajobst commented on May 2, 2023

    @terrajobst
    Contributor

    Video

    • OptionsValidatorAttribute
      • This belongs with IValidateOptions<T> which is Microsoft.Extensions.Options
    • The other two attributes seem to belong to System.ComponentModel.DataAnnotations
    • Can we make the code generation a bit simpler so that people don't have to write stubs for the generated types?
    • ValidateTransitivelyAttribute
      • Most customers use the term recursively
      • ValidateObjectMembersAttribute
    • ValidateEnumerableAttribute
      • We're not validating the container.
      • ValidateEnumeratedItemsAttribute
    • The source generator should live in two places
      • The ref pack where Microsoft.Extensions.Options is in-box
      • The package Microsoft.Extensions.Options for people consuming it as such
    • Remove the conditionals
    namespace Microsoft.Extensions.Options;
    
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public sealed class OptionsValidatorAttribute : Attribute
    {
    }
    
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public sealed class ValidateEnumerableAttribute : ValidationAttribute
    {
        public ValidateEnumerableAttribute();
        public ValidateEnumerableAttribute(Type validator);
        public Type? Validator { get; }
    }
    
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public sealed class ValidateTransitivelyAttribute : ValidationAttribute
    {
        public ValidateTransitivelyAttribute();
        public ValidateTransitivelyAttribute(Type validator);
        public Type? Validator { get; }
    }
  19. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on May 2, 2023
  20. DamianEdwards commented on May 2, 2023

    @DamianEdwards
    Member

    Something that we didn't explicitly discuss in API review was support for IValidatableObject in the options validation source generator and the new validation attributes. The IValidatableObject interface exists in the System.ComponentModel.DataAnnotations namespace and thus I think it's reasonable to expect that the options validator source generator and the new attributes being introduced here (ValidateOptionsMembers and ValidateEnumeratedItems) based on DataAnnotations should include support for types that implement IValidatableObject. That potentially introduces some complication WRT the name ValidateObjectMembers, e.g. the following:

    public class MyFeatureOptions
    {
        [ValidateObjectMembers] // This name isn't such a great fit in this scenario
        public MyCustomOptions { get; set; }
    }
    
    public class MyCustomOptions : IValidatableObject
    {
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            // Custom validation logic here...
        }
    }

    The source generator should suppotr the top level options type itself implementing IValidatableObject too, e.g.:

    public class MyCustomOptions : IValidatableObject
    {
        [Required]
        public required string SomeRequiredSetting { get; set; } 
    
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            // Custom validation logic here...
        }
    }
    
    [OptionsValidator]
    internal sealed partial class MyCustomOptionsValidator : IValidateOptions<MyCustomOptions>
    {
    
    }
  21. tarekgh commented on Jun 20, 2023

    @tarekgh
    Member
  22. ghost locked as resolved and limited conversation to collaborators on Jul 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-Extensions-OptionsblockingMarks issues that we want to fast track in order to unblock other important workpartner-impactThis issue impacts a partner who needs to be kept updated

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions