diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md
index 2d1eab1db0293..c3732de3b02a1 100644
--- a/docs/project/list-of-diagnostics.md
+++ b/docs/project/list-of-diagnostics.md
@@ -235,7 +235,24 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
| __`SYSLIB1116`__ | *_`SYSLIB1100`-`SYSLIB1118` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1117`__ | *_`SYSLIB1100`-`SYSLIB1118` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1118`__ | *_`SYSLIB1100`-`SYSLIB1118` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
-
+| __`SYSLIB1201`__ | Options validation generator: Can't use 'ValidateObjectMembersAttribute' or `ValidateEnumeratedItemsAttribute` on fields or properties with open generic types. |
+| __`SYSLIB1202`__ | Options validation generator: A member type has no fields or properties to validate. |
+| __`SYSLIB1203`__ | Options validation generator: A type has no fields or properties to validate. |
+| __`SYSLIB1204`__ | Options validation generator: A type annotated with `OptionsValidatorAttribute` doesn't implement the necessary interface. |
+| __`SYSLIB1205`__ | Options validation generator: A type already includes an implementation of the 'Validate' method. |
+| __`SYSLIB1206`__ | Options validation generator: Can't validate private fields or properties. |
+| __`SYSLIB1207`__ | Options validation generator: Member type is not enumerable. |
+| __`SYSLIB1208`__ | Options validation generator: Validators used for transitive or enumerable validation must have a constructor with no parameters. |
+| __`SYSLIB1209`__ | Options validation generator: `OptionsValidatorAttribute` can't be applied to a static class. |
+| __`SYSLIB1210`__ | Options validation generator: Null validator type specified for the `ValidateObjectMembersAttribute` or 'ValidateEnumeratedItemsAttribute' attributes. |
+| __`SYSLIB1211`__ | Options validation generator: Unsupported circular references in model types. |
+| __`SYSLIB1212`__ | Options validation generator: Member potentially missing transitive validation. |
+| __`SYSLIB1213`__ | Options validation generator: Member potentially missing enumerable validation. |
+| __`SYSLIB1214`__ | *_`SYSLIB1214`-`SYSLIB1218` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
+| __`SYSLIB1215`__ | *_`SYSLIB1214`-`SYSLIB1218` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
+| __`SYSLIB1216`__ | *_`SYSLIB1214`-`SYSLIB1218` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
+| __`SYSLIB1217`__ | *_`SYSLIB1214`-`SYSLIB1218` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
+| __`SYSLIB1218`__ | *_`SYSLIB1214`-`SYSLIB1218` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
### Diagnostic Suppressions (`SYSLIBSUPPRESS****`)
diff --git a/src/libraries/Common/src/System/ThrowHelper.cs b/src/libraries/Common/src/System/ThrowHelper.cs
index 921394be4cd17..4257c05891e32 100644
--- a/src/libraries/Common/src/System/ThrowHelper.cs
+++ b/src/libraries/Common/src/System/ThrowHelper.cs
@@ -31,6 +31,46 @@ internal static void ThrowIfNull(
[DoesNotReturn]
#endif
private static void Throw(string? paramName) => throw new ArgumentNullException(paramName);
+
+ ///
+ /// Throws either an or an
+ /// if the specified string is or whitespace respectively.
+ ///
+ /// String to be checked for or whitespace.
+ /// The name of the parameter being checked.
+ /// The original value of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+#if NETCOREAPP3_0_OR_GREATER
+ [return: NotNull]
+#endif
+ public static string IfNullOrWhitespace(
+#if NETCOREAPP3_0_OR_GREATER
+ [NotNull]
+#endif
+ string? argument,
+ [CallerArgumentExpression(nameof(argument))] string paramName = "")
+ {
+#if !NETCOREAPP3_1_OR_GREATER
+ if (argument == null)
+ {
+ throw new ArgumentNullException(paramName);
+ }
+#endif
+
+ if (string.IsNullOrWhiteSpace(argument))
+ {
+ if (argument == null)
+ {
+ throw new ArgumentNullException(paramName);
+ }
+ else
+ {
+ throw new ArgumentException(paramName, "Argument is whitespace");
+ }
+ }
+
+ return argument;
+ }
}
}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs
new file mode 100644
index 0000000000000..e76363fee0e94
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using System;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal sealed class DiagDescriptors : DiagDescriptorsBase
+ {
+ private const string Category = "Microsoft.Extensions.Options.SourceGeneration";
+
+ public static DiagnosticDescriptor CantUseWithGenericTypes { get; } = Make(
+ id: "SYSLIB1201",
+ title: SR.CantUseWithGenericTypesTitle,
+ messageFormat: SR.CantUseWithGenericTypesMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor NoEligibleMember { get; } = Make(
+ id: "SYSLIB1202",
+ title: SR.NoEligibleMemberTitle,
+ messageFormat: SR.NoEligibleMemberMessage,
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Warning);
+
+ public static DiagnosticDescriptor NoEligibleMembersFromValidator { get; } = Make(
+ id: "SYSLIB1203",
+ title: SR.NoEligibleMembersFromValidatorTitle,
+ messageFormat: SR.NoEligibleMembersFromValidatorMessage,
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Warning);
+
+ public static DiagnosticDescriptor DoesntImplementIValidateOptions { get; } = Make(
+ id: "SYSLIB1204",
+ title: SR.DoesntImplementIValidateOptionsTitle,
+ messageFormat: SR.DoesntImplementIValidateOptionsMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor AlreadyImplementsValidateMethod { get; } = Make(
+ id: "SYSLIB1205",
+ title: SR.AlreadyImplementsValidateMethodTitle,
+ messageFormat: SR.AlreadyImplementsValidateMethodMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor MemberIsInaccessible { get; } = Make(
+ id: "SYSLIB1206",
+ title: SR.MemberIsInaccessibleTitle,
+ messageFormat: SR.MemberIsInaccessibleMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor NotEnumerableType { get; } = Make(
+ id: "SYSLIB1207",
+ title: SR.NotEnumerableTypeTitle,
+ messageFormat: SR.NotEnumerableTypeMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor ValidatorsNeedSimpleConstructor { get; } = Make(
+ id: "SYSLIB1208",
+ title: SR.ValidatorsNeedSimpleConstructorTitle,
+ messageFormat: SR.ValidatorsNeedSimpleConstructorMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor CantBeStaticClass { get; } = Make(
+ id: "SYSLIB1209",
+ title: SR.CantBeStaticClassTitle,
+ messageFormat: SR.CantBeStaticClassMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor NullValidatorType { get; } = Make(
+ id: "SYSLIB1210",
+ title: SR.NullValidatorTypeTitle,
+ messageFormat: SR.NullValidatorTypeMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor CircularTypeReferences { get; } = Make(
+ id: "SYSLIB1211",
+ title: SR.CircularTypeReferencesTitle,
+ messageFormat: SR.CircularTypeReferencesMessage,
+ category: Category);
+
+ public static DiagnosticDescriptor PotentiallyMissingTransitiveValidation { get; } = Make(
+ id: "SYSLIB1212",
+ title: SR.PotentiallyMissingTransitiveValidationTitle,
+ messageFormat: SR.PotentiallyMissingTransitiveValidationMessage,
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Warning);
+
+ public static DiagnosticDescriptor PotentiallyMissingEnumerableValidation { get; } = Make(
+ id: "SYSLIB1213",
+ title: SR.PotentiallyMissingEnumerableValidationTitle,
+ messageFormat: SR.PotentiallyMissingEnumerableValidationMessage,
+ category: Category,
+ defaultSeverity: DiagnosticSeverity.Warning);
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptorsBase.cs b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptorsBase.cs
new file mode 100644
index 0000000000000..f749e02394a67
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptorsBase.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ #pragma warning disable CA1052 // Static holder types should be Static or NotInheritable
+ internal class DiagDescriptorsBase
+ #pragma warning restore CA1052
+ {
+ protected static DiagnosticDescriptor Make(
+ string id,
+ string title,
+ string messageFormat,
+ string category,
+ DiagnosticSeverity defaultSeverity = DiagnosticSeverity.Error,
+ bool isEnabledByDefault = true)
+ {
+ return new(
+ id,
+ title,
+ messageFormat,
+ category,
+ defaultSeverity,
+ isEnabledByDefault);
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs
new file mode 100644
index 0000000000000..91ad41a630bf6
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs
@@ -0,0 +1,387 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ ///
+ /// Emits option validation.
+ ///
+ internal sealed class Emitter : EmitterBase
+ {
+ private const string StaticValidationAttributeHolderClassName = "__Attributes";
+ private const string StaticValidatorHolderClassName = "__Validators";
+ private const string StaticFieldHolderClassesNamespace = "__OptionValidationStaticInstances";
+ private const string StaticValidationAttributeHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{StaticValidationAttributeHolderClassName}";
+ private const string StaticValidatorHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{StaticValidatorHolderClassName}";
+ private const string StaticListType = "global::System.Collections.Generic.List";
+ private const string StaticValidationResultType = "global::System.ComponentModel.DataAnnotations.ValidationResult";
+ private const string StaticValidationAttributeType = "global::System.ComponentModel.DataAnnotations.ValidationAttribute";
+
+ private sealed record StaticFieldInfo(string FieldTypeFQN, int FieldOrder, string FieldName, IList InstantiationLines);
+
+ public string Emit(
+ IEnumerable validatorTypes,
+ CancellationToken cancellationToken)
+ {
+ var staticValidationAttributesDict = new Dictionary();
+ var staticValidatorsDict = new Dictionary();
+
+ foreach (var vt in validatorTypes.OrderBy(static lt => lt.Namespace + "." + lt.Name))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ GenValidatorType(vt, ref staticValidationAttributesDict, ref staticValidatorsDict);
+ }
+
+ GenStaticClassWithStaticReadonlyFields(staticValidationAttributesDict.Values, StaticFieldHolderClassesNamespace, StaticValidationAttributeHolderClassName);
+ GenStaticClassWithStaticReadonlyFields(staticValidatorsDict.Values, StaticFieldHolderClassesNamespace, StaticValidatorHolderClassName);
+
+ return Capture();
+ }
+
+ private void GenValidatorType(ValidatorType vt, ref Dictionary staticValidationAttributesDict, ref Dictionary staticValidatorsDict)
+ {
+ if (vt.Namespace.Length > 0)
+ {
+ OutLn($"namespace {vt.Namespace}");
+ OutOpenBrace();
+ }
+
+ foreach (var p in vt.ParentTypes)
+ {
+ OutLn(p);
+ OutOpenBrace();
+ }
+
+ if (vt.IsSynthetic)
+ {
+ OutGeneratedCodeAttribute();
+ OutLn($"internal sealed partial {vt.DeclarationKeyword} {vt.Name}");
+ }
+ else
+ {
+ OutLn($"partial {vt.DeclarationKeyword} {vt.Name}");
+ }
+
+ OutOpenBrace();
+
+ for (var i = 0; i < vt.ModelsToValidate.Count; i++)
+ {
+ var modelToValidate = vt.ModelsToValidate[i];
+
+ GenModelValidationMethod(modelToValidate, vt.IsSynthetic, ref staticValidationAttributesDict, ref staticValidatorsDict);
+ }
+
+ OutCloseBrace();
+
+ foreach (var _ in vt.ParentTypes)
+ {
+ OutCloseBrace();
+ }
+
+ if (vt.Namespace.Length > 0)
+ {
+ OutCloseBrace();
+ }
+ }
+
+ private void GenStaticClassWithStaticReadonlyFields(IEnumerable staticFields, string classNamespace, string className)
+ {
+ OutLn($"namespace {classNamespace}");
+ OutOpenBrace();
+
+ OutGeneratedCodeAttribute();
+ OutLn($"internal static class {className}");
+ OutOpenBrace();
+
+ var staticValidationAttributes = staticFields
+ .OrderBy(x => x.FieldOrder)
+ .ToArray();
+
+ for (var i = 0; i < staticValidationAttributes.Length; i++)
+ {
+ var attributeInstance = staticValidationAttributes[i];
+ OutIndent();
+ Out($"internal static readonly {attributeInstance.FieldTypeFQN} {attributeInstance.FieldName} = ");
+ for (var j = 0; j < attributeInstance.InstantiationLines.Count; j++)
+ {
+ var line = attributeInstance.InstantiationLines[j];
+ Out(line);
+ if (j != attributeInstance.InstantiationLines.Count - 1)
+ {
+ OutLn();
+ OutIndent();
+ }
+ else
+ {
+ Out(';');
+ }
+ }
+
+ OutLn();
+
+ if (i != staticValidationAttributes.Length - 1)
+ {
+ OutLn();
+ }
+ }
+
+ OutCloseBrace();
+
+ OutCloseBrace();
+ }
+
+ private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate)
+ {
+ if (modelToValidate.SelfValidates)
+ {
+ OutLn($"builder.AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));");
+ OutLn();
+ }
+ }
+
+ private void GenModelValidationMethod(
+ ValidatedModel modelToValidate,
+ bool makeStatic,
+ ref Dictionary staticValidationAttributesDict,
+ ref Dictionary staticValidatorsDict)
+ {
+ OutLn($"/// ");
+ OutLn($"/// Validates a specific named options instance (or all when is ).");
+ OutLn($"/// ");
+ OutLn($"/// The name of the options instance being validated.");
+ OutLn($"/// The options instance.");
+ OutLn($"/// Validation result.");
+ OutGeneratedCodeAttribute();
+
+ OutLn($"public {(makeStatic ? "static " : string.Empty)}global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, {modelToValidate.Name} options)");
+ OutOpenBrace();
+ OutLn($"var baseName = (string.IsNullOrEmpty(name) ? \"{modelToValidate.SimpleName}\" : name) + \".\";");
+ OutLn($"var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();");
+ OutLn($"var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);");
+
+ int capacity = modelToValidate.MembersToValidate.Max(static vm => vm.ValidationAttributes.Count);
+ if (capacity > 0)
+ {
+ OutLn($"var validationResults = new {StaticListType}<{StaticValidationResultType}>();");
+ OutLn($"var validationAttributes = new {StaticListType}<{StaticValidationAttributeType}>({capacity});");
+ }
+ OutLn();
+
+ bool cleanListsBeforeUse = false;
+ foreach (var vm in modelToValidate.MembersToValidate)
+ {
+ if (vm.ValidationAttributes.Count > 0)
+ {
+ GenMemberValidation(vm, ref staticValidationAttributesDict, cleanListsBeforeUse);
+ cleanListsBeforeUse = true;
+ OutLn();
+ }
+
+ if (vm.TransValidatorType is not null)
+ {
+ GenTransitiveValidation(vm, ref staticValidatorsDict);
+ OutLn();
+ }
+
+ if (vm.EnumerationValidatorType is not null)
+ {
+ GenEnumerationValidation(vm, ref staticValidatorsDict);
+ OutLn();
+ }
+ }
+
+ GenModelSelfValidationIfNecessary(modelToValidate);
+ OutLn($"return builder.Build();");
+ OutCloseBrace();
+ }
+
+ private void GenMemberValidation(ValidatedMember vm, ref Dictionary staticValidationAttributesDict, bool cleanListsBeforeUse)
+ {
+ OutLn($"context.MemberName = \"{vm.Name}\";");
+ OutLn($"context.DisplayName = baseName + \"{vm.Name}\";");
+
+ if (cleanListsBeforeUse)
+ {
+ OutLn($"validationResults.Clear();");
+ OutLn($"validationAttributes.Clear();");
+ }
+
+ foreach (var attr in vm.ValidationAttributes)
+ {
+ var staticValidationAttributeInstance = GetOrAddStaticValidationAttribute(ref staticValidationAttributesDict, attr);
+ OutLn($"validationAttributes.Add({StaticValidationAttributeHolderClassFQN}.{staticValidationAttributeInstance.FieldName});");
+ }
+
+ OutLn($"if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.{vm.Name}!, context, validationResults, validationAttributes))");
+ OutOpenBrace();
+ OutLn($"builder.AddResults(validationResults);");
+ OutCloseBrace();
+ }
+
+ private StaticFieldInfo GetOrAddStaticValidationAttribute(ref Dictionary staticValidationAttributesDict, ValidationAttributeInfo attr)
+ {
+ var attrInstantiationStatementLines = new List();
+
+ if (attr.ConstructorArguments.Count > 0)
+ {
+ attrInstantiationStatementLines.Add($"new {attr.AttributeName}(");
+
+ for (var i = 0; i < attr.ConstructorArguments.Count; i++)
+ {
+ if (i != attr.ConstructorArguments.Count - 1)
+ {
+ attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{attr.ConstructorArguments[i]},");
+ }
+ else
+ {
+ attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{attr.ConstructorArguments[i]})");
+ }
+ }
+ }
+ else
+ {
+ attrInstantiationStatementLines.Add($"new {attr.AttributeName}()");
+ }
+
+ if (attr.Properties.Count > 0)
+ {
+ attrInstantiationStatementLines.Add("{");
+
+ var propertiesOrderedByKey = attr.Properties
+ .OrderBy(p => p.Key)
+ .ToArray();
+
+ for (var i = 0; i < propertiesOrderedByKey.Length; i++)
+ {
+ var prop = propertiesOrderedByKey[i];
+ var notLast = i != propertiesOrderedByKey.Length - 1;
+ attrInstantiationStatementLines.Add($"{GetPaddingString(1)}{prop.Key} = {prop.Value}{(notLast ? "," : string.Empty)}");
+ }
+
+ attrInstantiationStatementLines.Add("}");
+ }
+
+ var instantiationStatement = string.Join(Environment.NewLine, attrInstantiationStatementLines);
+
+ if (!staticValidationAttributesDict.TryGetValue(instantiationStatement, out var staticValidationAttributeInstance))
+ {
+ var fieldNumber = staticValidationAttributesDict.Count + 1;
+ staticValidationAttributeInstance = new StaticFieldInfo(
+ FieldTypeFQN: attr.AttributeName,
+ FieldOrder: fieldNumber,
+ FieldName: $"A{fieldNumber}",
+ InstantiationLines: attrInstantiationStatementLines);
+
+ staticValidationAttributesDict.Add(instantiationStatement, staticValidationAttributeInstance);
+ }
+
+ return staticValidationAttributeInstance;
+ }
+
+ private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary staticValidatorsDict)
+ {
+ string callSequence;
+ if (vm.TransValidateTypeIsSynthetic)
+ {
+ callSequence = vm.TransValidatorType!;
+ }
+ else
+ {
+ var staticValidatorInstance = GetOrAddStaticValidator(ref staticValidatorsDict, vm.TransValidatorType!);
+
+ callSequence = $"{StaticValidatorHolderClassFQN}.{staticValidatorInstance.FieldName}";
+ }
+
+ var valueAccess = (vm.IsNullable && vm.IsValueType) ? ".Value" : string.Empty;
+
+ if (vm.IsNullable)
+ {
+ OutLn($"if (options.{vm.Name} is not null)");
+ OutOpenBrace();
+ OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
+ OutCloseBrace();
+ }
+ else
+ {
+ OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
+ }
+ }
+
+ private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary staticValidatorsDict)
+ {
+ var valueAccess = (vm.IsValueType && vm.IsNullable) ? ".Value" : string.Empty;
+ var enumeratedValueAccess = (vm.EnumeratedIsNullable && vm.EnumeratedIsValueType) ? ".Value" : string.Empty;
+ string callSequence;
+ if (vm.EnumerationValidatorTypeIsSynthetic)
+ {
+ callSequence = vm.EnumerationValidatorType!;
+ }
+ else
+ {
+ var staticValidatorInstance = GetOrAddStaticValidator(ref staticValidatorsDict, vm.EnumerationValidatorType!);
+
+ callSequence = $"{StaticValidatorHolderClassFQN}.{staticValidatorInstance.FieldName}";
+ }
+
+ if (vm.IsNullable)
+ {
+ OutLn($"if (options.{vm.Name} is not null)");
+ }
+
+ OutOpenBrace();
+
+ OutLn($"var count = 0;");
+ OutLn($"foreach (var o in options.{vm.Name}{valueAccess})");
+ OutOpenBrace();
+
+ if (vm.EnumeratedIsNullable)
+ {
+ OutLn($"if (o is not null)");
+ OutOpenBrace();
+ OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count}}]\", o{enumeratedValueAccess}));");
+ OutCloseBrace();
+
+ if (!vm.EnumeratedMayBeNull)
+ {
+ OutLn($"else");
+ OutOpenBrace();
+ OutLn($"builder.AddError(baseName + $\"{vm.Name}[{{count}}] is null\");");
+ OutCloseBrace();
+ }
+
+ OutLn($"count++;");
+ }
+ else
+ {
+ OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count++}}]\", o{enumeratedValueAccess}));");
+ }
+
+ OutCloseBrace();
+ OutCloseBrace();
+ }
+
+ #pragma warning disable CA1822 // Mark members as static: static should come before non-static, but we want the method to be here
+ private StaticFieldInfo GetOrAddStaticValidator(ref Dictionary staticValidatorsDict, string validatorTypeFQN)
+ #pragma warning restore CA1822
+ {
+ if (!staticValidatorsDict.TryGetValue(validatorTypeFQN, out var staticValidatorInstance))
+ {
+ var fieldNumber = staticValidatorsDict.Count + 1;
+ staticValidatorInstance = new StaticFieldInfo(
+ FieldTypeFQN: validatorTypeFQN,
+ FieldOrder: fieldNumber,
+ FieldName: $"V{fieldNumber}",
+ InstantiationLines: new[] { $"new {validatorTypeFQN}()" });
+
+ staticValidatorsDict.Add(validatorTypeFQN, staticValidatorInstance);
+ }
+
+ return staticValidatorInstance;
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/EmitterBase.cs b/src/libraries/Microsoft.Extensions.Options/gen/EmitterBase.cs
new file mode 100644
index 0000000000000..890c9bc598989
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/EmitterBase.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal class EmitterBase
+ {
+ public static string GeneratedCodeAttribute { get; } = $"global::System.CodeDom.Compiler.GeneratedCodeAttribute(" +
+ $"\"{typeof(EmitterBase).Assembly.GetName().Name}\", " +
+ $"\"{typeof(EmitterBase).Assembly.GetName().Version}\")";
+
+ public static string FilePreamble { get; } = @$"
+ //
+ #nullable enable
+ #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
+ ";
+
+ private const int DefaultStringBuilderCapacity = 1024;
+ private const int IndentChars = 4;
+
+ private readonly StringBuilder _sb = new(DefaultStringBuilderCapacity);
+ private readonly string[] _padding = new string[16];
+ private int _indent;
+
+ public EmitterBase(bool emitPreamble = true)
+ {
+ var padding = _padding;
+ for (int i = 0; i < padding.Length; i++)
+ {
+ padding[i] = new string(' ', i * IndentChars);
+ }
+
+ if (emitPreamble)
+ {
+ Out(FilePreamble);
+ }
+ }
+
+ protected void OutOpenBrace()
+ {
+ OutLn("{");
+ Indent();
+ }
+
+ protected void OutCloseBrace()
+ {
+ Unindent();
+ OutLn("}");
+ }
+
+ protected void OutCloseBraceWithExtra(string extra)
+ {
+ Unindent();
+ OutIndent();
+ Out("}");
+ Out(extra);
+ OutLn();
+ }
+
+ protected void OutIndent()
+ {
+ _ = _sb.Append(_padding[_indent]);
+ }
+
+ protected string GetPaddingString(byte indent)
+ {
+ return _padding[indent];
+ }
+
+ protected void OutLn()
+ {
+ _ = _sb.AppendLine();
+ }
+
+ protected void OutLn(string line)
+ {
+ OutIndent();
+ _ = _sb.AppendLine(line);
+ }
+
+ protected void OutPP(string line)
+ {
+ _ = _sb.AppendLine(line);
+ }
+
+ protected void OutEnumeration(IEnumerable e)
+ {
+ bool first = true;
+ foreach (var item in e)
+ {
+ if (!first)
+ {
+ Out(", ");
+ }
+
+ Out(item);
+ first = false;
+ }
+ }
+
+ protected void Out(string text) => _ = _sb.Append(text);
+ protected void Out(char ch) => _ = _sb.Append(ch);
+ protected void Indent() => _indent++;
+ protected void Unindent() => _indent--;
+ protected void OutGeneratedCodeAttribute() => OutLn($"[{GeneratedCodeAttribute}]");
+ protected string Capture() => _sb.ToString();
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs b/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs
new file mode 100644
index 0000000000000..b24f43b5170d3
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ [Generator]
+ public class Generator : IIncrementalGenerator
+ {
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ IncrementalValuesProvider<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> typeDeclarations = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ SymbolLoader.OptionsValidatorAttribute,
+ (node, _) => node is TypeDeclarationSyntax,
+ (context, _) => (TypeSyntax:context.TargetNode as TypeDeclarationSyntax, SemanticModel: context.SemanticModel))
+ .Where(static m => m.TypeSyntax is not null);
+
+ IncrementalValueProvider<(Compilation, ImmutableArray<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)>)> compilationAndTypes =
+ context.CompilationProvider.Combine(typeDeclarations.Collect());
+
+ context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => HandleAnnotatedTypes(source.Item1, source.Item2, spc));
+ }
+
+ private static void HandleAnnotatedTypes(Compilation compilation, ImmutableArray<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> types, SourceProductionContext context)
+ {
+ if (!SymbolLoader.TryLoad(compilation, out var symbolHolder))
+ {
+ // Not eligible compilation
+ return;
+ }
+
+ var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken);
+
+ var validatorTypes = parser.GetValidatorTypes(types);
+ if (validatorTypes.Count > 0)
+ {
+ var emitter = new Emitter();
+ var result = emitter.Emit(validatorTypes, context.CancellationToken);
+
+ context.AddSource("Validators.g.cs", SourceText.From(result, Encoding.UTF8));
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj b/src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj
new file mode 100644
index 0000000000000..12bc4a7c4d1e4
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj
@@ -0,0 +1,35 @@
+
+
+ netstandard2.0
+ false
+ false
+ true
+ cs
+ true
+ 4.4
+ $(MicrosoftCodeAnalysisVersion_4_4)
+ $(DefineConstants);ROSLYN4_4_OR_GREATER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedMember.cs b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedMember.cs
new file mode 100644
index 0000000000000..cfed013591674
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedMember.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal sealed record class ValidatedMember(
+ string Name,
+ List ValidationAttributes,
+ string? TransValidatorType,
+ bool TransValidateTypeIsSynthetic,
+ string? EnumerationValidatorType,
+ bool EnumerationValidatorTypeIsSynthetic,
+ bool IsNullable,
+ bool IsValueType,
+ bool EnumeratedIsNullable,
+ bool EnumeratedIsValueType,
+ bool EnumeratedMayBeNull);
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedModel.cs b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedModel.cs
new file mode 100644
index 0000000000000..0d40d930051b1
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatedModel.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal sealed record class ValidatedModel(
+ string Name,
+ string SimpleName,
+ bool SelfValidates,
+ List MembersToValidate);
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidationAttributeInfo.cs b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidationAttributeInfo.cs
new file mode 100644
index 0000000000000..419ceca3e1d29
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidationAttributeInfo.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal sealed record class ValidationAttributeInfo(string AttributeName)
+ {
+ public List ConstructorArguments { get; } = new();
+ public Dictionary Properties { get; } = new();
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatorType.cs b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatorType.cs
new file mode 100644
index 0000000000000..e104322c072ea
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Model/ValidatorType.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ internal sealed record class ValidatorType(
+ string Namespace,
+ string Name,
+ string NameWithoutGenerics,
+ string DeclarationKeyword,
+ List ParentTypes,
+ bool IsSynthetic,
+ IList ModelsToValidate);
+}
diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs
new file mode 100644
index 0000000000000..2a9215c676071
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs
@@ -0,0 +1,664 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.Extensions.Options.Generators
+{
+ ///
+ /// Holds an internal parser class that extracts necessary information for generating IValidateOptions.
+ ///
+ internal sealed class Parser
+ {
+ private const int NumValidationMethodArgs = 2;
+
+ private readonly CancellationToken _cancellationToken;
+ private readonly Compilation _compilation;
+ private readonly Action _reportDiagnostic;
+ private readonly SymbolHolder _symbolHolder;
+ private readonly Dictionary _synthesizedValidators = new(SymbolEqualityComparer.Default);
+ private readonly HashSet _visitedModelTypes = new(SymbolEqualityComparer.Default);
+
+ public Parser(
+ Compilation compilation,
+ Action reportDiagnostic,
+ SymbolHolder symbolHolder,
+ CancellationToken cancellationToken)
+ {
+ _compilation = compilation;
+ _cancellationToken = cancellationToken;
+ _reportDiagnostic = reportDiagnostic;
+ _symbolHolder = symbolHolder;
+ }
+
+ public IReadOnlyList GetValidatorTypes(IEnumerable<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> classes)
+ {
+ var results = new List();
+
+ foreach (var group in classes.GroupBy(x => x.TypeSyntax.SyntaxTree))
+ {
+ SemanticModel? sm = null;
+ foreach (var typeDec in group)
+ {
+ TypeDeclarationSyntax syntax = typeDec.TypeSyntax;
+ _cancellationToken.ThrowIfCancellationRequested();
+ sm ??= typeDec.SemanticModel;
+
+ var validatorType = sm.GetDeclaredSymbol(syntax) as ITypeSymbol;
+ if (validatorType is not null)
+ {
+ if (validatorType.IsStatic)
+ {
+ Diag(DiagDescriptors.CantBeStaticClass, syntax.GetLocation());
+ continue;
+ }
+
+ _visitedModelTypes.Clear();
+
+ var modelTypes = GetModelTypes(validatorType);
+ if (modelTypes.Count == 0)
+ {
+ // validator doesn't implement IValidateOptions
+ Diag(DiagDescriptors.DoesntImplementIValidateOptions, syntax.GetLocation(), validatorType.Name);
+ continue;
+ }
+
+ var modelsValidatorTypeValidates = new List(modelTypes.Count);
+
+ foreach (var modelType in modelTypes)
+ {
+ if (modelType.Kind == SymbolKind.ErrorType)
+ {
+ // the compiler will report this error for us
+ continue;
+ }
+ else
+ {
+ // keep track of the models we look at, to detect loops
+ _ = _visitedModelTypes.Add(modelType.WithNullableAnnotation(NullableAnnotation.None));
+ }
+
+ if (AlreadyImplementsValidateMethod(validatorType, modelType))
+ {
+ // this type already implements a validation function, we can't auto-generate a new one
+ Diag(DiagDescriptors.AlreadyImplementsValidateMethod, syntax.GetLocation(), validatorType.Name);
+ continue;
+ }
+
+ var membersToValidate = GetMembersToValidate(modelType, true);
+ if (membersToValidate.Count == 0)
+ {
+ // this type lacks any eligible members
+ Diag(DiagDescriptors.NoEligibleMembersFromValidator, syntax.GetLocation(), modelType.ToString(), validatorType.ToString());
+ continue;
+ }
+
+ modelsValidatorTypeValidates.Add(new ValidatedModel(
+ GetFQN(modelType),
+ modelType.Name,
+ ModelSelfValidates(modelType),
+ membersToValidate));
+ }
+
+ string keyword = GetTypeKeyword(validatorType);
+
+ // following code establishes the containment hierarchy for the generated type in terms of nested types
+
+ var parents = new List();
+ var parent = syntax.Parent as TypeDeclarationSyntax;
+
+ while (parent is not null && IsAllowedKind(parent.Kind()))
+ {
+ parents.Add($"partial {GetTypeKeyword(parent)} {parent.Identifier}{parent.TypeParameterList} {parent.ConstraintClauses}");
+ parent = parent.Parent as TypeDeclarationSyntax;
+ }
+
+ parents.Reverse();
+
+ results.Add(new ValidatorType(
+ validatorType.ContainingNamespace.IsGlobalNamespace ? string.Empty : validatorType.ContainingNamespace.ToString(),
+ GetMinimalFQN(validatorType),
+ GetMinimalFQNWithoutGenerics(validatorType),
+ keyword,
+ parents,
+ false,
+ modelsValidatorTypeValidates));
+ }
+ }
+ }
+
+ results.AddRange(_synthesizedValidators.Values);
+ _synthesizedValidators.Clear();
+
+ return results;
+ }
+
+ private static bool IsAllowedKind(SyntaxKind kind) =>
+ kind == SyntaxKind.ClassDeclaration ||
+ kind == SyntaxKind.StructDeclaration ||
+ kind == SyntaxKind.RecordStructDeclaration ||
+ kind == SyntaxKind.RecordDeclaration;
+
+ private static string GetTypeKeyword(ITypeSymbol type)
+ {
+ if (type.IsReferenceType)
+ {
+ return type.IsRecord ? "record class" : "class";
+ }
+
+ return type.IsRecord ? "record struct" : "struct";
+ }
+
+ private static string GetTypeKeyword(TypeDeclarationSyntax type) =>
+ type.Kind() switch
+ {
+ SyntaxKind.ClassDeclaration => "class",
+ SyntaxKind.RecordDeclaration => "record class",
+ SyntaxKind.RecordStructDeclaration => "record struct",
+ _ => type.Keyword.ValueText,
+ };
+
+ private static string GetFQN(ISymbol type)
+ => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));
+
+ private static string GetMinimalFQN(ISymbol type)
+ => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters));
+
+ private static string GetMinimalFQNWithoutGenerics(ISymbol type)
+ => type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
+
+ ///
+ /// Checks whether the given validator already implement the IValidationOptions>T< interface.
+ ///
+ private static bool AlreadyImplementsValidateMethod(INamespaceOrTypeSymbol validatorType, ISymbol modelType)
+ => validatorType
+ .GetMembers("Validate")
+ .Where(m => m.Kind == SymbolKind.Method)
+ .Select(m => (IMethodSymbol)m)
+ .Any(m => m.Parameters.Length == NumValidationMethodArgs
+ && m.Parameters[0].Type.SpecialType == SpecialType.System_String
+ && SymbolEqualityComparer.Default.Equals(m.Parameters[1].Type, modelType));
+
+ ///
+ /// Checks whether the given type contain any unbound generic type arguments.
+ ///
+ private static bool HasOpenGenerics(ITypeSymbol type, out string genericType)
+ {
+ if (type is INamedTypeSymbol mt)
+ {
+ if (mt.IsGenericType)
+ {
+ foreach (var ta in mt.TypeArguments)
+ {
+ if (ta.TypeKind == TypeKind.TypeParameter)
+ {
+ genericType = ta.Name;
+ return true;
+ }
+ }
+ }
+ }
+ else if (type is ITypeParameterSymbol)
+ {
+ genericType = type.Name;
+ return true;
+ }
+ else if (type is IArrayTypeSymbol ats)
+ {
+ return HasOpenGenerics(ats.ElementType, out genericType);
+ }
+
+ genericType = string.Empty;
+ return false;
+ }
+
+ private ITypeSymbol? GetEnumeratedType(ITypeSymbol type)
+ {
+ if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ {
+ // extract the T from a Nullable
+ type = ((INamedTypeSymbol)type).TypeArguments[0];
+ }
+
+ foreach (var implementingInterface in type.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)))
+ {
+ return implementingInterface.TypeArguments.First();
+ }
+ }
+
+ return null;
+ }
+
+ private List GetMembersToValidate(ITypeSymbol modelType, bool speculate)
+ {
+ // make a list of the most derived members in the model type
+
+ if (modelType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ {
+ // extract the T from a Nullable
+ modelType = ((INamedTypeSymbol)modelType).TypeArguments[0];
+ }
+
+ var members = modelType.GetMembers().ToList();
+ var addedMembers = new HashSet(members.Select(m => m.Name));
+ var baseType = modelType.BaseType;
+ while (baseType is not null && baseType.SpecialType != SpecialType.System_Object)
+ {
+ var baseMembers = baseType.GetMembers().Where(m => !addedMembers.Contains(m.Name));
+ members.AddRange(baseMembers);
+ addedMembers.UnionWith(baseMembers.Select(m => m.Name));
+ baseType = baseType.BaseType;
+ }
+
+ var membersToValidate = new List();
+ foreach (var member in members)
+ {
+ var memberInfo = GetMemberInfo(member, speculate);
+ if (memberInfo is not null)
+ {
+ if (member.DeclaredAccessibility != Accessibility.Public && member.DeclaredAccessibility != Accessibility.Internal)
+ {
+ Diag(DiagDescriptors.MemberIsInaccessible, member.Locations.First(), member.Name);
+ continue;
+ }
+
+ membersToValidate.Add(memberInfo);
+ }
+ }
+
+ return membersToValidate;
+ }
+
+ private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate)
+ {
+ ITypeSymbol memberType;
+ switch (member)
+ {
+ case IPropertySymbol prop:
+ memberType = prop.Type;
+ break;
+ case IFieldSymbol field:
+ if (field.AssociatedSymbol is not null)
+ {
+ // a backing field for a property, don't need those
+ return null;
+ }
+
+ memberType = field.Type;
+ break;
+ default:
+ // we only care about properties and fields
+ return null;
+ }
+
+ var validationAttrs = new List();
+ string? transValidatorTypeName = null;
+ string? enumerationValidatorTypeName = null;
+ var enumeratedIsNullable = false;
+ var enumeratedIsValueType = false;
+ var enumeratedMayBeNull = false;
+ var transValidatorIsSynthetic = false;
+ var enumerationValidatorIsSynthetic = false;
+
+ foreach (var attribute in member.GetAttributes().Where(a => a.AttributeClass is not null))
+ {
+ var attributeType = attribute.AttributeClass!;
+ var attrLoc = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation();
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateObjectMembersAttributeSymbol))
+ {
+ if (HasOpenGenerics(memberType, out var genericType))
+ {
+ Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType);
+ #pragma warning disable S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored
+ speculate = false;
+ #pragma warning restore S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored
+ continue;
+ }
+
+ if (attribute.ConstructorArguments.Length == 1)
+ {
+ var transValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol;
+ if (transValidatorType is not null)
+ {
+ if (CanValidate(transValidatorType, memberType))
+ {
+ if (transValidatorType.Constructors.Where(c => !c.Parameters.Any()).Any())
+ {
+ transValidatorTypeName = transValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ }
+ else
+ {
+ Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, transValidatorType.Name);
+ }
+ }
+ else
+ {
+ Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, transValidatorType.Name, memberType.Name);
+ }
+ }
+ else
+ {
+ Diag(DiagDescriptors.NullValidatorType, attrLoc);
+ }
+ }
+ else if (!_visitedModelTypes.Add(memberType.WithNullableAnnotation(NullableAnnotation.None)))
+ {
+ Diag(DiagDescriptors.CircularTypeReferences, attrLoc, memberType.ToString());
+ speculate = false;
+ continue;
+ }
+
+ if (transValidatorTypeName == null)
+ {
+ transValidatorIsSynthetic = true;
+ transValidatorTypeName = AddSynthesizedValidator(memberType, member);
+ }
+
+ // pop the stack
+ _ = _visitedModelTypes.Remove(memberType.WithNullableAnnotation(NullableAnnotation.None));
+ }
+ else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateEnumeratedItemsAttributeSymbol))
+ {
+ var enumeratedType = GetEnumeratedType(memberType);
+ if (enumeratedType == null)
+ {
+ Diag(DiagDescriptors.NotEnumerableType, attrLoc, memberType);
+ speculate = false;
+ continue;
+ }
+
+ enumeratedIsNullable = enumeratedType.IsReferenceType || enumeratedType.NullableAnnotation == NullableAnnotation.Annotated;
+ enumeratedIsValueType = enumeratedType.IsValueType;
+ enumeratedMayBeNull = enumeratedType.NullableAnnotation == NullableAnnotation.Annotated;
+
+ if (HasOpenGenerics(enumeratedType, out var genericType))
+ {
+ Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType);
+ speculate = false;
+ continue;
+ }
+
+ if (attribute.ConstructorArguments.Length == 1)
+ {
+ var enumerationValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol;
+ if (enumerationValidatorType is not null)
+ {
+ if (CanValidate(enumerationValidatorType, enumeratedType))
+ {
+ if (enumerationValidatorType.Constructors.Where(c => c.Parameters.Length == 0).Any())
+ {
+ enumerationValidatorTypeName = enumerationValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ }
+ else
+ {
+ Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, enumerationValidatorType.Name);
+ }
+ }
+ else
+ {
+ Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, enumerationValidatorType.Name, enumeratedType.Name);
+ }
+ }
+ else
+ {
+ Diag(DiagDescriptors.NullValidatorType, attrLoc);
+ }
+ }
+ else if (!_visitedModelTypes.Add(enumeratedType.WithNullableAnnotation(NullableAnnotation.None)))
+ {
+ Diag(DiagDescriptors.CircularTypeReferences, attrLoc, enumeratedType.ToString());
+ speculate = false;
+ continue;
+ }
+
+ if (enumerationValidatorTypeName == null)
+ {
+ enumerationValidatorIsSynthetic = true;
+ enumerationValidatorTypeName = AddSynthesizedValidator(enumeratedType, member);
+ }
+
+ // pop the stack
+ _ = _visitedModelTypes.Remove(enumeratedType.WithNullableAnnotation(NullableAnnotation.None));
+ }
+ else if (ConvertTo(attributeType, _symbolHolder.ValidationAttributeSymbol))
+ {
+ var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
+ validationAttrs.Add(validationAttr);
+
+ foreach (var constructorArgument in attribute.ConstructorArguments)
+ {
+ validationAttr.ConstructorArguments.Add(GetArgumentExpression(constructorArgument.Type!, constructorArgument.Value));
+ }
+
+ foreach (var namedArgument in attribute.NamedArguments)
+ {
+ validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
+ }
+ }
+ }
+
+ // generate a warning if the field/property seems like it should be transitively validated
+ if (transValidatorTypeName == null && speculate && memberType.SpecialType == SpecialType.None)
+ {
+ if (!HasOpenGenerics(memberType, out var genericType))
+ {
+ var membersToValidate = GetMembersToValidate(memberType, false);
+ if (membersToValidate.Count > 0)
+ {
+ Diag(DiagDescriptors.PotentiallyMissingTransitiveValidation, member.GetLocation(), memberType.Name, member.Name);
+ }
+ }
+ }
+
+ // generate a warning if the field/property seems like it should be enumerated
+ if (enumerationValidatorTypeName == null && speculate)
+ {
+ var enumeratedType = GetEnumeratedType(memberType);
+ if (enumeratedType is not null)
+ {
+ if (!HasOpenGenerics(enumeratedType, out var genericType))
+ {
+ var membersToValidate = GetMembersToValidate(enumeratedType, false);
+ if (membersToValidate.Count > 0)
+ {
+ Diag(DiagDescriptors.PotentiallyMissingEnumerableValidation, member.GetLocation(), enumeratedType.Name, member.Name);
+ }
+ }
+ }
+ }
+
+ if (validationAttrs.Count > 0 || transValidatorTypeName is not null || enumerationValidatorTypeName is not null)
+ {
+ return new(
+ member.Name,
+ validationAttrs,
+ transValidatorTypeName,
+ transValidatorIsSynthetic,
+ enumerationValidatorTypeName,
+ enumerationValidatorIsSynthetic,
+ memberType.IsReferenceType || memberType.NullableAnnotation == NullableAnnotation.Annotated,
+ memberType.IsValueType,
+ enumeratedIsNullable,
+ enumeratedIsValueType,
+ enumeratedMayBeNull);
+ }
+
+ return null;
+ }
+
+ private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member)
+ {
+ var mt = modelType.WithNullableAnnotation(NullableAnnotation.None);
+ if (mt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ {
+ // extract the T from a Nullable
+ mt = ((INamedTypeSymbol)mt).TypeArguments[0];
+ }
+
+ if (_synthesizedValidators.TryGetValue(mt, out var validator))
+ {
+ return "global::" + validator.Namespace + "." + validator.Name;
+ }
+
+ var membersToValidate = GetMembersToValidate(mt, true);
+ if (membersToValidate.Count == 0)
+ {
+ // this type lacks any eligible members
+ Diag(DiagDescriptors.NoEligibleMember, member.GetLocation(), mt.ToString(), member.ToString());
+ return null;
+ }
+
+ var model = new ValidatedModel(
+ GetFQN(mt),
+ mt.Name,
+ false,
+ membersToValidate);
+
+ var validatorTypeName = "__" + mt.Name + "Validator__";
+
+ var result = new ValidatorType(
+ mt.ContainingNamespace.IsGlobalNamespace ? string.Empty : mt.ContainingNamespace.ToString(),
+ validatorTypeName,
+ validatorTypeName,
+ "class",
+ new List(),
+ true,
+ new[] { model });
+
+ _synthesizedValidators[mt] = result;
+ return "global::" + (result.Namespace.Length > 0 ? result.Namespace + "." + result.Name : result.Name);
+ }
+
+ private bool ConvertTo(ITypeSymbol source, ITypeSymbol dest)
+ {
+ var conversion = _compilation.ClassifyConversion(source, dest);
+ return conversion.IsReference && conversion.IsImplicit;
+ }
+
+ private bool ModelSelfValidates(ITypeSymbol modelType)
+ {
+ foreach (var implementingInterface in modelType.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.IValidatableObjectSymbol))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private List GetModelTypes(ITypeSymbol validatorType)
+ {
+ var result = new List();
+ foreach (var implementingInterface in validatorType.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol))
+ {
+ result.Add(implementingInterface.TypeArguments.First());
+ }
+ }
+
+ return result;
+ }
+
+ private bool CanValidate(ITypeSymbol validatorType, ISymbol modelType)
+ {
+ foreach (var implementingInterface in validatorType.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol))
+ {
+ var t = implementingInterface.TypeArguments.First();
+ if (SymbolEqualityComparer.Default.Equals(modelType, t))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private string GetArgumentExpression(ITypeSymbol type, object? value)
+ {
+ if (value == null)
+ {
+ return "null";
+ }
+
+ if (type.SpecialType == SpecialType.System_Boolean)
+ {
+ return (bool)value ? "true" : "false";
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(type, _symbolHolder.TypeSymbol) &&
+ value is INamedTypeSymbol sym)
+ {
+ return $"typeof({sym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})";
+ }
+
+ if (type.SpecialType == SpecialType.System_String)
+ {
+ return $@"""{EscapeString(value.ToString())}""";
+ }
+
+ if (type.SpecialType == SpecialType.System_Char)
+ {
+ return $@"'{EscapeString(value.ToString())}'";
+ }
+
+ return $"({type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){Convert.ToString(value, CultureInfo.InvariantCulture)}";
+ }
+
+ private static readonly char[] _specialChars = { '\n', '\r', '"', '\\' };
+
+ private static string EscapeString(string s)
+ {
+ int index = s.IndexOfAny(_specialChars);
+ if (index < 0)
+ {
+ return s;
+ }
+
+ var sb = new StringBuilder(s.Length);
+ _ = sb.Append(s, 0, index);
+
+ while (index < s.Length)
+ {
+ _ = s[index] switch
+ {
+ '\n' => sb.Append("\\n"),
+ '\r' => sb.Append("\\r"),
+ '"' => sb.Append("\\\""),
+ '\\' => sb.Append("\\\\"),
+ var other => sb.Append(other),
+ };
+
+ index++;
+ }
+
+ return sb.ToString();
+ }
+
+ private void Diag(DiagnosticDescriptor desc, Location? location)
+ {
+ _reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty