diff --git a/iothub/device/samples/convention-based-samples/TemperatureController/SystemTextJsonWritablePropertyResponse.cs b/iothub/device/samples/convention-based-samples/TemperatureController/SystemTextJsonWritablePropertyResponse.cs index d0bb306ed7..97a8a84d64 100644 --- a/iothub/device/samples/convention-based-samples/TemperatureController/SystemTextJsonWritablePropertyResponse.cs +++ b/iothub/device/samples/convention-based-samples/TemperatureController/SystemTextJsonWritablePropertyResponse.cs @@ -18,13 +18,13 @@ public sealed class SystemTextJsonWritablePropertyResponse : IWritablePropertyRe /// /// Convenience constructor for specifying the properties. /// - /// The unserialized property value. + /// The unserialized property value. /// The acknowledgment code, usually an HTTP Status Code e.g. 200, 400. /// The acknowledgment version, as supplied in the property update request. /// The acknowledgment description, an optional, human-readable message about the result of the property update. - public SystemTextJsonWritablePropertyResponse(object propertyValue, int ackCode, long ackVersion, string ackDescription = default) + public SystemTextJsonWritablePropertyResponse(object value, int ackCode, long ackVersion, string ackDescription = default) { - Value = propertyValue; + Value = value; AckCode = ackCode; AckVersion = ackVersion; AckDescription = ackDescription; diff --git a/iothub/device/tests/ClientPropertyCollectionTests.cs b/iothub/device/tests/ClientPropertyCollectionTests.cs new file mode 100644 index 0000000000..71c58426c2 --- /dev/null +++ b/iothub/device/tests/ClientPropertyCollectionTests.cs @@ -0,0 +1,320 @@ +// 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 System.Collections.Generic; +using FluentAssertions; +using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Client.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class ClientPropertyCollectionTests + { + private const string BoolPropertyName = "boolProperty"; + private const string DoublePropertyName = "doubleProperty"; + private const string FloatPropertyName = "floatProperty"; + private const string IntPropertyName = "intProperty"; + private const string ShortPropertyName = "shortProperty"; + private const string StringPropertyName = "stringPropertyName"; + private const string ObjectPropertyName = "objectPropertyName"; + private const string ArrayPropertyName = "arrayPropertyName"; + private const string MapPropertyName = "mapPropertyName"; + private const string DateTimePropertyName = "dateTimePropertyName"; + + private const bool BoolPropertyValue = false; + private const double DoublePropertyValue = 1.001; + private const float FloatPropertyValue = 1.2f; + private const int IntPropertyValue = 12345678; + private const short ShortPropertyValue = 1234; + private const string StringPropertyValue = "propertyValue"; + + private const string ComponentName = "testableComponent"; + private const string WritablePropertyDescription = "testableWritablePropertyDescription"; + private const string UpdatedPropertyValue = "updatedPropertyValue"; + + private static readonly DateTimeOffset s_dateTimePropertyValue = DateTimeOffset.Now; + private static readonly CustomClientProperty s_objectPropertyValue = new CustomClientProperty { Id = 123, Name = "testName" }; + + private static readonly List s_arrayPropertyValues = new List + { + 1, + "someString", + false, + s_objectPropertyValue + }; + + private static readonly Dictionary s_mapPropertyValues = new Dictionary + { + { "key1", "value1" }, + { "key2", 123 }, + { "key3", s_objectPropertyValue } + }; + + [TestMethod] + public void ClientPropertyCollection_CanAddSimpleObjectsAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection + { + { StringPropertyName, StringPropertyValue }, + { BoolPropertyName, BoolPropertyValue }, + { DoublePropertyName, DoublePropertyValue }, + { FloatPropertyName, FloatPropertyValue }, + { IntPropertyName, IntPropertyValue }, + { ShortPropertyName, ShortPropertyValue }, + { ObjectPropertyName, s_objectPropertyValue }, + { ArrayPropertyName, s_arrayPropertyValues }, + { MapPropertyName, s_mapPropertyValues }, + { DateTimePropertyName, s_dateTimePropertyValue } + }; + + clientProperties.TryGetValue(StringPropertyName, out string stringOutValue); + stringOutValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(BoolPropertyName, out bool boolOutValue); + boolOutValue.Should().Be(BoolPropertyValue); + + clientProperties.TryGetValue(DoublePropertyName, out double doubleOutValue); + doubleOutValue.Should().Be(DoublePropertyValue); + + clientProperties.TryGetValue(FloatPropertyName, out float floatOutValue); + floatOutValue.Should().Be(FloatPropertyValue); + + clientProperties.TryGetValue(IntPropertyName, out int intOutValue); + intOutValue.Should().Be(IntPropertyValue); + + clientProperties.TryGetValue(ShortPropertyName, out short shortOutValue); + shortOutValue.Should().Be(ShortPropertyValue); + + clientProperties.TryGetValue(ObjectPropertyName, out CustomClientProperty objectOutValue); + objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); + objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); + + clientProperties.TryGetValue(ArrayPropertyName, out List arrayOutValue); + arrayOutValue.Should().HaveSameCount(s_arrayPropertyValues); + arrayOutValue.Should().BeEquivalentTo(s_arrayPropertyValues); + + clientProperties.TryGetValue(MapPropertyName, out Dictionary mapOutValue); + mapOutValue.Should().HaveSameCount(s_mapPropertyValues); + mapOutValue.Should().BeEquivalentTo(s_mapPropertyValues); + + clientProperties.TryGetValue(DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + dateTimeOutValue.Should().Be(s_dateTimePropertyValue); + } + + [TestMethod] + public void ClientPropertyCollection_AddSimpleObjectAgainThrowsException() + { + var clientProperties = new ClientPropertyCollection + { + { StringPropertyName, StringPropertyValue } + }; + + Action act = () => clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + act.Should().Throw("\"Add\" method does not support adding a key that already exists in the collection."); + } + + [TestMethod] + public void ClientPropertyCollection_CanUpdateSimpleObjectAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection + { + { StringPropertyName, StringPropertyValue } + }; + clientProperties.TryGetValue(StringPropertyName, out string outValue); + outValue.Should().Be(StringPropertyValue); + + clientProperties.AddOrUpdateRootProperty(StringPropertyName, UpdatedPropertyValue); + clientProperties.TryGetValue(StringPropertyName, out string outValueChanged); + outValueChanged.Should().Be(UpdatedPropertyValue, "\"AddOrUpdate\" should overwrite the value if the key already exists in the collection."); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddNullPropertyAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + clientProperties.AddRootProperty(IntPropertyName, null); + + clientProperties.TryGetValue(StringPropertyName, out string outStringValue); + outStringValue.Should().Be(StringPropertyValue); + + bool nullPropertyPresent = clientProperties.TryGetValue(IntPropertyName, out int? outIntValue); + nullPropertyPresent.Should().BeTrue(); + outIntValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddMultiplePropertyAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + clientProperties.AddRootProperty(IntPropertyName, IntPropertyValue); + + clientProperties.TryGetValue(StringPropertyName, out string outStringValue); + outStringValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(IntPropertyName, out int outIntValue); + outIntValue.Should().Be(IntPropertyValue); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddSimpleObjectWithComponentAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection + { + { ComponentName, new Dictionary { + { StringPropertyName, StringPropertyValue }, + { BoolPropertyName, BoolPropertyValue }, + { DoublePropertyName, DoublePropertyValue }, + { FloatPropertyName, FloatPropertyValue }, + { IntPropertyName, IntPropertyValue }, + { ShortPropertyName, ShortPropertyValue }, + { ObjectPropertyName, s_objectPropertyValue }, + { ArrayPropertyName, s_arrayPropertyValues }, + { MapPropertyName, s_mapPropertyValues }, + { DateTimePropertyName, s_dateTimePropertyValue } } + } + }; + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string stringOutValue); + stringOutValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(ComponentName, BoolPropertyName, out bool boolOutValue); + boolOutValue.Should().Be(BoolPropertyValue); + + clientProperties.TryGetValue(ComponentName, DoublePropertyName, out double doubleOutValue); + doubleOutValue.Should().Be(DoublePropertyValue); + + clientProperties.TryGetValue(ComponentName, FloatPropertyName, out float floatOutValue); + floatOutValue.Should().Be(FloatPropertyValue); + + clientProperties.TryGetValue(ComponentName, IntPropertyName, out int intOutValue); + intOutValue.Should().Be(IntPropertyValue); + + clientProperties.TryGetValue(ComponentName, ShortPropertyName, out short shortOutValue); + shortOutValue.Should().Be(ShortPropertyValue); + + clientProperties.TryGetValue(ComponentName, ObjectPropertyName, out CustomClientProperty objectOutValue); + objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); + objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); + + clientProperties.TryGetValue(ComponentName, ArrayPropertyName, out List arrayOutValue); + arrayOutValue.Should().HaveSameCount(s_arrayPropertyValues); + arrayOutValue.Should().BeEquivalentTo(s_arrayPropertyValues); + + clientProperties.TryGetValue(ComponentName, MapPropertyName, out Dictionary mapOutValue); + mapOutValue.Should().HaveSameCount(s_mapPropertyValues); + mapOutValue.Should().BeEquivalentTo(s_mapPropertyValues); + + clientProperties.TryGetValue(ComponentName, DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + dateTimeOutValue.Should().Be(s_dateTimePropertyValue); + } + + [TestMethod] + public void ClientPropertyCollection_AddSimpleObjectWithComponentAgainThrowsException() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + Action act = () => clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + act.Should().Throw("\"Add\" method does not support adding a key that already exists in the collection."); + } + + [TestMethod] + public void ClientPropertyCollection_CanUpdateSimpleObjectWithComponentAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); + outValue.Should().Be(StringPropertyValue); + + clientProperties.AddOrUpdateComponentProperty(ComponentName, StringPropertyName, UpdatedPropertyValue); + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValueChanged); + outValueChanged.Should().Be(UpdatedPropertyValue, "\"AddOrUpdate\" should overwrite the value if the key already exists in the collection."); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddNullPropertyWithComponentAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + clientProperties.AddComponentProperty(ComponentName, IntPropertyName, null); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outStringValue); + outStringValue.Should().Be(StringPropertyValue); + + bool nullPropertyPresent = clientProperties.TryGetValue(ComponentName, IntPropertyName, out int? outIntValue); + nullPropertyPresent.Should().BeTrue(); + outIntValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddMultiplePropertyWithComponentAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + clientProperties.AddComponentProperty(ComponentName, IntPropertyName, IntPropertyValue); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outStringValue); + outStringValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(ComponentName, IntPropertyName, out int outIntValue); + outIntValue.Should().Be(IntPropertyValue); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddSimpleWritablePropertyAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + + var writableResponse = new NewtonsoftJsonWritablePropertyResponse(StringPropertyValue, StatusCodes.OK, 2, WritablePropertyDescription); + clientProperties.AddRootProperty(StringPropertyName, writableResponse); + + clientProperties.TryGetValue(StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + outValue.Value.Should().Be(writableResponse.Value); + outValue.AckCode.Should().Be(writableResponse.AckCode); + outValue.AckVersion.Should().Be(writableResponse.AckVersion); + outValue.AckDescription.Should().Be(writableResponse.AckDescription); + } + + [TestMethod] + public void ClientPropertyCollection_CanAddWritablePropertyWithComponentAndGetBackWithoutDeviceClient() + { + var clientProperties = new ClientPropertyCollection(); + + var writableResponse = new NewtonsoftJsonWritablePropertyResponse(StringPropertyValue, StatusCodes.OK, 2, WritablePropertyDescription); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, writableResponse); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + outValue.Value.Should().Be(writableResponse.Value); + outValue.AckCode.Should().Be(writableResponse.AckCode); + outValue.AckVersion.Should().Be(writableResponse.AckVersion); + outValue.AckDescription.Should().Be(writableResponse.AckDescription); + } + + [TestMethod] + public void ClientPropertyCollection_AddingComponentAddsComponentIdentifier() + { + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); + clientProperties.TryGetValue(ComponentName, ConventionBasedConstants.ComponentIdentifierKey, out string componentOut); + + outValue.Should().Be(StringPropertyValue); + componentOut.Should().Be(ConventionBasedConstants.ComponentIdentifierValue); + } + } + + internal class CustomClientProperty + { + // The properties in here need to be public otherwise NewtonSoft.Json cannot serialize and deserialize them properly. + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs b/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs new file mode 100644 index 0000000000..fb7c35bf9d --- /dev/null +++ b/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs @@ -0,0 +1,284 @@ +// 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 System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Devices.Client.Tests +{ + [TestClass] + [TestCategory("Unit")] + // These tests test the deserialization of the service response to a ClientPropertyCollection. + // This flow is convention aware and uses NewtonSoft.Json for deserialization. + // For the purpose of these tests we will create an instance of a Twin class to simulate the service response. + public class ClientPropertyCollectionTestsNewtonsoft + { + internal const string BoolPropertyName = "boolProperty"; + internal const string DoublePropertyName = "doubleProperty"; + internal const string FloatPropertyName = "floatProperty"; + internal const string IntPropertyName = "intProperty"; + internal const string ShortPropertyName = "shortProperty"; + internal const string StringPropertyName = "stringPropertyName"; + internal const string ObjectPropertyName = "objectPropertyName"; + internal const string ArrayPropertyName = "arrayPropertyName"; + internal const string MapPropertyName = "mapPropertyName"; + internal const string DateTimePropertyName = "dateTimePropertyName"; + internal const string ComponentName = "testableComponent"; + + private const bool BoolPropertyValue = false; + private const double DoublePropertyValue = 1.001; + private const float FloatPropertyValue = 1.2f; + private const int IntPropertyValue = 12345678; + private const short ShortPropertyValue = 1234; + private const string StringPropertyValue = "propertyValue"; + + private const string UpdatedPropertyValue = "updatedPropertyValue"; + + private static readonly DateTimeOffset s_dateTimePropertyValue = DateTimeOffset.Now; + private static readonly CustomClientProperty s_objectPropertyValue = new CustomClientProperty { Id = 123, Name = "testName" }; + + private static readonly List s_arrayPropertyValues = new List + { + 1, + "someString", + false, + s_objectPropertyValue + }; + + private static readonly Dictionary s_mapPropertyValues = new Dictionary + { + { "key1", "value1" }, + { "key2", 123 }, + { "key3", s_objectPropertyValue } + }; + + // Create an object that represents all of the properties as root-level properties. + private static readonly RootLevelProperties s_rootLevelProperties = new RootLevelProperties + { + BooleanProperty = BoolPropertyValue, + DoubleProperty = DoublePropertyValue, + FloatProperty = FloatPropertyValue, + IntProperty = IntPropertyValue, + ShortProperty = ShortPropertyValue, + StringProperty = StringPropertyValue, + ObjectProperty = s_objectPropertyValue, + ArrayProperty = s_arrayPropertyValues, + MapProperty = s_mapPropertyValues, + DateTimeProperty = s_dateTimePropertyValue + }; + + // Create an object that represents all of the properties as component-level properties. + // This adds the "__t": "c" component identifier as a part of "ComponentProperties" class declaration. + private static readonly ComponentLevelProperties s_componentLevelProperties = new ComponentLevelProperties + { + Properties = new ComponentProperties + { + BooleanProperty = BoolPropertyValue, + DoubleProperty = DoublePropertyValue, + FloatProperty = FloatPropertyValue, + IntProperty = IntPropertyValue, + ShortProperty = ShortPropertyValue, + StringProperty = StringPropertyValue, + ObjectProperty = s_objectPropertyValue, + ArrayProperty = s_arrayPropertyValues, + MapProperty = s_mapPropertyValues, + DateTimeProperty = s_dateTimePropertyValue + }, + }; + + // Create a writable property response with the expected values. + private static readonly IWritablePropertyResponse s_writablePropertyResponse = new NewtonsoftJsonWritablePropertyResponse( + propertyValue: StringPropertyValue, + ackCode: StatusCodes.OK, + ackVersion: 2, + ackDescription: "testableWritablePropertyDescription"); + + // Create a JObject instance that represents a writable property response sent for a root-level property. + private static readonly JObject s_writablePropertyResponseJObject = new JObject( + new JProperty(StringPropertyName, JObject.FromObject(s_writablePropertyResponse))); + + // Create a JObject instance that represents a writable property response sent for a component-level property. + // This adds the "__t": "c" component identifier to the constructed JObject. + private static readonly JObject s_writablePropertyResponseWithComponentJObject = new JObject( + new JProperty(ComponentName, new JObject( + new JProperty(ConventionBasedConstants.ComponentIdentifierKey, ConventionBasedConstants.ComponentIdentifierValue), + new JProperty(StringPropertyName, JObject.FromObject(s_writablePropertyResponse))))); + + // The above constructed json objects are used for initializing a twin response. + // This is because we are using a Twin instance to simulate the service response. + + private static TwinCollection collectionToRoundTrip = new TwinCollection(JsonConvert.SerializeObject(s_rootLevelProperties)); + private static TwinCollection collectionWithComponentToRoundTrip = new TwinCollection(JsonConvert.SerializeObject(s_componentLevelProperties)); + private static TwinCollection collectionWritablePropertyToRoundTrip = new TwinCollection(s_writablePropertyResponseJObject, null); + private static TwinCollection collectionWritablePropertyWithComponentToRoundTrip = new TwinCollection(s_writablePropertyResponseWithComponentJObject, null); + + [TestMethod] + public void ClientPropertyCollectionNewtonsoft_CanGetValue() + { + var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionToRoundTrip, DefaultPayloadConvention.Instance); + + clientProperties.TryGetValue(StringPropertyName, out string stringOutValue); + stringOutValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(BoolPropertyName, out bool boolOutValue); + boolOutValue.Should().Be(BoolPropertyValue); + + clientProperties.TryGetValue(DoublePropertyName, out double doubleOutValue); + doubleOutValue.Should().Be(DoublePropertyValue); + + clientProperties.TryGetValue(FloatPropertyName, out float floatOutValue); + floatOutValue.Should().Be(FloatPropertyValue); + + clientProperties.TryGetValue(IntPropertyName, out int intOutValue); + intOutValue.Should().Be(IntPropertyValue); + + clientProperties.TryGetValue(ShortPropertyName, out short shortOutValue); + shortOutValue.Should().Be(ShortPropertyValue); + + clientProperties.TryGetValue(ObjectPropertyName, out CustomClientProperty objectOutValue); + objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); + objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); + + // The two lists won't be exactly equal since TryGetValue doesn't implement nested deserialization + // => the complex object inside the list is deserialized to a JObject. + clientProperties.TryGetValue(ArrayPropertyName, out List arrayOutValue); + arrayOutValue.Should().HaveSameCount(s_arrayPropertyValues); + + // The two dictionaries won't be exactly equal since TryGetValue doesn't implement nested deserialization + // => the complex object inside the dictionary is deserialized to a JObject. + clientProperties.TryGetValue(MapPropertyName, out Dictionary mapOutValue); + mapOutValue.Should().HaveSameCount(s_mapPropertyValues); + + clientProperties.TryGetValue(DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + dateTimeOutValue.Should().Be(s_dateTimePropertyValue); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonsoft_CanGetValueWithComponent() + { + var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string stringOutValue); + stringOutValue.Should().Be(StringPropertyValue); + + clientProperties.TryGetValue(ComponentName, BoolPropertyName, out bool boolOutValue); + boolOutValue.Should().Be(BoolPropertyValue); + + clientProperties.TryGetValue(ComponentName, DoublePropertyName, out double doubleOutValue); + doubleOutValue.Should().Be(DoublePropertyValue); + + clientProperties.TryGetValue(ComponentName, FloatPropertyName, out float floatOutValue); + floatOutValue.Should().Be(FloatPropertyValue); + + clientProperties.TryGetValue(ComponentName, IntPropertyName, out int intOutValue); + intOutValue.Should().Be(IntPropertyValue); + + clientProperties.TryGetValue(ComponentName, ShortPropertyName, out short shortOutValue); + shortOutValue.Should().Be(ShortPropertyValue); + + clientProperties.TryGetValue(ComponentName, ObjectPropertyName, out CustomClientProperty objectOutValue); + objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); + objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); + + // The two lists won't be exactly equal since TryGetValue doesn't implement nested deserialization + // => the complex object inside the list is deserialized to a JObject. + clientProperties.TryGetValue(ComponentName, ArrayPropertyName, out List arrayOutValue); + arrayOutValue.Should().HaveSameCount(s_arrayPropertyValues); + + // The two dictionaries won't be exactly equal since TryGetValue doesn't implement nested deserialization + // => the complex object inside the dictionary is deserialized to a JObject. + clientProperties.TryGetValue(ComponentName, MapPropertyName, out Dictionary mapOutValue); + mapOutValue.Should().HaveSameCount(s_mapPropertyValues); + + clientProperties.TryGetValue(ComponentName, DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + dateTimeOutValue.Should().Be(s_dateTimePropertyValue); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonsoft_CanAddSimpleWritablePropertyAndGetBack() + { + var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWritablePropertyToRoundTrip, DefaultPayloadConvention.Instance); + + clientProperties.TryGetValue(StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + outValue.Value.Should().Be(StringPropertyValue); + outValue.AckCode.Should().Be(s_writablePropertyResponse.AckCode); + outValue.AckVersion.Should().Be(s_writablePropertyResponse.AckVersion); + outValue.AckDescription.Should().Be(s_writablePropertyResponse.AckDescription); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonsoft_CanAddWritablePropertyWithComponentAndGetBack() + { + var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWritablePropertyWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + outValue.Value.Should().Be(StringPropertyValue); + outValue.AckCode.Should().Be(s_writablePropertyResponse.AckCode); + outValue.AckVersion.Should().Be(s_writablePropertyResponse.AckVersion); + outValue.AckDescription.Should().Be(s_writablePropertyResponse.AckDescription); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonsoft_CanGetComponentIdentifier() + { + var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); + clientProperties.TryGetValue(ComponentName, ConventionBasedConstants.ComponentIdentifierKey, out string componentOut); + + outValue.Should().Be(StringPropertyValue); + componentOut.Should().Be(ConventionBasedConstants.ComponentIdentifierValue); + } + } + + internal class RootLevelProperties + { + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.BoolPropertyName)] + public bool BooleanProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.DoublePropertyName)] + public double DoubleProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.FloatPropertyName)] + public float FloatProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.IntPropertyName)] + public int IntProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ShortPropertyName)] + public short ShortProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.StringPropertyName)] + public string StringProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ObjectPropertyName)] + public object ObjectProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ArrayPropertyName)] + public IList ArrayProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.MapPropertyName)] + public IDictionary MapProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.DateTimePropertyName)] + public DateTimeOffset DateTimeProperty { get; set; } + } + + internal class ComponentProperties : RootLevelProperties + { + [JsonProperty(ConventionBasedConstants.ComponentIdentifierKey)] + public string ComponentIdentifier { get; } = ConventionBasedConstants.ComponentIdentifierValue; + } + + internal class ComponentLevelProperties + { + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentName)] + public ComponentProperties Properties { get; set; } + } +} diff --git a/iothub/device/tests/NumericHelpersTests.cs b/iothub/device/tests/NumericHelpersTests.cs new file mode 100644 index 0000000000..d1f4e72684 --- /dev/null +++ b/iothub/device/tests/NumericHelpersTests.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 System.Collections.Generic; +using System.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Client.Tests +{ + [TestClass] + [TestCategory("Unit")] + public class NumericHelpersTests + { + [TestMethod] + public void CanConvertNumericTypes() + { + TestNumericConversion(1.001d, true, 1.001f); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion("someString", false, 0); + TestNumericConversion(true, false, 0); + } + + private void TestNumericConversion(object input, bool canConvertExpected, T resultExpected) + { + bool canConvertActual = NumericHelpers.TryCastNumericTo(input, out T result); + + canConvertActual.Should().Be(canConvertExpected); + result.Should().Be(resultExpected); + } + } +} diff --git a/shared/src/NewtonsoftJsonPayloadSerializer.cs b/shared/src/NewtonsoftJsonPayloadSerializer.cs index 7d90b163a0..b38063178b 100644 --- a/shared/src/NewtonsoftJsonPayloadSerializer.cs +++ b/shared/src/NewtonsoftJsonPayloadSerializer.cs @@ -39,11 +39,11 @@ public override T DeserializeToType(string stringToDeserialize) /// public override T ConvertFromObject(object objectToConvert) { - if (objectToConvert == null) - { - return default; - } - return ((JToken)objectToConvert).ToObject(); + var token = JToken.FromObject(objectToConvert); + + return objectToConvert == null + ? default + : token.ToObject(); } /// diff --git a/shared/src/TwinCollection.cs b/shared/src/TwinCollection.cs index ff8fbb9f4f..8149e1f8fb 100644 --- a/shared/src/TwinCollection.cs +++ b/shared/src/TwinCollection.cs @@ -55,28 +55,28 @@ public TwinCollection(string twinJson, string metadataJson) } /// - /// Creates a using a JSON fragment as the body. + /// Creates a using the given JSON fragments for the body and metadata. /// /// JSON fragment containing the twin data. - internal TwinCollection(JObject twinJson) + /// JSON fragment containing the metadata. + public TwinCollection(JObject twinJson, JObject metadataJson) { JObject = twinJson ?? new JObject(); - - if (JObject.TryGetValue(MetadataName, out JToken metadataJToken)) - { - _metadata = metadataJToken as JObject; - } + _metadata = metadataJson; } /// - /// Creates a using the given JSON fragments for the body and metadata. + /// Creates a using a JSON fragment as the body. /// /// JSON fragment containing the twin data. - /// JSON fragment containing the metadata. - public TwinCollection(JObject twinJson, JObject metadataJson) + internal TwinCollection(JObject twinJson) { JObject = twinJson ?? new JObject(); - _metadata = metadataJson; + + if (JObject.TryGetValue(MetadataName, out JToken metadataJToken)) + { + _metadata = metadataJToken as JObject; + } } /// @@ -121,8 +121,6 @@ public int Count } } - internal JObject JObject { get; private set; } - /// /// Property Indexer /// @@ -227,6 +225,19 @@ public IEnumerator GetEnumerator() } } + /// + /// Clear metadata out of the collection + /// + public void ClearMetadata() + { + TryClearMetadata(MetadataName); + TryClearMetadata(LastUpdatedName); + TryClearMetadata(LastUpdatedVersionName); + TryClearMetadata(VersionName); + } + + internal JObject JObject { get; private set; } + private bool TryGetMemberInternal(string propertyName, out object result) { if (!JObject.TryGetValue(propertyName, out JToken value)) @@ -283,16 +294,5 @@ private void TryClearMetadata(string propertyName) JObject.Remove(propertyName); } } - - /// - /// Clear metadata out of the collection - /// - public void ClearMetadata() - { - TryClearMetadata(MetadataName); - TryClearMetadata(LastUpdatedName); - TryClearMetadata(LastUpdatedVersionName); - TryClearMetadata(VersionName); - } } }