From 3d3f172dd99aa552caf7e7a21ce6ceea6a4c003f Mon Sep 17 00:00:00 2001 From: Tony Han Date: Tue, 11 Jun 2024 14:33:35 +0800 Subject: [PATCH] Add serialisation compatibility of TimeSpan and DateTime (#16205) Co-authored-by: Mike Alhayek Co-authored-by: Hisham Bin Ateya --- .../OrchardCore.Abstractions/Json/JOptions.cs | 2 + .../Serialization/DateTimeJsonConverter.cs | 27 +++++++++++ .../Serialization/TimeSpanJsonConverter.cs | 28 +++++++++++ .../Data/ContentItemTests.cs | 48 ++++++++++++++++++- 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/DateTimeJsonConverter.cs create mode 100644 src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/JOptions.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/JOptions.cs index cde64d5a524..727e2feb931 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Json/JOptions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/JOptions.cs @@ -36,6 +36,8 @@ static JOptions() Default = new JsonSerializerOptions(Base); Default.Converters.Add(new DynamicJsonConverter()); Default.Converters.Add(new PathStringJsonConverter()); + Default.Converters.Add(new TimeSpanJsonConverter()); + Default.Converters.Add(new DateTimeJsonConverter()); Indented = new JsonSerializerOptions(Default) { diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/DateTimeJsonConverter.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/DateTimeJsonConverter.cs new file mode 100644 index 00000000000..9c2915aa2dc --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/DateTimeJsonConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OrchardCore.Json.Serialization; + +public class DateTimeJsonConverter : JsonConverter +{ + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert != typeof(DateTime)) + { + throw new ArgumentException("Unexpected type to convert.", nameof(typeToConvert)); + } + + if (!reader.TryGetDateTime(out DateTime value) && DateTime.TryParse(reader.GetString()!, out value)) + { + return value; + } + + return value; + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)); +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs new file mode 100644 index 00000000000..f7811abb64a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Json/Serialization/TimeSpanJsonConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OrchardCore.Json.Serialization; + +public class TimeSpanJsonConverter : JsonConverter +{ + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Unexpected token parsing TimeSpan. Expected a string, got '{reader.TokenType}'."); + } + + var stringValue = reader.GetString(); + + if (TimeSpan.TryParse(stringValue, out var timeSpan)) + { + return timeSpan; + } + + throw new JsonException($"Unable to convert '{stringValue}' to TimeSpan."); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); +} diff --git a/test/OrchardCore.Tests/Data/ContentItemTests.cs b/test/OrchardCore.Tests/Data/ContentItemTests.cs index 8d61337943c..91494ff1605 100644 --- a/test/OrchardCore.Tests/Data/ContentItemTests.cs +++ b/test/OrchardCore.Tests/Data/ContentItemTests.cs @@ -1,12 +1,58 @@ using System.Text.Json; using System.Text.Json.Dynamic; using System.Text.Json.Nodes; +using OrchardCore.ContentFields.Fields; using OrchardCore.ContentManagement; +using OrchardCore.Json.Serialization; namespace OrchardCore.Tests.Data { public class ContentItemTests { + /// + /// To validate + /// and + /// + [Fact] + public void JsonNode_WhenParseCalled_ConvertShortTimeFormatToTimeField() + { + // Arrange + var jsonStr = """ + { + "TimeFieldTest": { + "Value": "13:05" + }, + "DateTimeFieldTest": { + "Value": "2024-5-31 13:05" + }, + "TimezoneDateTimeFieldTest": { + "Value": "2022-12-13T21:02:18.399-05:00" + }, + "DateFieldTest": { + "Value": "2024-5-31" + } + } + """; + + // Act + var jobject = JsonNode.Parse(jsonStr); + var timeField = jobject.SelectNode("TimeFieldTest").ToObject(); + var dateField = jobject.SelectNode("DateFieldTest").ToObject(); + var dateTimeField = jobject.SelectNode("DateTimeFieldTest").ToObject(); + var timezoneDateTimeFieldTest = jobject.SelectNode("TimezoneDateTimeFieldTest").ToObject(); + + // Assert + Assert.Equal("13:05:00", timeField.Value.Value.ToString()); + Assert.Equal("2024-05-31", dateField.Value.Value.ToString("yyyy-MM-dd")); + Assert.Equal("2024-05-31 13:05", dateTimeField.Value.Value.ToString("yyyy-MM-dd HH:mm")); + Assert.Equal("13:05:00", JObject.FromObject(timeField).SelectNode("Value").ToString()); + Assert.Equal("2024-05-31T00:00:00Z", JObject.FromObject(dateField).SelectNode("Value").ToString()); + Assert.Equal("2024-05-31T13:05:00Z", JObject.FromObject(dateTimeField).SelectNode("Value").ToString()); + + var utcTime = TimeZoneInfo.ConvertTimeToUtc(timezoneDateTimeFieldTest.Value.Value); + Assert.Equal("2022-12-14 02:02:18", utcTime.ToString("yyyy-MM-dd HH:mm:ss")); + } + [Fact] public void ShouldSerializeContent() { @@ -163,7 +209,7 @@ public void ContentShouldStoreDateTimeFields() var json = JConvert.SerializeObject(contentItem); - Assert.Contains(@"""MyPart"":{""Text"":""test"",""myField"":{""Value"":""2024-01-01T10:42:00""}}", json); + Assert.Contains(@"""MyPart"":{""Text"":""test"",""myField"":{""Value"":""2024-01-01T10:42:00Z""}}", json); } [Fact]