From f57411a176c53efbfb59634592f2c7a0e58fad87 Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Fri, 31 Mar 2023 13:32:42 -0700 Subject: [PATCH] Docs and tests for disposed module client operations (#3216) --- .../src/ClientSettings/ITransportSettings.cs | 1 - iothub/device/src/DeviceClient.cs | 42 ++-- iothub/device/src/InternalClient.cs | 11 +- iothub/device/src/ModuleClient.cs | 136 ++++++++---- .../device/tests/ModuleClientDisposeTests.cs | 205 ++++++++++++++++++ 5 files changed, 320 insertions(+), 75 deletions(-) create mode 100644 iothub/device/tests/ModuleClientDisposeTests.cs diff --git a/iothub/device/src/ClientSettings/ITransportSettings.cs b/iothub/device/src/ClientSettings/ITransportSettings.cs index 188d3b0474..20be7e61ca 100644 --- a/iothub/device/src/ClientSettings/ITransportSettings.cs +++ b/iothub/device/src/ClientSettings/ITransportSettings.cs @@ -3,7 +3,6 @@ using System; using System.Net; -using Microsoft.Azure.Devices.Shared; namespace Microsoft.Azure.Devices.Client { diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index f390c22c7e..550cccd5ab 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -49,7 +49,7 @@ private DeviceClient(InternalClient internalClient) /// The fully-qualified DNS host name of IoT hub /// The authentication method that is used /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create(string hostname, IAuthenticationMethod authenticationMethod, ClientOptions options = default) { return Create(() => ClientFactory.Create(hostname, authenticationMethod, options)); @@ -62,7 +62,7 @@ public static DeviceClient Create(string hostname, IAuthenticationMethod authent /// The fully-qualified DNS host name of Gateway /// The authentication method that is used /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create( string hostname, string gatewayHostname, @@ -79,7 +79,7 @@ public static DeviceClient Create( /// The authentication method that is used /// The transportType used (HTTP1, AMQP or MQTT), /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create( string hostname, IAuthenticationMethod authenticationMethod, @@ -97,7 +97,7 @@ public static DeviceClient Create( /// The authentication method that is used /// The transportType used (Http1, AMQP or MQTT), /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create( string hostname, string gatewayHostname, @@ -115,7 +115,7 @@ public static DeviceClient Create( /// The authentication method that is used /// Prioritized list of transportTypes and their settings /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create(string hostname, IAuthenticationMethod authenticationMethod, ITransportSettings[] transportSettings, ClientOptions options = default) { @@ -130,7 +130,7 @@ public static DeviceClient Create(string hostname, IAuthenticationMethod authent /// The authentication method that is used /// Prioritized list of transportTypes and their settings /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient Create(string hostname, string gatewayHostname, IAuthenticationMethod authenticationMethod, ITransportSettings[] transportSettings, ClientOptions options = default) { @@ -142,7 +142,7 @@ public static DeviceClient Create(string hostname, string gatewayHostname, IAuth /// /// Connection string for the IoT hub (including DeviceId) /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString(string connectionString, ClientOptions options = default) { return Create(() => ClientFactory.CreateFromConnectionString(connectionString, options)); @@ -154,7 +154,7 @@ public static DeviceClient CreateFromConnectionString(string connectionString, C /// IoT hub-Scope Connection string for the IoT hub (without DeviceId) /// Id of the Device /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString(string connectionString, string deviceId, ClientOptions options = default) { return Create(() => ClientFactory.CreateFromConnectionString(connectionString, deviceId, options)); @@ -166,7 +166,7 @@ public static DeviceClient CreateFromConnectionString(string connectionString, s /// Connection string for the IoT hub (including DeviceId) /// Specifies whether Http1, AMQP or MQTT transport is used, /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString( string connectionString, TransportType transportType, @@ -182,7 +182,7 @@ public static DeviceClient CreateFromConnectionString( /// Id of the device /// The transportType used (Http1, AMQP or MQTT), /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString( string connectionString, string deviceId, @@ -198,7 +198,7 @@ public static DeviceClient CreateFromConnectionString( /// Connection string for the IoT hub (with DeviceId) /// Prioritized list of transports and their settings /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString( string connectionString, ITransportSettings[] transportSettings, @@ -214,7 +214,7 @@ public static DeviceClient CreateFromConnectionString( /// Id of the device /// Prioritized list of transportTypes and their settings /// The options that allow configuration of the device client instance during initialization. - /// A disposable DeviceClient instance + /// A disposable DeviceClient instance. public static DeviceClient CreateFromConnectionString( string connectionString, string deviceId, @@ -283,9 +283,7 @@ public RetryPolicyType RetryPolicy /// The change will take effect after any in-progress operations. /// /// The retry policy. The default is - /// new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); - // Codes_SRS_DEVICECLIENT_28_001: [This property shall be defaulted to the exponential retry strategy with back-off - // parameters for calculating delay in between retries.] + /// new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); public void SetRetryPolicy(IRetryPolicy retryPolicy) { InternalClient.SetRetryPolicy(retryPolicy); @@ -735,12 +733,10 @@ public void SetMethodHandler(string methodName, MethodCallback methodHandler, ob /// 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. + /// 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); @@ -766,11 +762,11 @@ public void Dispose() /// Includes a call to . /// /// - /// + /// /// await using var client = DeviceClient.CreateFromConnectionString(...); - /// + /// /// or - /// + /// /// var client = DeviceClient.CreateFromConnectionString(...); /// try /// { @@ -780,7 +776,7 @@ public void Dispose() /// { /// await client.DisposeAsync(); /// } - /// + /// /// [SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "SuppressFinalize is called by Dispose(), which this method calls.")] public async ValueTask DisposeAsync() diff --git a/iothub/device/src/InternalClient.cs b/iothub/device/src/InternalClient.cs index 290f93205b..25a7fabd71 100644 --- a/iothub/device/src/InternalClient.cs +++ b/iothub/device/src/InternalClient.cs @@ -115,7 +115,6 @@ 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; @@ -200,6 +199,7 @@ public InternalClient( if (Logging.IsEnabled) Logging.Exit(this, transportSettings, pipelineBuilder, nameof(InternalClient) + "_ctor"); } + internal bool IsDisposed { get; private set; } /// /// Diagnostic sampling percentage value, [0-100]; @@ -1120,7 +1120,6 @@ public async Task GetTwinAsync() /// The device twin object for the current device public Task GetTwinAsync(CancellationToken cancellationToken) { - // `GetTwinAsync` shall call `SendTwinGetAsync` on the transport to get the twin state. try { return InnerHandler.SendTwinGetAsync(cancellationToken); @@ -1379,7 +1378,7 @@ internal Task GetFileUploadSasUriAsync( FileUploadSasUriRequest request, CancellationToken cancellationToken = default) { - if (_isDisposed) + if (IsDisposed) { throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); } @@ -1391,7 +1390,7 @@ internal Task CompleteFileUploadAsync( FileUploadCompletionNotification notification, CancellationToken cancellationToken = default) { - if (_isDisposed) + if (IsDisposed) { throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); } @@ -1429,7 +1428,7 @@ public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken try { - if (_isDisposed) + if (IsDisposed) { throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); } @@ -1846,7 +1845,7 @@ internal void ValidateModuleTransportHandler(string apiName) public void Dispose() { - _isDisposed = true; + IsDisposed = true; InnerHandler?.Dispose(); _methodsSemaphore?.Dispose(); _moduleReceiveMessageSemaphore?.Dispose(); diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index 0b9e446b24..be97cced23 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -34,7 +34,7 @@ public class ModuleClient : IDisposable { private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; - private bool _isAnEdgeModule; + private readonly bool _isAnEdgeModule; private readonly ICertificateValidator _certValidator; internal InternalClient InternalClient { get; private set; } @@ -268,7 +268,7 @@ public string ProductInfo /// /// /// The default is: - /// new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); + /// new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100)); /// /// The retry policy. public void SetRetryPolicy(IRetryPolicy retryPolicy) @@ -283,14 +283,16 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// Explicitly open the ModuleClient 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 ModuleClient instance. /// + /// When the client has been disposed. public Task CloseAsync() => InternalClient.CloseAsync(); /// @@ -298,6 +300,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); /// @@ -305,6 +308,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// The message lockToken. /// The lock identifier for the previously received message + /// When the client has been disposed. public Task CompleteAsync(string lockToken) => InternalClient.CompleteAsync(lockToken); /// @@ -313,6 +317,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// The message lockToken. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The lock identifier for the previously received message public Task CompleteAsync(string lockToken, CancellationToken cancellationToken) => InternalClient.CompleteAsync(lockToken, cancellationToken); @@ -320,6 +325,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// Deletes a received message from the module queue. /// /// The message. + /// When the client has been disposed. /// The previously received message public Task CompleteAsync(Message message) => InternalClient.CompleteAsync(message); @@ -329,6 +335,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// A cancellation token to cancel the operation. /// The message. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The previously received message public Task CompleteAsync(Message message, CancellationToken cancellationToken) => InternalClient.CompleteAsync(message, cancellationToken); @@ -336,6 +343,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// Puts a received message back onto the module queue. /// /// The message lockToken. + /// When the client has been disposed. /// The previously received message public Task AbandonAsync(string lockToken) => InternalClient.AbandonAsync(lockToken); @@ -345,12 +353,14 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// The message lockToken. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The previously received message public Task AbandonAsync(string lockToken, CancellationToken cancellationToken) => InternalClient.AbandonAsync(lockToken, cancellationToken); /// /// Puts a received message back onto the module queue. /// + /// When the client has been disposed. /// The lock identifier for the previously received message public Task AbandonAsync(Message message) => InternalClient.AbandonAsync(message); @@ -360,12 +370,17 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// The message. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The lock identifier for the previously received message public Task AbandonAsync(Message message, CancellationToken cancellationToken) => InternalClient.AbandonAsync(message, cancellationToken); /// /// Sends an event to IoT 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. /// Thrown when a required parameter is null. /// Thrown if the service does not respond to the request within the timeout specified for the operation. @@ -379,16 +394,17 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// 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. /// The message containing the event public Task SendEventAsync(Message message) => InternalClient.SendEventAsync(message); /// /// Sends an event to IoT 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. /// A cancellation token to cancel the operation. /// Thrown when a required parameter is null. @@ -403,10 +419,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// 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. /// The message containing the event public Task SendEventAsync(Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(message, cancellationToken); @@ -415,6 +428,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// For more information on IoT Edge module routing . /// /// The messages. + /// When the client has been disposed. /// The task containing the event public Task SendEventBatchAsync(IEnumerable messages) => InternalClient.SendEventBatchAsync(messages); @@ -425,38 +439,47 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// An IEnumerable set of Message objects. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SendEventBatchAsync(IEnumerable messages, CancellationToken cancellationToken) => InternalClient.SendEventBatchAsync(messages, 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. + /// 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); /// /// 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. - /// + /// When the client has been disposed. public Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext, CancellationToken cancellationToken) => InternalClient.SetMethodHandlerAsync(methodName, methodHandler, userContext, cancellationToken); /// /// Sets a new delegate that is called for a method that doesn't have a delegate registered for its name. + /// + /// /// If a default delegate is already registered it will replace with the new delegate. /// A method handler can be unset by passing a null MethodCallback. - /// + /// /// 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); @@ -469,14 +492,15 @@ 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. + /// 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 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. /// + /// The name of the method to associate with the delegate. public void SetConnectionStatusChangesHandler(ConnectionStatusChangesHandler statusChangesHandler) => InternalClient.SetConnectionStatusChangesHandler(statusChangesHandler); @@ -502,11 +526,11 @@ public void Dispose() /// Includes a call to . /// /// - /// + /// /// await using var client = ModuleClient.CreateFromConnectionString(...); - /// + /// /// or - /// + /// /// var client = ModuleClient.CreateFromConnectionString(...); /// try /// { @@ -516,7 +540,7 @@ public void Dispose() /// { /// await client.DisposeAsync(); /// } - /// + /// /// [SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "SuppressFinalize is called by Dispose(), which this method calls.")] public async ValueTask DisposeAsync() @@ -538,20 +562,20 @@ protected virtual void Dispose(bool disposing) if (disposing) { InternalClient?.Dispose(); - InternalClient = null; } } /// /// Set a callback that will be called whenever the client receives a state update /// (desired or reported) from the service. - /// Set callback value to null to clear. /// /// /// This has the side-effect of subscribing to the PATCH topic on the service. + /// Set callback value to null to clear. /// /// 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); @@ -567,6 +591,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback /// Context object that will be passed into callback. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback callback, object userContext, CancellationToken cancellationToken) => InternalClient.SetDesiredPropertyUpdateCallbackAsync(callback, userContext, cancellationToken); @@ -574,6 +599,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback /// Retrieve a module twin object for the current module. /// /// The module twin object for the current module + /// When the client has been disposed. public Task GetTwinAsync() => InternalClient.GetTwinAsync(); /// @@ -581,6 +607,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The module twin object for the current module public Task GetTwinAsync(CancellationToken cancellationToken) => InternalClient.GetTwinAsync(cancellationToken); @@ -588,6 +615,7 @@ public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback /// 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); @@ -597,6 +625,7 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties) => /// Reported properties to push. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) => InternalClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken); @@ -607,6 +636,10 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, Can /// /// Sends an event to IoT 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 above list is not exhaustive. + /// /// The output target for sending the given message. /// The message to send. /// Thrown when a required parameter is null. @@ -621,10 +654,7 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, Can /// 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 above list is not exhaustive. - /// + /// When the client has been disposed. /// The message containing the event public Task SendEventAsync(string outputName, Message message) => InternalClient.SendEventAsync(outputName, message); @@ -632,6 +662,10 @@ public Task SendEventAsync(string outputName, Message message) => /// /// Sends an event to IoT 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 above list is not exhaustive. + /// /// The output target for sending the given message. /// The message to send. /// A cancellation token to cancel the operation. @@ -647,21 +681,22 @@ public Task SendEventAsync(string outputName, Message message) => /// 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 above list is not exhaustive. - /// + /// When the client has been disposed. /// The message containing the event public Task SendEventAsync(string outputName, Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(outputName, 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 + /// 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 as it does not support true batching. + /// 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. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SendEventBatchAsync(string outputName, IEnumerable messages) => InternalClient.SendEventBatchAsync(outputName, messages); @@ -674,6 +709,7 @@ public Task SendEventBatchAsync(string outputName, IEnumerable messages /// A list of one or more messages to send. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SendEventBatchAsync(string outputName, IEnumerable messages, CancellationToken cancellationToken) => InternalClient.SendEventBatchAsync(outputName, messages, cancellationToken); @@ -686,6 +722,7 @@ public Task SendEventBatchAsync(string outputName, IEnumerable messages /// The delegate to be used when a message is sent to the particular inputName. /// generic parameter to be interpreted by the client code. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext) => InternalClient.SetInputMessageHandlerAsync(inputName, messageHandler, userContext, _isAnEdgeModule); @@ -699,18 +736,22 @@ public Task SetInputMessageHandlerAsync(string inputName, MessageHandler message /// generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext, CancellationToken cancellationToken) => InternalClient.SetInputMessageHandlerAsync(inputName, messageHandler, userContext, _isAnEdgeModule, cancellationToken); /// - /// Sets a new default delegate which applies to all endpoints. If a delegate is already associated with - /// the input, it will be called, else the default delegate will be called. If a default delegate was set previously, - /// it will be overwritten. + /// Sets a new default delegate which applies to all endpoints. /// + /// + /// If a delegate is already associated with the input, it will be called, else the default delegate will be called. + /// If a default delegate was set previously, it will be overwritten. + /// /// The delegate to be called when a message is sent to any input. /// generic parameter to be interpreted by the client code. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SetMessageHandlerAsync(MessageHandler messageHandler, object userContext) => InternalClient.SetMessageHandlerAsync(messageHandler, userContext, _isAnEdgeModule); @@ -724,6 +765,7 @@ public Task SetMessageHandlerAsync(MessageHandler messageHandler, object userCon /// generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The task containing the event public Task SetMessageHandlerAsync(MessageHandler messageHandler, object userContext, CancellationToken cancellationToken) => InternalClient.SetMessageHandlerAsync(messageHandler, userContext, _isAnEdgeModule, cancellationToken); @@ -734,6 +776,7 @@ public Task SetMessageHandlerAsync(MessageHandler messageHandler, object userCon /// /// The unique identifier of the edge device to invoke the method on. /// The details of the method to invoke. + /// When the client has been disposed. /// The result of the method invocation. public Task InvokeMethodAsync(string deviceId, MethodRequest methodRequest) => InvokeMethodAsync(deviceId, methodRequest, CancellationToken.None); @@ -746,12 +789,10 @@ public Task InvokeMethodAsync(string deviceId, MethodRequest met /// The details of the method to invoke. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The result of the method invocation. - public Task InvokeMethodAsync(string deviceId, MethodRequest methodRequest, CancellationToken cancellationToken) - { - methodRequest.ThrowIfNull(nameof(methodRequest)); - return InvokeMethodAsync(GetDeviceMethodUri(deviceId), methodRequest, cancellationToken); - } + public Task InvokeMethodAsync(string deviceId, MethodRequest methodRequest, CancellationToken cancellationToken) => + InvokeMethodAsync(GetDeviceMethodUri(deviceId), methodRequest, cancellationToken); /// /// Interactively invokes a method from an edge module to a different edge module. @@ -761,6 +802,7 @@ public Task InvokeMethodAsync(string deviceId, MethodRequest met /// The unique identifier of the edge module to invoke the method on. /// The details of the method to invoke. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The result of the method invocation. public Task InvokeMethodAsync(string deviceId, string moduleId, MethodRequest methodRequest) => InvokeMethodAsync(deviceId, moduleId, methodRequest, CancellationToken.None); @@ -774,15 +816,19 @@ public Task InvokeMethodAsync(string deviceId, string moduleId, /// The details of the method to invoke. /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. + /// When the client has been disposed. /// The result of the method invocation. - public Task InvokeMethodAsync(string deviceId, string moduleId, MethodRequest methodRequest, CancellationToken cancellationToken) - { - methodRequest.ThrowIfNull(nameof(methodRequest)); - return InvokeMethodAsync(GetModuleMethodUri(deviceId, moduleId), methodRequest, cancellationToken); - } + public Task InvokeMethodAsync(string deviceId, string moduleId, MethodRequest methodRequest, CancellationToken cancellationToken) => + InvokeMethodAsync(GetModuleMethodUri(deviceId, moduleId), methodRequest, cancellationToken); private async Task InvokeMethodAsync(Uri uri, MethodRequest methodRequest, CancellationToken cancellationToken) { + if (InternalClient.IsDisposed) + { + throw new ObjectDisposedException("IoT client", DefaultDelegatingHandler.ClientDisposedMessage); + } + methodRequest.ThrowIfNull(nameof(methodRequest)); + HttpClientHandler httpClientHandler = null; Func customCertificateValidation = _certValidator.GetCustomCertificateValidation(); diff --git a/iothub/device/tests/ModuleClientDisposeTests.cs b/iothub/device/tests/ModuleClientDisposeTests.cs new file mode 100644 index 0000000000..d045cb2037 --- /dev/null +++ b/iothub/device/tests/ModuleClientDisposeTests.cs @@ -0,0 +1,205 @@ +// 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 module client result in an ObjectDisposedException. + /// + public class ModuleClientDisposeTests + { + private static ModuleClient s_client; + + [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 ModuleAuthenticationWithRegistrySymmetricKey("deviceId","moduleId", testSharedAccessKey)); + s_client = ModuleClient.CreateFromConnectionString(csBuilder.ToString()); + s_client.Dispose(); + } + + [TestMethod] + public async Task ModuleClient_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 ModuleClient_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 ModuleClient_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 ModuleClient_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 ModuleClient_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_SetMessageHandlerAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetMessageHandlerAsync( + (message, userContext) => Task.FromResult(MessageResponse.Completed), + null, + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModuleClient_SetInputMessageHandlerAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client + .SetInputMessageHandlerAsync( + "input", + (message, userContext) => Task.FromResult(MessageResponse.Completed), + null, + cts.Token) + .ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModuleClient_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 ModuleClient_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 ModuleClient_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 ModuleClient_SendEventAsync_ToOutput_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using var msg = new Message(); + Func op = async () => await s_client.SendEventAsync("output", msg, cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModuleClient_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 ModuleClient_SendEventBatchAsync_ToOutput_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + using var msg = new Message(); + Func op = async () => await s_client.SendEventBatchAsync("output", new[] { msg }, cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModuleClient_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 ModuleClient_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 ModuleClient_InvokeMethodAsync_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.InvokeMethodAsync("deviceId", new MethodRequest("name"), cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + + [TestMethod] + public async Task ModuleClient_InvokeMethodAsync_ToModule_ThrowsWhenClientIsDisposed() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + Func op = async () => await s_client.InvokeMethodAsync("deviceId", "moduleId", new MethodRequest("name"), cts.Token).ConfigureAwait(false); + await op.Should().ThrowAsync().ConfigureAwait(false); + } + } +}