From 90dbc722687c398a4956130c0c5f774fc44500cf Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Wed, 29 Mar 2023 14:13:36 -0700 Subject: [PATCH 1/2] Ensure device client methods throw an object disposed exception --- iothub/device/src/ClientFactory.cs | 1 + iothub/device/src/InternalClient.cs | 21 +- .../src/Pipeline/DefaultDelegatingHandler.cs | 8 +- .../device/tests/DeviceClientDisposeTests.cs | 201 ++++++++++++++++++ iothub/device/tests/DeviceClientTests.cs | 44 ---- 5 files changed, 225 insertions(+), 50 deletions(-) create mode 100644 iothub/device/tests/DeviceClientDisposeTests.cs diff --git a/iothub/device/src/ClientFactory.cs b/iothub/device/src/ClientFactory.cs index 24dea205ee..631c26ba22 100644 --- a/iothub/device/src/ClientFactory.cs +++ b/iothub/device/src/ClientFactory.cs @@ -535,6 +535,7 @@ private static IDeviceClientPipelineBuilder BuildPipeline() { var transporthandlerFactory = new TransportHandlerFactory(); IDeviceClientPipelineBuilder pipelineBuilder = new DeviceClientPipelineBuilder() + .With((ctx, innerHandler) => new DefaultDelegatingHandler(ctx, innerHandler)) .With((ctx, innerHandler) => new RetryDelegatingHandler(ctx, innerHandler)) .With((ctx, innerHandler) => new ErrorDelegatingHandler(ctx, innerHandler)) .With((ctx, innerHandler) => new ProtocolRoutingDelegatingHandler(ctx, innerHandler)) diff --git a/iothub/device/src/InternalClient.cs b/iothub/device/src/InternalClient.cs index 278be06b9b..290f93205b 100644 --- a/iothub/device/src/InternalClient.cs +++ b/iothub/device/src/InternalClient.cs @@ -115,6 +115,7 @@ internal class InternalClient : IDisposable private readonly HttpTransportHandler _fileUploadHttpTransportHandler; private readonly ITransportSettings[] _transportSettings; private readonly ClientOptions _clientOptions; + private volatile bool _isDisposed; // Stores message input names supported by the client module and their associated delegate. private volatile Dictionary> _receiveEventEndpoints; @@ -1378,6 +1379,11 @@ internal Task GetFileUploadSasUriAsync( FileUploadSasUriRequest request, CancellationToken cancellationToken = default) { + if (_isDisposed) + { + throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); + } + return _fileUploadHttpTransportHandler.GetFileUploadSasUriAsync(request, cancellationToken); } @@ -1385,6 +1391,11 @@ internal Task CompleteFileUploadAsync( FileUploadCompletionNotification notification, CancellationToken cancellationToken = default) { + if (_isDisposed) + { + throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); + } + return _fileUploadHttpTransportHandler.CompleteFileUploadAsync(notification, cancellationToken); } @@ -1413,10 +1424,15 @@ public Task UploadToBlobAsync(string blobName, Stream source) [Obsolete("This API has been split into three APIs: GetFileUploadSasUri, uploading to blob directly using the Azure Storage SDK, and CompleteFileUploadAsync")] public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken cancellationToken) { + if (Logging.IsEnabled) + Logging.Enter(this, blobName, source, nameof(UploadToBlobAsync)); + try { - if (Logging.IsEnabled) - Logging.Enter(this, blobName, source, nameof(UploadToBlobAsync)); + if (_isDisposed) + { + throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); + } if (string.IsNullOrEmpty(blobName)) { @@ -1830,6 +1846,7 @@ internal void ValidateModuleTransportHandler(string apiName) public void Dispose() { + _isDisposed = true; InnerHandler?.Dispose(); _methodsSemaphore?.Dispose(); _moduleReceiveMessageSemaphore?.Dispose(); diff --git a/iothub/device/src/Pipeline/DefaultDelegatingHandler.cs b/iothub/device/src/Pipeline/DefaultDelegatingHandler.cs index 269b78dc73..5c37988ee0 100644 --- a/iothub/device/src/Pipeline/DefaultDelegatingHandler.cs +++ b/iothub/device/src/Pipeline/DefaultDelegatingHandler.cs @@ -9,13 +9,13 @@ namespace Microsoft.Azure.Devices.Client.Transport { - internal abstract class DefaultDelegatingHandler : IDelegatingHandler + internal class DefaultDelegatingHandler : IDelegatingHandler { - protected const string ClientDisposedMessage = "The client has been disposed and is no longer usable."; + protected internal const string ClientDisposedMessage = "The client has been disposed and is no longer usable."; protected volatile bool _isDisposed; private volatile IDelegatingHandler _innerHandler; - protected DefaultDelegatingHandler(PipelineContext context, IDelegatingHandler innerHandler) + protected internal DefaultDelegatingHandler(PipelineContext context, IDelegatingHandler innerHandler) { Context = context; _innerHandler = innerHandler; @@ -206,7 +206,7 @@ public virtual void Dispose() GC.SuppressFinalize(this); } - protected void ThrowIfDisposed() + protected internal void ThrowIfDisposed() { if (_isDisposed) { diff --git a/iothub/device/tests/DeviceClientDisposeTests.cs b/iothub/device/tests/DeviceClientDisposeTests.cs new file mode 100644 index 0000000000..fda5f781c1 --- /dev/null +++ b/iothub/device/tests/DeviceClientDisposeTests.cs @@ -0,0 +1,201 @@ +// 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.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Azure.Devices.Client.Transport; +using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Client.Tests +{ + [TestClass] + /// + /// Ensure that any calls to a disposed device client result in an ObjectDisposedException. + /// + public class DeviceClientDisposeTests + { + private static DeviceClient s_client; + private const int DefaultTimeToLiveSeconds = 1 * 60 * 60; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + // Create a disposed device client for the tests in this class + var rndBytes = new byte[32]; + new Random().NextBytes(rndBytes); + string testSharedAccessKey = Convert.ToBase64String(rndBytes); + var csBuilder = IotHubConnectionStringBuilder.Create( + "contoso.azure-devices.net", + new DeviceAuthenticationWithRegistrySymmetricKey("deviceId", testSharedAccessKey)); + s_client = DeviceClient.CreateFromConnectionString(csBuilder.ToString()); + s_client.Dispose(); + } + + [TestMethod] + public async Task DeviceClient_OpenAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.OpenAsync(cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_CloseAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.CloseAsync(cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SetMethodHandlerAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetMethodHandlerAsync( + "methodName", + (request, userContext) => Task.FromResult(new MethodResponse(400)), + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SetMethodDefaultHandlerAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetMethodDefaultHandlerAsync( + (request, userContext) => Task.FromResult(new MethodResponse(400)), + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SetDesiredPropertyUpdateCallbackAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetDesiredPropertyUpdateCallbackAsync( + (desiredProperties, userContext) => TaskHelpers.CompletedTask, + null, + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SetReceiveMessageHandlerAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetReceiveMessageHandlerAsync( + (message, userContext) => TaskHelpers.CompletedTask, + null, + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_ReceiveAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.ReceiveAsync(cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_CompleteAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.CompleteAsync("fakeLockToken", cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_AbandonAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.AbandonAsync("fakeLockToken", cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_RejectAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.RejectAsync("fakeLockToken", cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SendEventAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using var msg = new Message(); + Func op = async () => await s_client.SendEventAsync(msg, cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_SendEventBatchAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using var msg = new Message(); + Func op = async () => await s_client.SendEventBatchAsync(new[] { msg }, cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_GetTwinAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.GetTwinAsync(cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_UpdateReportedPropertiesAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.UpdateReportedPropertiesAsync(new TwinCollection(), cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_UploadToBlobAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using var stream = new MemoryStream(); +#pragma warning disable CS0618 // Type or member is obsolete + Func op = async () => await s_client.UploadToBlobAsync("blobName", stream, cts.Token).ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_GetFileUploadSasUriAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.GetFileUploadSasUriAsync(new FileUploadSasUriRequest(), cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task DeviceClient_CompleteFileUploadAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.CompleteFileUploadAsync(new FileUploadCompletionNotification(), cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + } +} diff --git a/iothub/device/tests/DeviceClientTests.cs b/iothub/device/tests/DeviceClientTests.cs index 34aa626def..d51a288f02 100644 --- a/iothub/device/tests/DeviceClientTests.cs +++ b/iothub/device/tests/DeviceClientTests.cs @@ -154,8 +154,6 @@ public void DeviceClient_CreateFromConnectionString_WithModuleIdThrows() act.Should().Throw(); } - /* Tests_SRS_DEVICECLIENT_28_002: [This property shall be defaulted to 240000 (4 minutes).] */ - [TestMethod] public void DeviceClient_OperationTimeoutInMilliseconds_Property_DefaultValue() { @@ -312,7 +310,6 @@ await innerHandler } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_012: [** If the given methodRequestInternal argument is null, fail silently **]** public async Task DeviceClient_OnMethodCalled_NullMethodRequest() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -353,7 +350,6 @@ await deviceClient.SetMethodHandlerAsync("TestMethodName", (payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_020: [** If the given methodRequestInternal data is not valid json, respond with status code 400 (BAD REQUEST) **]** public async Task DeviceClient_OnMethodCalled_MethodRequestHasInvalidJson() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -374,7 +370,6 @@ await deviceClient.SetMethodHandlerAsync("TestMethodName", (payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_011: [ The OnMethodCalled shall invoke the specified delegate. ] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -395,7 +390,6 @@ await deviceClient.SetMethodHandlerAsync("TestMethodName", (payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_021: [** If the MethodResponse from the MethodHandler is not valid json, respond with status code 500 (USER CODE EXCEPTION) **]** public async Task DeviceClient_OnMethodCalled_MethodResponseHasInvalidJson() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -416,7 +410,6 @@ await deviceClient.SetMethodHandlerAsync("TestMethodName", (payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_012: [** If the given methodRequestInternal argument is null, fail silently **]** public async Task DeviceClient_OnMethodCalled_NullMethodRequest_With_SetMethodHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -461,8 +454,6 @@ public async Task DeviceClient_OnMethodCalled_MethodRequestHasEmptyBody_With_Set } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_011: [ The OnMethodCalled shall invoke the specified delegate. ] - // Tests_SRS_DEVICECLIENT_03_013: [Otherwise, the MethodResponseInternal constructor shall be invoked with the result supplied.] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_SetMethodHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -485,8 +476,6 @@ public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_Set } [TestMethod] - // Tests_SRS_DEVICECLIENT_24_002: [ The OnMethodCalled shall invoke the default delegate if there is no specified delegate for that method. ] - // Tests_SRS_DEVICECLIENT_03_013: [Otherwise, the MethodResponseInternal constructor shall be invoked with the result supplied.] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_SetMethodDefaultHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -507,8 +496,6 @@ await deviceClient.SetMethodDefaultHandlerAsync((payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_24_002: [ The OnMethodCalled shall invoke the default delegate if there is no specified delegate for that method. ] - // Tests_SRS_DEVICECLIENT_03_013: [Otherwise, the MethodResponseInternal constructor shall be invoked with the result supplied.] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_SetMethodHandlerNotMatchedAndDefaultHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -536,8 +523,6 @@ await deviceClient.SetMethodDefaultHandlerAsync((payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_011: [ The OnMethodCalled shall invoke the specified delegate. ] - // Tests_SRS_DEVICECLIENT_03_013: [Otherwise, the MethodResponseInternal constructor shall be invoked with the result supplied.] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_SetMethodHandlerAndDefaultHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -565,8 +550,6 @@ await deviceClient.SetMethodDefaultHandlerAsync((payload, context) => } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_011: [ The OnMethodCalled shall invoke the specified delegate. ] - // Tests_SRS_DEVICECLIENT_03_012: [If the MethodResponse does not contain result, the MethodResponseInternal constructor shall be invoked with no results.] public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_SetMethodHandler_With_No_Result() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -589,7 +572,6 @@ public async Task DeviceClient_OnMethodCalled_MethodRequestHasValidJson_With_Set } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_021: [** If the MethodResponse from the MethodHandler is not valid json, respond with status code 500 (USER CODE EXCEPTION) **]** public async Task DeviceClientOnMethodCalledMethodResponseHasInvalidJsonWithSetMethodHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -612,7 +594,6 @@ public async Task DeviceClientOnMethodCalledMethodResponseHasInvalidJsonWithSetM } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_013: [** If the given method does not have an associated delegate and no default delegate was registered, respond with status code 501 (METHOD NOT IMPLEMENTED) **]** public async Task DeviceClientOnMethodCalledNoMethodHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -627,8 +608,6 @@ public async Task DeviceClientOnMethodCalledNoMethodHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_001: [ It shall lazy-initialize the deviceMethods property. ] - // Tests_SRS_DEVICECLIENT_10_003: [ The given delegate will only be added if it is not null. ] public async Task DeviceClientSetMethodHandlerSetFirstMethodHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -675,8 +654,6 @@ public async Task DeviceClientSetMethodHandlerSetFirstMethodHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_001: [ It shall lazy-initialize the deviceMethods property. ] - // Tests_SRS_DEVICECLIENT_10_003: [ The given delegate will only be added if it is not null. ] public async Task DeviceClientSetMethodHandlerSetFirstMethodDefaultHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -723,7 +700,6 @@ public async Task DeviceClientSetMethodHandlerSetFirstMethodDefaultHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_002: [ If the given methodName already has an associated delegate, the existing delegate shall be removed. ] public async Task DeviceClientSetMethodHandlerOverwriteExistingDelegate() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -783,7 +759,6 @@ public async Task DeviceClientSetMethodHandlerOverwriteExistingDelegate() } [TestMethod] - // Tests_SRS_DEVICECLIENT_24_001: [ If the default callback has already been set, it is replaced with the new callback. ] public async Task DeviceClientSetMethodHandlerOverwriteExistingDefaultDelegate() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -843,8 +818,6 @@ public async Task DeviceClientSetMethodHandlerOverwriteExistingDefaultDelegate() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_004: [ The deviceMethods property shall be deleted if the last delegate has been removed. ] - // Tests_SRS_DEVICECLIENT_10_006: [ It shall DisableMethodsAsync when the last delegate has been removed. ] public async Task DeviceClientSetMethodHandlerUnsetLastMethodHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -887,8 +860,6 @@ public async Task DeviceClientSetMethodHandlerUnsetLastMethodHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_004: [ The deviceMethods property shall be deleted if the last delegate has been removed. ] - // Tests_SRS_DEVICECLIENT_10_006: [ It shall DisableMethodsAsync when the last delegate has been removed. ] public async Task DeviceClientSetMethodHandlerUnsetLastMethodHandlerWithDefaultHandlerSet() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -948,8 +919,6 @@ public async Task DeviceClientSetMethodHandlerUnsetLastMethodHandlerWithDefaultH } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_004: [ The deviceMethods property shall be deleted if the last delegate has been removed. ] - // Tests_SRS_DEVICECLIENT_10_006: [ It shall DisableMethodsAsync when the last delegate has been removed. ] public async Task DeviceClientSetMethodHandlerUnsetDefaultHandlerSet() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -1022,8 +991,6 @@ public async Task DeviceClientSetMethodHandlerUnsetWhenNoMethodHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_001: [ It shall lazy-initialize the deviceMethods property. ] - // Tests_SRS_DEVICECLIENT_10_003: [ The given delegate will only be added if it is not null. ] public async Task DeviceClientSetMethodHandlerSetFirstMethodHandlerWithSetMethodHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -1061,7 +1028,6 @@ public async Task DeviceClientSetMethodHandlerSetFirstMethodHandlerWithSetMethod } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_002: [ If the given methodName already has an associated delegate, the existing delegate shall be removed. ] public async Task DeviceClientSetMethodHandlerOverwriteExistingDelegateWithSetMethodHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -1123,8 +1089,6 @@ public async Task DeviceClientSetMethodHandlerOverwriteExistingDelegateWithSetMe } [TestMethod] - // Tests_SRS_DEVICECLIENT_10_004: [ The deviceMethods property shall be deleted if the last delegate has been removed. ] - // Tests_SRS_DEVICECLIENT_10_006: [ It shall DisableMethodsAsync when the last delegate has been removed. ] public async Task DeviceClientSetMethodHandlerUnsetLastMethodHandlerWithSetMethodHandler() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;DeviceId=dumpy;SharedAccessKey=dGVzdFN0cmluZzE="; @@ -1184,9 +1148,6 @@ public async Task DeviceClientSetMethodHandlerUnsetWhenNoMethodHandlerWithSetMet } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_024: [** `OnConnectionOpened` shall invoke the connectionStatusChangesHandler if ConnectionStatus is changed **]** - // Tests_SRS_DEVICECLIENT_28_025: [** `SetConnectionStatusChangesHandler` shall set connectionStatusChangesHandler **]** - // Tests_SRS_DEVICECLIENT_28_026: [** `SetConnectionStatusChangesHandler` shall unset connectionStatusChangesHandler if `statusChangesHandler` is null **]** public void DeviceClientOnConnectionOpenedInvokeHandlerForStatusChange() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -1210,7 +1171,6 @@ public void DeviceClientOnConnectionOpenedInvokeHandlerForStatusChange() } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_026: [** `SetConnectionStatusChangesHandler` shall unset connectionStatusChangesHandler if `statusChangesHandler` is null **]** public void DeviceClientOnConnectionOpenedWithNullHandler() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -1233,7 +1193,6 @@ public void DeviceClientOnConnectionOpenedWithNullHandler() } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_024: [** `OnConnectionOpened` shall invoke the connectionStatusChangesHandler if ConnectionStatus is changed **]** public void DeviceClientOnConnectionOpenedNotInvokeHandlerWithoutStatusChange() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -1263,8 +1222,6 @@ public void DeviceClientOnConnectionOpenedNotInvokeHandlerWithoutStatusChange() } [TestMethod] - // Tests_SRS_DEVICECLIENT_28_022: [** `OnConnectionClosed` shall invoke the RecoverConnections process. **]** - // Tests_SRS_DEVICECLIENT_28_023: [** `OnConnectionClosed` shall invoke the connectionStatusChangesHandler if ConnectionStatus is changed. **]** public void DeviceClientOnConnectionClosedInvokeHandlerAndRecoveryForStatusChange() { using var deviceClient = DeviceClient.CreateFromConnectionString(FakeConnectionString); @@ -2126,7 +2083,6 @@ public void DeviceClient_SetDesiredPropertyCallbackAsync_Cancelled_MaintainLegac act.Should().Throw(); } - private class TestDeviceAuthenticationWithTokenRefresh : DeviceAuthenticationWithTokenRefresh { // This authentication method relies on the default sas token time to live and renewal buffer set by the SDK. From 75721bd47db78f6ef5eacb1d09b7e441bc8351c3 Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Wed, 29 Mar 2023 17:50:12 -0700 Subject: [PATCH 2/2] Fix up docs --- iothub/device/src/DeviceClient.cs | 112 ++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 35 deletions(-) diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index ef9d301400..f390c22c7e 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -294,18 +294,21 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// Explicitly open the DeviceClient instance. /// + /// When the client has been disposed. public Task OpenAsync() => InternalClient.OpenAsync(); /// /// Explicitly open the DeviceClient instance. + /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. - /// + /// When the client has been disposed. public Task OpenAsync(CancellationToken cancellationToken) => InternalClient.OpenAsync(cancellationToken); /// /// Close the DeviceClient instance. /// + /// When the client has been disposed. public Task CloseAsync() => InternalClient.CloseAsync(); /// @@ -313,6 +316,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. public Task CloseAsync(CancellationToken cancellationToken) => InternalClient.CloseAsync(cancellationToken); /// @@ -320,10 +324,9 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// After handling a received message, a client should call , /// , or , and then dispose the message. /// - /// - /// . - /// - /// The receive message or null if there was no message until the default timeout + /// . + /// The receive message or null if there was no message until the default timeout. + /// When the client has been disposed. public Task ReceiveAsync() => InternalClient.ReceiveAsync(); /// @@ -333,12 +336,13 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// and then dispose the message. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. The inner exception will be /// . + /// When the client has been disposed. /// The received message or null if there was no message until cancellation token has expired public Task ReceiveAsync(CancellationToken cancellationToken) => InternalClient.ReceiveAsync(cancellationToken); @@ -349,22 +353,26 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// and then dispose the message. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// + /// When the client has been disposed. /// The received message or null if there was no message until the specified time has elapsed. public Task ReceiveAsync(TimeSpan timeout) => InternalClient.ReceiveAsync(timeout); /// /// Sets a new delegate for receiving a message from the device queue using a cancellation token. + /// + /// /// After handling a received message, a client should call , /// , or , /// and then dispose the message. /// If a null delegate is passed, it will disable the callback triggered on receiving messages from the service. + /// /// The delegate to be used when a could to device message is received by the client. /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. - /// + /// When the client has been disposed. public Task SetReceiveMessageHandlerAsync( ReceiveMessageCallback messageHandler, object userContext, @@ -375,6 +383,7 @@ public Task SetReceiveMessageHandlerAsync( /// Deletes a received message from the device queue. /// /// The message lockToken. + /// When the client has been disposed. public Task CompleteAsync(string lockToken) => InternalClient.CompleteAsync(lockToken); /// @@ -384,6 +393,7 @@ public Task SetReceiveMessageHandlerAsync( /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task CompleteAsync(string lockToken, CancellationToken cancellationToken) => InternalClient.CompleteAsync(lockToken, cancellationToken); @@ -391,6 +401,7 @@ public Task CompleteAsync(string lockToken, CancellationToken cancellationToken) /// Deletes a received message from the device queue. /// /// The message. + /// When the client has been disposed. public Task CompleteAsync(Message message) => InternalClient.CompleteAsync(message); /// @@ -400,6 +411,7 @@ public Task CompleteAsync(string lockToken, CancellationToken cancellationToken) /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task CompleteAsync(Message message, CancellationToken cancellationToken) => InternalClient.CompleteAsync(message, cancellationToken); @@ -407,23 +419,25 @@ public Task CompleteAsync(Message message, CancellationToken cancellationToken) /// Puts a received message back onto the device queue. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message lockToken. + /// When the client has been disposed. public Task AbandonAsync(string lockToken) => InternalClient.AbandonAsync(lockToken); /// /// Puts a received message back onto the device queue. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message lockToken. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task AbandonAsync(string lockToken, CancellationToken cancellationToken) => InternalClient.AbandonAsync(lockToken, cancellationToken); @@ -431,23 +445,25 @@ public Task AbandonAsync(string lockToken, CancellationToken cancellationToken) /// Puts a received message back onto the device queue. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message to abandon. + /// When the client has been disposed. public Task AbandonAsync(Message message) => InternalClient.AbandonAsync(message); /// /// Puts a received message back onto the device queue. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message to abandon. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task AbandonAsync(Message message, CancellationToken cancellationToken) => InternalClient.AbandonAsync(message, cancellationToken); @@ -455,23 +471,25 @@ public Task AbandonAsync(Message message, CancellationToken cancellationToken) = /// Deletes a received message from the device queue and indicates to the server that the message could not be processed. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message lockToken. + /// When the client has been disposed. public Task RejectAsync(string lockToken) => InternalClient.RejectAsync(lockToken); /// /// Deletes a received message from the device queue and indicates to the server that the message could not be processed. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// A cancellation token to cancel the operation. /// The message lockToken. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task RejectAsync(string lockToken, CancellationToken cancellationToken) => InternalClient.RejectAsync(lockToken, cancellationToken); @@ -479,29 +497,36 @@ public Task RejectAsync(string lockToken, CancellationToken cancellationToken) = /// Deletes a received message from the device queue and indicates to the server that the message could not be processed. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message. + /// When the client has been disposed. public Task RejectAsync(Message message) => InternalClient.RejectAsync(message); /// /// Deletes a received message from the device queue and indicates to the server that the message could not be processed. /// /// - /// You cannot reject or abandon messages over MQTT protocol. For more details, see + /// The client cannot reject or abandon messages over MQTT protocol. For more details, see /// . /// /// The message to reject. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. /// The inner exception will be . + /// When the client has been disposed. public Task RejectAsync(Message message, CancellationToken cancellationToken) => InternalClient.RejectAsync(message, cancellationToken); /// /// Sends an event to a hub /// + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error + /// details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The message to send. Should be disposed after sending. /// Thrown when a required parameter is null. /// Thrown if the service does not respond to the request within the timeout @@ -518,16 +543,17 @@ public Task RejectAsync(Message message, CancellationToken cancellationToken) => /// Thrown if an error occurs when communicating with IoT hub service. /// If is set to true then it is a transient exception. /// If is set to false then it is a non-transient exception. - /// - /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error - /// details and take steps accordingly. - /// Please note that the list of exceptions is not exhaustive. - /// + /// When the client has been disposed. public Task SendEventAsync(Message message) => InternalClient.SendEventAsync(message); /// /// Sends an event to a hub /// + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect + /// the error details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The message to send. Should be disposed after sending. /// A cancellation token to cancel the operation. /// Thrown when a required parameter is null. @@ -545,11 +571,7 @@ public Task RejectAsync(Message message, CancellationToken cancellationToken) => /// Thrown if an error occurs when communicating with IoT hub service. /// If is set to true then it is a transient exception. /// If is set to false then it is a non-transient exception. - /// - /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect - /// the error details and take steps accordingly. - /// Please note that the list of exceptions is not exhaustive. - /// + /// When the client has been disposed. public Task SendEventAsync(Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(message, cancellationToken); @@ -558,6 +580,7 @@ public Task SendEventAsync(Message message, CancellationToken cancellationToken) /// one after the other. /// /// A list of one or more messages to send. The messages should be disposed after sending. + /// When the client has been disposed. public Task SendEventBatchAsync(IEnumerable messages) => InternalClient.SendEventBatchAsync(messages); /// @@ -568,6 +591,7 @@ public Task SendEventAsync(Message message, CancellationToken cancellationToken) /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. The inner exception will be /// . + /// When the client has been disposed. public Task SendEventBatchAsync(IEnumerable messages, CancellationToken cancellationToken) => InternalClient.SendEventBatchAsync(messages, cancellationToken); @@ -587,6 +611,7 @@ public Task SendEventBatchAsync(IEnumerable messages, CancellationToken /// /// The name of the blob to upload. /// A stream with blob contents. Should be disposed after upload completes. + /// When the client has been disposed. [Obsolete("This API has been split into three APIs: GetFileUploadSasUri, uploading to blob directly using the Azure Storage SDK, and CompleteFileUploadAsync")] public Task UploadToBlobAsync(string blobName, Stream source) => InternalClient.UploadToBlobAsync(blobName, source); @@ -608,6 +633,7 @@ public Task SendEventBatchAsync(IEnumerable messages, CancellationToken /// A stream with blob contents.. Should be disposed after upload completes. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. [Obsolete("This API has been split into three APIs: GetFileUploadSasUri, uploading to blob directly using the Azure Storage SDK, and CompleteFileUploadAsync")] public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken cancellationToken) => InternalClient.UploadToBlobAsync(blobName, source, cancellationToken); @@ -618,6 +644,7 @@ public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken /// /// The request details for getting the SAS URI, including the destination blob name. /// The cancellation token. + /// When the client has been disposed. /// The file upload details to be used with the Azure Storage SDK in order to upload a file from this device. public Task GetFileUploadSasUriAsync( FileUploadSasUriRequest request, @@ -630,17 +657,21 @@ public Task GetFileUploadSasUriAsync( /// /// The notification details, including if the file upload succeeded. /// The cancellation token. + /// When the client has been disposed. public Task CompleteFileUploadAsync(FileUploadCompletionNotification notification, CancellationToken cancellationToken = default) => InternalClient.CompleteFileUploadAsync(notification, cancellationToken); /// /// Sets a new delegate for the named method. If a delegate is already associated with /// the named method, it will be replaced with the new delegate. + /// + /// /// A method handler can be unset by passing a null MethodCallback. + /// /// The name of the method to associate with the delegate. /// The delegate to be used when a method with the given name is called by the cloud service. /// generic parameter to be interpreted by the client code. - /// + /// When the client has been disposed. public Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext) => InternalClient.SetMethodHandlerAsync(methodName, methodHandler, userContext); @@ -648,13 +679,13 @@ public Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandle /// Sets a new delegate for the named method. If a delegate is already associated with /// the named method, it will be replaced with the new delegate. /// A method handler can be unset by passing a null MethodCallback. + /// /// The name of the method to associate with the delegate. /// The delegate to be used when a method with the given name is called by the cloud service. /// generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. - /// Thrown when the operation has been canceled. - /// + /// When the client has been disposed. public Task SetMethodHandlerAsync( string methodName, MethodCallback methodHandler, @@ -670,6 +701,7 @@ public Task SetMethodHandlerAsync( /// The delegate to be used when a method is called by the cloud service and there is /// no delegate registered for that method name. /// Generic parameter to be interpreted by the client code. + /// When the client has been disposed. public Task SetMethodDefaultHandlerAsync(MethodCallback methodHandler, object userContext) => InternalClient.SetMethodDefaultHandlerAsync(methodHandler, userContext); @@ -683,28 +715,32 @@ public Task SetMethodDefaultHandlerAsync(MethodCallback methodHandler, object us /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. - /// Thrown when the operation has been canceled. + /// When the client has been disposed. public Task SetMethodDefaultHandlerAsync(MethodCallback methodHandler, object userContext, CancellationToken cancellationToken) => InternalClient.SetMethodDefaultHandlerAsync(methodHandler, userContext, cancellationToken); /// /// Sets a new delegate for the named method. If a delegate is already associated with /// the named method, it will be replaced with the new delegate. + /// /// The name of the method to associate with the delegate. /// The delegate to be used when a method with the given name is called by the cloud service. /// generic parameter to be interpreted by the client code. - /// - + /// When the client has been disposed. [Obsolete("Please use SetMethodHandlerAsync.")] public void SetMethodHandler(string methodName, MethodCallback methodHandler, object userContext) => InternalClient.SetMethodHandler(methodName, methodHandler, userContext); /// - /// Sets a new delegate for the connection status changed callback. If a delegate is already associated, + /// Sets a new delegate for the connection status changed callback. + /// + /// + /// If a delegate is already associated, /// it will be replaced with the new delegate. Note that this callback will never be called if the client is configured to use /// HTTP, as that protocol is stateless. + /// /// The name of the method to associate with the delegate. - /// + /// When the client has been disposed. public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler statusChangesHandler) => InternalClient.SetConnectionStatusChangesHandler(statusChangesHandler); @@ -776,6 +812,7 @@ protected virtual void Dispose(bool disposing) /// /// Callback to call after the state update has been received and applied /// Context object that will be passed into callback + /// When the client has been disposed. [Obsolete("Please use SetDesiredPropertyUpdateCallbackAsync.")] public Task SetDesiredPropertyUpdateCallback(DesiredPropertyUpdateCallback callback, object userContext) => InternalClient.SetDesiredPropertyUpdateCallback(callback, userContext); @@ -790,6 +827,7 @@ public Task SetDesiredPropertyUpdateCallback(DesiredPropertyUpdateCallback callb /// /// Callback to call after the state update has been received and applied /// Context object that will be passed into callback + /// When the client has been disposed. public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback callback, object userContext) => InternalClient.SetDesiredPropertyUpdateCallbackAsync(callback, userContext); @@ -806,7 +844,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback /// A cancellation token to cancel the operation. /// TODO:azabbasi /// Thrown when the operation has been canceled. - /// Thrown when the operation has been canceled. + /// When the client has been disposed. public Task SetDesiredPropertyUpdateCallbackAsync( DesiredPropertyUpdateCallback callback, object userContext, @@ -817,6 +855,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync( /// Retrieve the device twin properties for the current device. /// For the complete device twin object, use Microsoft.Azure.Devices.RegistryManager.GetTwinAsync(string deviceId). /// + /// When the client has been disposed. /// The device twin object for the current device public Task GetTwinAsync() => InternalClient.GetTwinAsync(); @@ -827,6 +866,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync( /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. The inner exception will be /// . + /// When the client has been disposed. /// The device twin object for the current device public Task GetTwinAsync(CancellationToken cancellationToken) => InternalClient.GetTwinAsync(cancellationToken); @@ -834,6 +874,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync( /// Push reported property changes up to the service. /// /// Reported properties to push + /// When the client has been disposed. public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties) => InternalClient.UpdateReportedPropertiesAsync(reportedProperties); @@ -844,6 +885,7 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties) => /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. The inner exception will be /// . + /// When the client has been disposed. public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) => InternalClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); }