From 241d0f2a64945bbdacbbf80d0acf759cb9652dba Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Wed, 15 Nov 2023 10:54:16 -0800 Subject: [PATCH] SupportTimeSpan with RangeAttribute in Options validation source generator --- .../gen/Emitter.cs | 91 +++++++++++-- .../gen/Parser.cs | 41 ++++-- .../gen/SymbolHolder.cs | 1 + .../gen/SymbolLoader.cs | 4 + .../EmitterWithCustomValidator.netcore.g.cs | 8 +- .../EmitterWithCustomValidator.netfx.g.cs | 8 +- ...eneratedAttributesTest.netcore.lang10.g.cs | 81 +++++++++++- ...eneratedAttributesTest.netcore.lang11.g.cs | 81 +++++++++++- .../GeneratedAttributesTest.netfx.lang10.g.cs | 81 +++++++++++- .../GeneratedAttributesTest.netfx.lang11.g.cs | 81 +++++++++++- .../tests/SourceGeneration.Unit.Tests/Main.cs | 7 + .../Baselines/NetCoreApp/Validators.g.cs | 124 ++++++++++++++++-- .../Baselines/NetFX/Validators.g.cs | 120 +++++++++++++++-- .../Generated/OptionsValidationTests.cs | 38 ++++++ .../TestClasses/Models.cs | 20 +++ .../tests/TrimmingTests/ConfigureTests.cs | 7 +- 16 files changed, 712 insertions(+), 81 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs index bafb09781608b..467edba76d373 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs @@ -374,12 +374,83 @@ public void EmitCompareAttribute(string modifier, string prefix, string classNam """); } - public void EmitRangeAttribute(string modifier, string prefix, string className, string suffix) + public void EmitRangeAttribute(string modifier, string prefix, string className, string suffix, bool emitTimeSpanSupport) { OutGeneratedCodeAttribute(); string qualifiedClassName = $"{prefix}{suffix}_{className}"; + string initializationString = emitTimeSpanSupport ? + """ + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } + """ + : + """ + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + """; + + string convertValue = emitTimeSpanSupport ? + """ + if (OperandType == typeof(global::System.TimeSpan)) + { + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } + } + else + { + 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; + } + } + """ + : + """ + 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; + } + """; + + + OutLn($$""" [global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)] {{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}} @@ -414,19 +485,20 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); +{{initializationString}} } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -448,14 +520,7 @@ public override bool IsValid(object? value) 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; - } +{{convertValue}} var min = (global::System.IComparable)Minimum; var max = (global::System.IComparable)Maximum; @@ -574,7 +639,7 @@ private void GenValidationAttributesClasses() } else if (attributeData.Key == _symbolHolder.RangeAttributeSymbol.Name) { - EmitRangeAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, _optionsSourceGenContext.Suffix); + EmitRangeAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, _optionsSourceGenContext.Suffix, attributeData.Value is not null); } } diff --git a/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs index ab2c19de81923..8a88ec6f2f4e6 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs @@ -624,10 +624,21 @@ private void TrackCompareAttributeForSubstitution(AttributeData attribute, IType private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSymbol memberType, ref string attributeFullQualifiedName) { ImmutableArray constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray.Empty; - SpecialType argumentSpecialType = SpecialType.None; + ITypeSymbol? argumentType = null; + bool hasTimeSpanType = false; + + ITypeSymbol typeSymbol = memberType; + if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + } + if (constructorParameters.Length == 2) { - argumentSpecialType = constructorParameters[0].Type.SpecialType; + if (OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol)) + { + argumentType = constructorParameters[0].Type; + } } else if (constructorParameters.Length == 3) { @@ -641,23 +652,25 @@ private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSy } } - if (argumentValue is INamedTypeSymbol namedTypeSymbol && OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol)) + if (argumentValue is INamedTypeSymbol namedTypeSymbol) { - argumentSpecialType = namedTypeSymbol.SpecialType; + // When type is provided as a parameter, it has to match the property type. + if (OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol) && typeSymbol.SpecialType == namedTypeSymbol.SpecialType) + { + argumentType = namedTypeSymbol; + } + else if (SymbolEqualityComparer.Default.Equals(namedTypeSymbol, _symbolHolder.TimeSpanSymbol) && + (SymbolEqualityComparer.Default.Equals(typeSymbol, _symbolHolder.TimeSpanSymbol) || typeSymbol.SpecialType == SpecialType.System_String)) + { + hasTimeSpanType = true; + argumentType = _symbolHolder.TimeSpanSymbol; + } } } - 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. + if (argumentType is not null) { - _optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: false, out _); + _optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: hasTimeSpanType, out _); attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}"; } } diff --git a/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs b/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs index 3447a07d39830..8e7073f8ec218 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/SymbolHolder.cs @@ -23,6 +23,7 @@ internal sealed record class SymbolHolder( INamedTypeSymbol IValidatableObjectSymbol, INamedTypeSymbol GenericIEnumerableSymbol, INamedTypeSymbol TypeSymbol, + INamedTypeSymbol TimeSpanSymbol, INamedTypeSymbol ValidateObjectMembersAttributeSymbol, INamedTypeSymbol ValidateEnumeratedItemsAttributeSymbol); } diff --git a/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs b/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs index ea55622892975..5be4932d8d6c4 100644 --- a/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs +++ b/src/libraries/Microsoft.Extensions.Options/gen/SymbolLoader.cs @@ -19,6 +19,7 @@ internal static class SymbolLoader internal const string IValidatableObjectType = "System.ComponentModel.DataAnnotations.IValidatableObject"; internal const string IValidateOptionsType = "Microsoft.Extensions.Options.IValidateOptions`1"; internal const string TypeOfType = "System.Type"; + internal const string TimeSpanType = "System.TimeSpan"; 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"; @@ -42,6 +43,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold var validateOptionsSymbol = GetSymbol(IValidateOptionsType); var genericIEnumerableSymbol = GetSymbol(GenericIEnumerableType); var typeSymbol = GetSymbol(TypeOfType); + var timeSpanSymbol = GetSymbol(TimeSpanType); var validateObjectMembersAttribute = GetSymbol(ValidateObjectMembersAttribute); var validateEnumeratedItemsAttribute = GetSymbol(ValidateEnumeratedItemsAttribute); var unconditionalSuppressMessageAttributeSymbol = GetSymbol(UnconditionalSuppressMessageAttributeType); @@ -70,6 +72,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold validateOptionsSymbol == null || genericIEnumerableSymbol == null || typeSymbol == null || + timeSpanSymbol == null || validateObjectMembersAttribute == null || validateEnumeratedItemsAttribute == null) { @@ -93,6 +96,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold ivalidatableObjectSymbol, genericIEnumerableSymbol, typeSymbol, + timeSpanSymbol, validateObjectMembersAttribute, validateEnumeratedItemsAttribute); 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 index 2c5af12c5b5f2..38bacf966df05 100644 --- 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 @@ -99,19 +99,21 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) 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 index 9dc3ded5bd462..fe77e3e6bd924 100644 --- 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 @@ -97,19 +97,21 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) 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 index cc9864a2619c4..7cf1fe61e1a94 100644 --- 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 @@ -150,6 +150,26 @@ partial class OptionsUsingGeneratedAttributesValidator (builder ??= new()).AddResults(validationResults); } + context.MemberName = "P13"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P13" : $"{name}.P13"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A6); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P13, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P14"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P14" : $"{name}.P14"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A7); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P14, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build(); } } @@ -175,6 +195,16 @@ internal static class __Attributes_2C497155 internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute( "P5"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A6 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "23:59:59"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A7 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + typeof(global::System.TimeSpan), + "01:00:00", + "23:59:59"); } } namespace __OptionValidationStaticInstances @@ -395,19 +425,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -429,13 +474,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; 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 index 2a33e51b0b617..f7bba04603342 100644 --- 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 @@ -150,6 +150,26 @@ partial class OptionsUsingGeneratedAttributesValidator (builder ??= new()).AddResults(validationResults); } + context.MemberName = "P13"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P13" : $"{name}.P13"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A6); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P13, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P14"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P14" : $"{name}.P14"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A7); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P14, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build(); } } @@ -175,6 +195,16 @@ file static class __Attributes internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute( "P5"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A6 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "23:59:59"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A7 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "01:00:00", + "23:59:59"); } } namespace __OptionValidationStaticInstances @@ -395,19 +425,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -429,13 +474,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; 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 index 7f5eb90a20281..4b28eb159d147 100644 --- 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 @@ -118,6 +118,26 @@ partial class OptionsUsingGeneratedAttributesValidator (builder ??= new()).AddResults(validationResults); } + context.MemberName = "P13"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P13" : $"{name}.P13"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A5); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P13, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P14"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P14" : $"{name}.P14"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes_2C497155.A6); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P14, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build(); } } @@ -139,6 +159,16 @@ internal static class __Attributes_2C497155 internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_CompareAttribute( "P5"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "23:59:59"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute A6 = new __OptionValidationGeneratedAttributes.__SourceGen__2C497155_RangeAttribute( + typeof(global::System.TimeSpan), + "01:00:00", + "23:59:59"); } } namespace __OptionValidationStaticInstances @@ -310,19 +340,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -344,13 +389,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; 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 index 3ab56e21320a0..4c300abc6d05b 100644 --- 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 @@ -118,6 +118,26 @@ partial class OptionsUsingGeneratedAttributesValidator (builder ??= new()).AddResults(validationResults); } + context.MemberName = "P13"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P13" : $"{name}.P13"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A5); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P13, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P14"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingGeneratedAttributes.P14" : $"{name}.P14"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A6); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P14, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build(); } } @@ -139,6 +159,16 @@ file static class __Attributes internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute A4 = new __OptionValidationGeneratedAttributes.__SourceGen__CompareAttribute( "P5"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A5 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "23:59:59"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A6 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "01:00:00", + "23:59:59"); } } namespace __OptionValidationStaticInstances @@ -310,19 +340,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -344,13 +389,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; 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 623251707f87b..c72be0d72c2c0 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 @@ -1731,6 +1731,7 @@ public async Task GeneratedAttributesTest(LanguageVersion languageVersion) #endif //NETCOREAPP string source = $$""" + using System; using System.Collections.Generic; using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; @@ -1782,6 +1783,12 @@ public class OptionsUsingGeneratedAttributes [MaxLengthAttribute(5)] public List? P12 { get; set; } + + [RangeAttribute(typeof(TimeSpan), "00:00:00", "23:59:59")] + public string? P13 { get; set; } + + [RangeAttribute(typeof(TimeSpan), "01:00:00", "23:59:59")] + public TimeSpan P14 { get; set; } } [OptionsValidator] 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 956cae26e90f6..93c101431004c 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 @@ -1721,6 +1721,68 @@ partial class MultipleAttributeModelValidator } } namespace TestClasses.OptionsValidation +{ + partial class OptionsUsingRangeWithTimeSpanValidator + { + /// + /// 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::TestClasses.OptionsValidation.OptionsUsingRangeWithTimeSpan 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) ? "OptionsUsingRangeWithTimeSpan.P1" : $"{name}.P1"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P1, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P2"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P2" : $"{name}.P2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P3" : $"{name}.P3"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + 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 TestClasses.OptionsValidation { partial class RangeAttributeModelDateValidator { @@ -1742,7 +1804,7 @@ partial class RangeAttributeModelDateValidator context.MemberName = "Val"; context.DisplayName = string.IsNullOrEmpty(name) ? "RangeAttributeModelDate.Val" : $"{name}.Val"; - validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A20); if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val, context, validationResults, validationAttributes)) { (builder ??= new()).AddResults(validationResults); @@ -1838,7 +1900,7 @@ partial class RegularExpressionAttributeModelValidator context.MemberName = "Val"; context.DisplayName = string.IsNullOrEmpty(name) ? "RegularExpressionAttributeModel.Val" : $"{name}.Val"; - validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A20); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A21); if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val, context, validationResults, validationAttributes)) { (builder ??= new()).AddResults(validationResults); @@ -2039,6 +2101,11 @@ file static class __Attributes (int)9); internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A19 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "00:00:10"); + + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A20 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( typeof(global::System.DateTime), "1/2/2004", "3/4/2004") @@ -2046,7 +2113,7 @@ file static class __Attributes ParseLimitsInInvariantCulture = true }; - internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A20 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( + internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A21 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( "\\s"); } } @@ -2143,19 +2210,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -2177,13 +2259,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; 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 faae7d62d9c41..3c9f86fd84f8a 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 @@ -1637,6 +1637,66 @@ partial class MultipleAttributeModelValidator } } namespace TestClasses.OptionsValidation +{ + partial class OptionsUsingRangeWithTimeSpanValidator + { + /// + /// 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::TestClasses.OptionsValidation.OptionsUsingRangeWithTimeSpan 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) ? "OptionsUsingRangeWithTimeSpan.P1" : $"{name}.P1"; + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P1, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P2"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P2" : $"{name}.P2"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P2, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P3"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P3" : $"{name}.P3"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.P3, context, validationResults, validationAttributes)) + { + (builder ??= new()).AddResults(validationResults); + } + + context.MemberName = "P4"; + context.DisplayName = string.IsNullOrEmpty(name) ? "OptionsUsingRangeWithTimeSpan.P4" : $"{name}.P4"; + validationResults.Clear(); + validationAttributes.Clear(); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + 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 TestClasses.OptionsValidation { partial class RangeAttributeModelDateValidator { @@ -1746,7 +1806,7 @@ partial class RegularExpressionAttributeModelValidator context.MemberName = "Val"; context.DisplayName = string.IsNullOrEmpty(name) ? "RegularExpressionAttributeModel.Val" : $"{name}.Val"; - validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A19); + validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A20); if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val, context, validationResults, validationAttributes)) { (builder ??= new()).AddResults(validationResults); @@ -1940,7 +2000,12 @@ file static class __Attributes (int)5, (int)9); - internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A19 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( + internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A19 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute( + typeof(global::System.TimeSpan), + "00:00:00", + "00:00:10"); + + internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A20 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute( "\\s"); } } @@ -2037,19 +2102,34 @@ 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; } + private const string c_minMaxError = "The minimum and maximum values must be set to valid values."; + 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."); + throw new global::System.InvalidOperationException(c_minMaxError); } 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."); + if (OperandType == typeof(global::System.TimeSpan)) + { + if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) || + !global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum)) + { + throw new global::System.InvalidOperationException(c_minMaxError); + } + Minimum = timeSpanMinimum; + Maximum = timeSpanMaximum; + } + else + { + Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError); + } } int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum); if (cmp > 0) @@ -2071,13 +2151,35 @@ public override bool IsValid(object? value) System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture; object? convertedValue; - try + if (OperandType == typeof(global::System.TimeSpan)) { - convertedValue = ConvertValue(value, formatProvider); + if (value is global::System.TimeSpan) + { + convertedValue = value; + } + else if (value is string) + { + if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue)) + { + return false; + } + convertedValue = timeSpanValue; + } + else + { + throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator."); + } } - catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException) + else { - return false; + 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; diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Generated/OptionsValidationTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Generated/OptionsValidationTests.cs index 49941010f1ace..4f9770ce7a17c 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Generated/OptionsValidationTests.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/Generated/OptionsValidationTests.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Globalization; +using System.Linq; using Microsoft.Extensions.Options; using TestClasses.OptionsValidation; using Xunit; @@ -439,4 +440,41 @@ public void AttributePropertyModelTestOnErrorMessageResource() var modelValidator = new AttributePropertyModelValidator(); Utils.VerifyValidateOptionsResult(modelValidator.Validate(nameof(validModel), validModel), 1); } + + [Fact] + public void OptionsUsingRangeWithTimeSpanValid() + { + var validModel = new OptionsUsingRangeWithTimeSpan + { + P1 = TimeSpan.FromSeconds(1), + P2 = TimeSpan.FromSeconds(2), + P3 = "00:00:03", + P4 = "00:00:04", + }; + + var modelValidator = new OptionsUsingRangeWithTimeSpanValidator(); + ValidateOptionsResult result = modelValidator.Validate(nameof(validModel), validModel); + Assert.Equal(ValidateOptionsResult.Success, result); + + var invalidModel = new OptionsUsingRangeWithTimeSpan + { + P1 = TimeSpan.FromSeconds(11), + P2 = TimeSpan.FromSeconds(-2), + P3 = "01:00:03", + P4 = "02:00:04", + }; + result = modelValidator.Validate(nameof(invalidModel), invalidModel); + Assert.Equal(4, result.Failures.Count()); + + // null values pass the validation! + invalidModel = new OptionsUsingRangeWithTimeSpan + { + P1 = TimeSpan.FromSeconds(100), + P2 = null, + P3 = "00:01:00", + P4 = null, + }; + result = modelValidator.Validate(nameof(invalidModel), invalidModel); + Assert.Equal(2, result.Failures.Count()); + } } diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/TestClasses/Models.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/TestClasses/Models.cs index 240163023eb93..c245ebd783bdc 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/TestClasses/Models.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/TestClasses/Models.cs @@ -179,6 +179,21 @@ public class ComplexModel public TypeWithoutOptionsValidator? ValWithoutOptionsValidator { get; set; } } + public class OptionsUsingRangeWithTimeSpan + { + [Range(typeof(TimeSpan), "00:00:00", "00:00:10")] + public TimeSpan P1 { get; set; } + + [Range(typeof(TimeSpan), "00:00:00", "00:00:10")] + public TimeSpan? P2 { get; set; } + + [Range(typeof(TimeSpan), "00:00:00", "00:00:10")] + public string P3 { get; set; } + + [Range(typeof(TimeSpan), "00:00:00", "00:00:10")] + public string? P4 { get; set; } + } + [OptionsValidator] public partial class RequiredAttributeModelValidator : IValidateOptions { @@ -248,4 +263,9 @@ public partial class LeafModelValidator : IValidateOptions internal sealed partial class ComplexModelValidator : IValidateOptions { } + + [OptionsValidator] + internal sealed partial class OptionsUsingRangeWithTimeSpanValidator : IValidateOptions + { + } } diff --git a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs index 7a39d8a8810fd..90179563300b9 100644 --- a/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs +++ b/src/libraries/Microsoft.Extensions.Options/tests/TrimmingTests/ConfigureTests.cs @@ -46,7 +46,8 @@ optionsC is null || P2 = new List { "1234", "12345" }, P3 = "123456", P4 = "12345", - P5 = 7 + P5 = 7, + P6 = TimeSpan.FromSeconds(5), }; ValidateOptionsResult result = localOptionsValidator.Validate("", optionsUsingValidationAttributes); @@ -113,6 +114,10 @@ public class OptionsUsingValidationAttributes [Range(1, 10, MinimumIsExclusive = true, MaximumIsExclusive = true)] public int P5 { get; set; } + + [Range(typeof(TimeSpan), "00:00:00", "00:00:10")] + public TimeSpan P6 { get; set; } + } [OptionsValidator]