diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs index 7f80468702..8c98b952ca 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -55,8 +55,8 @@ public class PropertyApiDescriptionModel TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(propertyInfo.PropertyType), IsRequired = customAttributes.OfType().Any() || propertyInfo.GetCustomAttributesData().Any(attr => attr.AttributeType.Name == "RequiredMemberAttribute"), IsNullable = ReflectionHelper.IsNullable(propertyInfo), - Minimum = rangeAttribute != null ? Convert.ToString(rangeAttribute.Minimum, CultureInfo.InvariantCulture) : null, - Maximum = rangeAttribute != null ? Convert.ToString(rangeAttribute.Maximum, CultureInfo.InvariantCulture) : null, + Minimum = GetRangeBound(rangeAttribute, rangeAttribute?.Minimum), + Maximum = GetRangeBound(rangeAttribute, rangeAttribute?.Maximum), MinimumIsExclusive = GetMinimumIsExclusive(rangeAttribute), MaximumIsExclusive = GetMaximumIsExclusive(rangeAttribute), MinLength = customAttributes.OfType().FirstOrDefault()?.Length ?? customAttributes.OfType().FirstOrDefault()?.MinimumLength, @@ -65,6 +65,34 @@ public class PropertyApiDescriptionModel }; } + private static string? GetRangeBound(RangeAttribute? rangeAttribute, object? bound) + { + if (rangeAttribute == null || bound == null) + { + return null; + } + + // The Range(Type, string, string) constructor keeps its limits as strings until the + // first validation, so a numeric one is written in the culture of the declaring code. + if (bound is string text) + { + return decimal.TryParse(text, NumberStyles.Float, GetRangeLimitCulture(rangeAttribute), out var number) + ? number.ToString(CultureInfo.InvariantCulture) + : text; + } + + return Convert.ToString(bound, CultureInfo.InvariantCulture); + } + + private static CultureInfo GetRangeLimitCulture(RangeAttribute rangeAttribute) + { +#if NET8_0_OR_GREATER + return rangeAttribute.ParseLimitsInInvariantCulture ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture; +#else + return CultureInfo.CurrentCulture; +#endif + } + private static bool? GetMinimumIsExclusive(RangeAttribute? rangeAttribute) { if (rangeAttribute == null) diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs index b06f556d92..c2577624a2 100644 --- a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs @@ -246,6 +246,10 @@ public class FluentValidationApiDescription_Tests : AbpHttpFluentValidationTestB property.Minimum.ShouldBe("2"); property.Maximum.ShouldBe("9.5"); + + var typed = await GetPropertyAsync(nameof(CultureTestDto.TypedDecimalRangeValue)); + typed.Minimum.ShouldBe("2"); + typed.Maximum.ShouldBe("9.5"); } } diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs index 88d38382b7..104da7234a 100644 --- a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs @@ -9,6 +9,9 @@ public class CultureTestDto [Range(1.5, 9.5)] public double DecimalRangeValue { get; set; } + [Range(typeof(decimal), "1,5", "9,5")] + public decimal TypedDecimalRangeValue { get; set; } + [Range(typeof(DateTime), "2020-01-01", "2030-01-01")] public DateTime DateRangeValue { get; set; } } @@ -18,5 +21,6 @@ public class CultureTestDtoValidator : AbstractValidator public CultureTestDtoValidator() { RuleFor(x => x.DecimalRangeValue).GreaterThanOrEqualTo(2.0); + RuleFor(x => x.TypedDecimalRangeValue).GreaterThanOrEqualTo(2m); } } diff --git a/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs index 546782178f..a59239de54 100644 --- a/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs +++ b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs @@ -21,6 +21,27 @@ public class PropertyApiDescriptionModel_Tests } } + [Fact] + public void Create_Should_Write_Typed_Range_Bounds_With_The_Invariant_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var model = CreateModel(nameof(TestClass.TypedDecimalRangeValue)); + + model.Minimum.ShouldBe("1.5"); + model.Maximum.ShouldBe("9.5"); + } + } + + [Fact] + public void Create_Should_Keep_A_Typed_Range_Bound_That_Is_Not_A_Number() + { + var model = CreateModel(nameof(TestClass.DateRangeValue)); + + model.Minimum.ShouldBe("2020-01-01"); + model.Maximum.ShouldBe("2030-01-01"); + } + [Fact] public void Create_Should_Read_The_Exclusive_Bounds_Of_The_Range_Attribute() { @@ -62,6 +83,12 @@ public class PropertyApiDescriptionModel_Tests [Range(1.5, 9.5)] public double DecimalRangeValue { get; set; } + [Range(typeof(decimal), "1,5", "9,5")] + public decimal TypedDecimalRangeValue { get; set; } + + [Range(typeof(DateTime), "2020-01-01", "2030-01-01")] + public DateTime DateRangeValue { get; set; } + [Range(1, 100, MinimumIsExclusive = true, MaximumIsExclusive = true)] public int ExclusiveRangeValue { get; set; }