-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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]: Introduce a code generator to handle option validation #85475
Comments
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. |
Tagging subscribers to this area: @dotnet/area-extensions-options Issue DetailsBackground and motivationIn 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 Proposalnamespace 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 : Attribute
{
/// <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 : Attribute
{
/// <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 Usageusing 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
} 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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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
{
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.R9.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.R9.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.R9.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.R9.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.R9.Extensions.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.R9.Generators", "1.0.0.0")]
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Microsoft.R9.Extensions.ClusterMetadata.ServiceFabric.ServiceFabricMetadata options)
{
var baseName = (string.IsNullOrEmpty(name) ? "ServiceFabricMetadata" : name) + ".";
var builder = global::Microsoft.R9.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.R9.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.R9.Generators", "1.0.0.0")]
internal static class __Validators
{
}
} Note that the generated code shown here depends on #77404 Alternative DesignsNo response RisksNo response
|
Would it make sense to define this source generator at the |
Would that need to introduce a dependency on |
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 |
We should consider having the generator emit code that uses the Additionally, we should ensure the generated code is linker and native AOT friendly. This could be as simple as suppressing the warning from |
[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 |
@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:
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:
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. |
@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? |
One more note, I am not good in naming in general, the design guidelines suggest not naming the classes as verbs.
Would keeping the name as |
The WRT to the design of the generator attributes, we seemingly have three established patterns for using source generators in the framework today:
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. |
This is a good point. I think the pattern is to extend |
Great points on both topics, @DamianEdwards. Source Generator TriggerI 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 ValidationI think the topics that we would need to address for transitive validation to be handled in a general-purpose manner are:
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 |
Thanks @jeffhandley. Per feedback, We'll keep the original proposal mentioned in the description #85475 with the following changes:
I prefer to list the option listed in the comment #85475 as alternative design option and we can discuss that in the design review too. Just in case we get more feedback on that. @geeknoid could you please apply the minor change to the proposal? or do you want me to help with that? @eerhardt @davidfowl @stephentoub please let's know if you have any feedback before we can go ahead and schedule that for design review. |
@tarekgh What's the implication on the model to make ValidateEnumerableAttribute and ValidateTransitivelyAttribute derive from ValidationAttribute? ValidationAttribute has semantics associated with it (GetValidationResult, IsValid, etc). What would be their purpose/behavior? |
By deriving from It does limit us to producing a single validation result, which I think is the only drawback. If that's a major hurdle though, I'd rather revisit the possibility of a single attribute producing multiple results instead of needing to rely on the consumer augmenting |
@tarekgh asked a follow-up question about the I would still promote a We would not want traditional- and source-gen-based validation to behave differently, and we would not want the source generator to produce code that diverges from |
I think this is the key point. It also makes things like flowing |
Tagging subscribers to this area: @dotnet/area-extensions-options Issue DetailsBackground and motivationIn 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 Proposalnamespace 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 : Attribute
{
/// <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 : Attribute
{
/// <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 Usageusing 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
} 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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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.R9.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
{
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.R9.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.R9.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.R9.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.R9.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.R9.Extensions.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.R9.Generators", "1.0.0.0")]
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Microsoft.R9.Extensions.ClusterMetadata.ServiceFabric.ServiceFabricMetadata options)
{
var baseName = (string.IsNullOrEmpty(name) ? "ServiceFabricMetadata" : name) + ".";
var builder = global::Microsoft.R9.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.R9.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.R9.Generators", "1.0.0.0")]
internal static class __Validators
{
}
} Note that the generated code shown here depends on #77404 Alternative DesignsNo response RisksNo response
|
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; }
} |
Something that we didn't explicitly discuss in API review was support for 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 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>
{
} |
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
API Usage
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:
Note that the generated code shown here depends on #77404
Alternative Designs
No response
Risks
No response
The text was updated successfully, but these errors were encountered: