Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f82892d

Browse files
committed
Fix culture sensitivity in query strings (added AppContext switch for backwards-compatibility)
1 parent 8e8427e commit f82892d

File tree

5 files changed

+368
-203
lines changed

5 files changed

+368
-203
lines changed

‎src/JsonApiDotNetCore.Annotations/Resources/Internal/RuntimeTypeConverter.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,26 @@ namespace JsonApiDotNetCore.Resources.Internal;
88
[PublicAPI]
99
public static class RuntimeTypeConverter
1010
{
11+
private const string ParseQueryStringsUsingCurrentCultureSwitchName = "JsonApiDotNetCore.ParseQueryStringsUsingCurrentCulture";
12+
1113
public static object? ConvertType(object? value, Type type)
1214
{
1315
ArgumentGuard.NotNull(type);
1416

17+
// Earlier versions of JsonApiDotNetCore failed to pass CultureInfo.InvariantCulture in the parsing below, which resulted in the 'current'
18+
// culture being used. Unlike parsing JSON request/response bodies, this effectively meant that query strings were parsed based on the
19+
// OS-level regional settings of the web server.
20+
// Because this was fixed in a non-major release, the switch below enables to revert to the old behavior.
21+
22+
// With the switch activated, API developers can still choose between:
23+
// - Requiring localized date/number formats: parsing occurs using the OS-level regional settings (the default).
24+
// - Requiring culture-invariant date/number formats: requires setting CultureInfo.DefaultThreadCurrentCulture to CultureInfo.InvariantCulture at startup.
25+
// - Allowing clients to choose by sending an Accept-Language HTTP header: requires app.UseRequestLocalization() at startup.
26+
27+
CultureInfo? cultureInfo = AppContext.TryGetSwitch(ParseQueryStringsUsingCurrentCultureSwitchName, out bool useCurrentCulture) && useCurrentCulture
28+
? null
29+
: CultureInfo.InvariantCulture;
30+
1531
if (value == null)
1632
{
1733
if (!CanContainNull(type))
@@ -50,19 +66,19 @@ public static class RuntimeTypeConverter
5066

5167
if (nonNullableType == typeof(DateTime))
5268
{
53-
DateTime convertedValue = DateTime.Parse(stringValue, null, DateTimeStyles.RoundtripKind);
69+
DateTime convertedValue = DateTime.Parse(stringValue, cultureInfo, DateTimeStyles.RoundtripKind);
5470
return isNullableTypeRequested ? (DateTime?)convertedValue : convertedValue;
5571
}
5672

5773
if (nonNullableType == typeof(DateTimeOffset))
5874
{
59-
DateTimeOffset convertedValue = DateTimeOffset.Parse(stringValue, null, DateTimeStyles.RoundtripKind);
75+
DateTimeOffset convertedValue = DateTimeOffset.Parse(stringValue, cultureInfo, DateTimeStyles.RoundtripKind);
6076
return isNullableTypeRequested ? (DateTimeOffset?)convertedValue : convertedValue;
6177
}
6278

6379
if (nonNullableType == typeof(TimeSpan))
6480
{
65-
TimeSpan convertedValue = TimeSpan.Parse(stringValue);
81+
TimeSpan convertedValue = TimeSpan.Parse(stringValue,cultureInfo);
6682
return isNullableTypeRequested ? (TimeSpan?)convertedValue : convertedValue;
6783
}
6884

@@ -75,7 +91,7 @@ public static class RuntimeTypeConverter
7591
}
7692

7793
// https://bradwilson.typepad.com/blog/2008/07/creating-nullab.html
78-
return Convert.ChangeType(stringValue, nonNullableType);
94+
return Convert.ChangeType(stringValue, nonNullableType,cultureInfo);
7995
}
8096
catch (Exception exception) when (exception is FormatException or OverflowException or InvalidCastException or ArgumentException)
8197
{

‎test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using System.Net;
23
using System.Reflection;
34
using System.Text.Json.Serialization;
@@ -60,7 +61,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
6061
});
6162

6263
string attributeName = propertyName.Camelize();
63-
string route = $"/filterableResources?filter=equals({attributeName},'{propertyValue}')";
64+
string? attributeValue = Convert.ToString(propertyValue, CultureInfo.InvariantCulture);
65+
66+
string route = $"/filterableResources?filter=equals({attributeName},'{attributeValue}')";
6467

6568
// Act
6669
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
@@ -88,7 +91,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
8891
await dbContext.SaveChangesAsync();
8992
});
9093

91-
string route = $"/filterableResources?filter=equals(someDecimal,'{resource.SomeDecimal}')";
94+
string route = $"/filterableResources?filter=equals(someDecimal,'{resource.SomeDecimal.ToString(CultureInfo.InvariantCulture)}')";
9295

9396
// Act
9497
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
@@ -232,7 +235,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
232235
await dbContext.SaveChangesAsync();
233236
});
234237

235-
string route = $"/filterableResources?filter=equals(someTimeSpan,'{resource.SomeTimeSpan}')";
238+
string route = $"/filterableResources?filter=equals(someTimeSpan,'{resource.SomeTimeSpan:c}')";
236239

237240
// Act
238241
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /