Skip to content

Commit 9a629d4

Browse files
authored
(#249) JsonConverters use InvariantCulture. (#254)
1 parent 162b004 commit 9a629d4

File tree

14 files changed

+232
-76
lines changed

14 files changed

+232
-76
lines changed

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

+2-1
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

+2-1
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/TimeOnlyConverter.cs

+2-1
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

+1-2
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

+1-2
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

+1-1
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

+1-2
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

+61-17
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,84 @@
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+
// Use FileTime comparison because no FluentAssertions support for DateTime ignoring zone info.
23+
entity.UpdatedAt.ToFileTime().Should().Be(value.ToFileTime());
24+
});
25+
}
1426

15-
public DateTimeConverter_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 DateTimeConverter());
31+
const string json = """{"updatedAt":"2021-08-21T12:30:15.123Z"}""";
32+
DateTime value = new(2021, 8, 21, 12, 30, 15, 123, 456, DateTimeKind.Utc);
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(DateTime.MinValue);
46+
const string json = """{"updatedAt":"2021-08-21T12:30:15.123Z"}""";
47+
DateTime value = DateTime.Parse("2021-08-21T20:30:15.1234567+08:00");
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<FormatException>();
63+
}
64+
65+
[Theory]
66+
[MemberData(nameof(Locales))]
67+
public void Converter_HandlesNullDateInInput(string culture)
68+
{
69+
const string json = """{"updatedAt":null}""";
70+
DateTime value = DateTime.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 DateTime dt { get; set; }
82+
public DateTime UpdatedAt { get; set; }
4083
}
84+
#endregion
4185
}

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

+62-18
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,84 @@
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;
6+
using System.Text.RegularExpressions;
77

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.Should().Be(value);
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Datasync.Client.Serialization;
6+
using System.Globalization;
7+
using System.Text.Json;
8+
9+
#pragma warning disable IDE0028 // Simplify collection initialization
10+
11+
namespace CommunityToolkit.Datasync.Client.Test.Serialization;
12+
13+
[ExcludeFromCodeCoverage]
14+
public abstract class SerializerTests
15+
{
16+
protected static JsonSerializerOptions SerializerOptions
17+
=> DatasyncSerializer.JsonSerializerOptions;
18+
19+
public static TheoryData<string> Locales => new()
20+
{
21+
"fr-FR",
22+
"da-DA",
23+
"en-US"
24+
};
25+
26+
protected static void TestWithCulture(string culture, Action act)
27+
{
28+
CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
29+
Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
30+
try
31+
{
32+
act.Invoke();
33+
}
34+
finally
35+
{
36+
Thread.CurrentThread.CurrentCulture = currentCulture;
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)