Skip to content

Commit cc663df

Browse files
Add tests for RangeAttribute and respect ParseLimitsInInvariantCulture property (#3448)
- Port tests for `[RangeAttribute]` from #3283. - Respect `RangeAttribute.ParseLimitsInInvariantCulture`. - Refactor to avoid redundant reassignment. - Avoid converting `int` values to a string and re-parsing.
1 parent 61c0934 commit cc663df

2 files changed

Lines changed: 155 additions & 10 deletions

File tree

src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.ComponentModel;
22
using System.ComponentModel.DataAnnotations;
3+
using System.Globalization;
34
using Microsoft.AspNetCore.Mvc.ApiExplorer;
45
using Microsoft.AspNetCore.Routing.Constraints;
56
using Microsoft.OpenApi.Models;
@@ -229,8 +230,27 @@ private static void ApplyBase64Attribute(OpenApiSchema schema)
229230

230231
private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute)
231232
{
232-
#if NET
233+
if (rangeAttribute.Maximum is int maximumInteger)
234+
{
235+
// The range was set with the RangeAttribute(int, int) constructor
236+
schema.Maximum = maximumInteger;
237+
schema.Minimum = (int)rangeAttribute.Minimum;
238+
}
239+
else
240+
{
241+
// Parse the range from the RangeAttribute(double, double) or RangeAttribute(string, string) constructor.
242+
// Use the appropriate culture as the user may have specified a culture-specific format for the numbers
243+
// if they specified the value as a string. By default RangeAttribute uses the current culture, but it
244+
// can be set to use the invariant culture.
245+
var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture
246+
? CultureInfo.InvariantCulture
247+
: CultureInfo.CurrentCulture;
233248

249+
schema.Maximum = Convert.ToDecimal(rangeAttribute.Maximum, targetCulture);
250+
schema.Minimum = Convert.ToDecimal(rangeAttribute.Minimum, targetCulture);
251+
}
252+
253+
#if NET
234254
if (rangeAttribute.MinimumIsExclusive)
235255
{
236256
schema.ExclusiveMinimum = true;
@@ -240,16 +260,7 @@ private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute ran
240260
{
241261
schema.ExclusiveMaximum = true;
242262
}
243-
244263
#endif
245-
246-
schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum)
247-
? maximum
248-
: schema.Maximum;
249-
250-
schema.Minimum = decimal.TryParse(rangeAttribute.Minimum.ToString(), out decimal minimum)
251-
? minimum
252-
: schema.Minimum;
253264
}
254265

255266
private static void ApplyRangeRouteConstraint(OpenApiSchema schema, RangeRouteConstraint rangeRouteConstraint)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Globalization;
3+
using Microsoft.OpenApi.Models;
4+
5+
namespace Swashbuckle.AspNetCore.SwaggerGen.Test;
6+
7+
public static class OpenApiSchemaExtensionsTests
8+
{
9+
public static TheoryData<string, bool, RangeAttribute, string, string> TestCases()
10+
{
11+
bool[] isExclusive = [false, true];
12+
13+
string[] invariantOrEnglishCultures =
14+
[
15+
string.Empty,
16+
"en",
17+
"en-AU",
18+
"en-GB",
19+
"en-US",
20+
];
21+
22+
string[] commaForDecimalCultures =
23+
[
24+
"de-DE",
25+
"fr-FR",
26+
"sv-SE",
27+
];
28+
29+
Type[] fractionNumberTypes =
30+
[
31+
typeof(float),
32+
typeof(double),
33+
typeof(decimal),
34+
];
35+
36+
var testCases = new TheoryData<string, bool, RangeAttribute, string, string>();
37+
38+
foreach (var culture in invariantOrEnglishCultures)
39+
{
40+
foreach (var exclusive in isExclusive)
41+
{
42+
testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
43+
testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
44+
testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
45+
46+
foreach (var type in fractionNumberTypes)
47+
{
48+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
49+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56");
50+
}
51+
}
52+
}
53+
54+
foreach (var culture in commaForDecimalCultures)
55+
{
56+
foreach (var exclusive in isExclusive)
57+
{
58+
testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
59+
testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
60+
testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
61+
testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
62+
63+
foreach (var type in fractionNumberTypes)
64+
{
65+
testCases.Add(culture, exclusive, new(type, "1,23", "4,56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56");
66+
testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56");
67+
}
68+
}
69+
}
70+
71+
// Numbers using numeric format, such as with thousands separators
72+
testCases.Add("en-GB", false, new(typeof(float), "-12,445.7", "12,445.7"), "-12445.7", "12445.7");
73+
testCases.Add("fr-FR", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7");
74+
testCases.Add("sv-SE", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7");
75+
76+
// Decimal value that would lose precision if parsed as a float or double
77+
foreach (var exclusive in isExclusive)
78+
{
79+
testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "12345678901234567890.123456789", "12345678901234567890.123456789");
80+
testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "12345678901234567890.123456789", "12345678901234567890.123456789");
81+
}
82+
83+
return testCases;
84+
}
85+
86+
[Theory]
87+
[MemberData(nameof(TestCases))]
88+
public static void ApplyValidationAttributes_Handles_RangeAttribute_Correctly(
89+
string cultureName,
90+
bool isExclusive,
91+
RangeAttribute rangeAttribute,
92+
string expectedMinimum,
93+
string expectedMaximum)
94+
{
95+
// Arrange
96+
var minimum = decimal.Parse(expectedMinimum, CultureInfo.InvariantCulture);
97+
var maximum = decimal.Parse(expectedMaximum, CultureInfo.InvariantCulture);
98+
99+
var schema = new OpenApiSchema();
100+
101+
// Act
102+
using (CultureSwitcher.UseCulture(cultureName))
103+
{
104+
schema.ApplyValidationAttributes([rangeAttribute]);
105+
}
106+
107+
// Assert
108+
Assert.Equal(isExclusive ? true : null, schema.ExclusiveMinimum);
109+
Assert.Equal(isExclusive ? true : null, schema.ExclusiveMaximum);
110+
Assert.Equal(minimum, schema.Minimum);
111+
Assert.Equal(maximum, schema.Maximum);
112+
}
113+
114+
private sealed class CultureSwitcher : IDisposable
115+
{
116+
private readonly CultureInfo _previous;
117+
118+
private CultureSwitcher(string name)
119+
{
120+
_previous = CultureInfo.CurrentCulture;
121+
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name);
122+
}
123+
124+
public static CultureSwitcher UseCulture(string name) => new(name);
125+
126+
public void Dispose()
127+
{
128+
if (_previous is not null)
129+
{
130+
CultureInfo.CurrentCulture = _previous;
131+
}
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)