Skip to content

Commit 5c03c88

Browse files
github-actions[bot]tarekghericstjcarlossanlop
authored
[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: Eric StJohn <ericstj@microsoft.com> --------- Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com> Co-authored-by: Eric StJohn <ericstj@microsoft.com> Co-authored-by: Carlos Sánchez López <1175054+carlossanlop@users.noreply.github.com>
1 parent 54679f4 commit 5c03c88

File tree

5 files changed

+237
-9
lines changed

5 files changed

+237
-9
lines changed

src/libraries/Microsoft.Extensions.Options/gen/Generator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace Microsoft.Extensions.Options.Generators
1414
{
1515
[Generator]
16-
public class Generator : IIncrementalGenerator
16+
public class OptionsValidatorGenerator : IIncrementalGenerator
1717
{
1818
public void Initialize(IncrementalGeneratorInitializationContext context)
1919
{

src/libraries/Microsoft.Extensions.Options/gen/Parser.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Collections.Immutable;
67
using System.Globalization;
78
using System.Linq;
89
using System.Text;
@@ -469,14 +470,36 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
469470
var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
470471
validationAttrs.Add(validationAttr);
471472

472-
foreach (var constructorArgument in attribute.ConstructorArguments)
473+
ImmutableArray<IParameterSymbol> parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
474+
bool lastParameterDeclaredWithParamsKeyword = parameters.Length > 0 && parameters[parameters.Length - 1].IsParams;
475+
476+
ImmutableArray<TypedConstant> arguments = attribute.ConstructorArguments;
477+
478+
for (int i = 0; i < arguments.Length; i++)
473479
{
474-
validationAttr.ConstructorArguments.Add(GetArgumentExpression(constructorArgument.Type!, constructorArgument.Value));
480+
TypedConstant argument = arguments[i];
481+
if (argument.Kind == TypedConstantKind.Array)
482+
{
483+
bool isParams = lastParameterDeclaredWithParamsKeyword && i == arguments.Length - 1;
484+
validationAttr.ConstructorArguments.Add(GetArrayArgumentExpression(argument.Values, isParams));
485+
}
486+
else
487+
{
488+
validationAttr.ConstructorArguments.Add(GetArgumentExpression(argument.Type!, argument.Value));
489+
}
475490
}
476491

477492
foreach (var namedArgument in attribute.NamedArguments)
478493
{
479-
validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
494+
if (namedArgument.Value.Kind == TypedConstantKind.Array)
495+
{
496+
bool isParams = lastParameterDeclaredWithParamsKeyword && namedArgument.Key == parameters[parameters.Length - 1].Name;
497+
validationAttr.Properties.Add(namedArgument.Key, GetArrayArgumentExpression(namedArgument.Value.Values, isParams));
498+
}
499+
else
500+
{
501+
validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
502+
}
480503
}
481504
}
482505
}
@@ -637,6 +660,32 @@ private bool CanValidate(ITypeSymbol validatorType, ISymbol modelType)
637660
return false;
638661
}
639662

663+
private string GetArrayArgumentExpression(ImmutableArray<Microsoft.CodeAnalysis.TypedConstant> value, bool isParams)
664+
{
665+
var sb = new StringBuilder();
666+
if (!isParams)
667+
{
668+
sb.Append("new[] { ");
669+
}
670+
671+
for (int i = 0; i < value.Length; i++)
672+
{
673+
sb.Append(GetArgumentExpression(value[i].Type!, value[i].Value));
674+
675+
if (i < value.Length - 1)
676+
{
677+
sb.Append(", ");
678+
}
679+
}
680+
681+
if (!isParams)
682+
{
683+
sb.Append(" }");
684+
}
685+
686+
return sb.ToString();
687+
}
688+
640689
private string GetArgumentExpression(ITypeSymbol type, object? value)
641690
{
642691
if (value == null)

src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,15 +1610,15 @@ public partial class FirstModelValidator : IValidateOptions<FirstModel>
16101610

16111611
// Run the generator with C# 7.0 and verify that it fails.
16121612
var (diagnostics, generatedSources) = await RoslynTestUtils.RunGenerator(
1613-
new Generator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp7).ConfigureAwait(false);
1613+
new OptionsValidatorGenerator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp7).ConfigureAwait(false);
16141614

16151615
Assert.NotEmpty(diagnostics);
16161616
Assert.Equal("SYSLIB1216", diagnostics[0].Id);
16171617
Assert.Empty(generatedSources);
16181618

16191619
// Run the generator with C# 8.0 and verify that it succeeds.
16201620
(diagnostics, generatedSources) = await RoslynTestUtils.RunGenerator(
1621-
new Generator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp8).ConfigureAwait(false);
1621+
new OptionsValidatorGenerator(), refAssemblies.ToArray(), new[] { source }, includeBaseReferences: true, LanguageVersion.CSharp8).ConfigureAwait(false);
16221622

16231623
Assert.Empty(diagnostics);
16241624
Assert.Single(generatedSources);
@@ -1638,6 +1638,129 @@ public partial class FirstModelValidator : IValidateOptions<FirstModel>
16381638
Assert.Equal(0, diags.Length);
16391639
}
16401640

1641+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser), nameof(PlatformDetection.IsNetCore))]
1642+
public async Task DataAnnotationAttributesWithParams()
1643+
{
1644+
var (diagnostics, generatedSources) = await RunGenerator(@"""
1645+
public class MyOptions
1646+
{
1647+
[Required]
1648+
public string P1 { get; set; }
1649+
1650+
[Length(10, 20)]
1651+
public string P2 { get; set; }
1652+
1653+
[AllowedValues(10, 20, 30)]
1654+
public int P3 { get; set; }
1655+
1656+
[DeniedValues(""One"", ""Ten"", ""Hundred"")]
1657+
public string P4 { get; set; }
1658+
}
1659+
1660+
[OptionsValidator]
1661+
public partial class MyOptionsValidator : IValidateOptions<MyOptions>
1662+
{
1663+
}
1664+
""");
1665+
1666+
Assert.Empty(diagnostics);
1667+
Assert.Single(generatedSources);
1668+
1669+
var generatedSource = """
1670+
1671+
// <auto-generated/>
1672+
#nullable enable
1673+
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
1674+
namespace Test
1675+
{
1676+
partial class MyOptionsValidator
1677+
{
1678+
/// <summary>
1679+
/// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
1680+
/// </summary>
1681+
/// <param name="name">The name of the options instance being validated.</param>
1682+
/// <param name="options">The options instance.</param>
1683+
/// <returns>Validation result.</returns>
1684+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
1685+
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Test.MyOptions options)
1686+
{
1687+
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
1688+
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
1689+
var validationResults = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationResult>();
1690+
var validationAttributes = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(1);
1691+
1692+
context.MemberName = "P1";
1693+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P1" : $"{name}.P1";
1694+
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
1695+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P1, context, validationResults, validationAttributes))
1696+
{
1697+
(builder ??= new()).AddResults(validationResults);
1698+
}
1699+
1700+
context.MemberName = "P2";
1701+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P2" : $"{name}.P2";
1702+
validationResults.Clear();
1703+
validationAttributes.Clear();
1704+
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2);
1705+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes))
1706+
{
1707+
(builder ??= new()).AddResults(validationResults);
1708+
}
1709+
1710+
context.MemberName = "P3";
1711+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P3" : $"{name}.P3";
1712+
validationResults.Clear();
1713+
validationAttributes.Clear();
1714+
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3);
1715+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes))
1716+
{
1717+
(builder ??= new()).AddResults(validationResults);
1718+
}
1719+
1720+
context.MemberName = "P4";
1721+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.P4" : $"{name}.P4";
1722+
validationResults.Clear();
1723+
validationAttributes.Clear();
1724+
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4);
1725+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes))
1726+
{
1727+
(builder ??= new()).AddResults(validationResults);
1728+
}
1729+
1730+
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
1731+
}
1732+
}
1733+
}
1734+
namespace __OptionValidationStaticInstances
1735+
{
1736+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
1737+
file static class __Attributes
1738+
{
1739+
internal static readonly global::System.ComponentModel.DataAnnotations.RequiredAttribute A1 = new global::System.ComponentModel.DataAnnotations.RequiredAttribute();
1740+
1741+
internal static readonly global::System.ComponentModel.DataAnnotations.LengthAttribute A2 = new global::System.ComponentModel.DataAnnotations.LengthAttribute(
1742+
(int)10,
1743+
(int)20);
1744+
1745+
internal static readonly global::System.ComponentModel.DataAnnotations.AllowedValuesAttribute A3 = new global::System.ComponentModel.DataAnnotations.AllowedValuesAttribute(
1746+
(int)10, (int)20, (int)30);
1747+
1748+
internal static readonly global::System.ComponentModel.DataAnnotations.DeniedValuesAttribute A4 = new global::System.ComponentModel.DataAnnotations.DeniedValuesAttribute(
1749+
"One", "Ten", "Hundred");
1750+
}
1751+
}
1752+
namespace __OptionValidationStaticInstances
1753+
{
1754+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
1755+
file static class __Validators
1756+
{
1757+
}
1758+
}
1759+
1760+
""";
1761+
Assert.Equal(generatedSource.Replace("\r\n", "\n"), generatedSources[0].SourceText.ToString().Replace("\r\n", "\n"));
1762+
}
1763+
16411764
private static CSharpCompilation CreateCompilationForOptionsSource(string assemblyName, string source, string? refAssemblyPath = null)
16421765
{
16431766
// Ensure the generated source compiles
@@ -1676,7 +1799,7 @@ private static CSharpCompilation CreateCompilationForOptionsSource(string assemb
16761799
refAssemblies.Add(refAssembly);
16771800
}
16781801

1679-
return await RoslynTestUtils.RunGenerator(new Generator(), refAssemblies.ToArray(), new List<string> { source }, includeBaseReferences: true, languageVersion).ConfigureAwait(false);
1802+
return await RoslynTestUtils.RunGenerator(new OptionsValidatorGenerator(), refAssemblies.ToArray(), new List<string> { source }, includeBaseReferences: true, languageVersion).ConfigureAwait(false);
16801803
}
16811804

16821805
private static async Task<(IReadOnlyList<Diagnostic> diagnostics, ImmutableArray<GeneratedSourceResult> generatedSources)> RunGenerator(
@@ -1733,7 +1856,7 @@ private static CSharpCompilation CreateCompilationForOptionsSource(string assemb
17331856
assemblies.Add(Assembly.GetAssembly(typeof(Microsoft.Extensions.Options.ValidateObjectMembersAttribute))!);
17341857
}
17351858

1736-
var result = await RoslynTestUtils.RunGenerator(new Generator(), assemblies.ToArray(), new[] { text })
1859+
var result = await RoslynTestUtils.RunGenerator(new OptionsValidatorGenerator(), assemblies.ToArray(), new[] { text })
17371860
.ConfigureAwait(false);
17381861

17391862
return result;

src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,37 @@ public void TestValidationWithCyclicReferences()
211211
ValidateOptionsResult result2 = dataAnnotationValidateOptions.Validate("MyOptions", options);
212212
Assert.True(result1.Succeeded);
213213
}
214+
215+
#if NET8_0_OR_GREATER
216+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
217+
public void TestNewDataAnnotationFailures()
218+
{
219+
NewAttributesValidator sourceGenValidator = new();
220+
221+
OptionsUsingNewAttributes validOptions = new()
222+
{
223+
P1 = "123456", P2 = 2, P3 = 4, P4 = "c", P5 = "d"
224+
};
225+
226+
ValidateOptionsResult result = sourceGenValidator.Validate("OptionsUsingNewAttributes", validOptions);
227+
Assert.True(result.Succeeded);
228+
229+
OptionsUsingNewAttributes invalidOptions = new()
230+
{
231+
P1 = "123", P2 = 4, P3 = 1, P4 = "e", P5 = "c"
232+
};
233+
234+
result = sourceGenValidator.Validate("OptionsUsingNewAttributes", invalidOptions);
235+
236+
Assert.Equal(new []{
237+
"P1: The field OptionsUsingNewAttributes.P1 must be a string or collection type with a minimum length of '5' and maximum length of '10'.",
238+
"P2: The OptionsUsingNewAttributes.P2 field does not equal any of the values specified in AllowedValuesAttribute.",
239+
"P3: The OptionsUsingNewAttributes.P3 field equals one of the values specified in DeniedValuesAttribute.",
240+
"P4: The OptionsUsingNewAttributes.P4 field does not equal any of the values specified in AllowedValuesAttribute.",
241+
"P5: The OptionsUsingNewAttributes.P5 field equals one of the values specified in DeniedValuesAttribute."
242+
}, result.Failures);
243+
}
244+
#endif // NET8_0_OR_GREATER
214245
}
215246

216247
public class MyOptions
@@ -270,4 +301,29 @@ public struct MyOptionsStruct
270301
public partial class MySourceGenOptionsValidator : IValidateOptions<MyOptions>
271302
{
272303
}
304+
305+
#if NET8_0_OR_GREATER
306+
public class OptionsUsingNewAttributes
307+
{
308+
[Length(5, 10)]
309+
public string P1 { get; set; }
310+
311+
[AllowedValues(1, 2, 3)]
312+
public int P2 { get; set; }
313+
314+
[DeniedValues(1, 2, 3)]
315+
public int P3 { get; set; }
316+
317+
[AllowedValues(new object?[] { "a", "b", "c" })]
318+
public string P4 { get; set; }
319+
320+
[DeniedValues(new object?[] { "a", "b", "c" })]
321+
public string P5 { get; set; }
322+
}
323+
324+
[OptionsValidator]
325+
public partial class NewAttributesValidator : IValidateOptions<OptionsUsingNewAttributes>
326+
{
327+
}
328+
#endif // NET8_0_OR_GREATER
273329
}

src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/EmitterTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public async Task TestEmitter()
3232
}
3333

3434
var (d, r) = await RoslynTestUtils.RunGenerator(
35-
new Generator(),
35+
new OptionsValidatorGenerator(),
3636
new[]
3737
{
3838
Assembly.GetAssembly(typeof(RequiredAttribute))!,

0 commit comments

Comments
 (0)