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 ac19827a04..28e68a2128 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 @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; @@ -11,6 +13,21 @@ namespace Volo.Abp.Http.Modeling; [Serializable] public class PropertyApiDescriptionModel { + private static readonly HashSet NumericTypes = new HashSet + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal) + }; + public string Name { get; set; } = default!; public string? JsonName { get; set; } @@ -73,24 +90,41 @@ public class PropertyApiDescriptionModel } // 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. - // A limit that already reads as invariant is kept verbatim, because rewriting it can - // only lose precision: "1e-30" would come back as a zero from a decimal round trip. + // first validation, so a numeric one is still written in the culture that declared it. + // Converting it with the operand type of the attribute keeps the api definition + // independent of the culture, and reports the value the attribute itself validates + // against, which is not always the value that was written down. if (bound is string text) { - if (decimal.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out _)) + if (!NumericTypes.Contains(rangeAttribute.OperandType)) { return text; } - return decimal.TryParse(text, NumberStyles.Float, GetRangeLimitCulture(rangeAttribute), out var number) - ? number.ToString(CultureInfo.InvariantCulture) + var converted = ConvertRangeLimitOrNull(text, rangeAttribute); + return converted != null + ? Convert.ToString(converted, CultureInfo.InvariantCulture) : text; } return Convert.ToString(bound, CultureInfo.InvariantCulture); } + private static object? ConvertRangeLimitOrNull(string text, RangeAttribute rangeAttribute) + { + try + { + return TypeDescriptor + .GetConverter(rangeAttribute.OperandType) + .ConvertFromString(null, GetRangeLimitCulture(rangeAttribute), text); + } + catch (Exception) + { + // A limit the attribute can not convert itself is reported the way it was written. + return null; + } + } + private static CultureInfo GetRangeLimitCulture(RangeAttribute rangeAttribute) { #if NET8_0_OR_GREATER 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 f9274324d6..565d66c3c8 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 @@ -34,13 +34,25 @@ public class PropertyApiDescriptionModel_Tests } [Fact] - public void Create_Should_Keep_A_Typed_Range_Bound_That_Is_Already_Invariant() + public void Create_Should_Convert_A_Typed_Range_Bound_With_Its_Operand_Type() { - // A decimal round trip would turn a magnitude below its range into a zero. + // A magnitude outside the decimal range is still a valid double bound. var model = CreateModel(nameof(TestClass.ExponentRangeValue)); - model.Minimum.ShouldBe("1e-30"); - model.Maximum.ShouldBe("1e30"); + model.Minimum.ShouldBe("1E-30"); + model.Maximum.ShouldBe("1E+30"); + } + + [Fact] + public void Create_Should_Convert_A_Typed_Double_Range_Bound_Written_In_Another_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var model = CreateModel(nameof(TestClass.CultureExponentRangeValue)); + + model.Minimum.ShouldBe("1.5E+30"); + model.Maximum.ShouldBe("9.5E+30"); + } } [Fact] @@ -99,6 +111,9 @@ public class PropertyApiDescriptionModel_Tests [Range(typeof(double), "1e-30", "1e30", ParseLimitsInInvariantCulture = true)] public double ExponentRangeValue { get; set; } + [Range(typeof(double), "1,5E+30", "9,5E+30")] + public double CultureExponentRangeValue { get; set; } + [Range(typeof(DateTime), "2020-01-01", "2030-01-01")] public DateTime DateRangeValue { get; set; }