Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@ namespace JsonApiDotNetCore.Resources.Internal;
[PublicAPI]
public static class RuntimeTypeConverter
{
private const string ParseQueryStringsUsingCurrentCultureSwitchName = "JsonApiDotNetCore.ParseQueryStringsUsingCurrentCulture";

public static object? ConvertType(object? value, Type type)
{
ArgumentGuard.NotNull(type);

// Earlier versions of JsonApiDotNetCore failed to pass CultureInfo.InvariantCulture in the parsing below, which resulted in the 'current'
// culture being used. Unlike parsing JSON request/response bodies, this effectively meant that query strings were parsed based on the
// OS-level regional settings of the web server.
// Because this was fixed in a non-major release, the switch below enables to revert to the old behavior.

// With the switch activated, API developers can still choose between:
// - Requiring localized date/number formats: parsing occurs using the OS-level regional settings (the default).
// - Requiring culture-invariant date/number formats: requires setting CultureInfo.DefaultThreadCurrentCulture to CultureInfo.InvariantCulture at startup.
// - Allowing clients to choose by sending an Accept-Language HTTP header: requires app.UseRequestLocalization() at startup.

CultureInfo? cultureInfo = AppContext.TryGetSwitch(ParseQueryStringsUsingCurrentCultureSwitchName, out bool useCurrentCulture) && useCurrentCulture
? null
: CultureInfo.InvariantCulture;

if (value == null)
{
if (!CanContainNull(type))
Expand Down Expand Up @@ -50,19 +66,19 @@ public static class RuntimeTypeConverter

if (nonNullableType == typeof(DateTime))
{
DateTime convertedValue = DateTime.Parse(stringValue, null, DateTimeStyles.RoundtripKind);
DateTime convertedValue = DateTime.Parse(stringValue, cultureInfo, DateTimeStyles.RoundtripKind);
return isNullableTypeRequested ? (DateTime?)convertedValue : convertedValue;
}

if (nonNullableType == typeof(DateTimeOffset))
{
DateTimeOffset convertedValue = DateTimeOffset.Parse(stringValue, null, DateTimeStyles.RoundtripKind);
DateTimeOffset convertedValue = DateTimeOffset.Parse(stringValue, cultureInfo, DateTimeStyles.RoundtripKind);
return isNullableTypeRequested ? (DateTimeOffset?)convertedValue : convertedValue;
}

if (nonNullableType == typeof(TimeSpan))
{
TimeSpan convertedValue = TimeSpan.Parse(stringValue);
TimeSpan convertedValue = TimeSpan.Parse(stringValue, cultureInfo);
return isNullableTypeRequested ? (TimeSpan?)convertedValue : convertedValue;
}

Expand All @@ -75,7 +91,7 @@ public static class RuntimeTypeConverter
}

// https://bradwilson.typepad.com/blog/2008/07/creating-nullab.html
return Convert.ChangeType(stringValue, nonNullableType);
return Convert.ChangeType(stringValue, nonNullableType, cultureInfo);
}
catch (Exception exception) when (exception is FormatException or OverflowException or InvalidCastException or ArgumentException)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -60,7 +61,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
});

string attributeName = propertyName.Camelize();
string route = $"/filterableResources?filter=equals({attributeName},'{propertyValue}')";
string? attributeValue = Convert.ToString(propertyValue, CultureInfo.InvariantCulture);

string route = $"/filterableResources?filter=equals({attributeName},'{attributeValue}')";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
Expand Down Expand Up @@ -88,7 +91,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
await dbContext.SaveChangesAsync();
});

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

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
Expand Down Expand Up @@ -232,7 +235,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
await dbContext.SaveChangesAsync();
});

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

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
Expand Down
Loading