Skip to content

Commit

Permalink
Fix bug in date deserialization of device method response (Azure#3097)
Browse files Browse the repository at this point in the history
  • Loading branch information
brycewang-microsoft authored Feb 6, 2023
1 parent f2a506f commit 757fd5a
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 4 deletions.
6 changes: 2 additions & 4 deletions common/src/service/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public HttpClientHelper(
_defaultErrorMapping = defaultErrorMapping;
_defaultOperationTimeout = timeout;

JsonConvert.DefaultSettings = JsonSerializerSettingsInitializer.GetJsonSerializerSettingsDelegate();

// We need two types of HttpClients, one with our default operation timeout, and one without. The one without will rely on
// a cancellation token.

Expand Down Expand Up @@ -307,12 +309,8 @@ private static async Task<T> ReadResponseMessageAsync<T>(HttpResponseMessage mes
return (T)(object)message;
}

#if NET451
T entity = await message.Content.ReadAsAsync<T>(token).ConfigureAwait(false);
#else
string str = await message.Content.ReadHttpContentAsStringAsync(token).ConfigureAwait(false);
T entity = JsonConvert.DeserializeObject<T>(str);
#endif

// Etag in the header is considered authoritative
var eTagHolder = entity as IETagHolder;
Expand Down
66 changes: 66 additions & 0 deletions e2e/test/iothub/method/MethodE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Net;
using System.Text;
using System.Threading.Tasks;
Expand All @@ -11,6 +12,7 @@
using Microsoft.Azure.Devices.Common.Exceptions;
using Microsoft.Azure.Devices.E2ETests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

namespace Microsoft.Azure.Devices.E2ETests.Methods
{
Expand Down Expand Up @@ -301,6 +303,65 @@ await deviceClient
}
}

[TestMethod]
[Timeout(TestTimeoutMilliseconds)]
public async Task Method_ServiceInvokeDeviceMethodWithDateTimePayload_DoesNotThrow()
{
// arrange

var date = new DateTimeOffset(638107582284599400, TimeSpan.FromHours(1));

string responseJson = JsonConvert.SerializeObject(new TestDateTime { Iso8601String = date.ToString("o", CultureInfo.InvariantCulture) });
byte[] responseBytes = Encoding.UTF8.GetBytes(responseJson);

const string commandName = "GetDateTime";
bool deviceMethodCalledSuccessfully = false;
TestDevice testDevice = await TestDevice.GetTestDeviceAsync("DateTimeMethodPayloadTest").ConfigureAwait(false);
using DeviceClient deviceClient = testDevice.CreateDeviceClient(Client.TransportType.Mqtt);

try
{
await deviceClient.OpenAsync().ConfigureAwait(false);
await deviceClient
.SetMethodHandlerAsync(
commandName,
(methodRequest, userContext) =>
{
methodRequest.Name.Should().Be(commandName);
deviceMethodCalledSuccessfully = true;
return Task.FromResult(new MethodResponse(responseBytes, 200));
},
null)
.ConfigureAwait(false);

using var serviceClient = ServiceClient.CreateFromConnectionString(TestConfiguration.IotHub.ConnectionString);
CloudToDeviceMethod c2dMethod = new CloudToDeviceMethod(commandName, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)).SetPayloadJson(null);

// act

CloudToDeviceMethodResult result = await serviceClient.InvokeDeviceMethodAsync(testDevice.Id, c2dMethod).ConfigureAwait(false);
string actualResultJson = result.GetPayloadAsJson();
TestDateTime myDtoValue = JsonConvert.DeserializeObject<TestDateTime>(actualResultJson);
string value = myDtoValue.Iso8601String;

Action act = () => DateTimeOffset.ParseExact(value, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

// assert

deviceMethodCalledSuccessfully.Should().BeTrue();
responseJson.Should().Be(actualResultJson);
act.Should().NotThrow();
}
finally
{
// clean up

await deviceClient.SetMethodDefaultHandlerAsync(null, null).ConfigureAwait(false);
await deviceClient.CloseAsync().ConfigureAwait(false);
await testDevice.RemoveDeviceAsync().ConfigureAwait(false);
}
}

public static async Task ServiceSendMethodAndVerifyNotReceivedAsync(
string deviceId,
string methodName,
Expand Down Expand Up @@ -617,5 +678,10 @@ await Task

await moduleClient.CloseAsync().ConfigureAwait(false);
}

private class TestDateTime
{
public string Iso8601String { get; set; }
}
}
}
35 changes: 35 additions & 0 deletions shared/src/JsonSerializerSettingsInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.Shared
{
/// <summary>
/// A class to initialize JsonSerializerSettings which can be applied to the project.
/// </summary>
public 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.
/// </remarks>
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None
};

/// <summary>
/// Returns JsonSerializerSettings Func delegate
/// </summary>
public static Func<JsonSerializerSettings> GetJsonSerializerSettingsDelegate()
{
return () => Settings;
}
}
}

0 comments on commit 757fd5a

Please sign in to comment.