Description
Using the following example source:
public interface IMyValidationService { }
public sealed class MyOptions : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// This will throw since an IServiceProvider is not available in the ValidationContext
_ = validationContext.GetRequiredService<IMyValidationService>();
return [];
}
}
[OptionsValidator]
public sealed partial class MyValidateOptions :
IValidateOptions<MyOptions>
{
}
would generate an options validation implementation that looks like:
partial class MyValidateOptions
{
/// <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.SourceGeneration", "10.0.12.23009")]
#if !NET10_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
Justification = "The created ValidationContext object is used in a way that never call reflection")]
#endif
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Microsoft.Diagnostics.Tools.Monitor.MyOptions options)
{
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
#if NET10_0_OR_GREATER
string displayName = string.IsNullOrEmpty(name) ? "MyOptions.Validate" : $"{name}.Validate";
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options, displayName, null, null);
#else
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
#endif
context.MemberName = "Validate";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Validate" : $"{name}.Validate";
(builder ??= new()).AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
}
}
Note that the construction of the ValidationContext
passes null for the IServiceProvider
parameter.
It appears that the generator does not consider if an IServiceProvider
is available: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs#L697
This prevents IValidatableObject
implementations from using the ValidationContext.GetService
method because it will always return null. Here are a few examples where .NET Monitor uses this and would prevent it from adopting options validation source generation:
- https://github.com/dotnet/dotnet-monitor/blob/main/src/Tools/dotnet-monitor/CollectionRules/Options/ValidationHelper.cs#L40
- https://github.com/dotnet/dotnet-monitor/blob/main/src/Tools/dotnet-monitor/CollectionRules/Options/Actions/ValidateEgressProviderAttribute.cs#L25
Not to necessarily prescribe the solution, but it would be nice if the source generator would detect an IServiceProvider
parameter from the primary constructor or possibly a field from the IValidateOptions<T>
implementation and pass that instance when constructing the ValidationContext
.