未验证 提交 5c03c881 编写于 作者: G github-actions[bot] 提交者: GitHub

[release/8.0] Make Options source gen support Validation attributes having...

[release/8.0] Make Options source gen support Validation attributes having constructor with array parameters (#91934)

* Make Options source gen support Validation attributes having constructor with params Parameter

* delta change

* Rename the generator
Co-authored-by: NEric StJohn <ericstj@microsoft.com>

---------
Co-authored-by: NTarek Mahmoud Sayed <tarekms@microsoft.com>
Co-authored-by: NEric StJohn <ericstj@microsoft.com>
Co-authored-by: NCarlos Sánchez López <1175054+carlossanlop@users.noreply.github.com>
上级 54679f4b
......@@ -13,7 +13,7 @@
namespace Microsoft.Extensions.Options.Generators
{
[Generator]
public class Generator : IIncrementalGenerator
public class OptionsValidatorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
......
......@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
......@@ -469,14 +470,36 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
validationAttrs.Add(validationAttr);
foreach (var constructorArgument in attribute.ConstructorArguments)
ImmutableArray<IParameterSymbol> parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
bool lastParameterDeclaredWithParamsKeyword = parameters.Length > 0 && parameters[parameters.Length - 1].IsParams;
ImmutableArray<TypedConstant> arguments = attribute.ConstructorArguments;
for (int i = 0; i < arguments.Length; i++)
{
validationAttr.ConstructorArguments.Add(GetArgumentExpression(constructorArgument.Type!, constructorArgument.Value));
TypedConstant argument = arguments[i];
if (argument.Kind == TypedConstantKind.Array)
{
bool isParams = lastParameterDeclaredWithParamsKeyword && i == arguments.Length - 1;
validationAttr.ConstructorArguments.Add(GetArrayArgumentExpression(argument.Values, isParams));
}
else
{
validationAttr.ConstructorArguments.Add(GetArgumentExpression(argument.Type!, argument.Value));
}
}
foreach (var namedArgument in attribute.NamedArguments)
{
validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
if (namedArgument.Value.Kind == TypedConstantKind.Array)
{
bool isParams = lastParameterDeclaredWithParamsKeyword && namedArgument.Key == parameters[parameters.Length - 1].Name;
validationAttr.Properties.Add(namedArgument.Key, GetArrayArgumentExpression(namedArgument.Value.Values, isParams));
}
else
{
validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
}
}
}
}
......@@ -637,6 +660,32 @@ private bool CanValidate(ITypeSymbol validatorType, ISymbol modelType)
return false;
}
private string GetArrayArgumentExpression(ImmutableArray<Microsoft.CodeAnalysis.TypedConstant> value, bool isParams)
{
var sb = new StringBuilder();
if (!isParams)
{
sb.Append("new[] { ");
}
for (int i = 0; i < value.Length; i++)
{
sb.Append(GetArgumentExpression(value[i].Type!, value[i].Value));
if (i < value.Length - 1)
{
sb.Append(", ");
}
}
if (!isParams)
{
sb.Append(" }");
}
return sb.ToString();
}
private string GetArgumentExpression(ITypeSymbol type, object? value)
{
if (value == null)
......
......@@ -1610,7 +1610,7 @@ public partial class FirstModelValidator : IValidateOptions<FirstModel>
// Run the generator with C# 7.0 and verify that it fails.
var (diagnostics, generatedSources) = await RoslynTestUtils.RunGenerator(
new Generator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp7).ConfigureAwait(false);
new OptionsValidatorGenerator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp7).ConfigureAwait(false);
Assert.NotEmpty(diagnostics);
Assert.Equal("SYSLIB1216", diagnostics[0].Id);
......@@ -1618,7 +1618,7 @@ public partial class FirstModelValidator : IValidateOptions<FirstModel>
// Run the generator with C# 8.0 and verify that it succeeds.
(diagnostics, generatedSources) = await RoslynTestUtils.RunGenerator(
new Generator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp8).ConfigureAwait(false);
new OptionsValidatorGenerator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp8).ConfigureAwait(false);
Assert.Empty(diagnostics);
Assert.Single(generatedSources);
......@@ -1638,6 +1638,129 @@ public partial class FirstModelValidator : IValidateOptions<FirstModel>
Assert.Equal(0, diags.Length);
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNetCore))]
public async Task DataAnnotationAttributesWithParams()
{
var (diagnostics, generatedSources) = await RunGenerator(@"""
public class MyOptions
{
[Required]
public string P1 { get; set; }
[Length(10, 20)]
public string P2 { get; set; }
[AllowedValues(10, 20, 30)]
public int P3 { get; set; }
[DeniedValues(""One"", ""Ten"", ""Hundred"")]
public string P4 { get; set; }
}
[OptionsValidator]
public partial class MyOptionsValidator : IValidateOptions<MyOptions>
{
}
""");
Assert.Empty(diagnostics);
Assert.Single(generatedSources);
var generatedSource = """
// <auto-generated/>
#nullable enable
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
namespace Test
{
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.SourceGeneration", "42.42.42.42")]
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Test.MyOptions options)
{
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
var validationResults = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationResult>();
var validationAttributes = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(1);
context.MemberName = "P1";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P1" : $"{name}.P1";
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P1, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "P2";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P2" : $"{name}.P2";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "P3";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P3" : $"{name}.P3";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "P4";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P4" : $"{name}.P4";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
}
}
}
namespace __OptionValidationStaticInstances
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
file 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.LengthAttribute A2 = new global::System.ComponentModel.DataAnnotations.LengthAttribute(
(int)10,
(int)20);
internal static readonly global::System.ComponentModel.DataAnnotations.AllowedValuesAttribute A3 = new global::System.ComponentModel.DataAnnotations.AllowedValuesAttribute(
(int)10, (int)20, (int)30);
internal static readonly global::System.ComponentModel.DataAnnotations.DeniedValuesAttribute A4 = new global::System.ComponentModel.DataAnnotations.DeniedValuesAttribute(
"One", "Ten", "Hundred");
}
}
namespace __OptionValidationStaticInstances
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
file static class __Validators
{
}
}
""";
Assert.Equal(generatedSource.Replace("\r\n", "\n"), generatedSources[0].SourceText.ToString().Replace("\r\n", "\n"));
}
private static CSharpCompilation CreateCompilationForOptionsSource(string assemblyName, string source, string? refAssemblyPath = null)
{
// Ensure the generated source compiles
......@@ -1676,7 +1799,7 @@ private static CSharpCompilation CreateCompilationForOptionsSource(string assemb
refAssemblies.Add(refAssembly);
}
return await RoslynTestUtils.RunGenerator(new Generator(), refAssemblies.ToArray(), new List<string> { source }, includeBaseReferences: true, languageVersion).ConfigureAwait(false);
return await RoslynTestUtils.RunGenerator(new OptionsValidatorGenerator(), refAssemblies.ToArray(), new List<string> { source }, includeBaseReferences: true, languageVersion).ConfigureAwait(false);
}
private static async Task<(IReadOnlyList<Diagnostic> diagnostics, ImmutableArray<GeneratedSourceResult> generatedSources)> RunGenerator(
......@@ -1733,7 +1856,7 @@ private static CSharpCompilation CreateCompilationForOptionsSource(string assemb
assemblies.Add(Assembly.GetAssembly(typeof(Microsoft.Extensions.Options.ValidateObjectMembersAttribute))!);
}
var result = await RoslynTestUtils.RunGenerator(new Generator(), assemblies.ToArray(), new[] { text })
var result = await RoslynTestUtils.RunGenerator(new OptionsValidatorGenerator(), assemblies.ToArray(), new[] { text })
.ConfigureAwait(false);
return result;
......
......@@ -211,6 +211,37 @@ public void TestValidationWithCyclicReferences()
ValidateOptionsResult result2 = dataAnnotationValidateOptions.Validate("MyOptions", options);
Assert.True(result1.Succeeded);
}
#if NET8_0_OR_GREATER
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
public void TestNewDataAnnotationFailures()
{
NewAttributesValidator sourceGenValidator = new();
OptionsUsingNewAttributes validOptions = new()
{
P1 = "123456", P2 = 2, P3 = 4, P4 = "c", P5 = "d"
};
ValidateOptionsResult result = sourceGenValidator.Validate("OptionsUsingNewAttributes", validOptions);
Assert.True(result.Succeeded);
OptionsUsingNewAttributes invalidOptions = new()
{
P1 = "123", P2 = 4, P3 = 1, P4 = "e", P5 = "c"
};
result = sourceGenValidator.Validate("OptionsUsingNewAttributes", invalidOptions);
Assert.Equal(new []{
"P1: The field OptionsUsingNewAttributes.P1 must be a string or collection type with a minimum length of '5' and maximum length of '10'.",
"P2: The OptionsUsingNewAttributes.P2 field does not equal any of the values specified in AllowedValuesAttribute.",
"P3: The OptionsUsingNewAttributes.P3 field equals one of the values specified in DeniedValuesAttribute.",
"P4: The OptionsUsingNewAttributes.P4 field does not equal any of the values specified in AllowedValuesAttribute.",
"P5: The OptionsUsingNewAttributes.P5 field equals one of the values specified in DeniedValuesAttribute."
}, result.Failures);
}
#endif // NET8_0_OR_GREATER
}
public class MyOptions
......@@ -270,4 +301,29 @@ public struct MyOptionsStruct
public partial class MySourceGenOptionsValidator : IValidateOptions<MyOptions>
{
}
#if NET8_0_OR_GREATER
public class OptionsUsingNewAttributes
{
[Length(5, 10)]
public string P1 { get; set; }
[AllowedValues(1, 2, 3)]
public int P2 { get; set; }
[DeniedValues(1, 2, 3)]
public int P3 { get; set; }
[AllowedValues(new object?[] { "a", "b", "c" })]
public string P4 { get; set; }
[DeniedValues(new object?[] { "a", "b", "c" })]
public string P5 { get; set; }
}
[OptionsValidator]
public partial class NewAttributesValidator : IValidateOptions<OptionsUsingNewAttributes>
{
}
#endif // NET8_0_OR_GREATER
}
\ No newline at end of file
......@@ -32,7 +32,7 @@ public async Task TestEmitter()
}
var (d, r) = await RoslynTestUtils.RunGenerator(
new Generator(),
new OptionsValidatorGenerator(),
new[]
{
Assembly.GetAssembly(typeof(RequiredAttribute))!,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册