diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 4b46d49f5813f..aed0f89c89ced 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -251,7 +251,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1214`__ | Options validation generator: Can't validate constants, static fields or properties. | | __`SYSLIB1215`__ | Options validation generator: Validation attribute on the member is inaccessible from the validator type. | | __`SYSLIB1216`__ | C# language version not supported by the options validation source generator. | -| __`SYSLIB1217`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* | +| __`SYSLIB1217`__ | The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. | | __`SYSLIB1218`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* | | __`SYSLIB1219`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* | | __`SYSLIB1220`__ | JsonSourceGenerator encountered a [JsonConverterAttribute] with an invalid type argument. | diff --git a/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs index 141fc6b9c7f9a..49562a0a128c2 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs @@ -112,5 +112,12 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase messageFormat: SR.OptionsUnsupportedLanguageVersionMessage, category: Category, defaultSeverity: DiagnosticSeverity.Error); + + public static DiagnosticDescriptor IncompatibleWithTypeForValidationAttribute { get; } = Make( + id: "SYSLIB1217", + title: SR.TypeCannotBeUsedWithTheValidationAttributeTitle, + messageFormat: SR.TypeCannotBeUsedWithTheValidationAttributeMessage, + category: Category, + defaultSeverity: DiagnosticSeverity.Warning); } } diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs index 9e0cb659a6c94..41609ad4b2010 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -18,36 +19,39 @@ namespace Microsoft.Extensions.Options.Generators internal sealed class Emitter : EmitterBase { private const string StaticFieldHolderClassesNamespace = "__OptionValidationStaticInstances"; + internal const string StaticGeneratedValidationAttributesClassesNamespace = "__OptionValidationGeneratedAttributes"; + internal const string StaticAttributeClassNamePrefix = "__SourceGen_"; + internal const string StaticGeneratedMaxLengthAttributeClassesName = "__SourceGen_MaxLengthAttribute"; 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 const string StaticValidationContextType = "global::System.ComponentModel.DataAnnotations.ValidationContext"; private string _staticValidationAttributeHolderClassName = "__Attributes"; private string _staticValidatorHolderClassName = "__Validators"; private string _staticValidationAttributeHolderClassFQN; private string _staticValidatorHolderClassFQN; - private string _modifier; private string _TryGetValueNullableAnnotation; + private readonly SymbolHolder _symbolHolder; + private readonly OptionsSourceGenContext _optionsSourceGenContext; + private sealed record StaticFieldInfo(string FieldTypeFQN, int FieldOrder, string FieldName, IList InstantiationLines); - public Emitter(Compilation compilation, bool emitPreamble = true) : base(emitPreamble) + public Emitter(Compilation compilation, SymbolHolder symbolHolder, OptionsSourceGenContext optionsSourceGenContext, bool emitPreamble = true) : base(emitPreamble) { - if (((CSharpCompilation)compilation).LanguageVersion >= Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11) - { - _modifier = "file"; - } - else + _optionsSourceGenContext = optionsSourceGenContext; + + if (!_optionsSourceGenContext.IsLangVersion11AndAbove) { - _modifier = "internal"; - string suffix = $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}"; - _staticValidationAttributeHolderClassName += suffix; - _staticValidatorHolderClassName += suffix; + _staticValidationAttributeHolderClassName += _optionsSourceGenContext.Suffix; + _staticValidatorHolderClassName += _optionsSourceGenContext.Suffix; } _staticValidationAttributeHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidationAttributeHolderClassName}"; _staticValidatorHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidatorHolderClassName}"; _TryGetValueNullableAnnotation = GetNullableAnnotationStringForTryValidateValueToUseInGeneratedCode(compilation); + + _symbolHolder = symbolHolder; } public string Emit( @@ -65,6 +69,7 @@ public string Emit( GenStaticClassWithStaticReadonlyFields(staticValidationAttributesDict.Values, StaticFieldHolderClassesNamespace, _staticValidationAttributeHolderClassName); GenStaticClassWithStaticReadonlyFields(staticValidatorsDict.Values, StaticFieldHolderClassesNamespace, _staticValidatorHolderClassName); + GenValidationAttributesClasses(); return Capture(); } @@ -146,7 +151,7 @@ private void GenStaticClassWithStaticReadonlyFields(IEnumerable OutOpenBrace(); OutGeneratedCodeAttribute(); - OutLn($"{_modifier} static class {className}"); + OutLn($"{_optionsSourceGenContext.ClassModifier} static class {className}"); OutOpenBrace(); var staticValidationAttributes = staticFields @@ -186,6 +191,396 @@ private void GenStaticClassWithStaticReadonlyFields(IEnumerable OutCloseBrace(); } + public void EmitMaxLengthAttribute(string modifier, string prefix, string className, string linesToInsert, string suffix) + { + OutGeneratedCodeAttribute(); + + string qualifiedClassName = $"{prefix}{suffix}_{className}"; + + OutLn($$""" +[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} + { + private const int MaxAllowableLength = -1; + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a maximum length of '{1}'."; + public {{qualifiedClassName}}(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public {{qualifiedClassName}}(): base(() => DefaultErrorMessageString) { Length = MaxAllowableLength; } + public int Length { get; } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + public override bool IsValid(object? value) + { + if (Length == 0 || Length < -1) + { + throw new global::System.InvalidOperationException("MaxLengthAttribute must have a Length value that is greater than zero. Use MaxLength() without parameters to indicate that the string or array can have the maximum allowable length."); + } + if (value == null || MaxAllowableLength == Length) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + {{linesToInsert}}else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length <= Length; + } + } +"""); + } + + public void EmitMinLengthAttribute(string modifier, string prefix, string className, string linesToInsert, string suffix) + { + OutGeneratedCodeAttribute(); + + string qualifiedClassName = $"{prefix}{suffix}_{className}"; + + OutLn($$""" +[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public {{qualifiedClassName}}(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + {{linesToInsert}}else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } +"""); + } + + public void EmitLengthAttribute(string modifier, string prefix, string className, string linesToInsert, string suffix) + { + OutGeneratedCodeAttribute(); + + string qualifiedClassName = $"{prefix}{suffix}_{className}"; + + OutLn($$""" +[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} + { + private static string DefaultErrorMessageString => "The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'."; + public {{qualifiedClassName}}(int minimumLength, int maximumLength) : base(() => DefaultErrorMessageString) { MinimumLength = minimumLength; MaximumLength = maximumLength; } + public int MinimumLength { get; } + public int MaximumLength { get; } + public override bool IsValid(object? value) + { + if (MinimumLength < 0) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MinimumLength value that is zero or greater."); + } + if (MaximumLength < MinimumLength) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + {{linesToInsert}}else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return (uint)(length - MinimumLength) <= (uint)(MaximumLength - MinimumLength); + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, MinimumLength, MaximumLength); + } +"""); + } + + public void EmitCompareAttribute(string modifier, string prefix, string className, string linesToInsert, string suffix) + { + OutGeneratedCodeAttribute(); + + string qualifiedClassName = $"{prefix}{suffix}_{className}"; + + OutLn($$""" +[global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] + {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} + { + private static string DefaultErrorMessageString => "'{0}' and '{1}' do not match."; + public {{qualifiedClassName}}(string otherProperty) : base(() => DefaultErrorMessageString) + { + if (otherProperty == null) + { + throw new global::System.ArgumentNullException(nameof(otherProperty)); + } + OtherProperty = otherProperty; + } + public string OtherProperty { get; } + public override bool RequiresValidationContext => true; + + protected override {{StaticValidationResultType}}? IsValid(object? value, {{StaticValidationContextType}} validationContext) + { + bool result = true; + + {{linesToInsert}} + if (!result) + { + string[]? memberNames = validationContext.MemberName is null ? null : new string[] { validationContext.MemberName }; + return new {{StaticValidationResultType}}(FormatErrorMessage(validationContext.DisplayName), memberNames); + } + + return null; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, OtherProperty); + } +"""); + } + + public void EmitRangeAttribute(string modifier, string prefix, string className, string suffix) + { + OutGeneratedCodeAttribute(); + + string qualifiedClassName = $"{prefix}{suffix}_{className}"; + + OutLn($$""" +[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} + { + public {{qualifiedClassName}}(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public {{qualifiedClassName}}(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public {{qualifiedClassName}}(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +"""); + } + + private string GenerateStronglyTypedCodeForLengthAttributes(HashSet data) + { + if (data.Count == 0) + { + return string.Empty; + } + + StringBuilder sb = new(); + string padding = GetPaddingString(3); + + foreach (var type in data) + { + string typeName = (string)type; + sb.AppendLine($"else if (value is {typeName})"); + sb.AppendLine($"{padding}{{"); + sb.AppendLine($"{padding} length = (({typeName})value).Count;"); + sb.AppendLine($"{padding}}}"); + sb.Append($"{padding}"); + } + + return sb.ToString(); + } + + private string GenerateStronglyTypedCodeForCompareAttribute(HashSet? data) + { + if (data is null || data.Count == 0) + { + return string.Empty; + } + + StringBuilder sb = new(); + string padding = GetPaddingString(3); + bool first = true; + + foreach (var obj in data) + { + (string type, string property) = ((string, string))obj; + sb.Append(first ? $"if " : $"{padding}else if "); + sb.AppendLine($"(validationContext.ObjectInstance is {type} && OtherProperty == \"{property}\")"); + sb.AppendLine($"{padding}{{"); + sb.AppendLine($"{padding} result = Equals(value, (({type})validationContext.ObjectInstance).{property});"); + sb.AppendLine($"{padding}}}"); + first = false; + } + + return sb.ToString(); + } + + private void GenValidationAttributesClasses() + { + if (_optionsSourceGenContext.AttributesToGenerate.Count == 0) + { + return; + } + + var attributesData = _optionsSourceGenContext.AttributesToGenerate.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal).ToArray(); + + OutLn($"namespace {StaticGeneratedValidationAttributesClassesNamespace}"); + OutOpenBrace(); + + foreach (var attributeData in attributesData) + { + if (attributeData.Key == _symbolHolder.MaxLengthAttributeSymbol.Name) + { + string linesToInsert = attributeData.Value is not null ? GenerateStronglyTypedCodeForLengthAttributes((HashSet)attributeData.Value) : string.Empty; + EmitMaxLengthAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, linesToInsert, _optionsSourceGenContext.Suffix); + } + else if (attributeData.Key == _symbolHolder.MinLengthAttributeSymbol.Name) + { + string linesToInsert = attributeData.Value is not null ? GenerateStronglyTypedCodeForLengthAttributes((HashSet)attributeData.Value) : string.Empty; + EmitMinLengthAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, linesToInsert, _optionsSourceGenContext.Suffix); + } + else if (_symbolHolder.LengthAttributeSymbol is not null && attributeData.Key == _symbolHolder.LengthAttributeSymbol.Name) + { + string linesToInsert = attributeData.Value is not null ? GenerateStronglyTypedCodeForLengthAttributes((HashSet)attributeData.Value) : string.Empty; + EmitLengthAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, linesToInsert, _optionsSourceGenContext.Suffix); + } + else if (attributeData.Key == _symbolHolder.CompareAttributeSymbol.Name && attributeData.Value is not null) + { + string linesToInsert = GenerateStronglyTypedCodeForCompareAttribute((HashSet)attributeData.Value); + EmitCompareAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, linesToInsert: linesToInsert, _optionsSourceGenContext.Suffix); + } + else if (attributeData.Key == _symbolHolder.RangeAttributeSymbol.Name) + { + EmitRangeAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, _optionsSourceGenContext.Suffix); + } + } + + OutCloseBrace(); + } + private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate) { if (modelToValidate.SelfValidates) @@ -209,10 +604,18 @@ private void GenModelValidationMethod( OutLn($"/// Validation result."); OutGeneratedCodeAttribute(); + if (_symbolHolder.UnconditionalSuppressMessageAttributeSymbol is not null) + { + // We disable the warning on `new ValidationContext(object)` usage as we use it in a safe way that not require executing the reflection code. + // This is done by initializing the DisplayName in the context which is the part trigger reflection if it is not initialized. + OutLn($"[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\"Trimming\", \"IL2026:RequiresUnreferencedCode\","); + OutLn($" Justification = \"The created ValidationContext object is used in a way that never call reflection\")]"); + } + OutLn($"public {(makeStatic ? "static " : string.Empty)}global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, {modelToValidate.Name} options)"); OutOpenBrace(); OutLn($"global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;"); - OutLn($"var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);"); + OutLn($"var context = new {StaticValidationContextType}(options);"); int capacity = modelToValidate.MembersToValidate.Max(static vm => vm.ValidationAttributes.Count); if (capacity > 0) @@ -438,19 +841,5 @@ private StaticFieldInfo GetOrAddStaticValidator(ref Dictionary - /// Returns a non-randomized hash code for the given string. - /// We always return a positive value. - /// - internal static int GetNonRandomizedHashCode(string s) - { - uint result = 2166136261u; - foreach (char c in s) - { - result = (c ^ result) * 16777619; - } - return Math.Abs((int)result); - } } } diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs b/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs index 34533fc0a96b0..e9c1f55fcca51 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/Generator.cs @@ -38,12 +38,14 @@ private static void HandleAnnotatedTypes(Compilation compilation, ImmutableArray return; } - var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken); + OptionsSourceGenContext optionsSourceGenContext = new(compilation); + + var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, optionsSourceGenContext, context.CancellationToken); var validatorTypes = parser.GetValidatorTypes(types); if (validatorTypes.Count > 0) { - var emitter = new Emitter(compilation); + var emitter = new Emitter(compilation, symbolHolder!, optionsSourceGenContext); 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 index 5571341b06060..f5bad27937175 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj +++ b/src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Options/gen/OptionsSourceGenContext.cs b/src/libraries/Microsoft.Extensions.Options/gen/OptionsSourceGenContext.cs new file mode 100644 index 0000000000000..8da3e31776962 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/gen/OptionsSourceGenContext.cs @@ -0,0 +1,83 @@ +// 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 Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Versioning; + +namespace Microsoft.Extensions.Options.Generators +{ + internal sealed class OptionsSourceGenContext + { + public OptionsSourceGenContext(Compilation compilation) + { + IsLangVersion11AndAbove = ((CSharpCompilation)compilation).LanguageVersion >= Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11; + ClassModifier = IsLangVersion11AndAbove ? "file" : "internal"; + Suffix = IsLangVersion11AndAbove ? "" : $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}"; + } + + internal string Suffix { get; } + internal string ClassModifier { get; } + internal bool IsLangVersion11AndAbove { get; } + internal Dictionary?> AttributesToGenerate { get; set; } = new Dictionary?>(); + + internal void EnsureTrackingAttribute(string attributeName, bool createValue, out HashSet? value) + { + bool exist = AttributesToGenerate.TryGetValue(attributeName, out value); + if (value is null) + { + if (createValue) + { + value = new HashSet(); + } + + if (!exist || createValue) + { + AttributesToGenerate[attributeName] = value; + } + } + } + + internal static bool IsConvertibleBasicType(ITypeSymbol typeSymbol) + { + return typeSymbol.SpecialType switch + { + SpecialType.System_Boolean => true, + SpecialType.System_Byte => true, + SpecialType.System_Char => true, + SpecialType.System_DateTime => true, + SpecialType.System_Decimal => true, + SpecialType.System_Double => true, + SpecialType.System_Int16 => true, + SpecialType.System_Int32 => true, + SpecialType.System_Int64 => true, + SpecialType.System_SByte => true, + SpecialType.System_Single => true, + SpecialType.System_UInt16 => true, + SpecialType.System_UInt32 => true, + SpecialType.System_UInt64 => true, + SpecialType.System_String => true, + _ => false, + }; + } + + /// + /// Returns a non-randomized hash code for the given string. + /// We always return a positive value. + /// + internal static int GetNonRandomizedHashCode(string s) + { + uint result = 2166136261u; + foreach (char c in s) + { + result = (c ^ result) * 16777619; + } + + return Math.Abs((int)result); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs index 010b89562a917..47cb71c3411cd 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs @@ -25,6 +25,7 @@ internal sealed class Parser private readonly Compilation _compilation; private readonly Action _reportDiagnostic; private readonly SymbolHolder _symbolHolder; + private readonly OptionsSourceGenContext _optionsSourceGenContext; private readonly Dictionary _synthesizedValidators = new(SymbolEqualityComparer.Default); private readonly HashSet _visitedModelTypes = new(SymbolEqualityComparer.Default); @@ -32,12 +33,14 @@ public Parser( Compilation compilation, Action reportDiagnostic, SymbolHolder symbolHolder, + OptionsSourceGenContext optionsSourceGenContext, CancellationToken cancellationToken) { _compilation = compilation; _cancellationToken = cancellationToken; _reportDiagnostic = reportDiagnostic; _symbolHolder = symbolHolder; + _optionsSourceGenContext = optionsSourceGenContext; } public IReadOnlyList GetValidatorTypes(IEnumerable<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> classes) @@ -288,7 +291,7 @@ private List GetMembersToValidate(ITypeSymbol modelType, bool s ? memberLocation : lowerLocationInCompilation; - var memberInfo = GetMemberInfo(member, speculate, location, validatorType); + var memberInfo = GetMemberInfo(member, speculate, location, modelType, validatorType); if (memberInfo is not null) { if (member.DeclaredAccessibility != Accessibility.Public) @@ -304,7 +307,7 @@ private List GetMembersToValidate(ITypeSymbol modelType, bool s return membersToValidate; } - private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol validatorType) + private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol modelType, ITypeSymbol validatorType) { ITypeSymbol memberType; switch (member) @@ -325,7 +328,7 @@ private List GetMembersToValidate(ITypeSymbol modelType, bool s break; */ default: - // we only care about properties and fields + // we only care about properties return null; } @@ -467,7 +470,26 @@ private List GetMembersToValidate(ITypeSymbol modelType, bool s continue; } - var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + string attributeFullQualifiedName = attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MaxLengthAttributeSymbol) || + SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MinLengthAttributeSymbol) || + (_symbolHolder.LengthAttributeSymbol is not null && SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.LengthAttributeSymbol))) + { + if (!LengthBasedAttributeIsTrackedForSubstitution(memberType, location, attributeType, ref attributeFullQualifiedName)) + { + continue; + } + } + else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.CompareAttributeSymbol)) + { + TrackCompareAttributeForSubstitution(attribute, modelType, ref attributeFullQualifiedName); + } + else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.RangeAttributeSymbol)) + { + TrackRangeAttributeForSubstitution(attribute, memberType, ref attributeFullQualifiedName); + } + + var validationAttr = new ValidationAttributeInfo(attributeFullQualifiedName); validationAttrs.Add(validationAttr); ImmutableArray parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray.Empty; @@ -567,6 +589,79 @@ private List GetMembersToValidate(ITypeSymbol modelType, bool s return null; } + private bool LengthBasedAttributeIsTrackedForSubstitution(ITypeSymbol memberType, Location location, ITypeSymbol attributeType, ref string attributeFullQualifiedName) + { + if (memberType.SpecialType == SpecialType.System_String || ConvertTo(memberType, _symbolHolder.ICollectionSymbol)) + { + _optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: false, out _); + } + else if (ParserUtilities.TypeHasProperty(memberType, "Count", SpecialType.System_Int32)) + { + _optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: true, out HashSet? trackedTypeList); + trackedTypeList!.Add(memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + else + { + Diag(DiagDescriptors.IncompatibleWithTypeForValidationAttribute, location, attributeType.Name, memberType.Name); + return false; + } + + attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attributeType.Name}"; + return true; + } + + private void TrackCompareAttributeForSubstitution(AttributeData attribute, ITypeSymbol modelType, ref string attributeFullQualifiedName) + { + ImmutableArray constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray.Empty; + if (constructorParameters.Length == 1 && constructorParameters[0].Name == "otherProperty" && constructorParameters[0].Type.SpecialType == SpecialType.System_String) + { + _optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: true, out HashSet? trackedTypeList); + trackedTypeList!.Add((modelType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), (string)attribute.ConstructorArguments[0].Value!)); + attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}"; + } + } + + private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSymbol memberType, ref string attributeFullQualifiedName) + { + ImmutableArray constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray.Empty; + SpecialType argumentSpecialType = SpecialType.None; + if (constructorParameters.Length == 2) + { + argumentSpecialType = constructorParameters[0].Type.SpecialType; + } + else if (constructorParameters.Length == 3) + { + object? argumentValue = null; + for (int i = 0; i < constructorParameters.Length; i++) + { + if (constructorParameters[i].Name == "type") + { + argumentValue = attribute.ConstructorArguments[i].Value; + break; + } + } + + if (argumentValue is INamedTypeSymbol namedTypeSymbol && OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol)) + { + argumentSpecialType = namedTypeSymbol.SpecialType; + } + } + + ITypeSymbol typeSymbol = memberType; + if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + } + + if (argumentSpecialType != SpecialType.None && + OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol) && + (constructorParameters.Length != 3 || typeSymbol.SpecialType == argumentSpecialType)) // When type is provided as a parameter, it has to match the property type. + { + _optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: false, out _); + attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}"; + } + } + private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member, Location location, ITypeSymbol validatorType) { var mt = modelType.WithNullableAnnotation(NullableAnnotation.None); diff --git a/src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs b/src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs index d79ad4cccb653..675202538e12d 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs @@ -68,6 +68,29 @@ internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol inte return false; } + internal static bool TypeHasProperty(ITypeSymbol typeSymbol, string propertyName, SpecialType returnType) + { + ITypeSymbol? type = typeSymbol; + do + { + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + type = ((INamedTypeSymbol)type).TypeArguments[0]; // extract the T from a Nullable + } + + if (type.GetMembers(propertyName).OfType().Any(property => + property.Type.SpecialType == returnType && property.DeclaredAccessibility == Accessibility.Public && + !property.IsStatic && property.GetMethod != null && property.Parameters.IsEmpty)) + { + return true; + } + + type = type.BaseType; + } while (type is not null && type.SpecialType != SpecialType.System_Object); + + return false; + } + // Check if parameter has either simplified (i.e. "int?") or explicit (Nullable) nullable type declaration: internal static bool IsNullableOfT(this ITypeSymbol type) => type.SpecialType == SpecialType.System_Nullable_T || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx index 6293431eb7f90..7100030eecf13 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx @@ -213,4 +213,10 @@ The options validation source generator is not available in C# {0}. Please use language version {1} or greater. + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf index 953376c434d31..77769cb3ca4ab 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf @@ -152,6 +152,16 @@ U člena potenciálně chybí přenositelné ověření. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Typ validátoru {0} nemá konstruktor bez parametrů. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.de.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.de.xlf index bbd4d29cfc247..e5defa5eeb3e2 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.de.xlf @@ -152,6 +152,16 @@ Dem Member fehlt möglicherweise die transitive Validierung. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Der Validierungssteuerelementtyp "{0}" hat keinen parameterlosen Konstruktor. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.es.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.es.xlf index 0c4661573bbd4..728e01d7b598a 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.es.xlf @@ -152,6 +152,16 @@ Posiblemente falta la validación transitiva en el miembro. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. El tipo de validador {0} no tiene un constructor sin parámetros. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.fr.xlf index e1996eeca163c..d7f738a55f661 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.fr.xlf @@ -152,6 +152,16 @@ Le membre n’a peut-être pas de validation transitive. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Le type de validateur {0} n’a pas de constructeur sans paramètre. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.it.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.it.xlf index edcf16b967dd3..d3903a95f055c 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.it.xlf @@ -152,6 +152,16 @@ Il membro potrebbe non avere una convalida transitiva. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Il tipo di convalida {0} non dispone di un costruttore senza parametri. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ja.xlf index 89b2f23777fb2..2afc63bf19fda 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ja.xlf @@ -152,6 +152,16 @@ メンバーに推移性の検証がない可能性があります。 + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. バリデーター型 {0} にパラメーターなしのコンストラクターがありません。 diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ko.xlf index 817bc64eba8ac..6bb342f593154 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ko.xlf @@ -152,6 +152,16 @@ 멤버에 전이적 유효성 검사가 누락되었을 수 있습니다. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. 유효성 검사기 형식 {0}은(는) 매개 변수가 없는 생성자가 없습니다. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pl.xlf index 190ca17b56179..1262d4d0852b4 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pl.xlf @@ -152,6 +152,16 @@ W przypadku elementu członkowskiego może potencjalnie brakować weryfikacji przechodniej. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Typ modułu sprawdzania poprawności {0} nie ma konstruktora bez parametrów. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pt-BR.xlf index 24a4203391c01..ced617baa9556 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.pt-BR.xlf @@ -152,6 +152,16 @@ Membro potencialmente ausente na validação transitiva. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. O tipo de validador {0} não tem um construtor sem parâmetros. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ru.xlf index f14d71833a235..cf3c21772acf2 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.ru.xlf @@ -152,6 +152,16 @@ Возможно, в элементе отсутствует транзитивная проверка. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. Тип проверяющего элемента управления {0} не имеет конструктора без параметров. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.tr.xlf index 79e094b36b312..b9545e2def86b 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.tr.xlf @@ -152,6 +152,16 @@ Üyede geçişli doğrulama eksik olabilir. + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. {0} doğrulayıcı türü parametresiz bir oluşturucuya sahip değil. diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hans.xlf index 7688e0e628811..98d7e55197f65 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -152,6 +152,16 @@ 成员可能缺少可传递验证。 + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. 验证程序类型 {0} 没有无参数构造函数。 diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hant.xlf index 663bff9c18386..a3b9657f861b0 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -152,6 +152,16 @@ 成員可能遺漏轉移的驗證。 + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + Validator type {0} doesn't have a parameterless constructor. 驗證程式類型 {0} 沒有無參數建構函式。 diff --git a/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs b/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs index 55d382e403621..3447a07d39830 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs @@ -11,6 +11,13 @@ namespace Microsoft.Extensions.Options.Generators internal sealed record class SymbolHolder( INamedTypeSymbol OptionsValidatorSymbol, INamedTypeSymbol ValidationAttributeSymbol, + INamedTypeSymbol MaxLengthAttributeSymbol, + INamedTypeSymbol MinLengthAttributeSymbol, + INamedTypeSymbol CompareAttributeSymbol, + INamedTypeSymbol? LengthAttributeSymbol, + INamedTypeSymbol? UnconditionalSuppressMessageAttributeSymbol, + INamedTypeSymbol RangeAttributeSymbol, + INamedTypeSymbol ICollectionSymbol, INamedTypeSymbol DataTypeAttributeSymbol, INamedTypeSymbol ValidateOptionsSymbol, INamedTypeSymbol IValidatableObjectSymbol, diff --git a/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs b/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs index 94035cedacbf9..ea55622892975 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs @@ -9,6 +9,12 @@ internal static class SymbolLoader { public const string OptionsValidatorAttribute = "Microsoft.Extensions.Options.OptionsValidatorAttribute"; internal const string ValidationAttribute = "System.ComponentModel.DataAnnotations.ValidationAttribute"; + internal const string MaxLengthAttribute = "System.ComponentModel.DataAnnotations.MaxLengthAttribute"; + internal const string MinLengthAttribute = "System.ComponentModel.DataAnnotations.MinLengthAttribute"; + internal const string CompareAttribute = "System.ComponentModel.DataAnnotations.CompareAttribute"; + internal const string LengthAttribute = "System.ComponentModel.DataAnnotations.LengthAttribute"; + internal const string RangeAttribute = "System.ComponentModel.DataAnnotations.RangeAttribute"; + internal const string ICollectionType = "System.Collections.ICollection"; internal const string DataTypeAttribute = "System.ComponentModel.DataAnnotations.DataTypeAttribute"; internal const string IValidatableObjectType = "System.ComponentModel.DataAnnotations.IValidatableObject"; internal const string IValidateOptionsType = "Microsoft.Extensions.Options.IValidateOptions`1"; @@ -16,6 +22,7 @@ internal static class SymbolLoader internal const string ValidateObjectMembersAttribute = "Microsoft.Extensions.Options.ValidateObjectMembersAttribute"; internal const string ValidateEnumeratedItemsAttribute = "Microsoft.Extensions.Options.ValidateEnumeratedItemsAttribute"; internal const string GenericIEnumerableType = "System.Collections.Generic.IEnumerable`1"; + internal const string UnconditionalSuppressMessageAttributeType = "System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute"; public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder) { @@ -24,6 +31,12 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold // required var optionsValidatorSymbol = GetSymbol(OptionsValidatorAttribute); var validationAttributeSymbol = GetSymbol(ValidationAttribute); + var maxLengthAttributeSymbol = GetSymbol(MaxLengthAttribute); + var minLengthAttributeSymbol = GetSymbol(MinLengthAttribute); + var compareAttributeSymbol = GetSymbol(CompareAttribute); + var lengthAttributeSymbol = GetSymbol(LengthAttribute); + var rangeAttributeSymbol = GetSymbol(RangeAttribute); + var iCollectionSymbol = GetSymbol(ICollectionType); var dataTypeAttributeSymbol = GetSymbol(DataTypeAttribute); var ivalidatableObjectSymbol = GetSymbol(IValidatableObjectType); var validateOptionsSymbol = GetSymbol(IValidateOptionsType); @@ -31,10 +44,27 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold var typeSymbol = GetSymbol(TypeOfType); var validateObjectMembersAttribute = GetSymbol(ValidateObjectMembersAttribute); var validateEnumeratedItemsAttribute = GetSymbol(ValidateEnumeratedItemsAttribute); + var unconditionalSuppressMessageAttributeSymbol = GetSymbol(UnconditionalSuppressMessageAttributeType); + if (unconditionalSuppressMessageAttributeSymbol is not null) + { + var containingAssemblyName = unconditionalSuppressMessageAttributeSymbol.ContainingAssembly.Identity.Name; + if (!containingAssemblyName.Equals("System.Private.CoreLib", System.StringComparison.OrdinalIgnoreCase) && + !containingAssemblyName.Equals("System.Runtime", System.StringComparison.OrdinalIgnoreCase)) + { + // The compilation returns UnconditionalSuppressMessageAttribute symbol even if the attribute is not available like the case when running on .NET Framework. + // We need to make sure that the attribute is really available by checking the containing assembly which in .NET Core will be either System.Private.CoreLib or System.Runtime. + unconditionalSuppressMessageAttributeSymbol = null; + } + } #pragma warning disable S1067 // Expressions should not be too complex if (optionsValidatorSymbol == null || validationAttributeSymbol == null || + maxLengthAttributeSymbol == null || + minLengthAttributeSymbol == null || + compareAttributeSymbol == null || + rangeAttributeSymbol == null || + iCollectionSymbol == null || dataTypeAttributeSymbol == null || ivalidatableObjectSymbol == null || validateOptionsSymbol == null || @@ -51,6 +81,13 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold symbolHolder = new( optionsValidatorSymbol, validationAttributeSymbol, + maxLengthAttributeSymbol, + minLengthAttributeSymbol, + compareAttributeSymbol, + lengthAttributeSymbol, + unconditionalSuppressMessageAttributeSymbol, + rangeAttributeSymbol, + iCollectionSymbol, dataTypeAttributeSymbol, validateOptionsSymbol, ivalidatableObjectSymbol, diff --git a/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj b/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj index 7225edf84ba53..c7ea3e00049e3 100644 --- a/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj +++ b/src/libraries/Microsoft.Extensions.Options/src/Microsoft.Extensions.Options.csproj @@ -28,12 +28,13 @@ - + diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/DataAnnotationAttributesWithParams.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/DataAnnotationAttributesWithParams.g.cs new file mode 100644 index 0000000000000..c51c551222f42 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/DataAnnotationAttributesWithParams.g.cs @@ -0,0 +1,135 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace Test +{ + partial class MyOptionsValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] + 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(); + var validationAttributes = new global::System.Collections.Generic.List(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 __OptionValidationGeneratedAttributes.__SourceGen__LengthAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__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 + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__LengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'."; + public __SourceGen__LengthAttribute(int minimumLength, int maximumLength) : base(() => DefaultErrorMessageString) { MinimumLength = minimumLength; MaximumLength = maximumLength; } + public int MinimumLength { get; } + public int MaximumLength { get; } + public override bool IsValid(object? value) + { + if (MinimumLength < 0) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MinimumLength value that is zero or greater."); + } + if (MaximumLength < MinimumLength) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return (uint)(length - MinimumLength) <= (uint)(MaximumLength - MinimumLength); + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, MinimumLength, MaximumLength); + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netcore.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netcore.g.cs new file mode 100644 index 0000000000000..2c5af12c5b5f2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netcore.g.cs @@ -0,0 +1,175 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace HelloWorld +{ + partial struct MyOptionsValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::HelloWorld.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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "Val1"; + context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val1" : $"{name}.Val1"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "Val2"; + context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val2" : $"{name}.Val2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2, 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 __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + (int)1, + (int)3); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + file static class __Validators + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netfx.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netfx.g.cs new file mode 100644 index 0000000000000..9dc3ded5bd462 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/EmitterWithCustomValidator.netfx.g.cs @@ -0,0 +1,173 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace HelloWorld +{ + partial struct MyOptionsValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::HelloWorld.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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "Val1"; + context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val1" : $"{name}.Val1"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "Val2"; + context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val2" : $"{name}.Val2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2, 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 __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + (int)1, + (int)3); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + file static class __Validators + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang10.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang10.g.cs new file mode 100644 index 0000000000000..cc9864a2619c4 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang10.g.cs @@ -0,0 +1,471 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace ValidationTest +{ + partial class OptionsUsingGeneratedAttributesValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValidationTest.OptionsUsingGeneratedAttributes 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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "P0"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P0" : $"{name}.P0"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P0, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P1"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P1" : $"{name}.P1"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.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) ? "OptionsUsingGeneratedAttributes.P2" : $"{name}.P2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P3" : $"{name}.P3"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P5"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P5" : $"{name}.P5"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P5, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P6"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P6" : $"{name}.P6"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A5); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P6, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P7"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P7" : $"{name}.P7"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P7, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P8"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P8" : $"{name}.P8"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P8, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P9"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P9" : $"{name}.P9"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P9, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P10"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P10" : $"{name}.P10"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P10, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P11"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P11" : $"{name}.P11"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P11, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P12"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P12" : $"{name}.P12"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P12, 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")] + internal static class __Attributes_2C497155 + { + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_LengthAttribute A1 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_LengthAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MinLengthAttribute A3 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MinLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MaxLengthAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MaxLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute( + "P5"); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + internal static class __Validators_2C497155 + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] + internal class __SourceGen__2C497155_CompareAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "'{0}' and '{1}' do not match."; + public __SourceGen__2C497155_CompareAttribute(string otherProperty) : base(() => DefaultErrorMessageString) + { + if (otherProperty == null) + { + throw new global::System.ArgumentNullException(nameof(otherProperty)); + } + OtherProperty = otherProperty; + } + public string OtherProperty { get; } + public override bool RequiresValidationContext => true; + + protected override global::System.ComponentModel.DataAnnotations.ValidationResult? IsValid(object? value, global::System.ComponentModel.DataAnnotations.ValidationContext validationContext) + { + bool result = true; + + if (validationContext.ObjectInstance is global::ValidationTest.OptionsUsingGeneratedAttributes && OtherProperty == "P5") + { + result = Equals(value, ((global::ValidationTest.OptionsUsingGeneratedAttributes)validationContext.ObjectInstance).P5); + } + + if (!result) + { + string[]? memberNames = validationContext.MemberName is null ? null : new string[] { validationContext.MemberName }; + return new global::System.ComponentModel.DataAnnotations.ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames); + } + + return null; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, OtherProperty); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_LengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'."; + public __SourceGen__2C497155_LengthAttribute(int minimumLength, int maximumLength) : base(() => DefaultErrorMessageString) { MinimumLength = minimumLength; MaximumLength = maximumLength; } + public int MinimumLength { get; } + public int MaximumLength { get; } + public override bool IsValid(object? value) + { + if (MinimumLength < 0) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MinimumLength value that is zero or greater."); + } + if (MaximumLength < MinimumLength) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return (uint)(length - MinimumLength) <= (uint)(MaximumLength - MinimumLength); + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, MinimumLength, MaximumLength); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_MaxLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private const int MaxAllowableLength = -1; + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a maximum length of '{1}'."; + public __SourceGen__2C497155_MaxLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public __SourceGen__2C497155_MaxLengthAttribute(): base(() => DefaultErrorMessageString) { Length = MaxAllowableLength; } + public int Length { get; } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + public override bool IsValid(object? value) + { + if (Length == 0 || Length < -1) + { + throw new global::System.InvalidOperationException("MaxLengthAttribute must have a Length value that is greater than zero. Use MaxLength() without parameters to indicate that the string or array can have the maximum allowable length."); + } + if (value == null || MaxAllowableLength == Length) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length <= Length; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__2C497155_MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__2C497155_RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__2C497155_RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__2C497155_RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang11.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang11.g.cs new file mode 100644 index 0000000000000..2a33e51b0b617 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netcore.lang11.g.cs @@ -0,0 +1,471 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace ValidationTest +{ + partial class OptionsUsingGeneratedAttributesValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValidationTest.OptionsUsingGeneratedAttributes 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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "P0"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P0" : $"{name}.P0"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P0, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P1"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P1" : $"{name}.P1"; + validationResults.Clear(); + validationAttributes.Clear(); + 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) ? "OptionsUsingGeneratedAttributes.P2" : $"{name}.P2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P3" : $"{name}.P3"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P5"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P5" : $"{name}.P5"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P5, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P6"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P6" : $"{name}.P6"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A5); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P6, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P7"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P7" : $"{name}.P7"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P7, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P8"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P8" : $"{name}.P8"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P8, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P9"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P9" : $"{name}.P9"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P9, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P10"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P10" : $"{name}.P10"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P10, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P11"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P11" : $"{name}.P11"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P11, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P12"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P12" : $"{name}.P12"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P12, 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 __OptionValidationGeneratedAttributes.__SourceGen__LengthAttribute A1 = new __OptionValidationGeneratedAttributes.__SourceGen__LengthAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute A3 = new __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MaxLengthAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__MaxLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute( + "P5"); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + file static class __Validators + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] + file class __SourceGen__CompareAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "'{0}' and '{1}' do not match."; + public __SourceGen__CompareAttribute(string otherProperty) : base(() => DefaultErrorMessageString) + { + if (otherProperty == null) + { + throw new global::System.ArgumentNullException(nameof(otherProperty)); + } + OtherProperty = otherProperty; + } + public string OtherProperty { get; } + public override bool RequiresValidationContext => true; + + protected override global::System.ComponentModel.DataAnnotations.ValidationResult? IsValid(object? value, global::System.ComponentModel.DataAnnotations.ValidationContext validationContext) + { + bool result = true; + + if (validationContext.ObjectInstance is global::ValidationTest.OptionsUsingGeneratedAttributes && OtherProperty == "P5") + { + result = Equals(value, ((global::ValidationTest.OptionsUsingGeneratedAttributes)validationContext.ObjectInstance).P5); + } + + if (!result) + { + string[]? memberNames = validationContext.MemberName is null ? null : new string[] { validationContext.MemberName }; + return new global::System.ComponentModel.DataAnnotations.ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames); + } + + return null; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, OtherProperty); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__LengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or collection type with a minimum length of '{1}' and maximum length of '{2}'."; + public __SourceGen__LengthAttribute(int minimumLength, int maximumLength) : base(() => DefaultErrorMessageString) { MinimumLength = minimumLength; MaximumLength = maximumLength; } + public int MinimumLength { get; } + public int MaximumLength { get; } + public override bool IsValid(object? value) + { + if (MinimumLength < 0) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MinimumLength value that is zero or greater."); + } + if (MaximumLength < MinimumLength) + { + throw new global::System.InvalidOperationException("LengthAttribute must have a MaximumLength value that is greater than or equal to MinimumLength."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return (uint)(length - MinimumLength) <= (uint)(MaximumLength - MinimumLength); + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, MinimumLength, MaximumLength); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MaxLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private const int MaxAllowableLength = -1; + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a maximum length of '{1}'."; + public __SourceGen__MaxLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public __SourceGen__MaxLengthAttribute(): base(() => DefaultErrorMessageString) { Length = MaxAllowableLength; } + public int Length { get; } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + public override bool IsValid(object? value) + { + if (Length == 0 || Length < -1) + { + throw new global::System.InvalidOperationException("MaxLengthAttribute must have a Length value that is greater than zero. Use MaxLength() without parameters to indicate that the string or array can have the maximum allowable length."); + } + if (value == null || MaxAllowableLength == Length) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length <= Length; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang10.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang10.g.cs new file mode 100644 index 0000000000000..7f5eb90a20281 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang10.g.cs @@ -0,0 +1,386 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace ValidationTest +{ + partial class OptionsUsingGeneratedAttributesValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValidationTest.OptionsUsingGeneratedAttributes 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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P3" : $"{name}.P3"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P5"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P5" : $"{name}.P5"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P5, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P6"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P6" : $"{name}.P6"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P6, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P7"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P7" : $"{name}.P7"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P7, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P8"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P8" : $"{name}.P8"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P8, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P9"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P9" : $"{name}.P9"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P9, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P10"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P10" : $"{name}.P10"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P10, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P11"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P11" : $"{name}.P11"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P11, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P12"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P12" : $"{name}.P12"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P12, 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")] + internal static class __Attributes_2C497155 + { + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A1 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MinLengthAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MinLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MaxLengthAttribute A3 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_MaxLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute( + "P5"); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + internal static class __Validators_2C497155 + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] + internal class __SourceGen__2C497155_CompareAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "'{0}' and '{1}' do not match."; + public __SourceGen__2C497155_CompareAttribute(string otherProperty) : base(() => DefaultErrorMessageString) + { + if (otherProperty == null) + { + throw new global::System.ArgumentNullException(nameof(otherProperty)); + } + OtherProperty = otherProperty; + } + public string OtherProperty { get; } + public override bool RequiresValidationContext => true; + + protected override global::System.ComponentModel.DataAnnotations.ValidationResult? IsValid(object? value, global::System.ComponentModel.DataAnnotations.ValidationContext validationContext) + { + bool result = true; + + if (validationContext.ObjectInstance is global::ValidationTest.OptionsUsingGeneratedAttributes && OtherProperty == "P5") + { + result = Equals(value, ((global::ValidationTest.OptionsUsingGeneratedAttributes)validationContext.ObjectInstance).P5); + } + + if (!result) + { + string[]? memberNames = validationContext.MemberName is null ? null : new string[] { validationContext.MemberName }; + return new global::System.ComponentModel.DataAnnotations.ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames); + } + + return null; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, OtherProperty); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_MaxLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private const int MaxAllowableLength = -1; + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a maximum length of '{1}'."; + public __SourceGen__2C497155_MaxLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public __SourceGen__2C497155_MaxLengthAttribute(): base(() => DefaultErrorMessageString) { Length = MaxAllowableLength; } + public int Length { get; } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + public override bool IsValid(object? value) + { + if (Length == 0 || Length < -1) + { + throw new global::System.InvalidOperationException("MaxLengthAttribute must have a Length value that is greater than zero. Use MaxLength() without parameters to indicate that the string or array can have the maximum allowable length."); + } + if (value == null || MaxAllowableLength == Length) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length <= Length; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__2C497155_MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + internal class __SourceGen__2C497155_RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__2C497155_RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__2C497155_RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__2C497155_RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang11.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang11.g.cs new file mode 100644 index 0000000000000..3ab56e21320a0 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Baselines/GeneratedAttributesTest.netfx.lang11.g.cs @@ -0,0 +1,386 @@ + + // + #nullable enable + #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 + namespace ValidationTest +{ + partial class OptionsUsingGeneratedAttributesValidator + { + /// + /// Validates a specific named options instance (or all when is ). + /// + /// The name of the options instance being validated. + /// The options instance. + /// Validation result. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValidationTest.OptionsUsingGeneratedAttributes 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(); + var validationAttributes = new global::System.Collections.Generic.List(1); + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P3" : $"{name}.P3"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P4, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P5"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P5" : $"{name}.P5"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P5, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P6"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P6" : $"{name}.P6"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A4); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P6, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P7"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P7" : $"{name}.P7"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P7, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P8"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P8" : $"{name}.P8"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P8, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P9"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P9" : $"{name}.P9"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P9, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P10"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P10" : $"{name}.P10"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P10, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P11"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P11" : $"{name}.P11"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P11, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P12"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P12" : $"{name}.P12"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P12, 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 __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A1 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + (int)1, + (int)3); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MaxLengthAttribute A3 = new __OptionValidationGeneratedAttributes.__SourceGen__MaxLengthAttribute( + (int)5); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute( + "P5"); + } +} +namespace __OptionValidationStaticInstances +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + file static class __Validators + { + } +} +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false)] + file class __SourceGen__CompareAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "'{0}' and '{1}' do not match."; + public __SourceGen__CompareAttribute(string otherProperty) : base(() => DefaultErrorMessageString) + { + if (otherProperty == null) + { + throw new global::System.ArgumentNullException(nameof(otherProperty)); + } + OtherProperty = otherProperty; + } + public string OtherProperty { get; } + public override bool RequiresValidationContext => true; + + protected override global::System.ComponentModel.DataAnnotations.ValidationResult? IsValid(object? value, global::System.ComponentModel.DataAnnotations.ValidationContext validationContext) + { + bool result = true; + + if (validationContext.ObjectInstance is global::ValidationTest.OptionsUsingGeneratedAttributes && OtherProperty == "P5") + { + result = Equals(value, ((global::ValidationTest.OptionsUsingGeneratedAttributes)validationContext.ObjectInstance).P5); + } + + if (!result) + { + string[]? memberNames = validationContext.MemberName is null ? null : new string[] { validationContext.MemberName }; + return new global::System.ComponentModel.DataAnnotations.ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames); + } + + return null; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, OtherProperty); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MaxLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private const int MaxAllowableLength = -1; + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a maximum length of '{1}'."; + public __SourceGen__MaxLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public __SourceGen__MaxLengthAttribute(): base(() => DefaultErrorMessageString) { Length = MaxAllowableLength; } + public int Length { get; } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + public override bool IsValid(object? value) + { + if (Length == 0 || Length < -1) + { + throw new global::System.InvalidOperationException("MaxLengthAttribute must have a Length value that is greater than zero. Use MaxLength() without parameters to indicate that the string or array can have the maximum allowable length."); + } + if (value == null || MaxAllowableLength == Length) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length <= Length; + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else if (value is global::ValidationTest.FakeCount) + { + length = ((global::ValidationTest.FakeCount)value).Count; + } + else if (value is global::ValidationTest.FakeCountChild) + { + length = ((global::ValidationTest.FakeCountChild)value).Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs index aa51c9dfbae73..da893126a14b8 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs @@ -52,77 +52,15 @@ public partial struct MyOptionsValidator : IValidateOptions } """; - string generatedSource = """ - - // - #nullable enable - #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 - namespace HelloWorld -{ - partial struct MyOptionsValidator - { - /// - /// Validates a specific named options instance (or all when is ). - /// - /// The name of the options instance being validated. - /// The options instance. - /// Validation result. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] - public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::HelloWorld.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(); - var validationAttributes = new global::System.Collections.Generic.List(1); - - context.MemberName = "Val1"; - context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val1" : $"{name}.Val1"; - validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1); - if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1, context, validationResults, validationAttributes)) - { - (builder ??= new()).AddResults(validationResults); - } - - context.MemberName = "Val2"; - context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val2" : $"{name}.Val2"; - validationResults.Clear(); - validationAttributes.Clear(); - validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2); - if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2, 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.RangeAttribute A2 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( - (int)1, - (int)3); - } -} -namespace __OptionValidationStaticInstances -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] - file static class __Validators - { - } -} - -"""; - var (diagnostics, generatedSources) = await RunGeneratorOnOptionsSource(source); Assert.Empty(diagnostics); _ = Assert.Single(generatedSources); +#if NETCOREAPP + string generatedSource = File.ReadAllText(@"Baselines/EmitterWithCustomValidator.netcore.g.cs"); +#else + string generatedSource = File.ReadAllText(@"Baselines/EmitterWithCustomValidator.netfx.g.cs"); +#endif // NETCOREAPP Assert.Equal(generatedSource.Replace("\r\n", "\n"), generatedSources[0].SourceText.ToString().Replace("\r\n", "\n")); } @@ -1443,7 +1381,7 @@ internal sealed partial class ExtOptionsValidator : IValidateOptions Assert.Single(diagnostics); Assert.Equal(DiagDescriptors.InaccessibleValidationAttribute.Id, diagnostics[0].Id); string generatedSource = generatedSources[0].SourceText.ToString(); - Assert.Contains("global::System.ComponentModel.DataAnnotations.RangeAttribute", generatedSource); + Assert.Contains("__OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute", generatedSource); Assert.Contains("global::System.ComponentModel.DataAnnotations.RequiredAttribute", generatedSource); Assert.DoesNotContain("Timeout", generatedSource); @@ -1666,113 +1604,22 @@ public partial class MyOptionsValidator : IValidateOptions Assert.Empty(diagnostics); Assert.Single(generatedSources); - var generatedSource = """ - - // - #nullable enable - #pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103 - namespace Test -{ - partial class MyOptionsValidator - { - /// - /// Validates a specific named options instance (or all when is ). - /// - /// The name of the options instance being validated. - /// The options instance. - /// Validation result. - [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(); - var validationAttributes = new global::System.Collections.Generic.List(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 - { - } -} - -"""; + string generatedSource = File.ReadAllText(@"Baselines/DataAnnotationAttributesWithParams.g.cs"); 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) + private static CSharpCompilation CreateCompilationForOptionsSource(string assemblyName, string source, string? refAssemblyPath = null, LanguageVersion languageVersion = LanguageVersion.Default) { // Ensure the generated source compiles var compilation = CSharpCompilation - .Create(Path.GetRandomFileName()+".dll", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .Create($"{assemblyName}.dll", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddReferences(MetadataReference.CreateFromFile(AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == "System.Runtime").Location)) .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) .AddReferences(MetadataReference.CreateFromFile(typeof(RequiredAttribute).Assembly.Location)) .AddReferences(MetadataReference.CreateFromFile(typeof(OptionsValidatorAttribute).Assembly.Location)) .AddReferences(MetadataReference.CreateFromFile(typeof(IValidateOptions).Assembly.Location)) .AddReferences(MetadataReference.CreateFromFile(typeof(System.CodeDom.Compiler.GeneratedCodeAttribute).Assembly.Location)) - .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source)); + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(languageVersion))); if (refAssemblyPath is not null) { @@ -1861,4 +1708,109 @@ private static CSharpCompilation CreateCompilationForOptionsSource(string assemb return result; } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [InlineData(LanguageVersion.CSharp10)] + [InlineData(LanguageVersion.CSharp11)] + public async Task GeneratedAttributesTest(LanguageVersion languageVersion) + { + +#if NETCOREAPP + string lengthAttribute = $$""" + [LengthAttribute(1, 3)] + public string? P0 { get; set; } + + [LengthAttribute(1, 3)] + public FakeCount? P1 { get; set; } + + [LengthAttribute(1, 3)] + public FakeCountChild? P2 { get; set; } + """; +#else +string lengthAttribute = ""; +#endif //NETCOREAPP + + string source = $$""" + using System.Collections.Generic; + using Microsoft.Extensions.Options; + using System.ComponentModel.DataAnnotations; + + #nullable enable + + namespace ValidationTest + { + public class FakeCount + { + public FakeCount(int count) { Count = count; } + public int Count { get; } + } + public class FakeCountChild : FakeCount + { + public FakeCountChild(int count) : base(count) { } + } + + public class OptionsUsingGeneratedAttributes + { + {{lengthAttribute}} + + [RangeAttribute(1, 3)] + public int P3 { get; set; } + + [MinLengthAttribute(5)] + public string? P4 { get; set; } + + [MaxLengthAttribute(5)] + public string? P5 { get; set; } + + [CompareAttribute("P5")] + public string? P6 { get; set; } + + [MinLengthAttribute(5)] + public FakeCount? P7 { get; set; } + + [MinLengthAttribute(5)] + public FakeCountChild? P8 { get; set; } + + [MaxLengthAttribute(5)] + public FakeCount? P9 { get; set; } + + [MaxLengthAttribute(5)] + public FakeCountChild? P10 { get; set; } + + [MinLengthAttribute(5)] + public List? P11 { get; set; } + + [MaxLengthAttribute(5)] + public List? P12 { get; set; } + } + + [OptionsValidator] + public sealed partial class OptionsUsingGeneratedAttributesValidator : IValidateOptions + { + } + } + """; + + var (diagnostics, generatedSources) = await RunGeneratorOnOptionsSource(source, null, languageVersion); + Assert.Empty(diagnostics); + Assert.Single(generatedSources); + + string emittedSource = generatedSources[0].SourceText.ToString(); + SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(emittedSource, new CSharpParseOptions(languageVersion)); + var diags = syntaxTree.GetDiagnostics().ToArray(); + Assert.Empty(diags); + +#if NETCOREAPP + string generatedSource = File.ReadAllText(languageVersion == LanguageVersion.CSharp10 ? @"Baselines/GeneratedAttributesTest.netcore.lang10.g.cs" : @"Baselines/GeneratedAttributesTest.netcore.lang11.g.cs"); +#else + string generatedSource = File.ReadAllText(languageVersion == LanguageVersion.CSharp10 ? @"Baselines/GeneratedAttributesTest.netfx.lang10.g.cs" : @"Baselines/GeneratedAttributesTest.netfx.lang11.g.cs"); +#endif // NET8_0_OR_GREATER + Assert.Equal(generatedSource.Replace("\r\n", "\n"), emittedSource.Replace("\r\n", "\n")); + + CSharpCompilation compilation = CreateCompilationForOptionsSource(Path.GetRandomFileName(), source + emittedSource, refAssemblyPath: null, languageVersion); + var emitResult = compilation.Emit(new MemoryStream()); + + Assert.True(emitResult.Success); + // Console.WriteLine(emittedSource); + } } diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Microsoft.Extensions.Options.SourceGeneration.Unit.Tests.csproj b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Microsoft.Extensions.Options.SourceGeneration.Unit.Tests.csproj index d76cbd45302f9..f3a5f33b4a2b4 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Microsoft.Extensions.Options.SourceGeneration.Unit.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Microsoft.Extensions.Options.SourceGeneration.Unit.Tests.csproj @@ -14,6 +14,7 @@ + @@ -35,6 +36,12 @@ OutputItemType="Analyzer" ReferenceOutputAssembly="true" SetTargetFramework="TargetFramework=netstandard2.0"/> + + + PreserveNewest + + + diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs index 6109bccd29646..78783d7a537e2 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/OptionsRuntimeTests.cs @@ -266,6 +266,214 @@ public void TestNewDataAnnotationFailures() }, result.Failures); } #endif // NET8_0_OR_GREATER + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void TestCustomGeneratedAttributes() + { + OptionsUsingGeneratedAttributes noFailures = new OptionsUsingGeneratedAttributes() + { +#if NET8_0_OR_GREATER + P0 = "123", + P11 = new DateTime(2023, 2, 1), + P12 = 6, + P13 = 9, + P14 = new List() { "1", "2" }, + P15 = new FakeCount(5), + P16 = new FakeCountChild(5), + P17 = new int[] { 1, 2 }, + P18 = new List() { "1", "2", "3" }, + P19 = new FakeCount(3), + P20 = new FakeCountChild(3), + P23 = new List() { "1", "2", "3", "4" }, + P24 = new FakeCount(4), + P25 = new FakeCountChild(4), +#endif // NET8_0_OR_GREATER + P1 = 2, + P2 = "12345", + P3 = "12345", + P4 = "12345", + P5 = 4, + P6 = 4, + P7 = 15, + P8 = 15, + P9 = 2.5m, + P10 = 14.0, + P21 = new int[] { 1, 2, 3 }, + P22 = new int[] { 1, 2, 3, 4 }, + P26 = 14.0, + }; + List results = new(); + Assert.True(Validator.TryValidateObject(noFailures, new ValidationContext(noFailures), results, true)); + + OptionsUsingGeneratedAttributesValidator validator = new(); + Assert.True(validator.Validate("OptionsUsingGeneratedAttributes", noFailures).Succeeded); + + OptionsUsingGeneratedAttributes failing = new OptionsUsingGeneratedAttributes() + { +#if NET8_0_OR_GREATER + P0 = "", + P11 = new DateTime(2023, 1, 1), + P12 = 5, + P13 = 10, + P14 = new List() { "1" }, + P15 = new FakeCount(1), + P16 = new FakeCountChild(11), + P17 = new int[] { 1 }, + P18 = new List() { "1", "2" }, + P19 = new FakeCount(2), + P20 = new FakeCountChild(1), + P23 = new List() { "1", "2", "3", "4", "5" }, + P24 = new FakeCount(5), + P25 = new FakeCountChild(5), +#endif // NET8_0_OR_GREATER + P1 = 4, + P2 = "1234", + P3 = "123456", + P4 = "12345", + P5 = 10, + P6 = 10, + P7 = 5, + P8 = 5, + P9 = 4.0m, + P10 = 20.0, + P21 = new int[] { 1, 2 }, + P22 = new int[] { 1, 2, 3, 4, 5 }, + P26 = 20.0, + }; + + Assert.False(Validator.TryValidateObject(failing, new ValidationContext(failing), results, true)); + + ValidateOptionsResult generatorResult = validator.Validate("OptionsUsingGeneratedAttributes", failing); + Assert.True(generatorResult.Failed); + + Assert.Equal(new [] { +#if NET8_0_OR_GREATER + "P0: The field OptionsUsingGeneratedAttributes.P0 must be a string or collection type with a minimum length of '1' and maximum length of '3'.", + "P11: The field OptionsUsingGeneratedAttributes.P11 must be between 1/30/2023 12:00:00 AM and 12/30/2023 12:00:00 AM.", + "P12: The field OptionsUsingGeneratedAttributes.P12 must be between 5 exclusive and 10.", + "P13: The field OptionsUsingGeneratedAttributes.P13 must be between 5 and 10 exclusive.", + "P14: The field OptionsUsingGeneratedAttributes.P14 must be a string or collection type with a minimum length of '2' and maximum length of '10'.", + "P15: The field OptionsUsingGeneratedAttributes.P15 must be a string or collection type with a minimum length of '2' and maximum length of '10'.", + "P16: The field OptionsUsingGeneratedAttributes.P16 must be a string or collection type with a minimum length of '2' and maximum length of '10'.", + "P17: The field OptionsUsingGeneratedAttributes.P17 must be a string or collection type with a minimum length of '2' and maximum length of '10'.", + "P18: The field OptionsUsingGeneratedAttributes.P18 must be a string or array type with a minimum length of '3'.", + "P19: The field OptionsUsingGeneratedAttributes.P19 must be a string or array type with a minimum length of '3'.", + "P20: The field OptionsUsingGeneratedAttributes.P20 must be a string or array type with a minimum length of '3'.", + "P23: The field OptionsUsingGeneratedAttributes.P23 must be a string or array type with a maximum length of '4'.", + "P24: The field OptionsUsingGeneratedAttributes.P24 must be a string or array type with a maximum length of '4'.", + "P25: The field OptionsUsingGeneratedAttributes.P25 must be a string or array type with a maximum length of '4'.", +#endif // NET8_0_OR_GREATER + "P1: The field OptionsUsingGeneratedAttributes.P1 must be between 1 and 3.", + "P2: The field OptionsUsingGeneratedAttributes.P2 must be a string or array type with a minimum length of '5'.", + "P3: The field OptionsUsingGeneratedAttributes.P3 must be a string or array type with a maximum length of '5'.", + "P4: 'OptionsUsingGeneratedAttributes.P4' and 'P2' do not match.", + "P5: The field OptionsUsingGeneratedAttributes.P5 must be between 2 and 8.", + "P6: The field OptionsUsingGeneratedAttributes.P6 must be between 2 and 8.", + "P7: The field OptionsUsingGeneratedAttributes.P7 must be between 10 and 20.", + "P8: The field OptionsUsingGeneratedAttributes.P8 must be between 10 and 20.", + "P9: The field OptionsUsingGeneratedAttributes.P9 must be between 1.5 and 3.14.", + "P10: The field OptionsUsingGeneratedAttributes.P10 must be between 12.4 and 16.5.", + "P21: The field OptionsUsingGeneratedAttributes.P21 must be a string or array type with a minimum length of '3'.", + "P22: The field OptionsUsingGeneratedAttributes.P22 must be a string or array type with a maximum length of '4'.", + "P26: The field OptionsUsingGeneratedAttributes.P26 must be between 12.4 and 16.5.", + }, generatorResult.Failures); + + Assert.Equal(results.Count(), generatorResult.Failures.Count()); + } + } + + public class FakeCount(int count) { public int Count { get { return count; } } } + public class FakeCountChild(int count) : FakeCount(count) { } + + public class OptionsUsingGeneratedAttributes + { +#if NET8_0_OR_GREATER + [LengthAttribute(1, 3)] + public string? P0 { get; set; } + + [RangeAttribute(typeof(DateTime), "01/30/2023", "12/30/2023", ParseLimitsInInvariantCulture = true, ConvertValueInInvariantCulture = true)] + public DateTime P11 { get; set; } + + [RangeAttribute(5, 10, MinimumIsExclusive = true)] + public int P12 { get; set; } + + [RangeAttribute(5, 10, MaximumIsExclusive = true)] + public int P13 { get; set; } + + [LengthAttribute(2, 10)] + public List P14 { get; set; } + + [LengthAttribute(2, 10)] + public FakeCount P15 { get; set; } + + [LengthAttribute(2, 10)] + public FakeCountChild P16 { get; set; } + + [LengthAttribute(2, 10)] + public int[] P17 { get; set; } + + [MinLengthAttribute(3)] + public List P18 { get; set; } + + [MinLengthAttribute(3)] + public FakeCount P19 { get; set; } + + [MinLengthAttribute(3)] + public FakeCountChild P20 { get; set; } + + [MaxLengthAttribute(4)] + public List P23 { get; set; } + + [MaxLengthAttribute(4)] + public FakeCount P24 { get; set; } + + [MaxLengthAttribute(4)] + public FakeCountChild P25 { get; set; } +#endif // NET8_0_OR_GREATER + + [RangeAttribute(1, 3)] + public int P1 { get; set; } + + [MinLengthAttribute(5)] + public string? P2 { get; set; } + + [MaxLengthAttribute(5)] + public string? P3 { get; set; } + + [CompareAttribute("P2")] + public string? P4 { get; set; } + + [RangeAttribute(typeof(byte), "2", "8")] + public byte P5 { get; set; } + + [RangeAttribute(typeof(sbyte), "2", "8")] + public sbyte P6 { get; set; } + + [RangeAttribute(typeof(short), "10", "20")] + public short P7 { get; set; } + + [RangeAttribute(typeof(ulong), "10", "20")] + public ulong P8 { get; set; } + + [RangeAttribute(typeof(decimal), "1.5", "3.14")] + public decimal P9 { get; set; } + + [RangeAttribute(typeof(double), "12.40", "16.50")] + public double P10 { get; set; } + + [MinLengthAttribute(3)] + public int[] P21 { get; set; } + + [MaxLengthAttribute(4)] + public int[] P22 { get; set; } + + [RangeAttribute(typeof(double), "12.40", "16.50")] + public double? P26 { get; set; } + } + + [OptionsValidator] + public partial class OptionsUsingGeneratedAttributesValidator : IValidateOptions + { } public class MyOptions @@ -356,4 +564,5 @@ public partial class NewAttributesValidator : IValidateOptions The options validation source generator is not available in C# {0}. Please use language version {1} or greater. - \ No newline at end of file + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetCoreApp/Validators.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetCoreApp/Validators.g.cs index cc1ebda6414e6..c487888c9f16b 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetCoreApp/Validators.g.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetCoreApp/Validators.g.cs @@ -12,6 +12,8 @@ internal sealed partial class __ThirdModelNoNamespaceValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ThirdModelNoNamespace options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -40,6 +42,8 @@ partial class FirstValidatorNoNamespace /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::FirstModelNoNamespace options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -78,6 +82,8 @@ partial class SecondValidatorNoNamespace /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::SecondModelNoNamespace options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -108,6 +114,8 @@ partial class FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::CustomAttr.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -149,6 +157,8 @@ internal sealed partial class __SecondModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Enumeration.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -181,6 +191,8 @@ internal sealed partial class __ThirdModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Enumeration.ThirdModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -211,6 +223,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Enumeration.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -363,6 +377,8 @@ partial struct SecondValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Enumeration.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -394,6 +410,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::FileScopedNamespace.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -425,6 +443,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::FunnyStrings.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -456,6 +476,8 @@ internal sealed partial class __SecondModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Generics.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -487,6 +509,8 @@ partial class FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Generics.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -523,6 +547,8 @@ partial struct MultiValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::MultiModelValidator.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -553,6 +579,8 @@ partial struct MultiValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::MultiModelValidator.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -585,6 +613,8 @@ internal sealed partial class __ThirdModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.ThirdModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -618,6 +648,8 @@ partial record struct FifthValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -654,6 +686,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -701,6 +735,8 @@ partial struct FourthValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -737,6 +773,8 @@ partial struct SecondValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -774,6 +812,8 @@ partial struct ThirdValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::Nested.Container1.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -807,6 +847,8 @@ partial class FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RandomMembers.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -839,6 +881,8 @@ internal sealed partial class __ThirdModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RecordTypes.ThirdModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -870,6 +914,8 @@ partial record struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RecordTypes.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -913,6 +959,8 @@ partial record struct SecondValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RecordTypes.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -944,6 +992,8 @@ partial record class ThirdValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RecordTypes.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -976,6 +1026,8 @@ internal sealed partial class __SecondModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RepeatedTypes.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1012,6 +1064,8 @@ internal sealed partial class __ThirdModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RepeatedTypes.ThirdModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1043,6 +1097,8 @@ partial class FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::RepeatedTypes.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1108,6 +1164,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::SelfValidation.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1141,6 +1199,8 @@ internal sealed partial class __RangeAttributeModelDoubleValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RangeAttributeModelDouble options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1172,6 +1232,8 @@ internal sealed partial class __RequiredAttributeModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RequiredAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1203,6 +1265,8 @@ internal sealed partial class __TypeWithoutOptionsValidatorValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.TypeWithoutOptionsValidator options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1248,6 +1312,8 @@ partial class AttributePropertyModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.AttributePropertyModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1288,6 +1354,8 @@ partial class ComplexModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.ComplexModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1318,6 +1386,8 @@ partial class CustomTypeCustomValidationAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.CustomTypeCustomValidationAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1348,6 +1418,8 @@ partial class CustomValidationAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.CustomValidationAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1378,6 +1450,8 @@ partial class DataTypeAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.DataTypeAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1408,6 +1482,8 @@ partial class DerivedModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.DerivedModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1458,6 +1534,8 @@ partial class EmailAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.EmailAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1488,6 +1566,8 @@ partial class LeafModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.LeafModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1538,6 +1618,8 @@ partial class MultipleAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.MultipleAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1599,6 +1681,8 @@ partial class RangeAttributeModelDateValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RangeAttributeModelDate options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1629,6 +1713,8 @@ partial class RangeAttributeModelDoubleValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RangeAttributeModelDouble options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1659,6 +1745,8 @@ partial class RangeAttributeModelIntValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RangeAttributeModelInt options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1689,6 +1777,8 @@ partial class RegularExpressionAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RegularExpressionAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1719,6 +1809,8 @@ partial class RequiredAttributeModelValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::TestClasses.OptionsValidation.RequiredAttributeModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1750,6 +1842,8 @@ internal sealed partial class __SecondModelValidator__ /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public static global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValueTypes.SecondModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1781,6 +1875,8 @@ partial struct FirstValidator /// The options instance. /// Validation result. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "The created ValidationContext object is used in a way that never call reflection")] public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ValueTypes.FirstModel options) { global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null; @@ -1820,7 +1916,7 @@ 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.MinLengthAttribute A2 = new global::System.ComponentModel.DataAnnotations.MinLengthAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute( (int)5); internal static readonly global::CustomAttr.CustomAttribute A3 = new global::CustomAttr.CustomAttribute( @@ -1833,30 +1929,30 @@ file static class __Attributes false, "X"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A5 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)0, (int)10); internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A6 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( "\"\r\n\\\\"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A7 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A7 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (double)0.5, (double)0.9); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A8 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A8 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( typeof(global::System.DateTime), "1/2/2004", "3/4/2004"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A9 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A9 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3) { ErrorMessage = "ErrorMessage" }; - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A10 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A10 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3) { @@ -1880,19 +1976,19 @@ file static class __Attributes internal static readonly global::System.ComponentModel.DataAnnotations.DataTypeAttribute A15 = new global::System.ComponentModel.DataAnnotations.DataTypeAttribute( (global::System.ComponentModel.DataAnnotations.DataType)11); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A16 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A16 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A17 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A17 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)3, (int)5); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A18 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A18 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)5, (int)9); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A19 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A19 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( typeof(global::System.DateTime), "1/2/2004", "3/4/2004") @@ -1924,3 +2020,150 @@ file static class __Validators internal static readonly global::RecordTypes.ThirdValidator V7 = new global::RecordTypes.ThirdValidator(); } } +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetFX/Validators.g.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetFX/Validators.g.cs index ebdcb1ad6d6ba..7e998cea22cdd 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetFX/Validators.g.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Baselines/NetFX/Validators.g.cs @@ -1820,7 +1820,7 @@ 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.MinLengthAttribute A2 = new global::System.ComponentModel.DataAnnotations.MinLengthAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute A2 = new __OptionValidationGeneratedAttributes.__SourceGen__MinLengthAttribute( (int)5); internal static readonly global::CustomAttr.CustomAttribute A3 = new global::CustomAttr.CustomAttribute( @@ -1833,30 +1833,30 @@ file static class __Attributes false, "X"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A5 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)0, (int)10); internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A6 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( "\"\r\n\\\\"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A7 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A7 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (double)0.5, (double)0.9); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A8 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A8 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( typeof(global::System.DateTime), "1/2/2004", "3/4/2004"); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A9 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A9 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3) { ErrorMessage = "ErrorMessage" }; - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A10 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A10 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3) { @@ -1880,15 +1880,15 @@ file static class __Attributes internal static readonly global::System.ComponentModel.DataAnnotations.DataTypeAttribute A15 = new global::System.ComponentModel.DataAnnotations.DataTypeAttribute( (global::System.ComponentModel.DataAnnotations.DataType)11); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A16 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A16 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)1, (int)3); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A17 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A17 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)3, (int)5); - internal static readonly global::System.ComponentModel.DataAnnotations.RangeAttribute A18 = new global::System.ComponentModel.DataAnnotations.RangeAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A18 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( (int)5, (int)9); @@ -1916,3 +1916,150 @@ file static class __Validators internal static readonly global::RecordTypes.ThirdValidator V7 = new global::RecordTypes.ThirdValidator(); } } +namespace __OptionValidationGeneratedAttributes +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__MinLengthAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + private static string DefaultErrorMessageString => "The field {0} must be a string or array type with a minimum length of '{1}'."; + + public __SourceGen__MinLengthAttribute(int length) : base(() => DefaultErrorMessageString) { Length = length; } + public int Length { get; } + public override bool IsValid(object? value) + { + if (Length < -1) + { + throw new global::System.InvalidOperationException("MinLengthAttribute must have a Length value that is zero or greater."); + } + if (value == null) + { + return true; + } + + int length; + if (value is string stringValue) + { + length = stringValue.Length; + } + else if (value is System.Collections.ICollection collectionValue) + { + length = collectionValue.Count; + } + else + { + throw new global::System.InvalidCastException($"The field of type {value.GetType()} must be a string, array, or ICollection type."); + } + + return length >= Length; + } + public override string FormatErrorMessage(string name) => string.Format(global::System.Globalization.CultureInfo.CurrentCulture, ErrorMessageString, name, Length); + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")] + [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] + file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute + { + public __SourceGen__RangeAttribute(int minimum, int maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(int); + } + public __SourceGen__RangeAttribute(double minimum, double maximum) : base() + { + Minimum = minimum; + Maximum = maximum; + OperandType = typeof(double); + } + public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base() + { + OperandType = type; + NeedToConvertMinMax = true; + Minimum = minimum; + Maximum = maximum; + } + public object Minimum { get; private set; } + public object Maximum { get; private set; } + public bool MinimumIsExclusive { get; set; } + public bool MaximumIsExclusive { get; set; } + public global::System.Type OperandType { get; } + public bool ParseLimitsInInvariantCulture { get; set; } + public bool ConvertValueInInvariantCulture { get; set; } + public override string FormatErrorMessage(string name) => + string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum); + private bool NeedToConvertMinMax { get; } + private bool Initialized { get; set; } + public override bool IsValid(object? value) + { + if (!Initialized) + { + if (Minimum is null || Maximum is null) + { + throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + if (NeedToConvertMinMax) + { + System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values."); + } + int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); + if (cmp > 0) + { + throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'."); + } + else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive)) + { + throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value."); + } + Initialized = true; + } + + if (value is null or string { Length: 0 }) + { + return true; + } + + System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; + object? convertedValue; + + try + { + convertedValue = ConvertValue(value, formatProvider); + } + catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + { + return false; + } + + var min = (global::System.IComparable)Minimum; + var max = (global::System.IComparable)Maximum; + + return + (MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) && + (MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0); + } + private string GetValidationErrorMessage() + { + return (MinimumIsExclusive, MaximumIsExclusive) switch + { + (false, false) => "The field {0} must be between {1} and {2}.", + (true, false) => "The field {0} must be between {1} exclusive and {2}.", + (false, true) => "The field {0} must be between {1} and {2} exclusive.", + (true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.", + }; + } + private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider) + { + if (value is string stringValue) + { + value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider); + } + else + { + value = global::System.Convert.ChangeType(value, OperandType, formatProvider); + } + return value; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Resources/Strings.resx index 6b8167e94b119..90e5b01ed3b1b 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Resources/Strings.resx @@ -216,4 +216,10 @@ The options validation source generator is not available in C# {0}. Please use language version {1} or greater. - \ No newline at end of file + + The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. + + + The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures. + + diff --git a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs index 870b27bbe9e10..7a39d8a8810fd 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; class Program { @@ -37,6 +39,22 @@ optionsC is null || return -1; } + LocalOptionsValidator localOptionsValidator = new LocalOptionsValidator(); + OptionsUsingValidationAttributes optionsUsingValidationAttributes = new OptionsUsingValidationAttributes + { + P1 = "12345", + P2 = new List { "1234", "12345" }, + P3 = "123456", + P4 = "12345", + P5 = 7 + }; + + ValidateOptionsResult result = localOptionsValidator.Validate("", optionsUsingValidationAttributes); + if (result.Failed) + { + return -2; + } + return 100; } @@ -76,3 +94,29 @@ private class OptionsD public string OptionString { get; set; } } } + +public class OptionsUsingValidationAttributes +{ + [Required] + [MinLength(5)] + public string P1 { get; set; } + + [Required] + [MaxLength(5)] + public List P2 { get; set; } + + [Length(2, 8)] + public string P3 { get; set; } + + [Compare("P1")] + public string P4 { get; set; } + + [Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)] + public int P5 { get; set; } +} + +[OptionsValidator] +public partial class LocalOptionsValidator : IValidateOptions +{ +} + diff --git a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/Microsoft.Extensions.Options.TrimmingTests.proj b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/Microsoft.Extensions.Options.TrimmingTests.proj index 669ac862ad7b1..15b6dc0a6ea0e 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/Microsoft.Extensions.Options.TrimmingTests.proj +++ b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/Microsoft.Extensions.Options.TrimmingTests.proj @@ -7,10 +7,15 @@ Microsoft.Extensions.DependencyInjection - + + + + <_additionalProjectReference Include="<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Options\gen\Microsoft.Extensions.Options.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" SetTargetFramework="TargetFramework=netstandard2.0" />" /> + +