private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
        {
            var c2dReceiveExceptionsToBeIgnored = new Dictionary <Type, string>(_exceptionsToBeIgnored)
            {
                { typeof(DeviceMessageLockLostException), "Attempted to complete a received message whose lock token has expired" }
            };

            while (!cancellationToken.IsCancellationRequested)
            {
                if (!IsDeviceConnected)
                {
                    await Task.Delay(s_sleepDuration);

                    continue;
                }
                else if (_transportType == TransportType.Http1)
                {
                    // The call to ReceiveAsync over HTTP completes immediately, rather than waiting up to the specified
                    // time or when a cancellation token is signaled, so if we want it to poll at the same rate, we need
                    // to add an explicit delay here.
                    await Task.Delay(s_sleepDuration);
                }

                _logger.LogInformation($"Device waiting for C2D messages from the hub for {s_sleepDuration}." +
                                       $"\nUse the IoT Hub Azure Portal or Azure IoT Explorer to send a message to this device.");

                await RetryOperationHelper.RetryTransientExceptionsAsync(
                    operationName : "ReceiveAndCompleteC2DMessage",
                    asyncOperation : async() => await ReceiveMessageAndCompleteAsync(),
                    shouldExecuteOperation : () => IsDeviceConnected,
                    logger : _logger,
                    exceptionsToBeIgnored : c2dReceiveExceptionsToBeIgnored,
                    cancellationToken : cancellationToken);
            }
        }
        private async Task SendMessagesAsync(CancellationToken cancellationToken)
        {
            int messageCount = 0;

            while (!cancellationToken.IsCancellationRequested)
            {
                if (IsDeviceConnected)
                {
                    _logger.LogInformation($"Device sending message {++messageCount} to IoT hub.");

                    using Message message = PrepareMessage(messageCount);
                    await RetryOperationHelper.RetryTransientExceptionsAsync(
                        operationName : $"SendD2CMessage_{messageCount}",
                        asyncOperation : async() => await s_deviceClient.SendEventAsync(message),
                        shouldExecuteOperation : () => IsDeviceConnected,
                        logger : _logger,
                        exceptionsToBeIgnored : _exceptionsToBeIgnored,
                        cancellationToken : cancellationToken);

                    _logger.LogInformation($"Device sent message {messageCount} to IoT hub.");
                }

                await Task.Delay(s_sleepDuration);
            }
        }
        private async Task GetTwinAndDetectChangesAsync(CancellationToken cancellationToken)
        {
            Twin twin = null;

            // Allow a single thread to call GetTwin here
            await _initSemaphore.WaitAsync(cancellationToken);

            await RetryOperationHelper.RetryTransientExceptionsAsync(
                operationName : "GetTwin",
                asyncOperation : async() =>
            {
                twin = await s_deviceClient.GetTwinAsync();
                _logger.LogInformation($"Device retrieving twin values: {twin.ToJson()}");

                TwinCollection twinCollection     = twin.Properties.Desired;
                long serverDesiredPropertyVersion = twinCollection.Version;

                // Check if the desired property version is outdated on the local side.
                if (serverDesiredPropertyVersion > s_localDesiredPropertyVersion)
                {
                    _logger.LogDebug($"The desired property version cached on local is changing from {s_localDesiredPropertyVersion} to {serverDesiredPropertyVersion}.");
                    await HandleTwinUpdateNotificationsAsync(twinCollection, cancellationToken);
                }
            },
                shouldExecuteOperation : () => IsDeviceConnected,
                logger : _logger,
                exceptionsToBeIgnored : _exceptionsToBeIgnored,
                cancellationToken : cancellationToken);

            _initSemaphore.Release();
        }
        private async Task HandleTwinUpdateNotificationsAsync(TwinCollection twinUpdateRequest, object userContext)
        {
            CancellationToken cancellationToken = (CancellationToken)userContext;

            if (!cancellationToken.IsCancellationRequested)
            {
                var reportedProperties = new TwinCollection();

                _logger.LogInformation($"Twin property update requested: \n{twinUpdateRequest.ToJson()}");

                // For the purpose of this sample, we'll blindly accept all twin property write requests.
                foreach (KeyValuePair <string, object> desiredProperty in twinUpdateRequest)
                {
                    _logger.LogInformation($"Setting property {desiredProperty.Key} to {desiredProperty.Value}.");
                    reportedProperties[desiredProperty.Key] = desiredProperty.Value;
                }

                // For the purpose of this sample, we'll blindly accept all twin property write requests.
                await RetryOperationHelper.RetryTransientExceptionsAsync(
                    operationName : "UpdateReportedProperties",
                    asyncOperation : async() => await s_deviceClient.UpdateReportedPropertiesAsync(reportedProperties, cancellationToken),
                    shouldExecuteOperation : () => IsDeviceConnected,
                    logger : _logger,
                    exceptionsToBeIgnored : _exceptionsToBeIgnored,
                    cancellationToken : cancellationToken);
            }
        }
        private async Task InitializeAndSetupClientAsync(CancellationToken cancellationToken)
        {
            if (ShouldClientBeInitialized(s_connectionStatus))
            {
                // Allow a single thread to dispose and initialize the client instance.
                await _initSemaphore.WaitAsync(cancellationToken);

                try
                {
                    if (ShouldClientBeInitialized(s_connectionStatus))
                    {
                        _logger.LogDebug($"Attempting to initialize the client instance, current status={s_connectionStatus}");

                        // If the device client instance has been previously initialized, close and dispose it.
                        if (s_deviceClient != null)
                        {
                            try
                            {
                                await s_deviceClient.CloseAsync(cancellationToken);
                            }
                            catch (UnauthorizedException) { } // if the previous token is now invalid, this call may fail
                            s_deviceClient.Dispose();
                        }

                        s_deviceClient = DeviceClient.CreateFromConnectionString(_deviceConnectionStrings.First(), _transportType, _clientOptions);
                        s_deviceClient.SetConnectionStatusChangesHandler(ConnectionStatusChangeHandler);
                        _logger.LogDebug("Initialized the client instance.");
                    }
                }
                finally
                {
                    _initSemaphore.Release();
                }

                // Force connection now.
                // We have set the "shouldExecuteOperation" function to always try to open the connection.
                // OpenAsync() is an idempotent call, it has the same effect if called once or multiple times on the same client.
                await RetryOperationHelper.RetryTransientExceptionsAsync(
                    operationName : "OpenConnection",
                    asyncOperation : async() => await s_deviceClient.OpenAsync(cancellationToken),
                    shouldExecuteOperation : () => true,
                    logger : _logger,
                    exceptionsToBeIgnored : _exceptionsToBeIgnored,
                    cancellationToken : cancellationToken);

                _logger.LogDebug($"The client instance has been opened.");

                // You will need to subscribe to the client callbacks any time the client is initialized.
                await RetryOperationHelper.RetryTransientExceptionsAsync(
                    operationName : "SubscribeTwinUpdates",
                    asyncOperation : async() => await s_deviceClient.SetDesiredPropertyUpdateCallbackAsync(HandleTwinUpdateNotificationsAsync, cancellationToken),
                    shouldExecuteOperation : () => IsDeviceConnected,
                    logger : _logger,
                    exceptionsToBeIgnored : _exceptionsToBeIgnored,
                    cancellationToken : cancellationToken);

                _logger.LogDebug("The client has subscribed to desired property update notifications.");
            }
        }