From aee66f02a79c887e8500c942f2daad8be6965e5a Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 31 Aug 2026 10:11:28 +0800 Subject: [PATCH] Convert a typed range limit with the operand type of the attribute Range(Type, string, string) keeps its limits as strings until the first validation. They are converted the way the attribute converts them, so the reported limit is the one the server validates against whatever the culture of the request is, and a magnitude outside the decimal range survives it. --- .../Modeling/PropertyApiDescriptionModel.cs | 46 ++++++++++++++++--- .../PropertyApiDescriptionModel_Tests.cs | 23 ++++++++-- 2 files changed, 59 insertions(+), 10 deletions(-) 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; }