Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop using JObject.Parse for json string in TwinCollection #3113

Merged
merged 4 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
30 changes: 8 additions & 22 deletions e2e/test/iothub/twin/TwinE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
Expand All @@ -12,7 +11,6 @@
using Microsoft.Azure.Devices.Shared;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.Devices.E2ETests.Twins
{
Expand All @@ -37,15 +35,16 @@ public class TwinE2ETests : E2EMsTestBase
}
};

// ISO 8601 date-formatted 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_dateTimeValue = "2023-01-31T10:37:08.4599400";
brycewang-microsoft marked this conversation as resolved.
Show resolved Hide resolved

private static readonly TestDateTime s_dateTimeProperty = new()
{
Iso8601String = new DateTimeOffset(638107582284599400, TimeSpan.FromHours(1)).ToString("o", CultureInfo.InvariantCulture)
brycewang-microsoft marked this conversation as resolved.
Show resolved Hide resolved
Iso8601String = s_dateTimeValue,
};

// 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()
Expand Down Expand Up @@ -713,24 +712,11 @@ private async Task Twin_ServiceSetsDesiredPropertyAndDeviceReceivesItOnNextGetDa

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<JObject>(
brycewang-microsoft marked this conversation as resolved.
Show resolved Hide resolved
jsonString,
new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
});

twinPatch.Properties.Desired = new TwinCollection(jobjectProperty, null);
twinPatch.Properties.Desired = new TwinCollection(jsonString);
await registryManager.UpdateTwinAsync(testDevice.Id, twinPatch, "*").ConfigureAwait(false);

Twin deviceTwin = await deviceClient.GetTwinAsync().ConfigureAwait(false);
Assert.AreEqual<string>(deviceTwin.Properties.Desired["Iso8601String"].ToString(), s_iso8601DateTimeString);
Assert.AreEqual<string>(deviceTwin.Properties.Desired["Iso8601String"].ToString(), s_dateTimeValue);

await deviceClient.CloseAsync().ConfigureAwait(false);
await registryManager.CloseAsync().ConfigureAwait(false);
Expand Down
1 change: 0 additions & 1 deletion iothub/device/src/Transport/Amqp/AmqpUnit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client.Exceptions;
using Microsoft.Azure.Devices.Client.Extensions;
using Microsoft.Azure.Devices.Client.Transport.Amqp;
using Microsoft.Azure.Devices.Shared;

Expand Down
52 changes: 52 additions & 0 deletions iothub/device/tests/JsonSerializerSettingsInitializerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.Devices.Client.Tests
{
[TestClass]
[TestCategory("UnitTest")]
public class JsonSerializerSettingsInitializerTests
{
private static readonly string s_dateTimeString = "2023-01-31T10:37:08.4599400";
private static readonly string s_serializedPayloadString = "{\"Iso8601String\":\"2023-01-31T10:37:08.4599400\"}";

[TestMethod]
public void JsonSerializerSettingsInitializer_SerializesProperly()
{
// arrange
JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate();
var testDateTime = new TestDateTime { Iso8601String = s_dateTimeString };

// act
var result = JsonConvert.SerializeObject(testDateTime);
brycewang-microsoft marked this conversation as resolved.
Show resolved Hide resolved

// assert
result.Should().Be(s_serializedPayloadString);
}

[TestMethod]
public void JsonSerializerSettingsInitializer_DeserializesProperly()
{
// arrange
JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate();
string jsonStr = $@"{{""Iso8601String"":""{s_dateTimeString}""}}";

// act
JObject payload = JsonConvert.DeserializeObject<JObject>(jsonStr);

//assert
payload.ToString(Formatting.None).Should().Be(s_serializedPayloadString);
}

private class TestDateTime
{
public string Iso8601String { get; set; }
}
}
}
36 changes: 36 additions & 0 deletions provisioning/device/src/JsonSerializerSettingsInitializer.cs
Original file line number Diff line number Diff line change
@@ -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.Provisioning.Client
{
/// <summary>
/// A class to initialize JsonSerializerSettings which can be applied to the project.
/// </summary>
internal static class JsonSerializerSettingsInitializer
{
/// <summary>
/// A static instance of JsonSerializerSettings which sets DateParseHandling to None.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static readonly JsonSerializerSettings s_settings = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
};

/// <summary>
/// Returns JsonSerializerSettings Func delegate
/// </summary>
internal static Func<JsonSerializerSettings> GetJsonSerializerSettingsDelegate()
{
return () => s_settings;
}
}
}
3 changes: 3 additions & 0 deletions provisioning/device/src/ProvisioningDeviceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Provisioning.Client.Transport;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Client
{
Expand Down Expand Up @@ -47,6 +48,8 @@ private ProvisioningDeviceClient(
SecurityProvider securityProvider,
ProvisioningTransportHandler transport)
{
JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate();

_globalDeviceEndpoint = globalDeviceEndpoint;
_idScope = idScope;
_transport = transport;
Expand Down
36 changes: 36 additions & 0 deletions provisioning/service/src/JsonSerializerSettingsInitializer.cs
Original file line number Diff line number Diff line change
@@ -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.Provisioning.Service
{
/// <summary>
/// A class to initialize JsonSerializerSettings which can be applied to the project.
/// </summary>
internal static class JsonSerializerSettingsInitializer
{
/// <summary>
/// A static instance of JsonSerializerSettings which sets DateParseHandling to None.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static readonly JsonSerializerSettings s_settings = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
};

/// <summary>
/// Returns JsonSerializerSettings Func delegate
/// </summary>
internal static Func<JsonSerializerSettings> GetJsonSerializerSettingsDelegate()
{
return () => s_settings;
}
}
}
3 changes: 3 additions & 0 deletions provisioning/service/src/ProvisioningServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Common.Service.Auth;
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.Provisioning.Service
{
Expand Down Expand Up @@ -109,6 +110,8 @@ private ProvisioningServiceClient(string connectionString, HttpTransportSettings
throw new ArgumentException($"{nameof(connectionString)} cannot be empty string");
}

JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate();

_provisioningConnectionString = ServiceConnectionString.Parse(connectionString);
_contractApiHttp = new ContractApiHttp(
_provisioningConnectionString.HttpsEndpoint,
Expand Down
4 changes: 2 additions & 2 deletions shared/src/TwinCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public TwinCollection()
/// </summary>
/// <param name="twinJson">JSON fragment containing the twin data.</param>
public TwinCollection(string twinJson)
: this(JObject.Parse(twinJson))
: this(JsonConvert.DeserializeObject<JObject>(twinJson))
brycewang-microsoft marked this conversation as resolved.
Show resolved Hide resolved
{
}

Expand All @@ -50,7 +50,7 @@ public TwinCollection(string twinJson)
/// <param name="twinJson">JSON fragment containing the twin data.</param>
/// <param name="metadataJson">JSON fragment containing the metadata.</param>
public TwinCollection(string twinJson, string metadataJson)
: this(JObject.Parse(twinJson), JObject.Parse(metadataJson))
: this(JsonConvert.DeserializeObject<JObject>(twinJson), JsonConvert.DeserializeObject<JObject>(metadataJson))
{
}

Expand Down