@@ -8,10 +8,26 @@ namespace JsonApiDotNetCore.Resources.Internal;
88[ PublicAPI ]
99public 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 {
0 commit comments