diff --git a/configure_tls_protocol_version_and_ciphers.md b/configure_tls_protocol_version_and_ciphers.md index e19fee5a76..007f862d20 100644 --- a/configure_tls_protocol_version_and_ciphers.md +++ b/configure_tls_protocol_version_and_ciphers.md @@ -24,9 +24,9 @@ For more information, check out this article about [best practices with .NET and Also follow the instructions on how to [enable and disable ciphers]. [.NET Framework 4.5.1 is no longer supported]: https://devblogs.microsoft.com/dotnet/support-ending-for-the-net-framework-4-4-5-and-4-5-1/ -[TLS registry settings]: https://docs.microsoft.com/en-us/windows-server/security/tls/tls-registry-settings -[best practices with .NEt and TLS]: https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls -[enable and disable ciphers]: https://support.microsoft.com/en-us/help/245030/how-to-restrict-the-use-of-certain-cryptographic-algorithms-and-protoc +[TLS registry settings]: https://docs.microsoft.com/windows-server/security/tls/tls-registry-settings +[best practices with .NEt and TLS]: https://docs.microsoft.com/dotnet/framework/network-programming/tls +[enable and disable ciphers]: https://support.microsoft.com/help/245030/how-to-restrict-the-use-of-certain-cryptographic-algorithms-and-protoc ### Linux Instructions diff --git a/e2e/test/iothub/properties/PropertiesE2ETests.cs b/e2e/test/iothub/properties/PropertiesE2ETests.cs index a38c55d80c..13d907c604 100644 --- a/e2e/test/iothub/properties/PropertiesE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesE2ETests.cs @@ -118,6 +118,42 @@ await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( .ConfigureAwait(false); } + [LoggedTestMethod] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds_Mqtt() + { + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_Tcp_Only, + Guid.NewGuid().ToString()) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds_MqttWs() + { + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_WebSocket_Only, + Guid.NewGuid().ToString()) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds_Mqtt() + { + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_Tcp_Only, + s_mapOfPropertyValues) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds_MqttWs() + { + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_WebSocket_Only, + s_mapOfPropertyValues) + .ConfigureAwait(false); + } + [LoggedTestMethod] public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet_Mqtt() { @@ -305,6 +341,82 @@ await Task await deviceClient.CloseAsync().ConfigureAwait(false); } + private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(Client.TransportType transport, T propValue) + { + using var cts = new CancellationTokenSource(s_maxWaitTimeForCallback); + string propName = Guid.NewGuid().ToString(); + + Logger.Trace($"{nameof(Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync)}: name={propName}, value={propValue}"); + + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + + var writablePropertyCallbackSemaphore = new SemaphoreSlim(0, 1); + await deviceClient + .SubscribeToWritablePropertyUpdateRequestsAsync( + async (writableProperties, userContext) => + { + try + { + bool isPropertyPresent = writableProperties.TryGetValue(propName, out T propertyFromCollection); + + isPropertyPresent.Should().BeTrue(); + propertyFromCollection.Should().BeEquivalentTo(propValue); + userContext.Should().BeNull(); + + var writablePropertyAcks = new ClientPropertyCollection(); + foreach (KeyValuePair writableProperty in writableProperties) + { + if (writableProperty.Value is WritableClientProperty writableClientProperty) + { + writablePropertyAcks.Add(writableProperty.Key, writableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK)); + } + } + + await deviceClient.UpdateClientPropertiesAsync(writablePropertyAcks).ConfigureAwait(false); + } + finally + { + writablePropertyCallbackSemaphore.Release(); + } + }, + null, + cts.Token) + .ConfigureAwait(false); + + using var testDeviceCallbackHandler = new TestDeviceCallbackHandler(deviceClient, testDevice, Logger); + + await Task + .WhenAll( + RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, propName, propValue), + writablePropertyCallbackSemaphore.WaitAsync(cts.Token)) + .ConfigureAwait(false); + + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + + // Validate that the writable property update request was received + bool isWritablePropertyRequestPresent = clientProperties.WritablePropertyRequests.TryGetValue(propName, out T writablePropertyRequest); + isWritablePropertyRequestPresent.Should().BeTrue(); + writablePropertyRequest.Should().BeEquivalentTo(propValue); + + // Validate that the writable property update request was acknowledged + + bool isWritablePropertyAckPresent = clientProperties.ReportedFromClient.TryGetValue(propName, out IWritablePropertyResponse writablePropertyAck); + isWritablePropertyAckPresent.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAck.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentSpecific = clientProperties.ReportedFromClient.TryGetValue(propName, out NewtonsoftJsonWritablePropertyResponse writablePropertyAckNewtonSoft); + isWritablePropertyAckPresentSpecific.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAckNewtonSoft.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentAsValue = clientProperties.ReportedFromClient.TryGetValue(propName, out T writablePropertyAckValue); + isWritablePropertyAckPresentAsValue.Should().BeTrue(); + writablePropertyAckValue.Should().BeEquivalentTo(propValue); + } + private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(Client.TransportType transport) { string propName = Guid.NewGuid().ToString(); diff --git a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs index 3e3b23be75..1ce6d0fb7e 100644 --- a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs @@ -184,6 +184,42 @@ await PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyNameAsync( .ConfigureAwait(false); } + [LoggedTestMethod] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds_Mqtt() + { + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_Tcp_Only, + Guid.NewGuid().ToString()) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds_MqttWs() + { + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_WebSocket_Only, + Guid.NewGuid().ToString()) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds_Mqtt() + { + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_Tcp_Only, + s_mapOfPropertyValues) + .ConfigureAwait(false); + } + + [LoggedTestMethod] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds_MqttWs() + { + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync( + Client.TransportType.Mqtt_WebSocket_Only, + s_mapOfPropertyValues) + .ConfigureAwait(false); + } + private async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(Client.TransportType transport) { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); @@ -312,6 +348,88 @@ await Task await deviceClient.CloseAsync().ConfigureAwait(false); } + private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(Client.TransportType transport, T propValue) + { + using var cts = new CancellationTokenSource(s_maxWaitTimeForCallback); + string propName = Guid.NewGuid().ToString(); + + Logger.Trace($"{nameof(PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync)}: name={propName}, value={propValue}"); + + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + + var writablePropertyCallbackSemaphore = new SemaphoreSlim(0, 1); + await deviceClient + .SubscribeToWritablePropertyUpdateRequestsAsync( + async (writableProperties, userContext) => + { + try + { + bool isPropertyPresent = writableProperties.TryGetValue(ComponentName, propName, out T propertyFromCollection); + + isPropertyPresent.Should().BeTrue(); + propertyFromCollection.Should().BeEquivalentTo(propValue); + userContext.Should().BeNull(); + + var writablePropertyAcks = new ClientPropertyCollection(); + foreach (KeyValuePair writableProperty in writableProperties) + { + if (writableProperty.Value is IDictionary componentProperties) + { + foreach (KeyValuePair componentProperty in componentProperties) + { + if (componentProperty.Value is WritableClientProperty writableClientProperty) + { + writablePropertyAcks.AddComponentProperty(writableProperty.Key, componentProperty.Key, writableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK)); + } + } + } + } + + await deviceClient.UpdateClientPropertiesAsync(writablePropertyAcks).ConfigureAwait(false); + } + finally + { + writablePropertyCallbackSemaphore.Release(); + } + }, + null, + cts.Token) + .ConfigureAwait(false); + + using var testDeviceCallbackHandler = new TestDeviceCallbackHandler(deviceClient, testDevice, Logger); + + await Task + .WhenAll( + RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, ComponentName, propName, propValue), + writablePropertyCallbackSemaphore.WaitAsync(cts.Token)) + .ConfigureAwait(false); + + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + + // Validate that the writable property update request was received + bool isWritablePropertyRequestPresent = clientProperties.WritablePropertyRequests.TryGetValue(ComponentName, propName, out T writablePropertyRequest); + isWritablePropertyRequestPresent.Should().BeTrue(); + writablePropertyRequest.Should().BeEquivalentTo(propValue); + + // Validate that the writable property update request was acknowledged + + bool isWritablePropertyAckPresent = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out IWritablePropertyResponse writablePropertyAck); + isWritablePropertyAckPresent.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAck.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentSpecific = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out NewtonsoftJsonWritablePropertyResponse writablePropertyAckNewtonSoft); + isWritablePropertyAckPresentSpecific.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAckNewtonSoft.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentAsValue = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out T writablePropertyAckValue); + isWritablePropertyAckPresentAsValue.Should().BeTrue(); + writablePropertyAckValue.Should().BeEquivalentTo(propValue); + } + private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(Client.TransportType transport) { string propName = Guid.NewGuid().ToString(); diff --git a/iothub/device/devdoc/Convention-based operations.md b/iothub/device/devdoc/Convention-based operations.md index 25437da767..49216ecc89 100644 --- a/iothub/device/devdoc/Convention-based operations.md +++ b/iothub/device/devdoc/Convention-based operations.md @@ -79,7 +79,6 @@ public abstract class PayloadCollection : IEnumerable, IEnumerable> GetEnumerator(); public virtual byte[] GetPayloadObjectBytes(); public virtual string GetSerializedString(); - protected void SetCollection(PayloadCollection payloadCollection); IEnumerator System.Collections.IEnumerable.GetEnumerator(); public bool TryGetValue(string key, out T value); } @@ -127,15 +126,16 @@ public Task UpdateClientPropertiesAsync(ClientPr /// The global call back to handle all writable property updates. /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. -public Task SubscribeToWritablePropertiesEventAsync(Func callback, object userContext, CancellationToken cancellationToken = default); +public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func callback, object userContext, CancellationToken cancellationToken = default); ``` #### All related types ```csharp -public class ClientProperties : ClientPropertyCollection { +public class ClientProperties { public ClientProperties(); - public ClientPropertyCollection Writable { get; private set; } + public ClientPropertyCollection ReportedFromClient { get; private set; } + public ClientPropertyCollection WritablePropertyRequests { get; private set; } } public class ClientPropertyCollection : PayloadCollection { @@ -153,6 +153,12 @@ public class ClientPropertyCollection : PayloadCollection { public virtual bool TryGetValue(string componentName, string propertyName, out T propertyValue); } +public class WritableClientProperty { + public object Value { get; internal set; } + public long Version { get; internal set; } + public IWritablePropertyResponse AcknowledgeWith(int statusCode, string description = null); +} + public interface IWritablePropertyResponse { int AckCode { get; set; } string AckDescription { get; set; } @@ -169,7 +175,6 @@ public sealed class NewtonsoftJsonWritablePropertyResponse : IWritablePropertyRe } public class ClientPropertiesUpdateResponse { - public ClientPropertiesUpdateResponse(); public string RequestId { get; internal set; } public long Version { get; internal set; } } diff --git a/iothub/device/src/ClientPropertiesUpdateResponse.cs b/iothub/device/src/ClientPropertiesUpdateResponse.cs index 4db42ab13b..b204260794 100644 --- a/iothub/device/src/ClientPropertiesUpdateResponse.cs +++ b/iothub/device/src/ClientPropertiesUpdateResponse.cs @@ -8,6 +8,10 @@ namespace Microsoft.Azure.Devices.Client /// public class ClientPropertiesUpdateResponse { + internal ClientPropertiesUpdateResponse() + { + } + /// /// The request Id that is associated with the operation. /// diff --git a/iothub/device/src/ClientPropertyCollection.cs b/iothub/device/src/ClientPropertyCollection.cs index 54238bc213..17729ad2c0 100644 --- a/iothub/device/src/ClientPropertyCollection.cs +++ b/iothub/device/src/ClientPropertyCollection.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.Client { @@ -188,12 +190,23 @@ public virtual bool TryGetValue(string componentName, string propertyName, ou return false; } + // While retrieving the property value from the collection: + // 1. A property collection constructed by the client application - can be retrieved using dictionary indexer. + // 2. Client property received through writable property update callbacks - stored internally as a WritableClientProperty. + // 3. Client property returned through GetClientProperties: + // a. Client reported properties sent by the client application in response to writable property update requests - stored as a JSON object + // and needs to be converted to an IWritablePropertyResponse implementation using the payload serializer. + // b. Client reported properties sent by the client application - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + // c. Writable property update request received - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + if (Contains(componentName, propertyName)) { object componentProperties = Collection[componentName]; // If the ClientPropertyCollection was constructed by the user application (eg. for updating the client properties) - // then the componentProperties are retrieved as a dictionary. + // or was returned by the application as a writable property update request then the componentProperties are retrieved as a dictionary. // The required property value can be fetched from the dictionary directly. if (componentProperties is IDictionary nestedDictionary) { @@ -211,21 +224,32 @@ public virtual bool TryGetValue(string componentName, string propertyName, ou return true; } + // Case 1: // If the object is of type T or can be cast to type T, go ahead and return it. if (dictionaryElement is T valueRef - || NumericHelpers.TryCastNumericTo(dictionaryElement, out valueRef)) + || ObjectConversionHelpers.TryCastNumericTo(dictionaryElement, out valueRef)) { propertyValue = valueRef; return true; } + + // Case 2: + // Check if the retrieved value is a writable property update request + if (dictionaryElement is WritableClientProperty writableClientProperty) + { + object writableClientPropertyValue = writableClientProperty.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writableClientPropertyValue, Convention, out propertyValue)) + { + return true; + } + } } } } else { - // If the ClientPropertyCollection was constructed by the SDK (eg. when retrieving the client properties) - // then the componentProperties are retrieved as the json object that is defined in the PayloadConvention. - // The required property value then needs to be deserialized accordingly. try { // First verify that the retrieved dictionary contains the component identifier { "__t": "c" }. @@ -235,10 +259,46 @@ public virtual bool TryGetValue(string componentName, string propertyName, ou .TryGetNestedObjectValue(componentProperties, ConventionBasedConstants.ComponentIdentifierKey, out string componentIdentifierValue) && componentIdentifierValue == ConventionBasedConstants.ComponentIdentifierValue) { + Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out object retrievedPropertyValue); + + try + { + // Case 3a: + // Check if the retrieved value is a writable property update acknowledgment + var newtonsoftWritablePropertyResponse = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); + + if (typeof(IWritablePropertyResponse).IsAssignableFrom(typeof(T))) + { + // If T is IWritablePropertyResponse the property value should be of type IWritablePropertyResponse as defined in the PayloadSerializer. + // We'll convert the json object to NewtonsoftJsonWritablePropertyResponse and then convert it to the appropriate IWritablePropertyResponse object. + propertyValue = (T)Convention.PayloadSerializer.CreateWritablePropertyResponse( + newtonsoftWritablePropertyResponse.Value, + newtonsoftWritablePropertyResponse.AckCode, + newtonsoftWritablePropertyResponse.AckVersion, + newtonsoftWritablePropertyResponse.AckDescription); + return true; + } + + object writablePropertyValue = newtonsoftWritablePropertyResponse.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writablePropertyValue, Convention, out propertyValue)) + { + return true; + } + } + catch + { + // In case of an exception ignore it and continue. + } + + // Case 3b, 3c: // Since the value cannot be cast to directly, we need to try to convert it using the serializer. // If it can be successfully converted, go ahead and return it. - Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out propertyValue); - return true; + if (Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out propertyValue)) + { + return true; + } } } catch @@ -275,7 +335,50 @@ internal static ClientPropertyCollection WritablePropertyUpdateRequestsFromTwinC foreach (KeyValuePair property in twinCollection) { - propertyCollectionToReturn.Add(property.Key, payloadConvention.PayloadSerializer.DeserializeToType(Newtonsoft.Json.JsonConvert.SerializeObject(property.Value))); + object propertyValueAsObject = property.Value; + string propertyValueAsString = DefaultPayloadConvention.Instance.PayloadSerializer.SerializeToString(propertyValueAsObject); + + // Check if the property value is for a root property or a component property. + // A component property be a JObject and will have the "__t": "c" identifiers. + bool isComponentProperty = propertyValueAsObject is JObject + && payloadConvention.PayloadSerializer.TryGetNestedObjectValue(propertyValueAsString, ConventionBasedConstants.ComponentIdentifierKey, out string _); + + if (isComponentProperty) + { + // If this is a component property then the collection is a JObject with each individual property as a writable property update request. + var propertyValueAsJObject = (JObject)propertyValueAsObject; + var collectionDictionary = new Dictionary(propertyValueAsJObject.Count); + + foreach (KeyValuePair componentProperty in propertyValueAsJObject) + { + object individualPropertyValue; + if (componentProperty.Key == ConventionBasedConstants.ComponentIdentifierKey) + { + individualPropertyValue = componentProperty.Value; + } + else + { + individualPropertyValue = new WritableClientProperty + { + Convention = payloadConvention, + Value = payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(componentProperty.Value)), + Version = twinCollection.Version, + }; + } + collectionDictionary.Add(componentProperty.Key, individualPropertyValue); + } + propertyCollectionToReturn.Add(property.Key, collectionDictionary); + } + else + { + var writableProperty = new WritableClientProperty + { + Convention = payloadConvention, + Value = payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(propertyValueAsObject)), + Version = twinCollection.Version, + }; + propertyCollectionToReturn.Add(property.Key, writableProperty); + } } // The version information is not accessible via the enumerator, so assign it separately. propertyCollectionToReturn.Version = twinCollection.Version; @@ -306,7 +409,7 @@ internal static ClientPropertyCollection FromClientPropertiesAsDictionary(IDicti } else { - propertyCollectionToReturn.Add(property.Key, payloadConvention.PayloadSerializer.DeserializeToType(Newtonsoft.Json.JsonConvert.SerializeObject(property.Value))); + propertyCollectionToReturn.Add(property.Key, payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(property.Value))); } } diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index 0bc6d5d1fc..daeacbc299 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -300,7 +300,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The receive message or null if there was no message until the default timeout public Task ReceiveAsync() => InternalClient.ReceiveAsync(); @@ -312,7 +312,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. @@ -326,7 +326,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The receive message or null if there was no message until the specified time has elapsed public Task ReceiveAsync(TimeSpan timeout) => InternalClient.ReceiveAsync(timeout); @@ -380,7 +380,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// The previously received message @@ -391,7 +391,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// A cancellation token to cancel the operation. @@ -404,7 +404,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// The lock identifier for the previously received message @@ -415,7 +415,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// A cancellation token to cancel the operation. @@ -428,7 +428,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// The previously received message @@ -439,7 +439,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// A cancellation token to cancel the operation. /// The message lockToken. @@ -452,7 +452,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// The lock identifier for the previously received message @@ -463,7 +463,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// A cancellation token to cancel the operation. @@ -527,7 +527,7 @@ public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken InternalClient.UploadToBlobAsync(blobName, source, cancellationToken); /// - /// Get a file upload SAS URI which the Azure Storage SDK can use to upload a file to blob for this device. See this documentation for more details + /// Get a file upload SAS URI which the Azure Storage SDK can use to upload a file to blob for this device. See this documentation for more details /// /// The request details for getting the SAS URI, including the destination blob name. /// The cancellation token. @@ -536,7 +536,7 @@ public Task GetFileUploadSasUriAsync(FileUploadSasUriR InternalClient.GetFileUploadSasUriAsync(request, cancellationToken); /// - /// Notify IoT Hub that a device's file upload has finished. See this documentation for more details + /// Notify IoT Hub that a device's file upload has finished. See this documentation for more details /// /// The notification details, including if the file upload succeeded. /// The cancellation token. diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index 04a63de0ec..92dee49ee0 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -360,7 +360,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The messages. /// The task containing the event @@ -368,7 +368,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing /// Sends a batch of events to IoT hub. Requires AMQP or AMQP over WebSockets. + /// For more information on IoT Edge module routing /// Sends a batch of events to IoT hub. Requires AMQP or AMQP over WebSockets. /// /// An IEnumerable set of Message objects. /// A cancellation token to cancel the operation. @@ -539,7 +539,7 @@ public Task SendEventAsync(string outputName, Message message, CancellationToken /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The output target for sending the given message /// A list of one or more messages to send @@ -550,7 +550,7 @@ public Task SendEventBatchAsync(string outputName, IEnumerable messages /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The output target for sending the given message /// A list of one or more messages to send diff --git a/iothub/device/src/NumericHelpers.cs b/iothub/device/src/NumericHelpers.cs deleted file mode 100644 index d9b836c90e..0000000000 --- a/iothub/device/src/NumericHelpers.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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.Globalization; - -namespace Microsoft.Azure.Devices.Client -{ - internal class NumericHelpers - { - internal static bool TryCastNumericTo(object input, out T result) - { - if (TryGetNumeric(input)) - { - try - { - result = (T)Convert.ChangeType(input, typeof(T), CultureInfo.InvariantCulture); - return true; - } - catch - { - } - } - - result = default; - return false; - } - - private static bool TryGetNumeric(object expression) - { - if (expression == null) - { - return false; - } - - return double.TryParse( - Convert.ToString( - expression, - CultureInfo.InvariantCulture), - NumberStyles.Any, - NumberFormatInfo.InvariantInfo, - out _); - } - } -} diff --git a/iothub/device/src/ObjectConversionHelpers.cs b/iothub/device/src/ObjectConversionHelpers.cs new file mode 100644 index 0000000000..eb327e43ad --- /dev/null +++ b/iothub/device/src/ObjectConversionHelpers.cs @@ -0,0 +1,82 @@ +// 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.Globalization; +using Microsoft.Azure.Devices.Shared; + +namespace Microsoft.Azure.Devices.Client +{ + internal class ObjectConversionHelpers + { + internal static bool TryCastOrConvert(object objectToCastOrConvert, PayloadConvention payloadConvention, out T value) + { + // If the object is of type T or can be cast to type T, go ahead and return it. + if (TryCast(objectToCastOrConvert, out value)) + { + return true; + } + + try + { + // If the cannot be cast to directly we need to try to convert it using the serializer. + // If it can be successfully converted, go ahead and return it. + value = payloadConvention.PayloadSerializer.ConvertFromObject(objectToCastOrConvert); + return true; + } + catch + { + } + + value = default; + return false; + } + + internal static bool TryCast(object objectToCast, out T value) + { + if (objectToCast is T valueRef + || TryCastNumericTo(objectToCast, out valueRef)) + { + value = valueRef; + return true; + } + + value = default; + return false; + } + + internal static bool TryCastNumericTo(object input, out T result) + { + if (TryGetNumeric(input)) + { + try + { + result = (T)Convert.ChangeType(input, typeof(T), CultureInfo.InvariantCulture); + return true; + } + catch + { + } + } + + result = default; + return false; + } + + private static bool TryGetNumeric(object expression) + { + if (expression == null) + { + return false; + } + + return double.TryParse( + Convert.ToString( + expression, + CultureInfo.InvariantCulture), + NumberStyles.Any, + NumberFormatInfo.InvariantInfo, + out _); + } + } +} diff --git a/iothub/device/src/PayloadCollection.cs b/iothub/device/src/PayloadCollection.cs index c9baeb4856..d4dcbc2295 100644 --- a/iothub/device/src/PayloadCollection.cs +++ b/iothub/device/src/PayloadCollection.cs @@ -127,28 +127,85 @@ public bool TryGetValue(string key, out T value) return false; } + // While retrieving the telemetry value from the collection, a simple dictionary indexer should work. + // While retrieving the property value from the collection: + // 1. A property collection constructed by the client application - can be retrieved using dictionary indexer. + // 2. Client property received through writable property update callbacks - stored internally as a WritableClientProperty. + // 3. Client property returned through GetClientProperties: + // a. Client reported properties sent by the client application in response to writable property update requests - stored as a JSON object + // and needs to be converted to an IWritablePropertyResponse implementation using the payload serializer. + // b. Client reported properties sent by the client application - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + // c. Writable property update request received - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. if (Collection.ContainsKey(key)) { + object retrievedPropertyValue = Collection[key]; + // If the value associated with the key is null, then return true with the default value of the type passed in. - if (Collection[key] == null) + if (retrievedPropertyValue == null) { value = default; return true; } + // Case 1: // If the object is of type T or can be cast to type T, go ahead and return it. - if (Collection[key] is T valueRef - || NumericHelpers.TryCastNumericTo(Collection[key], out valueRef)) + if (ObjectConversionHelpers.TryCast(retrievedPropertyValue, out value)) { - value = valueRef; return true; } + // Case 2: + // Check if the retrieved value is a writable property update request + if (retrievedPropertyValue is WritableClientProperty writableClientProperty) + { + object writableClientPropertyValue = writableClientProperty.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writableClientPropertyValue, Convention, out value)) + { + return true; + } + } + try { - // If the value cannot be cast to directly, we need to try to convert it using the serializer. + try + { + // Case 3a: + // Check if the retrieved value is a writable property update acknowledgment + var newtonsoftWritablePropertyResponse = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); + + if (typeof(IWritablePropertyResponse).IsAssignableFrom(typeof(T))) + { + // If T is IWritablePropertyResponse the property value should be of type IWritablePropertyResponse as defined in the PayloadSerializer. + // We'll convert the json object to NewtonsoftJsonWritablePropertyResponse and then convert it to the appropriate IWritablePropertyResponse object. + value = (T)Convention.PayloadSerializer.CreateWritablePropertyResponse( + newtonsoftWritablePropertyResponse.Value, + newtonsoftWritablePropertyResponse.AckCode, + newtonsoftWritablePropertyResponse.AckVersion, + newtonsoftWritablePropertyResponse.AckDescription); + return true; + } + + var writablePropertyValue = newtonsoftWritablePropertyResponse.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writablePropertyValue, Convention, out value)) + { + return true; + } + } + catch + { + // In case of an exception ignore it and continue. + } + + // Case 3b, 3c: + // If the value is neither a writable property nor can be cast to directly, we need to try to convert it using the serializer. // If it can be successfully converted, go ahead and return it. - value = Convention.PayloadSerializer.ConvertFromObject(Collection[key]); + value = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); return true; } catch diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs b/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs index 65d46003b9..e899c55994 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs @@ -200,7 +200,7 @@ public bool CertificateRevocationCheck /// Setting a will message is a way for clients to notify other subscribed clients about ungraceful disconnects in an appropriate way. /// In response to the ungraceful disconnect, the service will send the last-will message to the configured telemetry channel. /// The telemetry channel can be either the default Events endpoint or a custom endpoint defined by IoT Hub routing. - /// For more details, refer to https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. + /// For more details, refer to https://docs.microsoft.com/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. /// public bool HasWill { get; set; } @@ -209,7 +209,7 @@ public bool CertificateRevocationCheck /// /// /// The telemetry channel can be either the default Events endpoint or a custom endpoint defined by IoT Hub routing. - /// For more details, refer to https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. + /// For more details, refer to https://docs.microsoft.com/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. /// public IWillMessage WillMessage { get; set; } diff --git a/iothub/device/src/WritableClientProperty.cs b/iothub/device/src/WritableClientProperty.cs new file mode 100644 index 0000000000..409c876048 --- /dev/null +++ b/iothub/device/src/WritableClientProperty.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.Devices.Shared; + +namespace Microsoft.Azure.Devices.Client +{ + /// + /// The writable property update request received from service. + /// + /// + /// A writable property update request should be acknowledged by the device or module by sending a reported property. + /// This type contains a convenience method to format the reported property as per IoT Plug and Play convention. + /// For more details see . + /// + public class WritableClientProperty + { + internal WritableClientProperty() + { + } + + /// + /// The value of the writable property update request. + /// + public object Value { get; internal set; } + + /// + /// The version number associated with the writable property update request. + /// + public long Version { get; internal set; } + + internal PayloadConvention Convention { get; set; } + + /// + /// Creates a writable property update response that can be reported back to the service. + /// + /// + /// This writable property update response will contain the property value and version supplied in the writable property update request. + /// If you would like to construct your own writable property update response with custom value and version number, you can + /// create an instance of . + /// See for more details. + /// + /// An acknowledgment code that uses an HTTP status code. + /// An optional acknowledgment description. + /// A writable property update response that can be reported back to the service. + public IWritablePropertyResponse AcknowledgeWith(int statusCode, string description = default) + { + return Convention.PayloadSerializer.CreateWritablePropertyResponse(Value, statusCode, Version, description); + } + } +} diff --git a/iothub/device/tests/DeviceClientTests.cs b/iothub/device/tests/DeviceClientTests.cs index 5cb0672808..715b0ead00 100644 --- a/iothub/device/tests/DeviceClientTests.cs +++ b/iothub/device/tests/DeviceClientTests.cs @@ -107,7 +107,7 @@ public void DeviceClient_ParsmHostNameGatewayAuthMethodTransportArray_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_Params_GatewayAuthMethod_Works() { @@ -118,7 +118,7 @@ public void DeviceClient_Params_GatewayAuthMethod_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_ParamsGatewayAuthMethodTransport_Works() { @@ -129,7 +129,7 @@ public void DeviceClient_ParamsGatewayAuthMethodTransport_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_ParamsGatewayAuthMethodTransportArray_Works() { diff --git a/iothub/device/tests/NumericHelpersTests.cs b/iothub/device/tests/ObjectCastHelpersTests.cs similarity index 88% rename from iothub/device/tests/NumericHelpersTests.cs rename to iothub/device/tests/ObjectCastHelpersTests.cs index caa3ad28e8..52cb0905cb 100644 --- a/iothub/device/tests/NumericHelpersTests.cs +++ b/iothub/device/tests/ObjectCastHelpersTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Devices.Client.Test { [TestClass] [TestCategory("Unit")] - public class NumericHelpersTests + public class ObjectCastHelpersTests { [TestMethod] public void CanConvertNumericTypes() @@ -27,7 +27,7 @@ public void CanConvertNumericTypes() private void TestNumericConversion(object input, bool canConvertExpected, T resultExpected) { - bool canConvertActual = NumericHelpers.TryCastNumericTo(input, out T result); + bool canConvertActual = ObjectConversionHelpers.TryCastNumericTo(input, out T result); canConvertActual.Should().Be(canConvertExpected); result.Should().Be(resultExpected); diff --git a/iothub/service/src/Common/Data/AccessRights.cs b/iothub/service/src/Common/Data/AccessRights.cs index 9887752de9..93f019e0ad 100644 --- a/iothub/service/src/Common/Data/AccessRights.cs +++ b/iothub/service/src/Common/Data/AccessRights.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Devices.Common.Data { /// /// Shared access policy permissions of IoT hub. - /// For more information, see . + /// For more information, see . /// [Flags] [JsonConverter(typeof(StringEnumConverter))] @@ -19,14 +19,14 @@ public enum AccessRights /// /// Grants read access to the identity registry. /// Identity registry stores information about the devices and modules permitted to connect to the IoT hub. - /// For more information, see . + /// For more information, see . /// RegistryRead = 1, /// /// Grants read and write access to the identity registry. /// Identity registry stores information about the devices and modules permitted to connect to the IoT hub. - /// For more information, see . + /// For more information, see . /// RegistryWrite = RegistryRead | 2, diff --git a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs index 9f434063d8..ab2d7548f4 100644 --- a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs +++ b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs @@ -56,7 +56,7 @@ public static DigitalTwinClient CreateFromConnectionString(string connectionStri /// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static DigitalTwinClient Create( string hostName, @@ -124,7 +124,7 @@ public virtual async Task> GetDi /// /// Updates a digital twin. - /// For further information on how to create the json-patch, see + /// For further information on how to create the json-patch, see /// /// The Id of the digital twin. /// The application/json-patch+json operations to be performed on the specified digital twin. diff --git a/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs b/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs index a1639436f7..8b12d44899 100644 --- a/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs +++ b/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs @@ -23,7 +23,7 @@ public class UpdateOperationsUtility /// /// Include an add property operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -62,7 +62,7 @@ public void AppendAddPropertyOp(string path, object value) /// /// Include a replace property operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -101,7 +101,7 @@ public void AppendReplacePropertyOp(string path, object value) /// /// Include a remove operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -145,7 +145,7 @@ public void AppendRemoveOp(string path) /// /// Include an add component operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// This utility appends the "$metadata" identifier to the property values, @@ -182,7 +182,7 @@ public void AppendAddComponentOp(string path, Dictionary propert /// /// Include a replace component operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// This utility appends the "$metadata" identifier to the property values, diff --git a/iothub/service/src/FeedbackStatusCode.cs b/iothub/service/src/FeedbackStatusCode.cs index 617bd20bf1..74a99ee213 100644 --- a/iothub/service/src/FeedbackStatusCode.cs +++ b/iothub/service/src/FeedbackStatusCode.cs @@ -14,32 +14,32 @@ public enum FeedbackStatusCode { /// /// Indicates that the cloud-to-device message was successfully delivered to the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Success = 0, /// /// Indicates that the cloud-to-device message expired before it could be delivered to the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Expired = 1, /// /// Indicates that the cloud-to-device message has been placed in a dead-lettered state. /// This happens when the message reaches the maximum count for the number of times it can transition between enqueued and invisible states. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// DeliveryCountExceeded = 2, /// /// Indicates that the cloud-to-device message was rejected by the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Rejected = 3, /// /// Indicates that the cloud-to-device message was purged from IoT Hub. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Purged = 4 } diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 32421512b4..18b97cea4c 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -100,7 +100,7 @@ public static JobClient CreateFromConnectionString(string connectionString, Http /// The HTTP transport settings. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static JobClient Create( string hostName, diff --git a/iothub/service/src/JobProperties.cs b/iothub/service/src/JobProperties.cs index fbab545b2b..ca5bb057ee 100644 --- a/iothub/service/src/JobProperties.cs +++ b/iothub/service/src/JobProperties.cs @@ -8,7 +8,7 @@ namespace Microsoft.Azure.Devices { /// /// Contains properties of a Job. - /// See online documentation for more infomration. + /// See online documentation for more infomration. /// public class JobProperties { diff --git a/iothub/service/src/ManagedIdentity.cs b/iothub/service/src/ManagedIdentity.cs index 2fa2467be1..8ef757b1a5 100644 --- a/iothub/service/src/ManagedIdentity.cs +++ b/iothub/service/src/ManagedIdentity.cs @@ -10,8 +10,8 @@ namespace Microsoft.Azure.Devices { /// /// The managed identity used to access the storage account for IoT hub import and export jobs. - /// For more information on managed identity configuration on IoT hub, see . - /// For more information on managed identities, see + /// For more information on managed identity configuration on IoT hub, see . + /// For more information on managed identities, see /// public class ManagedIdentity { diff --git a/iothub/service/src/MessageSystemPropertyNames.cs b/iothub/service/src/MessageSystemPropertyNames.cs index 86c559a4d8..d768fade28 100644 --- a/iothub/service/src/MessageSystemPropertyNames.cs +++ b/iothub/service/src/MessageSystemPropertyNames.cs @@ -39,7 +39,7 @@ public static class MessageSystemPropertyNames /// /// The number of times a message can transition between the Enqueued and Invisible states. /// After the maximum number of transitions, the IoT hub sets the state of the message to dead-lettered. - /// For more information, see + /// For more information, see /// public const string DeliveryCount = "iothub-deliverycount"; diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index ed6882f3d1..7568d07f2a 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -134,7 +134,7 @@ public static RegistryManager CreateFromConnectionString(string connectionString /// The HTTP transport settings. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static RegistryManager Create( string hostName, diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index 5ee7c3856a..d5ba708c8a 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -138,7 +138,7 @@ public static ServiceClient CreateFromConnectionString(string connectionString, /// The options that allow configuration of the service client instance during initialization. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static ServiceClient Create( string hostName, @@ -385,7 +385,7 @@ public virtual Task PurgeMessageQueueAsync(string devic /// /// Get the which can deliver acknowledgments for messages sent to a device/module from IoT Hub. /// This call is made over AMQP. - /// For more information see . + /// For more information see . /// /// An instance of . public virtual FeedbackReceiver GetFeedbackReceiver() @@ -396,7 +396,7 @@ public virtual FeedbackReceiver GetFeedbackReceiver() /// /// Get the which can deliver notifications for file upload operations. /// This call is made over AMQP. - /// For more information see . + /// For more information see . /// /// An instance of . public virtual FileNotificationReceiver GetFileNotificationReceiver() diff --git a/provisioning/device/src/PlugAndPlay/PnpConvention.cs b/provisioning/device/src/PlugAndPlay/PnpConvention.cs index b3b29e0645..4a64f8244b 100644 --- a/provisioning/device/src/PlugAndPlay/PnpConvention.cs +++ b/provisioning/device/src/PlugAndPlay/PnpConvention.cs @@ -15,7 +15,7 @@ public static class PnpConvention /// /// /// For more information on device provisioning service and plug and play compatibility, - /// and PnP device certification, see . + /// and PnP device certification, see . /// The DPS payload should be in the format: /// /// { diff --git a/readme.md b/readme.md index 1436873e22..87c34d3955 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ Due to security considerations, build logs are not publicly available. | Microsoft.Azure.Devices.DigitalTwin.Service | N/A | [![NuGet][pnp-service-prerelease]][pnp-service-nuget] | > Note: -> Device streaming feature is not being included in our newer preview releases as there is no active development going on in the service. For more details on the feature, see [here](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-device-streams-overview). It is not recommended to take dependency on preview nugets for production applications as breaking changes can be introduced in preview nugets. +> Device streaming feature is not being included in our newer preview releases as there is no active development going on in the service. For more details on the feature, see [here](https://docs.microsoft.com/azure/iot-hub/iot-hub-device-streams-overview). It is not recommended to take dependency on preview nugets for production applications as breaking changes can be introduced in preview nugets. > > The feature has not been included in any preview release after [2020-10-14](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/preview_2020-10-14). However, the feature is still available under previews/deviceStreaming branch. > diff --git a/shared/src/NewtonsoftJsonPayloadSerializer.cs b/shared/src/NewtonsoftJsonPayloadSerializer.cs index b38063178b..349c76676b 100644 --- a/shared/src/NewtonsoftJsonPayloadSerializer.cs +++ b/shared/src/NewtonsoftJsonPayloadSerializer.cs @@ -54,10 +54,23 @@ public override bool TryGetNestedObjectValue(object nestedObject, string prop { return false; } - if (((JObject)nestedObject).TryGetValue(propertyName, out JToken element)) + + try + { + // The supplied nested object is either a JObject or the string representation of a JObject. + JObject nestedObjectAsJObject = nestedObject.GetType() == typeof(string) + ? DeserializeToType((string)nestedObject) + : nestedObject as JObject; + + if (nestedObjectAsJObject != null && nestedObjectAsJObject.TryGetValue(propertyName, out JToken element)) + { + outValue = element.ToObject(); + return true; + } + } + catch { - outValue = element.ToObject(); - return true; + // Catch and ignore any exceptions caught } return false; } diff --git a/shared/src/PayloadSerializer.cs b/shared/src/PayloadSerializer.cs index 9fced3233e..15e7ee435f 100644 --- a/shared/src/PayloadSerializer.cs +++ b/shared/src/PayloadSerializer.cs @@ -56,7 +56,8 @@ public abstract class PayloadSerializer /// An example of this would be a property under the component. /// /// The type to convert the retrieved property to. - /// The object that might contain the nested property. + /// The object that might contain the nested property. + /// This needs to be in the json object equivalent format as required by the serializer or the string representation of it. /// The name of the property to be retrieved. /// True if the nested object contains an element with the specified key; otherwise, it returns false. /// diff --git a/supported_platforms.md b/supported_platforms.md index 0944a25974..a963a60020 100644 --- a/supported_platforms.md +++ b/supported_platforms.md @@ -35,7 +35,7 @@ OS name: "windows server 2016", version: "10.0", arch: "amd64", family: "windows ## Ubuntu 1604 -Note that, while we only directly test on Ubuntu 1604, we do generally support other [Linux distributions supported by .NET core](https://docs.microsoft.com/en-us/dotnet/core/install/linux). +Note that, while we only directly test on Ubuntu 1604, we do generally support other [Linux distributions supported by .NET core](https://docs.microsoft.com/dotnet/core/install/linux). Nightly test platform details: