Skip to content

Commit

Permalink
feat(iot-device): Add convenience method for acknowledging writable p…
Browse files Browse the repository at this point in the history
…roperty update requests
  • Loading branch information
abhipsaMisra authored Aug 31, 2021
1 parent 47c2621 commit fc774ff
Show file tree
Hide file tree
Showing 30 changed files with 624 additions and 123 deletions.
6 changes: 3 additions & 3 deletions configure_tls_protocol_version_and_ciphers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
112 changes: 112 additions & 0 deletions e2e/test/iothub/properties/PropertiesE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -305,6 +341,82 @@ await Task
await deviceClient.CloseAsync().ConfigureAwait(false);
}

private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync<T>(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<string, object> 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<T>(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<T>(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();
Expand Down
118 changes: 118 additions & 0 deletions e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -312,6 +348,88 @@ await Task
await deviceClient.CloseAsync().ConfigureAwait(false);
}

private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync<T>(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<string, object> writableProperty in writableProperties)
{
if (writableProperty.Value is IDictionary<string, object> componentProperties)
{
foreach (KeyValuePair<string, object> 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<T>(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<T>(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();
Expand Down
15 changes: 10 additions & 5 deletions iothub/device/devdoc/Convention-based operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public abstract class PayloadCollection : IEnumerable, IEnumerable<KeyValuePair<
public IEnumerator<KeyValuePair<string, object>> GetEnumerator();
public virtual byte[] GetPayloadObjectBytes();
public virtual string GetSerializedString();
protected void SetCollection(PayloadCollection payloadCollection);
IEnumerator System.Collections.IEnumerable.GetEnumerator();
public bool TryGetValue<T>(string key, out T value);
}
Expand Down Expand Up @@ -127,15 +126,16 @@ public Task<ClientPropertiesUpdateResponse> UpdateClientPropertiesAsync(ClientPr
/// <param name="callback">The global call back to handle all writable property updates.</param>
/// <param name="userContext">Generic parameter to be interpreted by the client code.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
public Task SubscribeToWritablePropertiesEventAsync(Func<ClientPropertyCollection, object, Task> callback, object userContext, CancellationToken cancellationToken = default);
public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func<ClientPropertyCollection, object, Task> 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 {
Expand All @@ -153,6 +153,12 @@ public class ClientPropertyCollection : PayloadCollection {
public virtual bool TryGetValue<T>(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; }
Expand All @@ -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; }
}
Expand Down
4 changes: 4 additions & 0 deletions iothub/device/src/ClientPropertiesUpdateResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ namespace Microsoft.Azure.Devices.Client
/// </summary>
public class ClientPropertiesUpdateResponse
{
internal ClientPropertiesUpdateResponse()
{
}

/// <summary>
/// The request Id that is associated with the <see cref="InternalClient.UpdateClientPropertiesAsync(ClientPropertyCollection, System.Threading.CancellationToken)"/> operation.
/// </summary>
Expand Down
Loading

0 comments on commit fc774ff

Please sign in to comment.