Skip to content

Commit 128aef0

Browse files
authored
(#250) Use Invariant Culture in JsonConverters (#255)
1 parent c2bc2e8 commit 128aef0

File tree

15 files changed

+230
-75
lines changed

15 files changed

+230
-75
lines changed

src/CommunityToolkit.Datasync.Client/Serialization/DateTimeConverter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Globalization;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78

@@ -31,5 +32,5 @@ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
3132

3233
/// <inheritdoc />
3334
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
34-
=> writer.WriteStringValue(value.ToUniversalTime().ToString(format));
35+
=> writer.WriteStringValue(value.ToUniversalTime().ToString(format, CultureInfo.InvariantCulture));
3536
}

src/CommunityToolkit.Datasync.Client/Serialization/DateTimeOffsetConverter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Globalization;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78

@@ -31,5 +32,5 @@ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConver
3132

3233
/// <inheritdoc />
3334
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
34-
=> writer.WriteStringValue(value.ToUniversalTime().UtcDateTime.ToString(format));
35+
=> writer.WriteStringValue(value.ToUniversalTime().UtcDateTime.ToString(format, CultureInfo.InvariantCulture));
3536
}

src/CommunityToolkit.Datasync.Client/Serialization/JsonExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class JsonExtensions
1717
/// <param name="reader">The <see cref="Utf8JsonReader"/> to assert.</param>
1818
/// <param name="expectedTokenType">The expected <see cref="JsonTokenType"/> of the current token.</param>
1919
/// <exception cref="JsonException">The current token did not match the <paramref name="expectedTokenType"/>.</exception>
20-
public static void Expect(in this Utf8JsonReader reader, JsonTokenType expectedTokenType)
20+
public static void Expect(this Utf8JsonReader reader, JsonTokenType expectedTokenType)
2121
{
2222
if (reader.TokenType != expectedTokenType)
2323
{

src/CommunityToolkit.Datasync.Client/Serialization/TimeOnlyConverter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Globalization;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78

@@ -31,5 +32,5 @@ public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
3132

3233
/// <inheritdoc />
3334
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
34-
=> writer.WriteStringValue(value.ToString(format));
35+
=> writer.WriteStringValue(value.ToString(format, CultureInfo.InvariantCulture));
3536
}

src/CommunityToolkit.Datasync.Server.Abstractions/Json/DateTimeConverter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ namespace CommunityToolkit.Datasync.Server.Abstractions.Json;
1515
public class DateTimeConverter : JsonConverter<DateTime>
1616
{
1717
private const string format = "yyyy-MM-dd'T'HH:mm:ss.fffK";
18-
private static readonly CultureInfo culture = new("en-US");
1918

2019
/// <inheritdoc />
2120
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2221
=> DateTime.Parse(reader.GetString() ?? string.Empty);
2322

2423
/// <inheritdoc />
2524
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
26-
=> writer.WriteStringValue(value.ToUniversalTime().ToString(format, culture));
25+
=> writer.WriteStringValue(value.ToUniversalTime().ToString(format, CultureInfo.InvariantCulture));
2726
}

src/CommunityToolkit.Datasync.Server.Abstractions/Json/DateTimeOffsetConverter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ namespace CommunityToolkit.Datasync.Server.Abstractions.Json;
1515
public class DateTimeOffsetConverter : JsonConverter<DateTimeOffset>
1616
{
1717
private const string format = "yyyy-MM-dd'T'HH:mm:ss.fffK";
18-
private static readonly CultureInfo culture = new("en-US");
1918

2019
/// <inheritdoc />
2120
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2221
=> DateTimeOffset.Parse(reader.GetString() ?? string.Empty);
2322

2423
/// <inheritdoc />
2524
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
26-
=> writer.WriteStringValue(value.ToUniversalTime().UtcDateTime.ToString(format, culture));
25+
=> writer.WriteStringValue(value.ToUniversalTime().UtcDateTime.ToString(format, CultureInfo.InvariantCulture));
2726
}

src/CommunityToolkit.Datasync.Server.Abstractions/Json/JsonExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class JsonExtensions
1717
/// <param name="reader">The <see cref="Utf8JsonReader"/> to assert.</param>
1818
/// <param name="expectedTokenType">The expected <see cref="JsonTokenType"/> of the current token.</param>
1919
/// <exception cref="JsonException">The current token did not match the <paramref name="expectedTokenType"/>.</exception>
20-
public static void Expect(in this Utf8JsonReader reader, JsonTokenType expectedTokenType)
20+
public static void Expect(this Utf8JsonReader reader, JsonTokenType expectedTokenType)
2121
{
2222
if (reader.TokenType != expectedTokenType)
2323
{

src/CommunityToolkit.Datasync.Server.Abstractions/Json/TimeOnlyConverter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ namespace CommunityToolkit.Datasync.Server.Abstractions.Json;
1515
public class TimeOnlyConverter : JsonConverter<TimeOnly>
1616
{
1717
private const string format = "HH:mm:ss.fff";
18-
private static readonly CultureInfo culture = new("en-US");
1918

2019
/// <inheritdoc />
2120
public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2221
=> TimeOnly.Parse(reader.GetString() ?? string.Empty);
2322

2423
/// <inheritdoc />
2524
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
26-
=> writer.WriteStringValue(value.ToString(format, culture));
25+
=> writer.WriteStringValue(value.ToString(format, CultureInfo.InvariantCulture));
2726
}

tests/CommunityToolkit.Datasync.Client.Test/Serialization/DateTimeConverter_Tests.cs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,83 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.Datasync.Client.Serialization;
65
using System.Text.Json;
76

87
namespace CommunityToolkit.Datasync.Client.Test.Serialization;
98

109
[ExcludeFromCodeCoverage]
11-
public class DateTimeConverter_Tests
10+
public class DateTimeConverter_Tests : SerializerTests
1211
{
13-
private readonly JsonSerializerOptions serializerOptions;
12+
[Theory]
13+
[MemberData(nameof(Locales))]
14+
public void Converter_ReadsJson(string culture)
15+
{
16+
const string json = """{"updatedAt":"2021-08-21T12:30:15.123+00:00"}""";
17+
DateTime value = new(2021, 8, 21, 12, 30, 15, 123, DateTimeKind.Utc);
18+
19+
TestWithCulture(culture, () =>
20+
{
21+
Entity entity = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
22+
entity.UpdatedAt.ToFileTime().Should().Be(value.ToFileTime());
23+
});
24+
}
1425

15-
public DateTimeConverter_Tests()
26+
[Theory]
27+
[MemberData(nameof(Locales))]
28+
public void Converter_WritesJson(string culture)
1629
{
17-
this.serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
18-
this.serializerOptions.Converters.Add(new DateTimeConverter());
30+
const string json = """{"updatedAt":"2021-08-21T12:30:15.123Z"}""";
31+
DateTime value = new(2021, 8, 21, 12, 30, 15, 123, 456, DateTimeKind.Utc);
32+
33+
TestWithCulture(culture, () =>
34+
{
35+
Entity entity = new() { UpdatedAt = value };
36+
string actual = JsonSerializer.Serialize(entity, SerializerOptions);
37+
Assert.Equal(json, actual);
38+
});
1939
}
2040

21-
[Fact]
22-
public void Read_Null_Works()
41+
[Theory]
42+
[MemberData(nameof(Locales))]
43+
public void Converter_WritesJson_WithTimeZone(string culture)
2344
{
24-
string json = """{"dt":null}""";
25-
SUT actual = JsonSerializer.Deserialize<SUT>(json, this.serializerOptions);
26-
actual.dt.Should().Be(DateTime.MinValue);
45+
const string json = """{"updatedAt":"2021-08-21T12:30:15.123Z"}""";
46+
DateTime value = DateTime.Parse("2021-08-21T20:30:15.1234567+08:00");
47+
48+
TestWithCulture(culture, () =>
49+
{
50+
Entity entity = new() { UpdatedAt = value };
51+
string actual = JsonSerializer.Serialize(entity, SerializerOptions);
52+
Assert.Equal(json, actual);
53+
});
2754
}
2855

2956
[Fact]
30-
public void Read_Int_Throws()
57+
public void Converter_ThrowsOnBadDateInInput()
3158
{
32-
string json = """{"dt":42}""";
33-
Action act = () => _ = JsonSerializer.Deserialize<SUT>(json, this.serializerOptions);
34-
act.Should().Throw<JsonException>();
59+
const string json = """{"updatedAt":"foo"}""";
60+
Action act = () => _ = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
61+
act.Should().Throw<FormatException>();
62+
}
63+
64+
[Theory]
65+
[MemberData(nameof(Locales))]
66+
public void Converter_HandlesNullDateInInput(string culture)
67+
{
68+
const string json = """{"updatedAt":null}""";
69+
DateTime value = DateTime.MinValue;
70+
71+
TestWithCulture(culture, () =>
72+
{
73+
Entity entity = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
74+
entity.UpdatedAt.Should().Be(value);
75+
});
3576
}
3677

37-
class SUT
78+
#region Models
79+
public class Entity
3880
{
39-
public DateTime dt { get; set; }
81+
public DateTime UpdatedAt { get; set; }
4082
}
83+
#endregion
4184
}

tests/CommunityToolkit.Datasync.Client.Test/Serialization/DateTimeOffsetConverter_Tests.cs

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,78 @@
88
namespace CommunityToolkit.Datasync.Client.Test.Serialization;
99

1010
[ExcludeFromCodeCoverage]
11-
public class DateTimeOffsetConverter_Tests
11+
public class DateTimeOffsetConverter_Tests : SerializerTests
1212
{
13-
private readonly JsonSerializerOptions serializerOptions;
13+
[Theory]
14+
[MemberData(nameof(Locales))]
15+
public void Converter_ReadsJson(string culture)
16+
{
17+
const string json = "{\"updatedAt\":\"2021-08-21T12:30:15.123+00:00\"}";
18+
DateTimeOffset value = new(2021, 8, 21, 12, 30, 15, 123, TimeSpan.Zero);
19+
20+
TestWithCulture(culture, () =>
21+
{
22+
Entity entity = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
23+
entity.UpdatedAt.ToFileTime().Should().Be(value.ToFileTime());
24+
});
25+
}
1426

15-
public DateTimeOffsetConverter_Tests()
27+
[Theory]
28+
[MemberData(nameof(Locales))]
29+
public void Converter_WritesJson(string culture)
1630
{
17-
this.serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
18-
this.serializerOptions.Converters.Add(new DateTimeOffsetConverter());
31+
const string json = "{\"updatedAt\":\"2021-08-21T12:30:15.123Z\"}";
32+
DateTimeOffset value = new(2021, 8, 21, 12, 30, 15, 123, 456, TimeSpan.Zero);
33+
34+
TestWithCulture(culture, () =>
35+
{
36+
Entity entity = new() { UpdatedAt = value };
37+
string actual = JsonSerializer.Serialize(entity, SerializerOptions);
38+
Assert.Equal(json, actual);
39+
});
1940
}
2041

21-
[Fact]
22-
public void Read_Null_Works()
42+
[Theory]
43+
[MemberData(nameof(Locales))]
44+
public void Converter_WritesJson_WithTimeZone(string culture)
2345
{
24-
string json = """{"dt":null}""";
25-
SUT actual = JsonSerializer.Deserialize<SUT>(json, this.serializerOptions);
26-
actual.dt.Should().Be(DateTimeOffset.MinValue);
46+
const string json = "{\"updatedAt\":\"2021-08-21T12:30:15.123Z\"}";
47+
DateTimeOffset value = new(2021, 8, 21, 20, 30, 15, 123, 456, TimeSpan.FromHours(8));
48+
49+
TestWithCulture(culture, () =>
50+
{
51+
Entity entity = new() { UpdatedAt = value };
52+
string actual = JsonSerializer.Serialize(entity, SerializerOptions);
53+
Assert.Equal(json, actual);
54+
});
2755
}
2856

2957
[Fact]
30-
public void Read_Int_Throws()
58+
public void Converter_ThrowsOnBadDateInInput()
3159
{
32-
string json = """{"dt":42}""";
33-
Action act = () => _ = JsonSerializer.Deserialize<SUT>(json, this.serializerOptions);
34-
act.Should().Throw<JsonException>();
60+
const string json = "{\"updatedAt\":\"foo\"}";
61+
Action act = () => _ = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
62+
act.Should().Throw<Exception>();
63+
}
64+
65+
[Theory]
66+
[MemberData(nameof(Locales))]
67+
public void Converter_HandlesNullDateInInput(string culture)
68+
{
69+
const string json = """{"updatedAt":null}""";
70+
DateTimeOffset value = DateTimeOffset.MinValue;
71+
72+
TestWithCulture(culture, () =>
73+
{
74+
Entity entity = JsonSerializer.Deserialize<Entity>(json, SerializerOptions);
75+
entity.UpdatedAt.Should().Be(value);
76+
});
3577
}
3678

37-
class SUT
79+
#region Models
80+
public class Entity
3881
{
39-
public DateTimeOffset dt { get; set; }
82+
public DateTimeOffset UpdatedAt { get; set; }
4083
}
41-
}
84+
#endregion
85+
}

0 commit comments

Comments
 (0)