Skip to content

Commit a8d5e7d

Browse files
authored
Options Source Gen Fixes (#91363)
1 parent 97a98cd commit a8d5e7d

File tree

9 files changed

+740
-763
lines changed

9 files changed

+740
-763
lines changed

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

+66-19
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Collections.Immutable;
67
using System.Linq;
78
using System.Threading;
89
using Microsoft.CodeAnalysis;
910
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
1012

1113
namespace Microsoft.Extensions.Options.Generators
1214
{
@@ -25,6 +27,7 @@ internal sealed class Emitter : EmitterBase
2527
private string _staticValidationAttributeHolderClassFQN;
2628
private string _staticValidatorHolderClassFQN;
2729
private string _modifier;
30+
private string _TryGetValueNullableAnnotation;
2831

2932
private sealed record StaticFieldInfo(string FieldTypeFQN, int FieldOrder, string FieldName, IList<string> InstantiationLines);
3033

@@ -37,13 +40,14 @@ public Emitter(Compilation compilation, bool emitPreamble = true) : base(emitPre
3740
else
3841
{
3942
_modifier = "internal";
40-
string suffix = $"_{new Random().Next():X8}";
43+
string suffix = $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}";
4144
_staticValidationAttributeHolderClassName += suffix;
4245
_staticValidatorHolderClassName += suffix;
4346
}
4447

4548
_staticValidationAttributeHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidationAttributeHolderClassName}";
4649
_staticValidatorHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidatorHolderClassName}";
50+
_TryGetValueNullableAnnotation = GetNullableAnnotationStringForTryValidateValueToUseInGeneratedCode(compilation);
4751
}
4852

4953
public string Emit(
@@ -65,6 +69,31 @@ public string Emit(
6569
return Capture();
6670
}
6771

72+
/// <summary>
73+
/// Returns the nullable annotation string to use in the code generation according to the first parameter of
74+
/// <see cref="System.ComponentModel.DataAnnotations.Validator.TryValidateValue(object, ValidationContext, ICollection{ValidationResult}, IEnumerable{ValidationAttribute})"/> is nullable annotated.
75+
/// </summary>
76+
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
77+
/// <returns>"!" if the first parameter is not nullable annotated, otherwise an empty string.</returns>
78+
/// <remarks>
79+
/// In .NET 8.0 we have changed the nullable annotation on first parameter of the method cref="System.ComponentModel.DataAnnotations.Validator.TryValidateValue(object, ValidationContext, ICollection{ValidationResult}, IEnumerable{ValidationAttribute})"/>
80+
/// The source generator need to detect if we need to append "!" to the first parameter of the method call when running on down-level versions.
81+
/// </remarks>
82+
private static string GetNullableAnnotationStringForTryValidateValueToUseInGeneratedCode(Compilation compilation)
83+
{
84+
INamedTypeSymbol? validatorTypeSymbol = compilation.GetBestTypeByMetadataName("System.ComponentModel.DataAnnotations.Validator");
85+
if (validatorTypeSymbol is not null)
86+
{
87+
ImmutableArray<ISymbol> members = validatorTypeSymbol.GetMembers("TryValidateValue");
88+
if (members.Length == 1 && members[0] is IMethodSymbol tryValidateValueMethod)
89+
{
90+
return tryValidateValueMethod.Parameters[0].NullableAnnotation == NullableAnnotation.NotAnnotated ? "!" : string.Empty;
91+
}
92+
}
93+
94+
return "!";
95+
}
96+
6897
private void GenValidatorType(ValidatorType vt, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
6998
{
7099
if (vt.Namespace.Length > 0)
@@ -161,7 +190,7 @@ private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate)
161190
{
162191
if (modelToValidate.SelfValidates)
163192
{
164-
OutLn($"builder.AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));");
193+
OutLn($"(builder ??= new()).AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));");
165194
OutLn();
166195
}
167196
}
@@ -182,8 +211,7 @@ private void GenModelValidationMethod(
182211

183212
OutLn($"public {(makeStatic ? "static " : string.Empty)}global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, {modelToValidate.Name} options)");
184213
OutOpenBrace();
185-
OutLn($"var baseName = (string.IsNullOrEmpty(name) ? \"{modelToValidate.SimpleName}\" : name) + \".\";");
186-
OutLn($"var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();");
214+
OutLn($"global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;");
187215
OutLn($"var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);");
188216

189217
int capacity = modelToValidate.MembersToValidate.Max(static vm => vm.ValidationAttributes.Count);
@@ -199,33 +227,33 @@ private void GenModelValidationMethod(
199227
{
200228
if (vm.ValidationAttributes.Count > 0)
201229
{
202-
GenMemberValidation(vm, ref staticValidationAttributesDict, cleanListsBeforeUse);
230+
GenMemberValidation(vm, modelToValidate.SimpleName, ref staticValidationAttributesDict, cleanListsBeforeUse);
203231
cleanListsBeforeUse = true;
204232
OutLn();
205233
}
206234

207235
if (vm.TransValidatorType is not null)
208236
{
209-
GenTransitiveValidation(vm, ref staticValidatorsDict);
237+
GenTransitiveValidation(vm, modelToValidate.SimpleName, ref staticValidatorsDict);
210238
OutLn();
211239
}
212240

213241
if (vm.EnumerationValidatorType is not null)
214242
{
215-
GenEnumerationValidation(vm, ref staticValidatorsDict);
243+
GenEnumerationValidation(vm, modelToValidate.SimpleName, ref staticValidatorsDict);
216244
OutLn();
217245
}
218246
}
219247

220248
GenModelSelfValidationIfNecessary(modelToValidate);
221-
OutLn($"return builder.Build();");
249+
OutLn($"return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();");
222250
OutCloseBrace();
223251
}
224252

225-
private void GenMemberValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, bool cleanListsBeforeUse)
253+
private void GenMemberValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, bool cleanListsBeforeUse)
226254
{
227255
OutLn($"context.MemberName = \"{vm.Name}\";");
228-
OutLn($"context.DisplayName = baseName + \"{vm.Name}\";");
256+
OutLn($"context.DisplayName = string.IsNullOrEmpty(name) ? \"{modelName}.{vm.Name}\" : $\"{{name}}.{vm.Name}\";");
229257

230258
if (cleanListsBeforeUse)
231259
{
@@ -239,9 +267,9 @@ private void GenMemberValidation(ValidatedMember vm, ref Dictionary<string, Stat
239267
OutLn($"validationAttributes.Add({_staticValidationAttributeHolderClassFQN}.{staticValidationAttributeInstance.FieldName});");
240268
}
241269

242-
OutLn($"if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.{vm.Name}!, context, validationResults, validationAttributes))");
270+
OutLn($"if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.{vm.Name}{_TryGetValueNullableAnnotation}, context, validationResults, validationAttributes))");
243271
OutOpenBrace();
244-
OutLn($"builder.AddResults(validationResults);");
272+
OutLn($"(builder ??= new()).AddResults(validationResults);");
245273
OutCloseBrace();
246274
}
247275

@@ -305,7 +333,7 @@ private StaticFieldInfo GetOrAddStaticValidationAttribute(ref Dictionary<string,
305333
return staticValidationAttributeInstance;
306334
}
307335

308-
private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
336+
private void GenTransitiveValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
309337
{
310338
string callSequence;
311339
if (vm.TransValidateTypeIsSynthetic)
@@ -321,20 +349,22 @@ private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary<string,
321349

322350
var valueAccess = (vm.IsNullable && vm.IsValueType) ? ".Value" : string.Empty;
323351

352+
var baseName = $"string.IsNullOrEmpty(name) ? \"{modelName}.{vm.Name}\" : $\"{{name}}.{vm.Name}\"";
353+
324354
if (vm.IsNullable)
325355
{
326356
OutLn($"if (options.{vm.Name} is not null)");
327357
OutOpenBrace();
328-
OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
358+
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({baseName}, options.{vm.Name}{valueAccess}));");
329359
OutCloseBrace();
330360
}
331361
else
332362
{
333-
OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
363+
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({baseName}, options.{vm.Name}{valueAccess}));");
334364
}
335365
}
336366

337-
private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
367+
private void GenEnumerationValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
338368
{
339369
var valueAccess = (vm.IsValueType && vm.IsNullable) ? ".Value" : string.Empty;
340370
var enumeratedValueAccess = (vm.EnumeratedIsNullable && vm.EnumeratedIsValueType) ? ".Value" : string.Empty;
@@ -365,22 +395,25 @@ private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary<string,
365395
{
366396
OutLn($"if (o is not null)");
367397
OutOpenBrace();
368-
OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count}}]\", o{enumeratedValueAccess}));");
398+
var propertyName = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count}}]\" : $\"{{name}}.{vm.Name}[{{count}}]\"";
399+
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({propertyName}, o{enumeratedValueAccess}));");
369400
OutCloseBrace();
370401

371402
if (!vm.EnumeratedMayBeNull)
372403
{
373404
OutLn($"else");
374405
OutOpenBrace();
375-
OutLn($"builder.AddError(baseName + $\"{vm.Name}[{{count}}] is null\");");
406+
var error = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count}}] is null\" : $\"{{name}}.{vm.Name}[{{count}}] is null\"";
407+
OutLn($"(builder ??= new()).AddError({error});");
376408
OutCloseBrace();
377409
}
378410

379411
OutLn($"count++;");
380412
}
381413
else
382414
{
383-
OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count++}}]\", o{enumeratedValueAccess}));");
415+
var propertyName = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count++}}] is null\" : $\"{{name}}.{vm.Name}[{{count++}}] is null\"";
416+
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({propertyName}, o{enumeratedValueAccess}));");
384417
}
385418

386419
OutCloseBrace();
@@ -405,5 +438,19 @@ private StaticFieldInfo GetOrAddStaticValidator(ref Dictionary<string, StaticFie
405438

406439
return staticValidatorInstance;
407440
}
441+
442+
/// <summary>
443+
/// Returns a non-randomized hash code for the given string.
444+
/// We always return a positive value.
445+
/// </summary>
446+
internal static int GetNonRandomizedHashCode(string s)
447+
{
448+
uint result = 2166136261u;
449+
foreach (char c in s)
450+
{
451+
result = (c ^ result) * 16777619;
452+
}
453+
return Math.Abs((int)result);
454+
}
408455
}
409456
}

src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<ItemGroup>
2222
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
23+
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
2324
<Compile Include="DiagDescriptors.cs" />
2425
<Compile Include="DiagDescriptorsBase.cs" />
2526
<Compile Include="Emitter.cs" />

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

+7
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ private static bool HasOpenGenerics(ITypeSymbol type, out string genericType)
240240
type = ((INamedTypeSymbol)type).TypeArguments[0];
241241
}
242242

243+
// Check first if the type is IEnumerable<T> interface
244+
if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _symbolHolder.GenericIEnumerableSymbol))
245+
{
246+
return ((INamedTypeSymbol)type).TypeArguments[0];
247+
}
248+
249+
// Check first if the type implement IEnumerable<T> interface
243250
foreach (var implementingInterface in type.AllInterfaces)
244251
{
245252
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)))

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ internal sealed record class SymbolHolder(
1414
INamedTypeSymbol DataTypeAttributeSymbol,
1515
INamedTypeSymbol ValidateOptionsSymbol,
1616
INamedTypeSymbol IValidatableObjectSymbol,
17+
INamedTypeSymbol GenericIEnumerableSymbol,
1718
INamedTypeSymbol TypeSymbol,
18-
INamedTypeSymbol? ValidateObjectMembersAttributeSymbol,
19-
INamedTypeSymbol? ValidateEnumeratedItemsAttributeSymbol);
19+
INamedTypeSymbol ValidateObjectMembersAttributeSymbol,
20+
INamedTypeSymbol ValidateEnumeratedItemsAttributeSymbol);
2021
}

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

+12-15
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,33 @@ internal static class SymbolLoader
1515
internal const string TypeOfType = "System.Type";
1616
internal const string ValidateObjectMembersAttribute = "Microsoft.Extensions.Options.ValidateObjectMembersAttribute";
1717
internal const string ValidateEnumeratedItemsAttribute = "Microsoft.Extensions.Options.ValidateEnumeratedItemsAttribute";
18+
internal const string GenericIEnumerableType = "System.Collections.Generic.IEnumerable`1";
1819

1920
public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder)
2021
{
21-
INamedTypeSymbol? GetSymbol(string metadataName, bool optional = false)
22-
{
23-
var symbol = compilation.GetTypeByMetadataName(metadataName);
24-
if (symbol == null && !optional)
25-
{
26-
return null;
27-
}
28-
29-
return symbol;
30-
}
22+
INamedTypeSymbol? GetSymbol(string metadataName) => compilation.GetTypeByMetadataName(metadataName);
3123

3224
// required
3325
var optionsValidatorSymbol = GetSymbol(OptionsValidatorAttribute);
3426
var validationAttributeSymbol = GetSymbol(ValidationAttribute);
3527
var dataTypeAttributeSymbol = GetSymbol(DataTypeAttribute);
3628
var ivalidatableObjectSymbol = GetSymbol(IValidatableObjectType);
3729
var validateOptionsSymbol = GetSymbol(IValidateOptionsType);
30+
var genericIEnumerableSymbol = GetSymbol(GenericIEnumerableType);
3831
var typeSymbol = GetSymbol(TypeOfType);
32+
var validateObjectMembersAttribute = GetSymbol(ValidateObjectMembersAttribute);
33+
var validateEnumeratedItemsAttribute = GetSymbol(ValidateEnumeratedItemsAttribute);
3934

4035
#pragma warning disable S1067 // Expressions should not be too complex
4136
if (optionsValidatorSymbol == null ||
4237
validationAttributeSymbol == null ||
4338
dataTypeAttributeSymbol == null ||
4439
ivalidatableObjectSymbol == null ||
4540
validateOptionsSymbol == null ||
46-
typeSymbol == null)
41+
genericIEnumerableSymbol == null ||
42+
typeSymbol == null ||
43+
validateObjectMembersAttribute == null ||
44+
validateEnumeratedItemsAttribute == null)
4745
{
4846
symbolHolder = default;
4947
return false;
@@ -56,11 +54,10 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
5654
dataTypeAttributeSymbol,
5755
validateOptionsSymbol,
5856
ivalidatableObjectSymbol,
57+
genericIEnumerableSymbol,
5958
typeSymbol,
60-
61-
// optional
62-
GetSymbol(ValidateObjectMembersAttribute, optional: true),
63-
GetSymbol(ValidateEnumeratedItemsAttribute, optional: true));
59+
validateObjectMembersAttribute,
60+
validateEnumeratedItemsAttribute);
6461

6562
return true;
6663
}

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

+8-9
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,30 @@ partial struct MyOptionsValidator
7070
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
7171
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::HelloWorld.MyOptions options)
7272
{
73-
var baseName = (string.IsNullOrEmpty(name) ? "MyOptions" : name) + ".";
74-
var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();
73+
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
7574
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
7675
var validationResults = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationResult>();
7776
var validationAttributes = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(1);
7877
7978
context.MemberName = "Val1";
80-
context.DisplayName = baseName + "Val1";
79+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val1" : $"{name}.Val1";
8180
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
82-
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1!, context, validationResults, validationAttributes))
81+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1, context, validationResults, validationAttributes))
8382
{
84-
builder.AddResults(validationResults);
83+
(builder ??= new()).AddResults(validationResults);
8584
}
8685
8786
context.MemberName = "Val2";
88-
context.DisplayName = baseName + "Val2";
87+
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val2" : $"{name}.Val2";
8988
validationResults.Clear();
9089
validationAttributes.Clear();
9190
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2);
92-
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2!, context, validationResults, validationAttributes))
91+
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2, context, validationResults, validationAttributes))
9392
{
94-
builder.AddResults(validationResults);
93+
(builder ??= new()).AddResults(validationResults);
9594
}
9695
97-
return builder.Build();
96+
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
9897
}
9998
}
10099
}

0 commit comments

Comments
 (0)