-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Closed
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-Extensions-OptionsblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important workpartner-impactThis issue impacts a partner who needs to be kept updatedThis issue impacts a partner who needs to be kept updated
Milestone
Description
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
maryamariyan, N0D4N and PaulusParssinen
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-Extensions-OptionsblockingMarks issues that we want to fast track in order to unblock other important workMarks issues that we want to fast track in order to unblock other important workpartner-impactThis issue impacts a partner who needs to be kept updatedThis issue impacts a partner who needs to be kept updated