From c1d6e97ab28b6dd1fccd9ad2fad881dc5d906488 Mon Sep 17 00:00:00 2001 From: brycewang-microsoft <94650966+brycewang-microsoft@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:30:50 -0800 Subject: [PATCH] Apply JsonSerializerSettings to hub device (#3107) --- common/src/service/HttpClientHelper.cs | 1 - e2e/test/iothub/method/MethodE2ETests.cs | 1 - e2e/test/iothub/twin/TwinE2ETests.cs | 87 ++++++++++++++++++- iothub/device/src/InternalClient.cs | 3 + .../src/JsonSerializerSettingsInitializer.cs | 36 ++++++++ .../src/JsonSerializerSettingsInitializer.cs | 13 +-- 6 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 iothub/device/src/JsonSerializerSettingsInitializer.cs rename {shared => iothub/service}/src/JsonSerializerSettingsInitializer.cs (68%) diff --git a/common/src/service/HttpClientHelper.cs b/common/src/service/HttpClientHelper.cs index e108987862..f8dd5e7e77 100644 --- a/common/src/service/HttpClientHelper.cs +++ b/common/src/service/HttpClientHelper.cs @@ -17,7 +17,6 @@ using Microsoft.Azure.Devices.Common.Extensions; using Microsoft.Azure.Devices.Shared; using Newtonsoft.Json; - using static Microsoft.Azure.Devices.Shared.HttpMessageHelper; namespace Microsoft.Azure.Devices diff --git a/e2e/test/iothub/method/MethodE2ETests.cs b/e2e/test/iothub/method/MethodE2ETests.cs index 0af215b5cc..ea9aa1be9b 100644 --- a/e2e/test/iothub/method/MethodE2ETests.cs +++ b/e2e/test/iothub/method/MethodE2ETests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Diagnostics.Tracing; using System.Globalization; using System.Net; using System.Text; diff --git a/e2e/test/iothub/twin/TwinE2ETests.cs b/e2e/test/iothub/twin/TwinE2ETests.cs index df64728dae..63bc80a1fe 100644 --- a/e2e/test/iothub/twin/TwinE2ETests.cs +++ b/e2e/test/iothub/twin/TwinE2ETests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client; @@ -11,6 +12,7 @@ using Microsoft.Azure.Devices.Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.E2ETests.Twins { @@ -23,7 +25,7 @@ public class TwinE2ETests : E2EMsTestBase private static readonly RegistryManager _registryManager = RegistryManager.CreateFromConnectionString(TestConfiguration.IotHub.ConnectionString); - private static readonly List s_listOfPropertyValues = new List + private static readonly List s_listOfPropertyValues = new() { 1, "someString", @@ -35,6 +37,15 @@ public class TwinE2ETests : E2EMsTestBase } }; + private static readonly TestDateTime s_dateTimeProperty = new() + { + Iso8601String = new DateTimeOffset(638107582284599400, TimeSpan.FromHours(1)).ToString("o", CultureInfo.InvariantCulture) + }; + + // ISO 8601 date time string with trailing zeros in the microseconds portion. This is to verify the Newtonsoft.Json known issue is worked around in the SDK. + // See https://github.com/JamesNK/Newtonsoft.Json/issues/1511 for more details about the known issue. + private static readonly string s_iso8601DateTimeString = "2023-01-31T10:37:08.4599400+01:00"; + [TestMethod] [Timeout(TestTimeoutMilliseconds)] public async Task Twin_DeviceSetsReportedPropertyAndGetsItBack_Mqtt() @@ -315,6 +326,42 @@ await Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetAsync( .ConfigureAwait(false); } + [TestMethod] + [Timeout(TestTimeoutMilliseconds)] + public async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGet_DateTimeProperties_Mqtt() + { + await Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDateTimePropertiesAsync( + Client.TransportType.Mqtt_Tcp_Only) + .ConfigureAwait(false); + } + + [TestMethod] + [Timeout(TestTimeoutMilliseconds)] + public async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGet_DateTimeProperties_MqttWs() + { + await Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDateTimePropertiesAsync( + Client.TransportType.Mqtt_WebSocket_Only) + .ConfigureAwait(false); + } + + [TestMethod] + [Timeout(TestTimeoutMilliseconds)] + public async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGet_DateTimeProperties_Amqp() + { + await Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDateTimePropertiesAsync( + Client.TransportType.Amqp_Tcp_Only) + .ConfigureAwait(false); + } + + [TestMethod] + [Timeout(TestTimeoutMilliseconds)] + public async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGet_DateTimeProperties_AmqpWs() + { + await Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDateTimePropertiesAsync( + Client.TransportType.Amqp_WebSocket_Only) + .ConfigureAwait(false); + } + [TestMethod] [Timeout(TestTimeoutMilliseconds)] public async Task Twin_DeviceSetsReportedPropertyAndServiceReceivesIt_Mqtt() @@ -656,6 +703,39 @@ private async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetAs await registryManager.CloseAsync().ConfigureAwait(false); } + private async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDateTimePropertiesAsync(Client.TransportType transport) + { + string jsonString = JsonConvert.SerializeObject(s_dateTimeProperty); + + using TestDevice testDevice = await TestDevice.GetTestDeviceAsync(_devicePrefix).ConfigureAwait(false); + using var registryManager = RegistryManager.CreateFromConnectionString(TestConfiguration.IotHub.ConnectionString); + using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + + var twinPatch = new Twin(); + + // Here is an example to create a TwinCollection with json object if the users use date-formatted + // string and don't want to drop the trailing zeros in the microseconds portion while parsing the + // json properties. The Newtonsoft.Json serializer settings added in SDK apply to serial-/deserialization + // during the client operations. Therefore, to ensure the datetime has the trailing zeros and this + // test can verify the returned peoperties not dropping them, manually set up "DateParseHandling.None" + // while creating a new TwinCollection object as the twin property. + JObject jobjectProperty = JsonConvert.DeserializeObject( + jsonString, + new JsonSerializerSettings + { + DateParseHandling = DateParseHandling.None + }); + + twinPatch.Properties.Desired = new TwinCollection(jobjectProperty, null); + await registryManager.UpdateTwinAsync(testDevice.Id, twinPatch, "*").ConfigureAwait(false); + + Twin deviceTwin = await deviceClient.GetTwinAsync().ConfigureAwait(false); + Assert.AreEqual(deviceTwin.Properties.Desired["Iso8601String"].ToString(), s_iso8601DateTimeString); + + await deviceClient.CloseAsync().ConfigureAwait(false); + await registryManager.CloseAsync().ConfigureAwait(false); + } + private async Task Twin_DeviceSetsReportedPropertyAndServiceReceivesItAsync(Client.TransportType transport) { string propName = Guid.NewGuid().ToString(); @@ -768,4 +848,9 @@ internal class CustomTwinProperty public string Name { get; set; } } + + internal class TestDateTime + { + public string Iso8601String { get; set; } + } } diff --git a/iothub/device/src/InternalClient.cs b/iothub/device/src/InternalClient.cs index ca61a9b4cf..7a3bcf9752 100644 --- a/iothub/device/src/InternalClient.cs +++ b/iothub/device/src/InternalClient.cs @@ -13,6 +13,7 @@ using System.IO; using Microsoft.Azure.Devices.Client.Exceptions; using System.ComponentModel; +using Newtonsoft.Json; #if NET451 @@ -161,6 +162,8 @@ public InternalClient( if (Logging.IsEnabled) Logging.Enter(this, transportSettings, pipelineBuilder, nameof(InternalClient) + "_ctor"); + JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate(); + TlsVersions.Instance.SetLegacyAcceptableVersions(); _transportSettings = transportSettings; diff --git a/iothub/device/src/JsonSerializerSettingsInitializer.cs b/iothub/device/src/JsonSerializerSettingsInitializer.cs new file mode 100644 index 0000000000..7bf0d3be45 --- /dev/null +++ b/iothub/device/src/JsonSerializerSettingsInitializer.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Client +{ + /// + /// A class to initialize JsonSerializerSettings which can be applied to the project. + /// + internal static class JsonSerializerSettingsInitializer + { + /// + /// A static instance of JsonSerializerSettings which sets DateParseHandling to None. + /// + /// + /// By default, serializing/deserializing with Newtonsoft.Json will try to parse date-formatted + /// strings to a date type, which drops trailing zeros in the microseconds date portion. By + /// specifying DateParseHandling with None, the original string will be read as-is. For more details + /// about the known issue, see https://github.com/JamesNK/Newtonsoft.Json/issues/1511. + /// + private static readonly JsonSerializerSettings s_settings = new JsonSerializerSettings + { + DateParseHandling = DateParseHandling.None + }; + + /// + /// Returns JsonSerializerSettings Func delegate + /// + internal static Func GetJsonSerializerSettingsDelegate() + { + return () => s_settings; + } + } +} diff --git a/shared/src/JsonSerializerSettingsInitializer.cs b/iothub/service/src/JsonSerializerSettingsInitializer.cs similarity index 68% rename from shared/src/JsonSerializerSettingsInitializer.cs rename to iothub/service/src/JsonSerializerSettingsInitializer.cs index 78422eec64..728ab2a4cc 100644 --- a/shared/src/JsonSerializerSettingsInitializer.cs +++ b/iothub/service/src/JsonSerializerSettingsInitializer.cs @@ -4,12 +4,12 @@ using System; using Newtonsoft.Json; -namespace Microsoft.Azure.Devices.Shared +namespace Microsoft.Azure.Devices { /// /// A class to initialize JsonSerializerSettings which can be applied to the project. /// - public static class JsonSerializerSettingsInitializer + internal static class JsonSerializerSettingsInitializer { /// /// A static instance of JsonSerializerSettings which sets DateParseHandling to None. @@ -17,9 +17,10 @@ public static class JsonSerializerSettingsInitializer /// /// By default, serializing/deserializing with Newtonsoft.Json will try to parse date-formatted /// strings to a date type, which drops trailing zeros in the microseconds date portion. By - /// specifying DateParseHandling with None, the original string will be read as-is. + /// specifying DateParseHandling with None, the original string will be read as-is. For more details + /// about the known issue, see https://github.com/JamesNK/Newtonsoft.Json/issues/1511. /// - public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + private static readonly JsonSerializerSettings s_settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }; @@ -27,9 +28,9 @@ public static class JsonSerializerSettingsInitializer /// /// Returns JsonSerializerSettings Func delegate /// - public static Func GetJsonSerializerSettingsDelegate() + internal static Func GetJsonSerializerSettingsDelegate() { - return () => Settings; + return () => s_settings; } } }