diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 86b6ad4217..5b905ac5c9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # Track1 .NET Azure IoT Hub and DPS SDKs -* @drwill-ms @timtay-microsoft @abhipsaMisra @vinagesh @azabbasi @bikamani @barustum @jamdavi \ No newline at end of file +* @drwill-ms @timtay-microsoft @abhipsaMisra @vinagesh @azabbasi @jamdavi diff --git a/azureiot.sln b/azureiot.sln index 3b894997b8..9533df05df 100644 --- a/azureiot.sln +++ b/azureiot.sln @@ -72,6 +72,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{719D18A7-E943-461B-B777-0AAEC43916F5}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + configure_tls_protocol_version_and_ciphers.md = configure_tls_protocol_version_and_ciphers.md + device_connection_and_reliability_readme.md = device_connection_and_reliability_readme.md + readme.md = readme.md + supported_platforms.md = supported_platforms.md test.runsettings = test.runsettings EndProjectSection EndProject diff --git a/common/src/service/ExceptionHandlingHelper.cs b/common/src/service/ExceptionHandlingHelper.cs index 947659eaad..f2943ddb11 100644 --- a/common/src/service/ExceptionHandlingHelper.cs +++ b/common/src/service/ExceptionHandlingHelper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -17,86 +18,77 @@ namespace Microsoft.Azure.Devices { internal class ExceptionHandlingHelper { - public static IDictionary>> GetDefaultErrorMapping() + private static readonly IReadOnlyDictionary>> s_mappings = + new Dictionary>> { - var mappings = new Dictionary>> { - { - HttpStatusCode.NoContent, - async (response) => new DeviceNotFoundException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.NotFound, - async (response) => new DeviceNotFoundException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.Conflict, - async (response) => new DeviceAlreadyExistsException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { HttpStatusCode.BadRequest, async (response) => new ArgumentException( - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) }, - - { - HttpStatusCode.Unauthorized, - async (response) => new UnauthorizedException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.Forbidden, - async (response) => new QuotaExceededException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.PreconditionFailed, - async (response) => new DeviceMessageLockLostException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.RequestEntityTooLarge, - async (response) => new MessageTooLargeException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.InternalServerError, - async (response) => new ServerErrorException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - HttpStatusCode.ServiceUnavailable, - async (response) => new ServerBusyException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - }, - - { - (HttpStatusCode)429, - async (response) => new ThrottlingException( - code: await GetExceptionCodeAsync(response).ConfigureAwait(false), - message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) - } - }; + HttpStatusCode.NoContent, + async (response) => new DeviceNotFoundException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.NotFound, + async (response) => new DeviceNotFoundException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.Conflict, + async (response) => new DeviceAlreadyExistsException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.BadRequest, async (response) => new ArgumentException( + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.Unauthorized, + async (response) => new UnauthorizedException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.Forbidden, + async (response) => new QuotaExceededException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.PreconditionFailed, + async (response) => new DeviceMessageLockLostException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.RequestEntityTooLarge, + async (response) => new MessageTooLargeException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.InternalServerError, + async (response) => new ServerErrorException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + HttpStatusCode.ServiceUnavailable, + async (response) => new ServerBusyException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + }, + { + (HttpStatusCode)429, + async (response) => new ThrottlingException( + code: await GetExceptionCodeAsync(response).ConfigureAwait(false), + message: await GetExceptionMessageAsync(response).ConfigureAwait(false)) + } + }; - return mappings; - } + public static IReadOnlyDictionary>> GetDefaultErrorMapping() => + s_mappings; public static Task GetExceptionMessageAsync(HttpResponseMessage response) { @@ -104,10 +96,10 @@ public static Task GetExceptionMessageAsync(HttpResponseMessage response } /// - /// Get the fully qualified error code from the http response message, if exists + /// Get the fully-qualified error code from the HTTP response message, if exists. /// - /// The http response message - /// The fully qualified error code, or the response status code if no error code was provided. + /// The HTTP response message + /// The fully-qualified error code, or the response status code, if no error code was provided. public static async Task GetExceptionCodeAsync(HttpResponseMessage response) { // First we will attempt to retrieve the error code from the response content. @@ -121,22 +113,67 @@ public static async Task GetExceptionCodeAsync(HttpResponseMessage re // to 'error code' enum mapping, the SDK will check if both values are a match. If so, the SDK will populate the exception with the proper Code. In the case where // there is a mismatch between the error code and the description, the SDK returns ErrorCode.InvalidErrorCode and log a warning. - int errorCode; + int errorCodeValue = (int)ErrorCode.InvalidErrorCode; try { - IoTHubExceptionResult responseContent = JsonConvert - .DeserializeObject(responseContentStr); - Dictionary messageFields = JsonConvert - .DeserializeObject>(responseContent.Message); + IoTHubExceptionResult responseContent = JsonConvert.DeserializeObject(responseContentStr); - if (messageFields != null - && messageFields.TryGetValue(CommonConstants.ErrorCode, out string errorCodeObj)) + try { - errorCode = Convert.ToInt32(errorCodeObj, CultureInfo.InvariantCulture); + Dictionary messageFields = JsonConvert.DeserializeObject>(responseContent.Message); + + if (messageFields != null + && messageFields.TryGetValue(CommonConstants.ErrorCode, out string errorCodeObj)) + { + // The result of TryParse is not being tracked since errorCodeValue has already been initialized to a default value of InvalidErrorCode. + _ = int.TryParse(errorCodeObj, NumberStyles.Any, CultureInfo.InvariantCulture, out errorCodeValue); + } } - else + catch (JsonReaderException ex) { - return ErrorCode.InvalidErrorCode; + if (Logging.IsEnabled) + Logging.Error(null, $"Failed to deserialize error message into a dictionary: {ex}. Message body: '{responseContentStr}.'"); + + // In some scenarios, the error response string is a ';' delimited string with the service-returned error code. + const char errorFieldsDelimiter = ';'; + string[] messageFields = responseContent.Message?.Split(errorFieldsDelimiter); + + if (messageFields != null) + { + foreach (string messageField in messageFields) + { +#if NET451 || NET472 || NETSTANDARD2_0 + if (messageField.IndexOf(CommonConstants.ErrorCode, StringComparison.OrdinalIgnoreCase) >= 0) +#else + if (messageField.Contains(CommonConstants.ErrorCode, StringComparison.OrdinalIgnoreCase)) +#endif + { + const char errorCodeDelimiter = ':'; + +#if NET451 || NET472 || NETSTANDARD2_0 + if (messageField.IndexOf(errorCodeDelimiter) >= 0) +#else + if (messageField.Contains(errorCodeDelimiter)) +#endif + { + string[] errorCodeFields = messageField.Split(errorCodeDelimiter); + if (Enum.TryParse(errorCodeFields[1], out ErrorCode errorCode)) + { + errorCodeValue = (int)errorCode; + } + } + } + break; + } + } + else + { + if (Logging.IsEnabled) + Logging.Error(null, $"Failed to deserialize error message into a dictionary and could not parse ';' delimited string either: {ex}." + + $" Message body: '{responseContentStr}.'"); + + return ErrorCode.InvalidErrorCode; + } } } catch (JsonReaderException ex) @@ -152,7 +189,7 @@ public static async Task GetExceptionCodeAsync(HttpResponseMessage re if (headerErrorCodeString != null && Enum.TryParse(headerErrorCodeString, out ErrorCode headerErrorCode)) { - if ((int)headerErrorCode == errorCode) + if ((int)headerErrorCode == errorCodeValue) { // We have a match. Therefore, return the proper error code. return headerErrorCode; @@ -160,7 +197,7 @@ public static async Task GetExceptionCodeAsync(HttpResponseMessage re if (Logging.IsEnabled) Logging.Error(null, $"There is a mismatch between the error code retrieved from the response content and the response header." + - $"Content error code: {errorCode}. Header error code description: {(int)headerErrorCode}."); + $"Content error code: {errorCodeValue}. Header error code description: {(int)headerErrorCode}."); } return ErrorCode.InvalidErrorCode; diff --git a/common/src/service/HttpClientHelper.cs b/common/src/service/HttpClientHelper.cs index fb17cebcce..a3ac7fccb3 100644 --- a/common/src/service/HttpClientHelper.cs +++ b/common/src/service/HttpClientHelper.cs @@ -45,14 +45,14 @@ internal sealed class HttpClientHelper : IHttpClientHelper public HttpClientHelper( Uri baseAddress, IAuthorizationHeaderProvider authenticationHeaderProvider, - IDictionary>> defaultErrorMapping, + IReadOnlyDictionary>> defaultErrorMapping, TimeSpan timeout, IWebProxy customHttpProxy, int connectionLeaseTimeoutMilliseconds) { _baseAddress = baseAddress; _authenticationHeaderProvider = authenticationHeaderProvider; - _defaultErrorMapping = new ReadOnlyDictionary>>(defaultErrorMapping); + _defaultErrorMapping = defaultErrorMapping; _defaultOperationTimeout = timeout; // We need two types of HttpClients, one with our default operation timeout, and one without. The one without will rely on @@ -924,7 +924,6 @@ internal static HttpMessageHandler CreateDefaultHttpMessageHandler(IWebProxy web #endif #pragma warning restore CA2000 // Dispose objects before losing scope - if (webProxy != DefaultWebProxySettings.Instance) { httpMessageHandler.UseProxy = webProxy != null; diff --git a/device_connection_and_reliability_readme.md b/device_connection_and_reliability_readme.md new file mode 100644 index 0000000000..dcdd357930 --- /dev/null +++ b/device_connection_and_reliability_readme.md @@ -0,0 +1,85 @@ +# Azure IoT Device Client .NET SDK + +## Device connection and messaging reliability + +### Overview + +In this document you will find information about: + +- The connection authentication and renewal methods. +- The reconnection logic and retry policies. +- The timeout controls. + +### Connection authentication + +Authentication can be done using one of the following: + +- [SAS tokens for the device](https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-sas?tabs=node#use-sas-tokens-as-a-device) - Using IoT hub [device shared access key](https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-sas?tabs=node#use-a-shared-access-policy-to-access-on-behalf-of-a-device) or [symmetric key](https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-sas?tabs=node#use-a-symmetric-key-in-the-identity-registry) from DPS identity registry +- [x509 certificate](https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-sas#supported-x509-certificates) - Self signed or [CA-signed](https://docs.microsoft.com/azure/iot-hub/iot-hub-x509ca-overview) +- [TPM based authentication](https://azure.microsoft.com/blog/device-provisioning-identity-attestation-with-tpm/) + +Samples: +- IoT hub device shared access key based authentication sample - [DeviceReconnectionSample](https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/master/iot-hub/Samples/device/DeviceReconnectionSample/DeviceReconnectionSample.cs#L102) +- Device provisioning service symmetric key based authentication sample - [ProvisioningDeviceClientSample](https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/master/provisioning/Samples/device/SymmetricKeySample/ProvisioningDeviceClientSample.cs#L62) +- x509 based authentication sample using CA-signed certificates - [X509DeviceCertWithChainSample](https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/master/iot-hub/Samples/device/X509DeviceCertWithChainSample/Program.cs#L43) +- TPM based authentication sample - [ProvisioningDeviceClientSample](https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/master/provisioning/Samples/device/TpmSample/ProvisioningDeviceClientSample.cs#L49) + +When using SAS tokens, authentication can be done by: + +- Providing the shared access key of the IoT hub and letting the SDK create the SAS tokens by using one of the `CreateFromConnectionString` methods on the [DeviceClient](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceclient). + + If you choose this option, the SDK will create the SAS tokens and renew them before expiry. The default values for time-to-live and renewal buffer can be changed using the `ClientOptions` properties. + + - [SasTokenTimeToLive](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.clientoptions.sastokentimetolive): The suggested time-to-live value for tokens generated for SAS authenticated clients. Default value is 60 minutes. + - [SasTokenRenewalBuffer](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.clientoptions.sastokenrenewalbuffer): The time buffer before expiry when the token should be renewed, expressed as a percentage of the time-to-live. Acceptable values lie between 0 and 100. Default value is 15%. + + > Note: If the shared access policy name is not specified in the connection string, the audience for the token generation will be set by default to - `/devices/` + +- Providing only the shared access signature + + If you only provide the shared access signature, there will never be any renewal handled by the SDK. + +- Providing your own SAS token using [DeviceAuthenticationWithTokenRefresh](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtokenrefresh) + + If you choose to use `DeviceAuthenticationWithTokenRefresh` to provide your own implementation of token generation, you can provide the time-to-live and time buffer before expiry through the `DeviceAuthenticationWithTokenRefresh` constructor. The `ClientOptions` only apply to other `IAunthenticationMethod` implementations. + +When using x509 certificates, [DeviceAuthenticationWithX509Certificate](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithx509certificate) can be used. The client authentication will be valid until the certificate is valid. Any renewal will have to be done manually and the client needs to be recreated. + +When using TPM based authentication, the [DeviceAuthenticationWithTpm](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtpm) can be used. TPM based authentication will eventually generate a SAS token but is more secure than using the shared access key of the IoT hub to generate the token. + +### Authentication methods implemented by the SDK + +The different `IAuthenticationMethod` implementations provided by the SDK are: + +- [DeviceAuthenticationWithRegistrySymmetricKey](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithregistrysymmetrickey) - Authentication method that uses the symmetric key associated with the device in the device registry. +- [DeviceAuthenticationWithSharedAccessPolicyKey](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithsharedaccesspolicykey) - Authentication method that uses a shared access policy key. +- [DeviceAuthenticationWithToken](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtoken) - Authentication method that uses a shared access signature token. +- [DeviceAuthenticationWithTokenRefresh](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtokenrefresh) - Abstract class that can be implemented to generate a shared access signature token and allows for token refresh. +- [DeviceAuthenticationWithTpm](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithtpm) - Authentication method that uses a shared access signature token generated using TPM and allows for token refresh. +- [DeviceAuthenticationWithX509Certificate](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.deviceauthenticationwithx509certificate) - Authentication method that uses a X.509 certificates. + +### Connection retry logic + +For both AMQP and MQTT, the SDK will try to reconnect anytime there is any network related disruption. The default retry policy does not have a time limit and will follow exponential back-off. + +> Note: The default retry policy has support for jitter, which ensures that if you have N devices that disconnected at the same time, all of them won't start reconnecting with the same delay. + +For more details on the default retry policy and how to override it, see [retry policy documentation](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/iothub/device/devdoc/retrypolicy.md). + +HTTP is a stateless protocol and will work whenever there is network connectivity. + +### Timeout controls + +There are different timeout values that can be configured for the `DeviceClient`/`ModuleClient` based on the protocol. These values are configuarable through the following transport settings that are passed while creating the client. Once the client is created, the settings cannot be changed. The client will need to be recreated with new settings to make changes. + +AMQP timeout settings: + +- [IdleTimeout](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.amqptransportsettings.idletimeout) - The interval that the client establishes with the service, for sending keep-alive pings. The default value is 2 minutes. +- [OperationTimeout](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.amqptransportsettings.operationtimeout) - The time to wait for any operation to complete. The default is 1 minute. +- [OpenTimeout](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.amqptransportsettings.opentimeout) - This value is not used (TODO: Confirm and update) + +MQTT timeout settings: + +- [ConnectArrivalTimeout](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.transport.mqtt.mqtttransportsettings.connectarrivaltimeout) - The time to wait for receiving an acknowledgment for a CONNECT packet. The default is 1 minute. +- [KeepAliveInSeconds](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.transport.mqtt.mqtttransportsettings.keepaliveinseconds) - The interval, in seconds, that the client establishes with the service, for sending keep-alive pings. The default value is 5 minutes. The client will send a ping request 4 times per keep-alive duration set. It will wait for 30 seconds for the ping response, else mark the connection as disconnected. +- [DeviceReceiveAckTimeout](https://docs.microsoft.com/dotnet/api/microsoft.azure.devices.client.transport.mqtt.mqtttransportsettings.devicereceiveacktimeout) - The time a device will wait for an acknowledgment from service. The default is 5 minutes. diff --git a/e2e/test/Helpers/AmqpConnectionStatusChange.cs b/e2e/test/helpers/AmqpConnectionStatusChange.cs similarity index 100% rename from e2e/test/Helpers/AmqpConnectionStatusChange.cs rename to e2e/test/helpers/AmqpConnectionStatusChange.cs diff --git a/e2e/test/Helpers/ConsoleEventListener.cs b/e2e/test/helpers/ConsoleEventListener.cs similarity index 58% rename from e2e/test/Helpers/ConsoleEventListener.cs rename to e2e/test/helpers/ConsoleEventListener.cs index 8d7a98e74f..d1a3b8d368 100644 --- a/e2e/test/Helpers/ConsoleEventListener.cs +++ b/e2e/test/helpers/ConsoleEventListener.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -10,16 +9,21 @@ namespace System.Diagnostics.Tracing public sealed class ConsoleEventListener : EventListener { // Configure this value to filter all the necessary events when OnEventSourceCreated is called. - // OnEventSourceCreated is triggered as soon as the EventListener is registered and an event source is created. - // So trying to configure this value in the ConsoleEventListener constructor does not work. - // The OnEventSourceCreated can be triggered sooner than the filter is initialized in the ConsoleEventListener constructor. - private static readonly string[] s_eventFilters = new string[] { "DotNetty-Default", "Microsoft-Azure-Devices", "Azure-Core", "Azure-Identity" }; + // The EventListener base class constructor creates an event listener in which all events are disabled by default. + // EventListener constructor also causes the OnEventSourceCreated callback to fire. + // Since our ConsoleEventListener uses the OnEventSourceCreated callback to enable events, the event filter needs to be + // initialized before OnEventSourceCreated is called. For this reason we cannot use ConsoleEventListener constructor + // to initialize the event filter (base class constructors are called before derived class constructors). + // The OnEventSourceCreated will be triggered sooner than the filter is initialized in the ConsoleEventListener constructor. + // As a result we will need to define the event filter list as a static variable. + // Link to EventListener sourcecode: https://github.com/dotnet/runtime/blob/6696065ab0f517f5a9e5f55c559df0010a816dbe/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs#L4009-L4018 + private static readonly string[] s_eventFilter = new string[] { "DotNetty-Default", "Microsoft-Azure-Devices", "Azure-Core", "Azure-Identity" }; private readonly object _lock = new object(); protected override void OnEventSourceCreated(EventSource eventSource) { - if (s_eventFilters.Any(filter => eventSource.Name.StartsWith(filter, StringComparison.OrdinalIgnoreCase))) + if (s_eventFilter.Any(filter => eventSource.Name.StartsWith(filter, StringComparison.OrdinalIgnoreCase))) { base.OnEventSourceCreated(eventSource); EnableEvents( diff --git a/e2e/test/Helpers/CustomWebProxy.cs b/e2e/test/helpers/CustomWebProxy.cs similarity index 100% rename from e2e/test/Helpers/CustomWebProxy.cs rename to e2e/test/helpers/CustomWebProxy.cs diff --git a/e2e/test/Helpers/HostNameHelper.cs b/e2e/test/helpers/HostNameHelper.cs similarity index 100% rename from e2e/test/Helpers/HostNameHelper.cs rename to e2e/test/helpers/HostNameHelper.cs diff --git a/e2e/test/Helpers/ImportExportDevicesHelpers.cs b/e2e/test/helpers/ImportExportDevicesHelpers.cs similarity index 100% rename from e2e/test/Helpers/ImportExportDevicesHelpers.cs rename to e2e/test/helpers/ImportExportDevicesHelpers.cs diff --git a/e2e/test/Helpers/RetryOperationHelper.cs b/e2e/test/helpers/RetryOperationHelper.cs similarity index 100% rename from e2e/test/Helpers/RetryOperationHelper.cs rename to e2e/test/helpers/RetryOperationHelper.cs diff --git a/e2e/test/Helpers/StorageContainer.cs b/e2e/test/helpers/StorageContainer.cs similarity index 100% rename from e2e/test/Helpers/StorageContainer.cs rename to e2e/test/helpers/StorageContainer.cs diff --git a/e2e/test/Helpers/TestDevice.cs b/e2e/test/helpers/TestDevice.cs similarity index 100% rename from e2e/test/Helpers/TestDevice.cs rename to e2e/test/helpers/TestDevice.cs diff --git a/e2e/test/Helpers/TestDeviceCallbackHandler.cs b/e2e/test/helpers/TestDeviceCallbackHandler.cs similarity index 100% rename from e2e/test/Helpers/TestDeviceCallbackHandler.cs rename to e2e/test/helpers/TestDeviceCallbackHandler.cs diff --git a/e2e/test/Helpers/TestModule.cs b/e2e/test/helpers/TestModule.cs similarity index 100% rename from e2e/test/Helpers/TestModule.cs rename to e2e/test/helpers/TestModule.cs diff --git a/e2e/test/Helpers/digitaltwins/models/TemperatureControllerTwin.cs b/e2e/test/helpers/digitaltwins/models/TemperatureControllerTwin.cs similarity index 100% rename from e2e/test/Helpers/digitaltwins/models/TemperatureControllerTwin.cs rename to e2e/test/helpers/digitaltwins/models/TemperatureControllerTwin.cs diff --git a/e2e/test/Helpers/digitaltwins/models/ThermostatTwin.cs b/e2e/test/helpers/digitaltwins/models/ThermostatTwin.cs similarity index 100% rename from e2e/test/Helpers/digitaltwins/models/ThermostatTwin.cs rename to e2e/test/helpers/digitaltwins/models/ThermostatTwin.cs diff --git a/e2e/test/Helpers/logging/EventSourceTestLogger.cs b/e2e/test/helpers/logging/EventSourceTestLogger.cs similarity index 100% rename from e2e/test/Helpers/logging/EventSourceTestLogger.cs rename to e2e/test/helpers/logging/EventSourceTestLogger.cs diff --git a/e2e/test/Helpers/logging/LoggedTestMethod.cs b/e2e/test/helpers/logging/LoggedTestMethod.cs similarity index 100% rename from e2e/test/Helpers/logging/LoggedTestMethod.cs rename to e2e/test/helpers/logging/LoggedTestMethod.cs diff --git a/e2e/test/Helpers/logging/LoggingPropertyNames.cs b/e2e/test/helpers/logging/LoggingPropertyNames.cs similarity index 100% rename from e2e/test/Helpers/logging/LoggingPropertyNames.cs rename to e2e/test/helpers/logging/LoggingPropertyNames.cs diff --git a/e2e/test/Helpers/logging/MsTestLogger.cs b/e2e/test/helpers/logging/MsTestLogger.cs similarity index 100% rename from e2e/test/Helpers/logging/MsTestLogger.cs rename to e2e/test/helpers/logging/MsTestLogger.cs diff --git a/e2e/test/Helpers/logging/TestLogger.cs b/e2e/test/helpers/logging/TestLogger.cs similarity index 100% rename from e2e/test/Helpers/logging/TestLogger.cs rename to e2e/test/helpers/logging/TestLogger.cs diff --git a/e2e/test/Helpers/logging/VerboseTestLogger.cs b/e2e/test/helpers/logging/VerboseTestLogger.cs similarity index 100% rename from e2e/test/Helpers/logging/VerboseTestLogger.cs rename to e2e/test/helpers/logging/VerboseTestLogger.cs diff --git a/e2e/test/Helpers/templates/FaultInjection.cs b/e2e/test/helpers/templates/FaultInjection.cs similarity index 100% rename from e2e/test/Helpers/templates/FaultInjection.cs rename to e2e/test/helpers/templates/FaultInjection.cs diff --git a/e2e/test/Helpers/templates/FaultInjectionPoolingOverAmqp.cs b/e2e/test/helpers/templates/FaultInjectionPoolingOverAmqp.cs similarity index 100% rename from e2e/test/Helpers/templates/FaultInjectionPoolingOverAmqp.cs rename to e2e/test/helpers/templates/FaultInjectionPoolingOverAmqp.cs diff --git a/e2e/test/Helpers/templates/PoolingOverAmqp.cs b/e2e/test/helpers/templates/PoolingOverAmqp.cs similarity index 100% rename from e2e/test/Helpers/templates/PoolingOverAmqp.cs rename to e2e/test/helpers/templates/PoolingOverAmqp.cs diff --git a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 index 65e1dd9f15..a23ecb4fbd 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 +++ b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 @@ -71,13 +71,13 @@ Function CleanUp-Certs() { Write-Host "`nCleaning up old certs and files that may cause conflicts." $certsToDelete1 = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Issuer.Contains("CN=$subjectPrefix") } - $certsToDelete2 = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Issuer.Contains("CN=$groupCertCommonName") } + $certsToDelete2 = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Issuer.Contains("CN=$groupCertCommonName") } $certsToDelete3 = Get-ChildItem "Cert:\LocalMachine\My" | Where-Object { $_.Issuer.Contains("CN=$deviceCertCommonName") } $certsToDelete = $certsToDelete1 + $certsToDelete2 + $certsToDelete3 $title = "Cleaning up certs." - $certsToDeleteSubjectNames = $certsToDelete | foreach-object {$_.Subject} + $certsToDeleteSubjectNames = $certsToDelete | foreach-object {$_.Subject} $certsToDeleteSubjectNames = $certsToDeleteSubjectNames -join "`n" $question = "Are you sure you want to delete the following certs?`n`n$certsToDeleteSubjectNames" $choices = '&Yes', '&No' @@ -122,7 +122,7 @@ $hubUploadCertificateName = "rootCA" $iothubUnitsToBeCreated = 1 $managedIdentityName = "$ResourceGroup-user-msi" -# OpenSSL has dropped support for SHA1 signed certificates in ubuntu 20.04, so our test resources will use SHA256 signed certificates instead. +# OpenSSL has dropped support for SHA1 signed certificates in Ubuntu 20.04, so our test resources will use SHA256 signed certificates instead. $certificateHashAlgorithm = "SHA256" ################################################################################################# @@ -166,7 +166,7 @@ if (-not ($keyVaultName -match "^[a-zA-Z][a-zA-Z0-9-]{1,22}[a-zA-Z0-9]$")) } ######################################################################################################## -# Generate self-signed certs and to use in DPS and IoT Hub +# Generate self-signed certs and to use in DPS and IoT hub # New certs will be generated each time you run the script as the script cleans up in the end ######################################################################################################## @@ -266,7 +266,7 @@ Export-Certificate -cert $individualDeviceCert -FilePath $individualDeviceCertPa Export-PFXCertificate -cert $individualDeviceCert -filePath $individualDevicePfxPath -password $certPassword | Out-Null $dpsIndividualX509PfxCertificate = [Convert]::ToBase64String((Get-Content $individualDevicePfxPath -AsByteStream)); -# IoT hub certificate for authemtication. The tests are not setup to use a password for the certificate so create the certificate is created with no password. +# IoT hub certificate for authentication. The tests are not setup to use a password for the certificate so create the certificate is created with no password. $iotHubCert = New-SelfSignedCertificate ` -DnsName "$iotHubCertCommonName" ` -KeySpec Signature ` @@ -275,7 +275,7 @@ $iotHubCert = New-SelfSignedCertificate ` -CertStoreLocation "Cert:\LocalMachine\My" ` -NotAfter (Get-Date).AddYears(2) -# IoT hub certificate signed by intermediate certificate for authemtication. +# IoT hub certificate signed by intermediate certificate for authentication. $iotHubChainDeviceCert = New-SelfSignedCertificate ` -DnsName "$iotHubCertChainDeviceCommonName" ` -KeySpec Signature ` @@ -422,22 +422,22 @@ $iotHubName = az deployment group show -g $ResourceGroup -n $deploymentName --qu ################################################################################################################################################# # Configure an AAD app and create self signed certs and get the bytes to generate more content info. ################################################################################################################################################# -Write-Host "`nCreating App Registration $logAnalyticsAppRegnName" +Write-Host "`nCreating app registration $logAnalyticsAppRegnName" $logAnalyticsAppRegUrl = "http://$logAnalyticsAppRegnName" -az ad sp create-for-rbac -n $logAnalyticsAppRegUrl --role "Reader" --scope $resourceGroupId --output none -$logAnalyticsAppId = az ad app list --display-name $logAnalyticsAppRegnName --query "[?displayName=='$logAnalyticsAppRegnName'].appId" --output tsv -Write-Host "`nApplication $logAnalyticsAppRegnName with Id $logAnalyticsAppId was created successfully." +$logAnalyticsAppId = az ad sp create-for-rbac -n $logAnalyticsAppRegUrl --role "Reader" --scope $resourceGroupId --query "appId" --output tsv +Write-Host "`nCreated application $logAnalyticsAppRegnName with Id $logAnalyticsAppId." ################################################################################################################################################# # Configure an AAD app to perform IoT hub data actions. ################################################################################################################################################# -Write-Host "`nCreating App Registration $iotHubAadTestAppRegName" +Write-Host "`nCreating app registration $iotHubAadTestAppRegName for IoT hub data actions" $iotHubAadTestAppRegUrl = "http://$iotHubAadTestAppRegName" $iotHubDataContributorRoleId = "4fc6c259987e4a07842ec321cc9d413f" $iotHubScope = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Devices/IotHubs/$iotHubName" -$iotHubAadTestAppPassword = az ad sp create-for-rbac -n $iotHubAadTestAppRegUrl --role $iotHubDataContributorRoleId --scope $iotHubScope --query password --output tsv -$iotHubAadTestAppId = az ad app list --display-name $iotHubAadTestAppRegName --query "[?displayName=='$iotHubAadTestAppRegName'].appId" --output tsv -Write-Host "`nApplication $iotHubAadTestAppRegName with Id $iotHubAadTestAppId was created successfully." +$iotHubAadTestAppInfo = az ad sp create-for-rbac -n $iotHubAadTestAppRegUrl --role $iotHubDataContributorRoleId --scope $iotHubScope --query '{appId:appId, password:password}' | ConvertFrom-Json +$iotHubAadTestAppPassword = $iotHubAadTestAppInfo.password +$iotHubAadTestAppId = $iotHubAadTestAppInfo.appId +Write-Host "`nCreated application $iotHubAadTestAppRegName with Id $iotHubAadTestAppId." ################################################################################################################################################# # Add role assignement for User assinged managed identity to be able to perform import and export jobs on the IoT hub. @@ -448,30 +448,30 @@ $msiResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/p az role assignment create --assignee $msiPrincipalId --role 'Storage Blob Data Contributor' --scope $resourceGroupId --output none ################################################################################################################################## -# Granting the iot hub system idenitty Storage blob contributor access on the resoruce group +# Granting the IoT hub system identity storage blob contributor access on the resoruce group ################################################################################################################################## Write-Host "`nGranting the system identity on the hub $iotHubName Storage Blob Data Contributor permissions on resource group: $ResourceGroup." $systemIdentityPrincipal = az resource list -n $iotHubName --query [0].identity.principalId --out tsv az role assignment create --assignee $systemIdentityPrincipal --role "Storage Blob Data Contributor" --scope $resourceGroupId --output none ################################################################################################################################## -# Uploading ROOT CA certificate to IoTHub and verifying +# Uploading root CA certificate to IoT hub and verifying ################################################################################################################################## $certExits = az iot hub certificate list -g $ResourceGroup --hub-name $iotHubName --query "value[?name=='$hubUploadCertificateName']" --output tsv if ($certExits) { - Write-Host "`nDeleting existing certificate from IoT Hub." + Write-Host "`nDeleting existing certificate from IoT hub." $etag = az iot hub certificate show -g $ResourceGroup --hub-name $iotHubName --name $hubUploadCertificateName --query 'etag' az iot hub certificate delete -g $ResourceGroup --hub-name $iotHubName --name $hubUploadCertificateName --etag $etag } -Write-Host "`nUploading new certificate to IoT Hub." +Write-Host "`nUploading new certificate to IoT hub." az iot hub certificate create -g $ResourceGroup --path $rootCertPath --hub-name $iotHubName --name $hubUploadCertificateName --output none $isVerified = az iot hub certificate show -g $ResourceGroup --hub-name $iotHubName --name $hubUploadCertificateName --query 'properties.isVerified' --output tsv if ($isVerified -eq 'false') { - Write-Host "`nVerifying certificate uploaded to IotHub." + Write-Host "`nVerifying certificate uploaded to IoT hub." $etag = az iot hub certificate show -g $ResourceGroup --hub-name $iotHubName --name $hubUploadCertificateName --query 'etag' $requestedCommonName = az iot hub certificate generate-verification-code -g $ResourceGroup --hub-name $iotHubName --name $hubUploadCertificateName -e $etag --query 'properties.verificationCode' $verificationCertArgs = @{ @@ -489,19 +489,84 @@ if ($isVerified -eq 'false') } ################################################################################################################################## -# Create device in IoTHub that uses a certificate signed by intermediate certificate +# Fetch the iothubowner policy details +################################################################################################################################## +$iothubownerSasPolicy = "iothubowner" +$iothubownerSasPrimaryKey = az iot hub policy show --hub-name $iotHubName --name $iothubownerSasPolicy --query 'primaryKey' + +################################################################################################################################## +# Create device in IoT hub that uses a certificate signed by intermediate certificate ################################################################################################################################## $iotHubCertChainDevice = az iot hub device-identity list -g $ResourceGroup --hub-name $iotHubName --query "[?deviceId=='$iotHubCertChainDeviceCommonName'].deviceId" --output tsv if (-not $iotHubCertChainDevice) { - Write-Host "`nCreating device $iotHubCertChainDeviceCommonName on IoT Hub." + Write-Host "`nCreating X509 CA certificate authenticated device $iotHubCertChainDeviceCommonName on IoT hub." az iot hub device-identity create -g $ResourceGroup --hub-name $iotHubName --device-id $iotHubCertChainDeviceCommonName --am x509_ca } ################################################################################################################################## -# Uploading certificate to DPS, verifying and creating enrollment groups +# Create the IoT devices and modules that are used by the .NET samples +################################################################################################################################## +$iotHubSasBasedDeviceId = "DoNotDeleteDevice1" +$iotHubSasBasedDevice = az iot hub device-identity list -g $ResourceGroup --hub-name $iotHubName --query "[?deviceId=='$iotHubSasBasedDeviceId'].deviceId" --output tsv + +if (-not $iotHubSasBasedDevice) +{ + Write-Host "`nCreating SAS-based device $iotHubSasBasedDeviceId on IoT hub." + az iot hub device-identity create -g $ResourceGroup --hub-name $iotHubName --device-id $iotHubSasBasedDeviceId --ee +} +$iotHubSasBasedDeviceConnectionString = az iot hub device-identity connection-string show --device-id $iotHubSasBasedDeviceId --hub-name $iotHubName --resource-group $ResourceGroup --output tsv + +$iotHubSasBasedModuleId = "DoNotDeleteModule1" +$iotHubSasBasedModule = az iot hub module-identity list -g $ResourceGroup --hub-name $iotHubName --device-id $iotHubSasBasedDeviceId --query "[?moduleId=='$iotHubSasBasedModuleId'].moduleId" --output tsv + +if (-not $iotHubSasBasedModule) +{ + Write-Host "`nCreating SAS based module $iotHubSasBasedModuleId under device $iotHubSasBasedDeviceId on IoT hub." + az iot hub module-identity create -g $ResourceGroup --hub-name $iotHubName --device-id $iotHubSasBasedDeviceId --module-id $iotHubSasBasedModuleId +} +$iotHubSasBasedModuleConnectionString = az iot hub module-identity connection-string show --device-id $iotHubSasBasedDeviceId --module-id $iotHubSasBasedModuleId --hub-name $iotHubName --resource-group $ResourceGroup --output tsv + +$thermostatSampleDeviceId = "ThermostatSample_DoNotDelete" +$thermostatSampleDevice = az iot hub device-identity list -g $ResourceGroup --hub-name $iotHubName --query "[?deviceId=='$thermostatSampleDeviceId'].deviceId" --output tsv + +if (-not $thermostatSampleDevice) +{ + Write-Host "`nCreating SAS-based device $thermostatSampleDeviceId on IoT hub." + az iot hub device-identity create -g $ResourceGroup --hub-name $iotHubName --device-id $thermostatSampleDeviceId --ee +} +$thermostatSampleDeviceConnectionString = az iot hub device-identity connection-string show --device-id $thermostatSampleDeviceId --hub-name $iotHubName --resource-group $ResourceGroup --output tsv + +$temperatureControllerSampleDeviceId = "TemperatureControllerSample_DoNotDelete" +$temperatureControllerSampleDevice = az iot hub device-identity list -g $ResourceGroup --hub-name $iotHubName --query "[?deviceId=='$temperatureControllerSampleDeviceId'].deviceId" --output tsv + +if (-not $temperatureControllerSampleDevice) +{ + Write-Host "`nCreating SAS-based device $temperatureControllerSampleDeviceId on IoT hub." + az iot hub device-identity create -g $ResourceGroup --hub-name $iotHubName --device-id $temperatureControllerSampleDeviceId --ee +} +$temperatureControllerSampleDeviceConnectionString = az iot hub device-identity connection-string show --device-id $temperatureControllerSampleDeviceId --hub-name $iotHubName --resource-group $ResourceGroup --output tsv + +################################################################################################################################## +# Create the DPS enrollments that are used by the .NET samples +################################################################################################################################## + +$symmetricKeySampleEnrollmentRegistrationId = "SymmetricKeySampleIndividualEnrollment" +$symmetricKeyEnrollmentExists = az iot dps enrollment list -g $ResourceGroup --dps-name $dpsName --query "[?deviceId=='$symmetricKeySampleEnrollmentRegistrationId'].deviceId" --output tsv +if ($symmetricKeyEnrollmentExists) +{ + Write-Host "`nDeleting existing individual enrollment $symmetricKeySampleEnrollmentRegistrationId." + az iot dps enrollment delete -g $ResourceGroup --dps-name $dpsName --enrollment-id $symmetricKeySampleEnrollmentRegistrationId +} +Write-Host "`nAdding individual enrollment $symmetricKeySampleEnrollmentRegistrationId." +az iot dps enrollment create -g $ResourceGroup --dps-name $dpsName --enrollment-id $symmetricKeySampleEnrollmentRegistrationId --attestation-type symmetrickey --output none + +$symmetricKeySampleEnrollmentPrimaryKey = az iot dps enrollment show -g $ResourceGroup --dps-name $dpsName --enrollment-id $symmetricKeySampleEnrollmentRegistrationId --show-keys --query 'attestation.symmetricKey.primaryKey' --output tsv + +################################################################################################################################## +# Uploading certificate to DPS, verifying, and creating enrollment groups ################################################################################################################################## $dpsIdScope = az iot dps show -g $ResourceGroup --name $dpsName --query 'properties.idScope' --output tsv @@ -522,12 +587,12 @@ if ($isVerified -eq 'false') $etag = az iot dps certificate show -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName --query 'etag' $requestedCommonName = az iot dps certificate generate-verification-code -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName -e $etag --query 'properties.verificationCode' $verificationCertArgs = @{ - "-DnsName" = $requestedCommonName; - "-CertStoreLocation" = "cert:\LocalMachine\My"; - "-NotAfter" = (get-date).AddYears(2); - "-TextExtension" = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1", "2.5.29.19={text}ca=FALSE&pathlength=0"); - "-HashAlgorithm" = $certificateHashAlgorithm; - "-Signer" = $rootCACert; + "-DnsName" = $requestedCommonName; + "-CertStoreLocation" = "cert:\LocalMachine\My"; + "-NotAfter" = (get-date).AddYears(2); + "-TextExtension" = @("2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1", "2.5.29.19={text}ca=FALSE&pathlength=0"); + "-HashAlgorithm" = $certificateHashAlgorithm; + "-Signer" = $rootCACert; } $verificationCert = New-SelfSignedCertificate @verificationCertArgs Export-Certificate -cert $verificationCert -filePath $verificationCertPath -Type Cert | Out-Null @@ -587,7 +652,7 @@ Remove-Item -r $selfSignedCerts Write-Host "`nWriting secrets to KeyVault $keyVaultName." az keyvault set-policy -g $ResourceGroup --name $keyVaultName --object-id $userObjectId --secret-permissions delete get list set --output none -az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-CONNECTION-STRING" --value $iotHubConnectionString --output none # IoT Hub Connection string Environment variable for Java +az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-CONNECTION-STRING" --value $iotHubConnectionString --output none az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-PFX-X509-THUMBPRINT" --value $iotHubThumbprint --output none az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-PROXY-SERVER-ADDRESS" --value $proxyServerAddress --output none az keyvault secret set --vault-name $keyVaultName --name "FAR-AWAY-IOTHUB-HOSTNAME" --value $farHubHostName --output none @@ -616,7 +681,7 @@ az keyvault secret set --vault-name $keyVaultName --name "HUB-CHAIN-INTERMEDIATE az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-X509-CHAIN-DEVICE-NAME" --value $iotHubCertChainDeviceCommonName --output none az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-USER-ASSIGNED-MSI-RESOURCE-ID" --value $msiResourceId --output none -# Below Environment variables are only used in Java +# These environment variables are only used in Java az keyvault secret set --vault-name $keyVaultName --name "IOT-DPS-CONNECTION-STRING" --value $dpsConnectionString --output none # DPS Connection string Environment variable for Java az keyvault secret set --vault-name $keyVaultName --name "IOT-DPS-ID-SCOPE" --value $dpsIdScope --output none # DPS ID Scope Environment variable for Java az keyvault secret set --vault-name $keyVaultName --name "FAR-AWAY-IOTHUB-CONNECTION-STRING" --value $farHubConnectionString --output none @@ -631,8 +696,19 @@ az keyvault secret set --vault-name $keyVaultName --name "PROVISIONING-CONNECTIO az keyvault secret set --vault-name $keyVaultName --name "E2E-IKEY" --value $instrumentationKey --output none +# These environment variables are used by .NET samples +az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-DEVICE-CONN-STRING" --value $iotHubSasBasedDeviceConnectionString --output none +az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-MODULE-CONN-STRING" --value $iotHubSasBasedModuleConnectionString --output none +az keyvault secret set --vault-name $keyVaultName --name "PNP-TC-DEVICE-CONN-STRING" --value $temperatureControllerSampleDeviceConnectionString --output none +az keyvault secret set --vault-name $keyVaultName --name "PNP-THERMOSTAT-DEVICE-CONN-STRING" --value $thermostatSampleDeviceConnectionString --output none +az keyvault secret set --vault-name $keyVaultName --name "PATH-TO-DEVICE-PREFIX-FOR-DELETION-FILE" --value "csharp_devices_list.csv" --output none +az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-SAS-KEY" --value $iothubownerSasPrimaryKey --output none +az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-SAS-KEY-NAME" --value $iothubownerSasPolicy --output none +az keyvault secret set --vault-name $keyVaultName --name "DPS-SYMMETRIC-KEY-INDIVIDUAL-ENROLLMENT-REGISTRATION-ID" --value $symmetricKeySampleEnrollmentRegistrationId --output none +az keyvault secret set --vault-name $keyVaultName --name "DPS-SYMMETRIC-KEY-INDIVIDUAL-ENROLLEMNT-PRIMARY-KEY" --value $symmetricKeySampleEnrollmentPrimaryKey --output none + ################################################################################################################################### -# Run docker containers for TPM simulators and Proxy +# Run docker containers for TPM simulators and proxy ################################################################################################################################### if (-not (docker images -q aziotbld/testtpm)) @@ -660,7 +736,7 @@ $file = New-Item -Path $loadScriptDir -Name $loadScriptName -ItemType "file" -Fo Add-Content -Path $file.PSPath -Value "$PSScriptRoot\LoadEnvironmentVariablesFromKeyVault.ps1 -SubscriptionId $SubscriptionId -KeyVaultName $keyVaultName" ############################################################################################################################ -# Configure Environment Variables +# Configure environment variables ############################################################################################################################ Invoke-Expression "$loadScriptDir\$loadScriptName" diff --git a/e2e/test/prerequisites/readme.md b/e2e/test/prerequisites/readme.md index 91d3e9aea3..edfa8ad4fe 100644 --- a/e2e/test/prerequisites/readme.md +++ b/e2e/test/prerequisites/readme.md @@ -43,7 +43,7 @@ docker run -d --restart unless-stopped --name azure-iot-tpmsim -p 127.0.0.1:2321 Alternatives: -- Stand-alone executable for Windows: https://www.microsoft.com/en-us/download/details.aspx?id=52507 +- Stand-alone executable for Windows: https://www.microsoft.com/download/details.aspx?id=52507 ### Proxy Server diff --git a/e2e/test/provisioning/ProvisioningE2ETests.cs b/e2e/test/provisioning/ProvisioningE2ETests.cs index 0cd498d760..201681fd20 100644 --- a/e2e/test/provisioning/ProvisioningE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningE2ETests.cs @@ -470,9 +470,8 @@ public async Task ProvisioningDeviceClient_ValidRegistrationId_TimeSpanTimeoutRe { await ProvisioningDeviceClient_ValidRegistrationId_Register_Ok(Client.TransportType.Amqp_Tcp_Only, AttestationMechanismType.SymmetricKey, EnrollmentType.Individual, TimeSpan.Zero).ConfigureAwait(false); } - catch (ProvisioningTransportException ex) when (ex.InnerException is SocketException && ((SocketException) ex.InnerException).SocketErrorCode == SocketError.TimedOut) + catch (OperationCanceledException) { - // The expected exception is a bit different in AMQP compared to MQTT/HTTPS return; // expected exception was thrown, so exit the test } diff --git a/iothub/device/src/AmqpTransportSettings.cs b/iothub/device/src/AmqpTransportSettings.cs index 272844f6e8..59b56192bf 100644 --- a/iothub/device/src/AmqpTransportSettings.cs +++ b/iothub/device/src/AmqpTransportSettings.cs @@ -115,12 +115,18 @@ public AmqpTransportSettings(TransportType transportType, uint prefetchCount, Am /// /// Specify client-side heartbeat interval. + /// The interval, that the client establishes with the service, for sending keep alive pings. /// The default value is 2 minutes. /// + /// + /// The client will consider the connection as disconnected if the keep alive ping fails. + /// Setting a very low idle timeout value can cause aggressive reconnects, and might not give the + /// client enough time to establish a connection before disconnecting and reconnecting. + /// public TimeSpan IdleTimeout { get; set; } /// - /// The operation timeout + /// The time to wait for any operation to complete. The default is 1 minute. /// public TimeSpan OperationTimeout { @@ -129,8 +135,11 @@ public TimeSpan OperationTimeout } /// - /// The open timeout + /// The open timeout. The default is 1 minute. /// + /// + /// This property is currently unused. + /// public TimeSpan OpenTimeout { get => _openTimeout; @@ -172,7 +181,7 @@ public TransportType GetTransportType() } /// - /// Returns the default current receive timeout + /// The time to wait for a receive operation. The default value is 1 minute. /// public TimeSpan DefaultReceiveTimeout => DefaultOperationTimeout; diff --git a/iothub/device/src/Common/ErrorCode.cs b/iothub/device/src/Common/ErrorCode.cs deleted file mode 100644 index c7fd41a90b..0000000000 --- a/iothub/device/src/Common/ErrorCode.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.Azure.Devices.Client.Errors -{ - /// - /// Unique code for each instance of DeviceGateway exception that identifies the error condition that caused the failure. - /// - /// - /// These error codes will allow us to do automatic analysis and aggregation of error responses sent from resource provider and frontend. - /// - public enum ErrorCode - { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - None = -1, - OrchestrationStateInvalid = 100, - OrchestrationRunningOnIotHub = 101, - IotHubNotFoundInDatabase = 102, - NoMatchingResourcePoolFound = 103, - ResourcePoolNotFound = 104, - NoMatchingResourceFound = 105, - MultipleMatchingResourcesFound = 106, - GarbageCollectionFailed = 107, - IotHubUpdateFailed = 108, - InvalidEventHubAccessRight = 109, - - /// - /// Bad Request - /// - AuthorizationRulesExceededQuota = 200, - - InvalidIotHubName = 201, - InvalidOperationId = 202, - IotHubNameNotAvailable = 203, - SystemPropertiesNotAllowed = 204, - - /// - /// Internal Error - /// - IotHubActivationFailed = 300, - - IotHubDeletionFailed = 301, - IotHubExportFailed = 302, - IotHubsExportFailed = 303, - IotHubImportFailed = 304, - IotHubsImportFailed = 305, - WinFabApplicationUpgradeFailed = 306, - WinFabClusterUpgradeFailed = 307, - IotHubInvalidStateTransition = 308, - IotHubStateTransitionNotDefined = 309, - IotHubInvalidProperties = 310, - - /// - /// Not found - /// - KeyNameNotFound = 400, - - /// - /// Internal Warning Range 1000-1299 - /// - WinFabApplicationCleanupNotAttempted = 1000 - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - } -} diff --git a/iothub/device/src/Common/Exceptions/DeviceMaximumQueueDepthExceededException.cs b/iothub/device/src/Common/Exceptions/DeviceMaximumQueueDepthExceededException.cs index 88b693b4ad..ed80d08fb0 100644 --- a/iothub/device/src/Common/Exceptions/DeviceMaximumQueueDepthExceededException.cs +++ b/iothub/device/src/Common/Exceptions/DeviceMaximumQueueDepthExceededException.cs @@ -8,7 +8,9 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when an attempt to enqueue a message fails because the message queue for the device is already full. + /// This exception actually corresponds to IoTHubQuotaExceeded. For more information on what causes this error + /// and steps to resolve, see . + /// The exception type has not been changed to avoid breaking changes but the inner exception has the correct exception type. /// [Serializable] public sealed class DeviceMaximumQueueDepthExceededException : IotHubException diff --git a/iothub/device/src/Common/Exceptions/DeviceMessageLockLostException.cs b/iothub/device/src/Common/Exceptions/DeviceMessageLockLostException.cs index e296539033..c2c0d80a08 100644 --- a/iothub/device/src/Common/Exceptions/DeviceMessageLockLostException.cs +++ b/iothub/device/src/Common/Exceptions/DeviceMessageLockLostException.cs @@ -8,10 +8,15 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when an attempt to communicate with a device fails because the lock token was lost (if the connection is lost and regained for example). This timeout has the same effect as if the message was abandonned. + /// This exception is thrown when attempting to reject/abandon/complete a cloud-to-device message with a lock + /// token that has already expired. The lock token expires after the lock timeout set by the service, or if your + /// client connection was lost and regained while receiving the message but before you could reject/abandon/complete it. /// /// - /// An abandoned message will be re-enqueued in the per-device queue, and the instance will receive it again. A rejected message will be deleted from the queue and not received again by the device. + /// An abandoned message will be re-enqueued in the per-device/module queue, and the instance will receive it again. + /// A rejected message will be deleted from the queue and not received again by the device. + /// For more information on the cause for this error and how to resolve, see . + /// For more information on cloud-to-device message lifecycle, see . /// [Serializable] public class DeviceMessageLockLostException : IotHubException diff --git a/iothub/device/src/Common/Exceptions/DeviceNotFoundException.cs b/iothub/device/src/Common/Exceptions/DeviceNotFoundException.cs index 32cf197bd0..270f5f8e6a 100644 --- a/iothub/device/src/Common/Exceptions/DeviceNotFoundException.cs +++ b/iothub/device/src/Common/Exceptions/DeviceNotFoundException.cs @@ -8,7 +8,13 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when an attempt to communicate with a device fails because the given device identifier cannot be found. + /// The exception is thrown when the device is disabled and will be used to set the status to device disabled in the + /// connection status handler. This exception also corresponds to the following error codes on operation responses: + /// + /// AmqpErrorCode.NotFound + /// HttpStatusCode.NotFound + /// HttpStatusCode.NoContent + /// /// [Serializable] public sealed class DeviceNotFoundException : IotHubException diff --git a/iothub/device/src/Common/Exceptions/ExceptionHandlingHelper.cs b/iothub/device/src/Common/Exceptions/ExceptionHandlingHelper.cs index 46a984c2fe..aceb772cef 100644 --- a/iothub/device/src/Common/Exceptions/ExceptionHandlingHelper.cs +++ b/iothub/device/src/Common/Exceptions/ExceptionHandlingHelper.cs @@ -24,7 +24,7 @@ public static IDictionary new MessageTooLargeException(await GetExceptionMessageAsync(response).ConfigureAwait(false))); mappings.Add(HttpStatusCode.InternalServerError, async (response) => new ServerErrorException(await GetExceptionMessageAsync(response).ConfigureAwait(false))); mappings.Add(HttpStatusCode.ServiceUnavailable, async (response) => new ServerBusyException(await GetExceptionMessageAsync(response).ConfigureAwait(false))); - mappings.Add((System.Net.HttpStatusCode)429, async (response) => new IotHubThrottledException(await GetExceptionMessageAsync(response).ConfigureAwait(false), null)); + mappings.Add((HttpStatusCode)429, async (response) => new IotHubThrottledException(await GetExceptionMessageAsync(response).ConfigureAwait(false), null)); return mappings; } diff --git a/iothub/device/src/Common/Exceptions/IotHubCommunicationException.cs b/iothub/device/src/Common/Exceptions/IotHubCommunicationException.cs index 23a445b09b..165433891e 100644 --- a/iothub/device/src/Common/Exceptions/IotHubCommunicationException.cs +++ b/iothub/device/src/Common/Exceptions/IotHubCommunicationException.cs @@ -7,8 +7,16 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when an attempt to communicate with the IoT Hub service fails. + /// This exception is thrown when an attempt to communicate with the IoT hub service fails due to transient + /// network errors after exhausting all the retries based on the retry policy set on the client or + /// due to operation timeouts. /// + /// + /// By default, the SDK indefinitely retries dropped connections, unless the retry policy is overridden. + /// For more information on the SDK's retry policy and how to override it, see . + /// When the exception is thrown due to operation timeouts, the inner exception will have OperationCanceledException. + /// Retrying operations failed due to timeouts could resolve the error. + /// [Serializable] public sealed class IotHubCommunicationException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/IotHubSuspendedException.cs b/iothub/device/src/Common/Exceptions/IotHubSuspendedException.cs index 92ef6d7450..9016aff6e6 100644 --- a/iothub/device/src/Common/Exceptions/IotHubSuspendedException.cs +++ b/iothub/device/src/Common/Exceptions/IotHubSuspendedException.cs @@ -8,7 +8,8 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when the IoT Hub has been suspended. + /// This exception is thrown when the IoT hub has been suspended. This is likely due to exceeding Azure + /// spending limits. To resolve the error, check the Azure bill and ensure there are enough credits. /// [Serializable] public class IotHubSuspendedException : IotHubException diff --git a/iothub/device/src/Common/Exceptions/IotHubThrottledException.cs b/iothub/device/src/Common/Exceptions/IotHubThrottledException.cs index 3de839542d..2e4a4ad7a2 100644 --- a/iothub/device/src/Common/Exceptions/IotHubThrottledException.cs +++ b/iothub/device/src/Common/Exceptions/IotHubThrottledException.cs @@ -8,8 +8,12 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when the service requires exponential back-off because it has exceeded the maximum number of allowed active requests. + /// This exception is thrown when the requests to the IoT hub exceed the limits based on the tier of the hub. + /// Retrying with exponential back-off could resolve this error. /// + /// + /// For information on the IoT hub quotas and throttling, see . + /// [Serializable] public sealed class IotHubThrottledException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/MessageTooLargeException.cs b/iothub/device/src/Common/Exceptions/MessageTooLargeException.cs index 2ee5d1a167..964ae011c2 100644 --- a/iothub/device/src/Common/Exceptions/MessageTooLargeException.cs +++ b/iothub/device/src/Common/Exceptions/MessageTooLargeException.cs @@ -10,6 +10,9 @@ namespace Microsoft.Azure.Devices.Client.Exceptions /// /// The exception that is thrown when an attempt to send a message fails because the length of the message exceeds the maximum size allowed. /// + /// + /// When the message is too large for IoT Hub you will receive this exception. You should attempt to reduce your message size and send again. For more information on message sizes, see IoT Hub quotas and throttling | Other limits + /// [Serializable] public sealed class MessageTooLargeException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/QuotaExceededException.cs b/iothub/device/src/Common/Exceptions/QuotaExceededException.cs index 6b2755b8b5..f32923ddac 100644 --- a/iothub/device/src/Common/Exceptions/QuotaExceededException.cs +++ b/iothub/device/src/Common/Exceptions/QuotaExceededException.cs @@ -7,8 +7,11 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when an attempt to add a device fails because the maximum number of registered devices has been reached. + /// The exception that is thrown by the device client when the daily message quota for the IoT hub is exceeded. /// + /// + /// To resolve this exception please review the Troubleshoot Quota Exceeded guide. + /// [Serializable] public sealed class QuotaExceededException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/ServerBusyException.cs b/iothub/device/src/Common/Exceptions/ServerBusyException.cs index 97ea195aee..da72daec63 100644 --- a/iothub/device/src/Common/Exceptions/ServerBusyException.cs +++ b/iothub/device/src/Common/Exceptions/ServerBusyException.cs @@ -9,6 +9,11 @@ namespace Microsoft.Azure.Devices.Client.Exceptions /// /// The exception that is thrown when the IoT Hub is busy. /// + /// + /// This exception typically means the service is unavailable due to high load or an unexpected error and is usually transient. + /// The best course of action is to retry your operation after some time. + /// By default, the SDK will utilize the retry strategy. + /// [Serializable] public sealed class ServerBusyException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/ServerErrorException.cs b/iothub/device/src/Common/Exceptions/ServerErrorException.cs index 3b907a940d..02ca731e78 100644 --- a/iothub/device/src/Common/Exceptions/ServerErrorException.cs +++ b/iothub/device/src/Common/Exceptions/ServerErrorException.cs @@ -6,8 +6,14 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown when the IoT Hub returned an error code. + /// The exception that is thrown when the IoT Hub returned an internal service error. /// + /// + /// This exception typically means the IoT Hub service has encountered an unexpected error and is usually transient. + /// Please review the 500xxx Internal errors + /// guide for more information. The best course of action is to retry your operation after some time. By default, + /// the SDK will utilize the retry strategy. + /// [Serializable] public sealed class ServerErrorException : IotHubException { diff --git a/iothub/device/src/Common/Exceptions/UnauthorizedException.cs b/iothub/device/src/Common/Exceptions/UnauthorizedException.cs index 1de938b09f..c0e20172c7 100644 --- a/iothub/device/src/Common/Exceptions/UnauthorizedException.cs +++ b/iothub/device/src/Common/Exceptions/UnauthorizedException.cs @@ -7,8 +7,11 @@ namespace Microsoft.Azure.Devices.Client.Exceptions { /// - /// The exception that is thrown if the current operation was not authorized. + /// The exception that is thrown when there is an authorization error. /// + /// + /// This exception means the client is not authorized to use the specified IoT Hub. Please review the 401003 IoTHubUnauthorized guide for more information. + /// [Serializable] public sealed class UnauthorizedException : IotHubException { diff --git a/iothub/device/src/Edge/TrustBundleProvider.cs b/iothub/device/src/Edge/TrustBundleProvider.cs index 949b214559..d7bf525a03 100644 --- a/iothub/device/src/Edge/TrustBundleProvider.cs +++ b/iothub/device/src/Edge/TrustBundleProvider.cs @@ -17,7 +17,7 @@ internal class TrustBundleProvider : ITrustBundleProvider private static readonly ITransientErrorDetectionStrategy s_transientErrorDetectionStrategy = new ErrorDetectionStrategy(); private static readonly RetryStrategy s_transientRetryStrategy = - new TransientFaultHandling.ExponentialBackoff( + new ExponentialBackoffRetryStrategy( retryCount: 3, minBackoff: TimeSpan.FromSeconds(2), maxBackoff: TimeSpan.FromSeconds(30), diff --git a/iothub/device/src/HsmAuthentication/HttpHsmSignatureProvider.cs b/iothub/device/src/HsmAuthentication/HttpHsmSignatureProvider.cs index 98d1c2d27b..64ad119b95 100644 --- a/iothub/device/src/HsmAuthentication/HttpHsmSignatureProvider.cs +++ b/iothub/device/src/HsmAuthentication/HttpHsmSignatureProvider.cs @@ -25,8 +25,11 @@ internal class HttpHsmSignatureProvider : ISignatureProvider private static readonly ITransientErrorDetectionStrategy s_transientErrorDetectionStrategy = new ErrorDetectionStrategy(); - private static readonly RetryStrategy s_transientRetryStrategy = - new TransientFaultHandling.ExponentialBackoff(retryCount: 3, minBackoff: TimeSpan.FromSeconds(2), maxBackoff: TimeSpan.FromSeconds(30), deltaBackoff: TimeSpan.FromSeconds(3)); + private static readonly RetryStrategy s_transientRetryStrategy = new ExponentialBackoffRetryStrategy( + retryCount: 3, + minBackoff: TimeSpan.FromSeconds(2), + maxBackoff: TimeSpan.FromSeconds(30), + deltaBackoff: TimeSpan.FromSeconds(3)); public HttpHsmSignatureProvider(string providerUri, string apiVersion) { @@ -69,7 +72,8 @@ public async Task SignAsync(string moduleId, string generationId, string BaseUrl = HttpClientHelper.GetBaseUrl(_providerUri) }; - SignResponse response = await SignAsyncWithRetryAsync(hsmHttpClient, moduleId, generationId, signRequest).ConfigureAwait(false); + SignResponse response = await SignAsyncWithRetryAsync(hsmHttpClient, moduleId, generationId, signRequest) + .ConfigureAwait(false); return Convert.ToBase64String(response.Digest); } @@ -91,10 +95,16 @@ public async Task SignAsync(string moduleId, string generationId, string } } - private async Task SignAsyncWithRetryAsync(HttpHsmClient hsmHttpClient, string moduleId, string generationId, SignRequest signRequest) + private async Task SignAsyncWithRetryAsync( + HttpHsmClient hsmHttpClient, + string moduleId, + string generationId, + SignRequest signRequest) { var transientRetryPolicy = new RetryPolicy(s_transientErrorDetectionStrategy, s_transientRetryStrategy); - SignResponse response = await transientRetryPolicy.ExecuteAsync(() => hsmHttpClient.SignAsync(_apiVersion, moduleId, generationId, signRequest)).ConfigureAwait(false); + SignResponse response = await transientRetryPolicy + .ExecuteAsync(() => hsmHttpClient.SignAsync(_apiVersion, moduleId, generationId, signRequest)) + .ConfigureAwait(false); return response; } diff --git a/iothub/device/src/Http1TransportSettings.cs b/iothub/device/src/Http1TransportSettings.cs index b16efc486d..d8d638d30e 100644 --- a/iothub/device/src/Http1TransportSettings.cs +++ b/iothub/device/src/Http1TransportSettings.cs @@ -13,7 +13,7 @@ namespace Microsoft.Azure.Devices.Client /// public sealed class Http1TransportSettings : ITransportSettings { - private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromMinutes(1); /// /// Initializes a new instance of the class. @@ -40,6 +40,9 @@ public TransportType GetTransportType() /// /// The time to wait for a receive operation. The default value is 1 minute. /// + /// + /// This property is currently unused. + /// public TimeSpan DefaultReceiveTimeout => s_defaultOperationTimeout; /// diff --git a/iothub/device/src/ITransportSettings.cs b/iothub/device/src/ITransportSettings.cs index 045e932eb6..e4fccefdc3 100644 --- a/iothub/device/src/ITransportSettings.cs +++ b/iothub/device/src/ITransportSettings.cs @@ -17,7 +17,7 @@ public interface ITransportSettings TransportType GetTransportType(); /// - /// The default receive timeout. + /// The time to wait for a receive operation. /// TimeSpan DefaultReceiveTimeout { get; } } diff --git a/iothub/device/src/InternalClient.cs b/iothub/device/src/InternalClient.cs index 4a3468af90..b583802eb9 100644 --- a/iothub/device/src/InternalClient.cs +++ b/iothub/device/src/InternalClient.cs @@ -1409,13 +1409,20 @@ private async Task OnDeviceMessageReceivedAsync(Message message) // Grab this semaphore so that there is no chance that the _deviceReceiveMessageCallback instance is set in between the read of the // item1 and the read of the item2 await _deviceReceiveMessageSemaphore.WaitAsync().ConfigureAwait(false); - ReceiveMessageCallback callback = _deviceReceiveMessageCallback?.Item1; - object callbackContext = _deviceReceiveMessageCallback?.Item2; - _deviceReceiveMessageSemaphore.Release(); - if (callback != null) + try + { + ReceiveMessageCallback callback = _deviceReceiveMessageCallback?.Item1; + object callbackContext = _deviceReceiveMessageCallback?.Item2; + + if (callback != null) + { + _ = callback.Invoke(message, callbackContext); + } + } + finally { - await callback.Invoke(message, callbackContext).ConfigureAwait(false); + _deviceReceiveMessageSemaphore.Release(); } if (Logging.IsEnabled) diff --git a/iothub/device/src/IotHubConnectionStringBuilder.cs b/iothub/device/src/IotHubConnectionStringBuilder.cs index 1b731910c8..f235da764d 100644 --- a/iothub/device/src/IotHubConnectionStringBuilder.cs +++ b/iothub/device/src/IotHubConnectionStringBuilder.cs @@ -289,7 +289,7 @@ private void Validate() ValidateFormat(DeviceId, DeviceIdPropertyName, s_idNameRegex); if (!string.IsNullOrEmpty(ModuleId)) { - ValidateFormat(ModuleId, DeviceIdPropertyName, s_idNameRegex); + ValidateFormat(ModuleId, ModuleIdPropertyName, s_idNameRegex); } ValidateFormatIfSpecified(SharedAccessKeyName, SharedAccessKeyNamePropertyName, s_sharedAccessKeyNameRegex); diff --git a/iothub/device/src/Microsoft.Azure.Devices.Client.csproj b/iothub/device/src/Microsoft.Azure.Devices.Client.csproj index c16021849f..59f0aafab8 100644 --- a/iothub/device/src/Microsoft.Azure.Devices.Client.csproj +++ b/iothub/device/src/Microsoft.Azure.Devices.Client.csproj @@ -25,7 +25,7 @@ - 1.37.2 + 1.38.0 Microsoft Azure IoT Device Client SDK True True @@ -68,6 +68,8 @@ + + @@ -75,6 +77,8 @@ + + @@ -82,8 +86,6 @@ - - diff --git a/iothub/device/src/RetryPolicies/ExponentialBackoff.cs b/iothub/device/src/RetryPolicies/ExponentialBackoff.cs index dd1d7fc333..e46d149962 100644 --- a/iothub/device/src/RetryPolicies/ExponentialBackoff.cs +++ b/iothub/device/src/RetryPolicies/ExponentialBackoff.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using Microsoft.Azure.Devices.Client.TransientFaultHandling; namespace Microsoft.Azure.Devices.Client { @@ -10,7 +11,7 @@ namespace Microsoft.Azure.Devices.Client /// public class ExponentialBackoff : IRetryPolicy { - private readonly TransientFaultHandling.ExponentialBackoff _exponentialBackoffRetryStrategy; + private readonly ExponentialBackoffRetryStrategy _exponentialBackoffRetryStrategy; /// /// Creates an instance of ExponentialBackoff. @@ -22,7 +23,7 @@ public class ExponentialBackoff : IRetryPolicy public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) { - _exponentialBackoffRetryStrategy = new TransientFaultHandling.ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff); + _exponentialBackoffRetryStrategy = new ExponentialBackoffRetryStrategy(retryCount, minBackoff, maxBackoff, deltaBackoff); } /// diff --git a/iothub/device/src/TransientFaultHandling/ExponentialBackoff.cs b/iothub/device/src/TransientFaultHandling/ExponentialBackoffRetryStrategy.cs similarity index 66% rename from iothub/device/src/TransientFaultHandling/ExponentialBackoff.cs rename to iothub/device/src/TransientFaultHandling/ExponentialBackoffRetryStrategy.cs index 0813e4c20c..b76c505610 100644 --- a/iothub/device/src/TransientFaultHandling/ExponentialBackoff.cs +++ b/iothub/device/src/TransientFaultHandling/ExponentialBackoffRetryStrategy.cs @@ -1,72 +1,76 @@ -//Copyright(c) Microsoft.All rights reserved. -//Microsoft would like to thank its contributors, a list -//of whom are at http://aka.ms/entlib-contributors +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Microsoft would like to thank its contributors, a list of whom are at http://aka.ms/entlib-contributors using System; -//Licensed under the Apache License, Version 2.0 (the "License"); you -//may not use this file except in compliance with the License. You may -//obtain a copy of the License at +// Source licensed under the Apache License, Version 2.0 (the "License"); you +// may not use this file except in compliance with the License. You may +// obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 -//Unless required by applicable law or agreed to in writing, software -//distributed under the License is distributed on an "AS IS" BASIS, -//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -//implied. See the License for the specific language governing permissions -//and limitations under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing permissions +// and limitations under the License. // THIS FILE HAS BEEN MODIFIED FROM ITS ORIGINAL FORM. // Change Log: // 9/1/2017 jasminel Renamed namespace to Microsoft.Azure.Devices.Client.TransientFaultHandling and modified access modifier to internal. +// 7/12/2021 drwill Renamed class from ExponentialBackoff to ExponentialBackoffRetryStrategy to avoid naming internal conflict. namespace Microsoft.Azure.Devices.Client.TransientFaultHandling { /// /// A retry strategy with back-off parameters for calculating the exponential delay between retries. /// - internal class ExponentialBackoff : RetryStrategy + internal class ExponentialBackoffRetryStrategy : RetryStrategy { - private readonly int _retryCount; + private static readonly Random s_random = new Random(); + private readonly int _retryCount; private readonly TimeSpan _minBackoff; - private readonly TimeSpan _maxBackoff; - private readonly TimeSpan _deltaBackoff; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ExponentialBackoff() : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) + public ExponentialBackoffRetryStrategy() + : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) { } /// - /// Initializes a new instance of the class with the specified retry settings. + /// Initializes a new instance of the class with the specified retry settings. /// /// The maximum number of retry attempts. /// The minimum back-off time /// The maximum back-off time. /// The value that will be used to calculate a random delta in the exponential delay between retries. - public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, RetryStrategy.DefaultFirstFastRetry) + public ExponentialBackoffRetryStrategy(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, RetryStrategy.DefaultFirstFastRetry) { } /// - /// Initializes a new instance of the class with the specified name and retry settings. + /// Initializes a new instance of the class with the specified name and retry settings. /// /// The name of the retry strategy. /// The maximum number of retry attempts. /// The minimum back-off time /// The maximum back-off time. /// The value that will be used to calculate a random delta in the exponential delay between retries. - public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, RetryStrategy.DefaultFirstFastRetry) + public ExponentialBackoffRetryStrategy(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, RetryStrategy.DefaultFirstFastRetry) { } /// - /// Initializes a new instance of the class with the specified name, retry settings, and fast retry option. + /// Initializes a new instance of the class with the specified name, retry settings, and fast retry option. /// /// The name of the retry strategy. /// The maximum number of retry attempts. @@ -74,7 +78,8 @@ public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, Time /// The maximum back-off time. /// The value that will be used to calculate a random delta in the exponential delay between retries. /// true to immediately retry in the first attempt; otherwise, false. The subsequent retries will remain subject to the configured retry interval. - public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) : base(name, firstFastRetry) + public ExponentialBackoffRetryStrategy(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) + : base(name, firstFastRetry) { Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); @@ -97,11 +102,9 @@ public override ShouldRetry GetShouldRetry() { if (currentRetryCount < _retryCount) { - Random random = new Random(); - double exponentialInterval = (Math.Pow(2.0, currentRetryCount) - 1.0) - * random.Next( + * s_random.Next( (int)_deltaBackoff.TotalMilliseconds * 8 / 10, (int)_deltaBackoff.TotalMilliseconds * 12 / 10) + _minBackoff.TotalMilliseconds; diff --git a/iothub/device/src/TransientFaultHandling/RetryPolicy.cs b/iothub/device/src/TransientFaultHandling/RetryPolicy.cs index 5e46d23f65..2131a7cbec 100644 --- a/iothub/device/src/TransientFaultHandling/RetryPolicy.cs +++ b/iothub/device/src/TransientFaultHandling/RetryPolicy.cs @@ -151,7 +151,7 @@ public RetryPolicy( TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) + : this(errorDetectionStrategy, new ExponentialBackoffRetryStrategy(retryCount, minBackoff, maxBackoff, deltaBackoff)) { } diff --git a/iothub/device/src/TransientFaultHandling/RetryStrategy.cs b/iothub/device/src/TransientFaultHandling/RetryStrategy.cs index c18c5c4eef..a73ef0779e 100644 --- a/iothub/device/src/TransientFaultHandling/RetryStrategy.cs +++ b/iothub/device/src/TransientFaultHandling/RetryStrategy.cs @@ -1,24 +1,26 @@ -//Copyright(c) Microsoft.All rights reserved. -//Microsoft would like to thank its contributors, a list -//of whom are at http://aka.ms/entlib-contributors +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Microsoft would like to thank its contributors, a list of whom are at http://aka.ms/entlib-contributors using System; -//Licensed under the Apache License, Version 2.0 (the "License"); you -//may not use this file except in compliance with the License. You may -//obtain a copy of the License at +// Source licensed under the Apache License, Version 2.0 (the "License"); you +// may not use this file except in compliance with the License. You may +// obtain a copy of the License at //http://www.apache.org/licenses/LICENSE-2.0 -//Unless required by applicable law or agreed to in writing, software -//distributed under the License is distributed on an "AS IS" BASIS, -//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -//implied. See the License for the specific language governing permissions -//and limitations under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing permissions +// and limitations under the License. // THIS FILE HAS BEEN MODIFIED FROM ITS ORIGINAL FORM. // Change Log: // 9/1/2017 jasminel Renamed namespace to Microsoft.Azure.Devices.Client.TransientFaultHandling and modified access modifier to internal. +// 7/12/2021 drwill Changed property+backing field to auto-property. namespace Microsoft.Azure.Devices.Client.TransientFaultHandling { @@ -63,73 +65,57 @@ internal abstract class RetryStrategy /// public const bool DefaultFirstFastRetry = true; - private static readonly RetryStrategy s_noRetry = new FixedInterval(0, DefaultRetryInterval); - - private static readonly RetryStrategy s_defaultFixed = new FixedInterval(DefaultClientRetryCount, DefaultRetryInterval); - - private static readonly RetryStrategy s_defaultProgressive = new Incremental(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement); - - private static readonly RetryStrategy s_defaultExponential = new ExponentialBackoff(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff); + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// + /// True to immediately retry in the first attempt; otherwise, false. + /// The subsequent retries will remain subject to the configured retry interval. + /// + protected RetryStrategy(string name, bool firstFastRetry) + { + Name = name; + FastFirstRetry = firstFastRetry; + } /// /// Returns a default policy that performs no retries, but invokes the action only once. /// - public static RetryStrategy NoRetry => s_noRetry; + public static RetryStrategy NoRetry { get; } = new FixedInterval(0, DefaultRetryInterval); /// - /// Returns a default policy that implements a fixed retry interval configured with the and parameters. - /// The default retry policy treats all caught exceptions as transient errors. + /// Returns a default policy that implements a fixed retry interval configured with the + /// and parameters. The default retry policy treats all caught exceptions as transient errors. /// - public static RetryStrategy DefaultFixed => s_defaultFixed; + public static RetryStrategy DefaultFixed { get; } = new FixedInterval(DefaultClientRetryCount, DefaultRetryInterval); /// - /// Returns a default policy that implements a progressive retry interval configured with the - /// , - /// , + /// Returns a default policy that implements a progressive retry interval configured with the + /// , + /// , /// and parameters. /// The default retry policy treats all caught exceptions as transient errors. /// - public static RetryStrategy DefaultProgressive => s_defaultProgressive; + public static RetryStrategy DefaultProgressive { get; } = new Incremental(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement); /// - /// Returns a default policy that implements a random exponential retry interval configured with the - /// , - /// , - /// , - /// and parameters. + /// Returns a default policy that implements a random exponential retry interval configured with the , + /// , , and parameters. /// The default retry policy treats all caught exceptions as transient errors. /// - public static RetryStrategy DefaultExponential => s_defaultExponential; + public static RetryStrategy DefaultExponential { get; } = new ExponentialBackoffRetryStrategy(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff); /// /// Gets or sets a value indicating whether the first retry attempt will be made immediately, /// whereas subsequent retries will remain subject to the retry interval. /// - public bool FastFirstRetry - { - get; - set; - } + public bool FastFirstRetry { get; set; } /// /// Gets the name of the retry strategy. /// - public string Name - { - get; - private set; - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// true to immediately retry in the first attempt; otherwise, false. The subsequent retries will remain subject to the configured retry interval. - protected RetryStrategy(string name, bool firstFastRetry) - { - Name = name; - FastFirstRetry = firstFastRetry; - } + public string Name { get; private set; } /// /// Returns the corresponding ShouldRetry delegate. diff --git a/iothub/device/src/Transport/AmqpIot/AmqpIotErrorAdapter.cs b/iothub/device/src/Transport/AmqpIot/AmqpIotErrorAdapter.cs index ee524c278e..aa4a9b982b 100644 --- a/iothub/device/src/Transport/AmqpIot/AmqpIotErrorAdapter.cs +++ b/iothub/device/src/Transport/AmqpIot/AmqpIotErrorAdapter.cs @@ -26,7 +26,6 @@ internal static class AmqpIotErrorAdapter public static readonly AmqpSymbol ArgumentError = AmqpIotConstants.Vendor + ":argument-error"; public static readonly AmqpSymbol ArgumentOutOfRangeError = AmqpIotConstants.Vendor + ":argument-out-of-range"; public static readonly AmqpSymbol DeviceContainerThrottled = AmqpIotConstants.Vendor + ":device-container-throttled"; - public static readonly AmqpSymbol PartitionNotFound = AmqpIotConstants.Vendor + ":partition-not-found"; public static readonly AmqpSymbol IotHubSuspended = AmqpIotConstants.Vendor + ":iot-hub-suspended"; public static Exception GetExceptionFromOutcome(Outcome outcome) @@ -240,8 +239,8 @@ public static Exception ToIotHubClientContract(Error error) else if (error.Condition.Equals(AmqpErrorCode.ResourceLimitExceeded)) { // Note: The DeviceMaximumQueueDepthExceededException is not supposed to be thrown here as it is being mapped to the incorrect error code - // Error code 403004 is only applicable to C2D (Service client); see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-troubleshoot-error-403004-devicemaximumqueuedepthexceeded - // Error code 403002 is applicable to D2C (Device client); see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-troubleshoot-error-403002-iothubquotaexceeded + // Error code 403004 is only applicable to C2D (Service client); see https://docs.microsoft.com/azure/iot-hub/iot-hub-troubleshoot-error-403004-devicemaximumqueuedepthexceeded + // Error code 403002 is applicable to D2C (Device client); see https://docs.microsoft.com/azure/iot-hub/iot-hub-troubleshoot-error-403002-iothubquotaexceeded // We have opted not to change the exception type thrown here since it will be a breaking change, alternatively, we are adding the correct exception type // as the inner exception. retException = new DeviceMaximumQueueDepthExceededException( diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs b/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs index 99fe30cbd9..65d46003b9 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs @@ -99,7 +99,7 @@ public bool CertificateRevocationCheck public bool DeviceReceiveAckCanTimeout { get; set; } /// - /// The time a device will wait, for an acknowledgment from service. + /// The time a device will wait for an acknowledgment from service. /// The default is 5 minutes. /// /// @@ -182,12 +182,14 @@ public bool CertificateRevocationCheck public bool CleanSession { get; set; } /// - /// The interval, in seconds, that the client establishes with the service, for sending keep alive pings. + /// The interval, in seconds, that the client establishes with the service, for sending keep-alive pings. /// The default is 300 seconds. /// /// /// The client will send a ping request 4 times per keep-alive duration set. /// It will wait for 30 seconds for the ping response, else mark the connection as disconnected. + /// Setting a very low keep-alive value can cause aggressive reconnects, and might not give the + /// client enough time to establish a connection before disconnecting and reconnecting. /// public int KeepAliveInSeconds { get; set; } diff --git a/iothub/device/tests/ExponentialBackoffTests.cs b/iothub/device/tests/ExponentialBackoffTests.cs index 8abb193ef5..9d4d756cea 100644 --- a/iothub/device/tests/ExponentialBackoffTests.cs +++ b/iothub/device/tests/ExponentialBackoffTests.cs @@ -16,7 +16,11 @@ public class ExponentialBackoffTests [TestCategory("Unit")] public void ExponentialBackoffDoesNotUnderflow() { - var exponentialBackoff = new TransientFaultHandling.ExponentialBackoff(MAX_RETRY_ATTEMPTS, RetryStrategy.DefaultMinBackoff, RetryStrategy.DefaultMaxBackoff, RetryStrategy.DefaultClientBackoff); + var exponentialBackoff = new ExponentialBackoffRetryStrategy( + MAX_RETRY_ATTEMPTS, + RetryStrategy.DefaultMinBackoff, + RetryStrategy.DefaultMaxBackoff, + RetryStrategy.DefaultClientBackoff); ShouldRetry shouldRetry = exponentialBackoff.GetShouldRetry(); for (int i = 1; i < MAX_RETRY_ATTEMPTS; i++) { diff --git a/iothub/device/tests/Microsoft.Azure.Devices.Client.Tests.csproj b/iothub/device/tests/Microsoft.Azure.Devices.Client.Tests.csproj index 3928ef3cfa..ea152b0a59 100644 --- a/iothub/device/tests/Microsoft.Azure.Devices.Client.Tests.csproj +++ b/iothub/device/tests/Microsoft.Azure.Devices.Client.Tests.csproj @@ -14,13 +14,17 @@ HsmAuthentication/**;$(DefaultItemExcludes) - - + + + + + + @@ -30,20 +34,12 @@ + + - - - - - - - - - - diff --git a/iothub/service/src/AmqpServiceClient.cs b/iothub/service/src/AmqpServiceClient.cs deleted file mode 100644 index 232f69e205..0000000000 --- a/iothub/service/src/AmqpServiceClient.cs +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Amqp; -using Microsoft.Azure.Amqp.Framing; -using Microsoft.Azure.Devices.Common; -using Microsoft.Azure.Devices.Common.Data; -using Microsoft.Azure.Devices.Common.Exceptions; -using Microsoft.Azure.Devices.Shared; -using AmqpTrace = Microsoft.Azure.Amqp.AmqpTrace; - -namespace Microsoft.Azure.Devices -{ - // This class uses a combination of AMQP and HTTP clients to perform operations. - internal sealed class AmqpServiceClient : ServiceClient - { - private const string StatisticsUriFormat = "/statistics/service?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string PurgeMessageQueueFormat = "/devices/{0}/commands?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string SendingPath = "/messages/deviceBound"; - - private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); - - private readonly FaultTolerantAmqpObject _faultTolerantSendingLink; - private readonly AmqpFeedbackReceiver _feedbackReceiver; - private readonly AmqpFileNotificationReceiver _fileNotificationReceiver; - private readonly IHttpClientHelper _httpClientHelper; - private readonly string _iotHubName; - private readonly ServiceClientOptions _clientOptions; - - private int _sendingDeliveryTag; - - public AmqpServiceClient( - IotHubConnectionProperties connectionProperties, - bool useWebSocketOnly, - ServiceClientTransportSettings transportSettings, - ServiceClientOptions options) - { - var iotHubConnection = new IotHubConnection(connectionProperties, useWebSocketOnly, transportSettings); - Connection = iotHubConnection; - OpenTimeout = IotHubConnection.DefaultOpenTimeout; - OperationTimeout = IotHubConnection.DefaultOperationTimeout; - _faultTolerantSendingLink = new FaultTolerantAmqpObject(CreateSendingLinkAsync, Connection.CloseLink); - _feedbackReceiver = new AmqpFeedbackReceiver(Connection); - _fileNotificationReceiver = new AmqpFileNotificationReceiver(Connection); - _iotHubName = connectionProperties.IotHubName; - _clientOptions = options; - _httpClientHelper = new HttpClientHelper( - connectionProperties.HttpsEndpoint, - connectionProperties, - ExceptionHandlingHelper.GetDefaultErrorMapping(), - s_defaultOperationTimeout, - transportSettings.HttpProxy, - transportSettings.ConnectionLeaseTimeoutMilliseconds); - - // Set the trace provider for the AMQP library. - AmqpTrace.Provider = new AmqpTransportLog(); - } - - internal AmqpServiceClient(IHttpClientHelper httpClientHelper) : base() - { - _httpClientHelper = httpClientHelper; - } - - internal AmqpServiceClient(IotHubConnection iotHubConnection, IHttpClientHelper httpClientHelper) - { - Connection = iotHubConnection; - _faultTolerantSendingLink = new FaultTolerantAmqpObject(CreateSendingLinkAsync, iotHubConnection.CloseLink); - _feedbackReceiver = new AmqpFeedbackReceiver(iotHubConnection); - _fileNotificationReceiver = new AmqpFileNotificationReceiver(iotHubConnection); - _httpClientHelper = httpClientHelper; - } - - public TimeSpan OpenTimeout { get; private set; } - - public TimeSpan OperationTimeout { get; private set; } - - public IotHubConnection Connection { get; private set; } - - // This call is executed over AMQP. - public override async Task OpenAsync() - { - Logging.Enter(this, $"Opening AmqpServiceClient", nameof(OpenAsync)); - - await _faultTolerantSendingLink.OpenAsync(OpenTimeout).ConfigureAwait(false); - await _feedbackReceiver.OpenAsync().ConfigureAwait(false); - - Logging.Exit(this, $"Opening AmqpServiceClient", nameof(OpenAsync)); - } - - // This call is executed over AMQP. - public async override Task CloseAsync() - { - Logging.Enter(this, $"Closing AmqpServiceClient", nameof(CloseAsync)); - - await _faultTolerantSendingLink.CloseAsync().ConfigureAwait(false); - await _feedbackReceiver.CloseAsync().ConfigureAwait(false); - await _fileNotificationReceiver.CloseAsync().ConfigureAwait(false); - await Connection.CloseAsync().ConfigureAwait(false); - - Logging.Exit(this, $"Closing AmqpServiceClient", nameof(CloseAsync)); - } - - // This call is executed over AMQP. - public async override Task SendAsync(string deviceId, Message message, TimeSpan? timeout = null) - { - Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}", nameof(SendAsync)); - - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentNullException(nameof(deviceId)); - } - - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - if (_clientOptions?.SdkAssignsMessageId == SdkAssignsMessageId.WhenUnset && message.MessageId == null) - { - message.MessageId = Guid.NewGuid().ToString(); - } - - if (message.IsBodyCalled) - { - message.ResetBody(); - } - - timeout ??= OperationTimeout; - - using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); - amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/messages/deviceBound"; - - try - { - SendingAmqpLink sendingLink = await GetSendingLinkAsync().ConfigureAwait(false); - Outcome outcome = await sendingLink - .SendMessageAsync(amqpMessage, IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), AmqpConstants.NullBinary, timeout.Value) - .ConfigureAwait(false); - - Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); - - if (outcome.DescriptorCode != Accepted.Code) - { - throw AmqpErrorMapper.GetExceptionFromOutcome(outcome); - } - } - catch (Exception ex) when (!(ex is TimeoutException) && !ex.IsFatal()) - { - Logging.Error(this, $"{nameof(SendAsync)} threw an exception: {ex}", nameof(SendAsync)); - throw AmqpClientHelper.ToIotHubClientContract(ex); - } - finally - { - Logging.Exit(this, $"Sending message [{message?.MessageId}] for device {deviceId}", nameof(SendAsync)); - } - } - - // This call is executed over HTTP. - public override Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Purging message queue for device: {deviceId}", nameof(PurgeMessageQueueAsync)); - - try - { - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new DeviceNotFoundException(deviceId)) } - }; - - return _httpClientHelper.DeleteAsync(GetPurgeMessageQueueAsyncUri(deviceId), errorMappingOverrides, null, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(PurgeMessageQueueAsync)} threw an exception: {ex}", nameof(PurgeMessageQueueAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Purging message queue for device: {deviceId}", nameof(PurgeMessageQueueAsync)); - } - } - - // This call is executed over AMQP. - public override FeedbackReceiver GetFeedbackReceiver() - { - return _feedbackReceiver; - } - - // This call is executed over AMQP. - public override FileNotificationReceiver GetFileNotificationReceiver() - { - return _fileNotificationReceiver; - } - - // This call is executed over HTTP. - public override Task GetServiceStatisticsAsync(CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting service statistics", nameof(GetServiceStatisticsAsync)); - - try - { - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } - }; - - return _httpClientHelper.GetAsync(GetStatisticsUri(), errorMappingOverrides, null, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetServiceStatisticsAsync)} threw an exception: {ex}", nameof(GetServiceStatisticsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting service statistics", nameof(GetServiceStatisticsAsync)); - } - } - - // This call is executed over HTTP. - public override Task InvokeDeviceMethodAsync(string deviceId, - CloudToDeviceMethod cloudToDeviceMethod, - CancellationToken cancellationToken) - { - return InvokeDeviceMethodAsync(GetDeviceMethodUri(deviceId), cloudToDeviceMethod, cancellationToken); - } - - // This call is executed over HTTP. - private Task InvokeDeviceMethodAsync(Uri uri, - CloudToDeviceMethod cloudToDeviceMethod, - CancellationToken cancellationToken) - { - Logging.Enter(this, $"Invoking device method for: {uri}", nameof(InvokeDeviceMethodAsync)); - - try - { - TimeSpan timeout = GetInvokeDeviceMethodOperationTimeout(cloudToDeviceMethod); - - return _httpClientHelper.PostAsync( - uri, - cloudToDeviceMethod, - timeout, - null, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(InvokeDeviceMethodAsync)} threw an exception: {ex}", nameof(InvokeDeviceMethodAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Invoking device method for: {uri}", nameof(InvokeDeviceMethodAsync)); - } - } - - // This call is executed over HTTP. - public override Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentNullException(nameof(deviceId)); - } - - if (string.IsNullOrWhiteSpace(moduleId)) - { - throw new ArgumentNullException(nameof(moduleId)); - } - - return InvokeDeviceMethodAsync(GetModuleMethodUri(deviceId, moduleId), cloudToDeviceMethod, cancellationToken); - } - - // This call is executed over AMQP. - public override async Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null) - { - Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); - - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentNullException(nameof(deviceId)); - } - - if (string.IsNullOrWhiteSpace(moduleId)) - { - throw new ArgumentNullException(nameof(moduleId)); - } - - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - if (_clientOptions?.SdkAssignsMessageId == SdkAssignsMessageId.WhenUnset && message.MessageId == null) - { - message.MessageId = Guid.NewGuid().ToString(); - } - - if (message.IsBodyCalled) - { - message.ResetBody(); - } - - timeout ??= OperationTimeout; - - using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); - amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/modules/" + WebUtility.UrlEncode(moduleId) + "/messages/deviceBound"; - try - { - SendingAmqpLink sendingLink = await GetSendingLinkAsync().ConfigureAwait(false); - Outcome outcome = await sendingLink - .SendMessageAsync( - amqpMessage, - IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), - AmqpConstants.NullBinary, - timeout.Value) - .ConfigureAwait(false); - - Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); - - if (outcome.DescriptorCode != Accepted.Code) - { - throw AmqpErrorMapper.GetExceptionFromOutcome(outcome); - } - } - catch (Exception ex) when (!ex.IsFatal()) - { - Logging.Error(this, $"{nameof(SendAsync)} threw an exception: {ex}", nameof(SendAsync)); - throw AmqpClientHelper.ToIotHubClientContract(ex); - } - finally - { - Logging.Exit(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); - } - } - - private async Task GetSendingLinkAsync() - { - Logging.Enter(this, $"_faultTolerantSendingLink = {_faultTolerantSendingLink?.GetHashCode()}", nameof(GetSendingLinkAsync)); - - try - { - if (!_faultTolerantSendingLink.TryGetOpenedObject(out SendingAmqpLink sendingLink)) - { - sendingLink = await _faultTolerantSendingLink.GetOrCreateAsync(OpenTimeout).ConfigureAwait(false); - } - - Logging.Info(this, $"Retrieved SendingAmqpLink [{sendingLink?.Name}]", nameof(GetSendingLinkAsync)); - - return sendingLink; - } - finally - { - Logging.Exit(this, $"_faultTolerantSendingLink = {_faultTolerantSendingLink?.GetHashCode()}", nameof(GetSendingLinkAsync)); - } - } - - private Task CreateSendingLinkAsync(TimeSpan timeout) - { - return Connection.CreateSendingLinkAsync(SendingPath, timeout); - } - - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - _faultTolerantSendingLink.Dispose(); - _fileNotificationReceiver.Dispose(); - _feedbackReceiver.Dispose(); - Connection.Dispose(); - _httpClientHelper.Dispose(); - } - } - - private static TimeSpan GetInvokeDeviceMethodOperationTimeout(CloudToDeviceMethod cloudToDeviceMethod) - { - // For InvokeDeviceMethod, we need to take into account the timeouts specified - // for the Device to connect and send a response. We also need to take into account - // the transmission time for the request send/receive - var timeout = TimeSpan.FromSeconds(15); // For wire time - timeout += TimeSpan.FromSeconds(cloudToDeviceMethod.ConnectionTimeoutInSeconds ?? 0); - timeout += TimeSpan.FromSeconds(cloudToDeviceMethod.ResponseTimeoutInSeconds ?? 0); - return timeout <= s_defaultOperationTimeout ? s_defaultOperationTimeout : timeout; - } - - private static Uri GetStatisticsUri() - { - return new Uri(StatisticsUriFormat, UriKind.Relative); - } - - private static Uri GetPurgeMessageQueueAsyncUri(string deviceId) - { - return new Uri(PurgeMessageQueueFormat.FormatInvariant(deviceId), UriKind.Relative); - } - - private static Uri GetDeviceMethodUri(string deviceId) - { - deviceId = WebUtility.UrlEncode(deviceId); - return new Uri(DeviceMethodUriFormat.FormatInvariant(deviceId), UriKind.Relative); - } - - private static Uri GetModuleMethodUri(string deviceId, string moduleId) - { - deviceId = WebUtility.UrlEncode(deviceId); - moduleId = WebUtility.UrlEncode(moduleId); - return new Uri(ModuleMethodUriFormat.FormatInvariant(deviceId, moduleId), UriKind.Relative); - } - } -} diff --git a/iothub/service/src/JobClient/CloudToDeviceMethod.cs b/iothub/service/src/CloudToDeviceMethod.cs similarity index 100% rename from iothub/service/src/JobClient/CloudToDeviceMethod.cs rename to iothub/service/src/CloudToDeviceMethod.cs diff --git a/iothub/service/src/Common/Data/AmqpErrorMapper.cs b/iothub/service/src/Common/Data/AmqpErrorMapper.cs index b49a00c947..edf1a537b7 100644 --- a/iothub/service/src/Common/Data/AmqpErrorMapper.cs +++ b/iothub/service/src/Common/Data/AmqpErrorMapper.cs @@ -10,187 +10,6 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { internal static class AmqpErrorMapper { - private const int MaxSizeInInfoMap = 32 * 1024; - - public static Tuple GenerateError(Exception ex) - { - if (ex is DeviceNotFoundException deviceNotFoundException) - { - return Tuple.Create(AmqpErrorCode.NotFound.ToString(), deviceNotFoundException.Message, deviceNotFoundException.TrackingId); - } - - if (ex is DeviceAlreadyExistsException deviceAlreadyExistsException) - { - return Tuple.Create(IotHubAmqpErrorCode.DeviceAlreadyExists.ToString(), deviceAlreadyExistsException.Message, deviceAlreadyExistsException.TrackingId); - } - - if (ex is IotHubThrottledException deviceContainerThrottledException) - { - return Tuple.Create(IotHubAmqpErrorCode.DeviceContainerThrottled.ToString(), deviceContainerThrottledException.Message, deviceContainerThrottledException.TrackingId); - } - - if (ex is QuotaExceededException quotaExceededException) - { - return Tuple.Create(IotHubAmqpErrorCode.QuotaExceeded.ToString(), quotaExceededException.Message, quotaExceededException.TrackingId); - } - - if (ex is DeviceMessageLockLostException messageLockLostException) - { - return Tuple.Create(IotHubAmqpErrorCode.MessageLockLostError.ToString(), messageLockLostException.Message, messageLockLostException.TrackingId); - } - - if (ex is MessageTooLargeException deviceMessageTooLargeException) - { - return Tuple.Create(AmqpErrorCode.MessageSizeExceeded.ToString(), deviceMessageTooLargeException.Message, deviceMessageTooLargeException.TrackingId); - } - - if (ex is DeviceMaximumQueueDepthExceededException queueDepthExceededException) - { - return Tuple.Create(AmqpErrorCode.ResourceLimitExceeded.ToString(), queueDepthExceededException.Message, queueDepthExceededException.TrackingId); - } - - if (ex is PreconditionFailedException preconditionFailedException) - { - return Tuple.Create(IotHubAmqpErrorCode.PreconditionFailed.ToString(), preconditionFailedException.Message, preconditionFailedException.TrackingId); - } - - if (ex is IotHubSuspendedException iotHubSuspendedException) - { - return Tuple.Create(IotHubAmqpErrorCode.IotHubSuspended.ToString(), iotHubSuspendedException.Message, iotHubSuspendedException.TrackingId); - } - - return Tuple.Create(AmqpErrorCode.InternalError.ToString(), ex.ToStringSlim(), (string)null); - } - - public static AmqpException ToAmqpException(Exception exception) - { - return ToAmqpException(exception, false); - } - - public static AmqpException ToAmqpException(Exception exception, bool includeStackTrace) - { - Error amqpError = ToAmqpError(exception, includeStackTrace); - return new AmqpException(amqpError); - } - - public static Error ToAmqpError(Exception exception) - { - return ToAmqpError(exception, false); - } - - public static Error ToAmqpError(Exception exception, bool includeStackTrace) - { - if (exception == null) - { - throw new ArgumentNullException(nameof(exception)); - } - - var error = new Error - { - Description = exception.Message - }; - - if (exception is AmqpException) - { - var amqpException = (AmqpException)exception; - error.Condition = amqpException.Error.Condition; - error.Info = amqpException.Error.Info; - } - else if (exception is UnauthorizedAccessException || exception is UnauthorizedException) - { - error.Condition = AmqpErrorCode.UnauthorizedAccess; - } - else if (exception is NotSupportedException) - { - error.Condition = AmqpErrorCode.NotImplemented; - } - else if (exception is DeviceNotFoundException) - { - error.Condition = AmqpErrorCode.NotFound; - } - else if (exception is IotHubNotFoundException) - { - error.Condition = IotHubAmqpErrorCode.IotHubNotFoundError; - } - else if (exception is DeviceMessageLockLostException) - { - error.Condition = IotHubAmqpErrorCode.MessageLockLostError; - } - else if (exception is MessageTooLargeException) - { - error.Condition = AmqpErrorCode.MessageSizeExceeded; - } - else if (exception is DeviceMaximumQueueDepthExceededException) - { - error.Condition = AmqpErrorCode.ResourceLimitExceeded; - } - else if (exception is TimeoutException) - { - error.Condition = IotHubAmqpErrorCode.TimeoutError; - } - else if (exception is InvalidOperationException) - { - error.Condition = AmqpErrorCode.NotAllowed; - } - else if (exception is ArgumentOutOfRangeException) - { - error.Condition = IotHubAmqpErrorCode.ArgumentOutOfRangeError; - } - else if (exception is ArgumentException) - { - error.Condition = IotHubAmqpErrorCode.ArgumentError; - } - else if (exception is PreconditionFailedException) - { - error.Condition = IotHubAmqpErrorCode.PreconditionFailed; - } - else if (exception is IotHubSuspendedException) - { - error.Condition = IotHubAmqpErrorCode.IotHubSuspended; - } - else if (exception is QuotaExceededException) - { - error.Condition = IotHubAmqpErrorCode.QuotaExceeded; - } - else if (exception is TimeoutException) - { - error.Condition = IotHubAmqpErrorCode.TimeoutError; - } - else - { - error.Condition = AmqpErrorCode.InternalError; - error.Description = error.Description; - } - // we will always need this to add trackingId - if (error.Info == null) - { - error.Info = new Fields(); - } - - string stackTrace; - if (includeStackTrace && !string.IsNullOrEmpty(stackTrace = exception.StackTrace)) - { - if (stackTrace.Length > MaxSizeInInfoMap) - { - stackTrace = stackTrace.Substring(0, MaxSizeInInfoMap); - } - - // error.Info came from AmqpException then it contains StackTraceName already. - if (!error.Info.TryGetValue(IotHubAmqpProperty.StackTraceName, out string _)) - { - error.Info.Add(IotHubAmqpProperty.StackTraceName, stackTrace); - } - } - - error.Info.TryGetValue(IotHubAmqpProperty.TrackingId, out string trackingId); -#pragma warning disable CS0618 // Type or member is obsolete only for external dependency. - trackingId = TrackingHelper.CheckAndAddGatewayIdToTrackingId(trackingId); -#pragma warning restore CS0618 // Type or member is obsolete only for external dependency. - error.Info[IotHubAmqpProperty.TrackingId] = trackingId; - - return error; - } - public static Exception GetExceptionFromOutcome(Outcome outcome) { Exception retException; diff --git a/iothub/service/src/Common/Exceptions/DeviceMessageLockLostException.cs b/iothub/service/src/Common/Exceptions/DeviceMessageLockLostException.cs index 21817881eb..7f2e3d6da6 100644 --- a/iothub/service/src/Common/Exceptions/DeviceMessageLockLostException.cs +++ b/iothub/service/src/Common/Exceptions/DeviceMessageLockLostException.cs @@ -7,9 +7,8 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when an attempt to communicate with a device fails - /// because the lock token was lost (if the connection is lost and regained for example). - /// This timeout has the same effect as if the message was abandoned. + /// This exception is not directly returned by the service for ServiceClient operations. However, the status code + /// HttpStatusCode.PreconditionFailed is converted to this exception. /// [Serializable] public class DeviceMessageLockLostException : IotHubException diff --git a/iothub/service/src/Common/Exceptions/ErrorCode.cs b/iothub/service/src/Common/Exceptions/ErrorCode.cs index 5d5947da15..4b1be0838c 100644 --- a/iothub/service/src/Common/Exceptions/ErrorCode.cs +++ b/iothub/service/src/Common/Exceptions/ErrorCode.cs @@ -1,128 +1,345 @@ // 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.ComponentModel; + namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// Error Codes for common IoT hub exceptions. + /// Error codes for common IoT hub response errors. /// public enum ErrorCode { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Used when the error code returned by the hub is unrecognized. If encountered, please report the issue so it can be added here. + /// InvalidErrorCode = 0, // BadRequest - 400 + + /// + /// The API version used by the SDK is not supported by the IoT hub endpoint used in this connection. + /// + /// Usually this would mean that the region of the hub doesn't yet support the API version. One should + /// consider downgrading to a previous version of the SDK that uses an older API version, or use a hub + /// in a region that supports it. + /// + /// InvalidProtocolVersion = 400001, + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] DeviceInvalidResultCount = 400002, + + /// + /// The client has requested an operation that the hub recognizes as invalid. Check the error message + /// for more information about what is invalid. + /// + // Note: although infrequent, this does appear in logs for "Amqp Message.Properties.To must contain the device identifier". + // and perhaps other cases. InvalidOperation = 400003, + + /// + /// Something in the request payload is invalid. Check the error message for more information about what + /// is invalid. + /// + // Note: one example found in logs is for invalid characters in a twin property name. ArgumentInvalid = 400004, + + /// + /// Something in the payload is unexpectedly null. Check the error message for more information about what is invalid. + /// + // Note: an example suggested is null method payloads, but our client converts null to a JSON null, which is allowed. ArgumentNull = 400005, + + /// + /// Returned by the service if a JSON object provided by this library cannot be parsed, for instance, if the JSON provided for + /// is invalid. + /// IotHubFormatError = 400006, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] DeviceStorageEntitySerializationError = 400007, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] BlobContainerValidationError = 400008, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] ImportWarningExistsError = 400009, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] InvalidSchemaVersion = 400010, + + /// + /// A devices with the same Id was present multiple times in the input request for bulk device registry operations. + /// + /// For more information on bulk registry operations, see . + /// + /// DeviceDefinedMultipleTimes = 400011, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] DeserializationError = 400012, + + /// + /// An error was encountered processing bulk registry operations. + /// + /// As this error is in the 4xx HTTP status code range, the service would have detected a problem with the job + /// request or user input. + /// + /// BulkRegistryOperationFailure = 400013, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] CannotRegisterModuleToModule = 400301, // Unauthorized - 401 + + /// + /// The error is internal to IoT hub and is likely transient. + /// + [Obsolete("This error does should not be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] IotHubNotFound = 401001, + /// + /// The SAS token has expired or IoT hub couldn't authenticate the authentication header, rule, or key. + /// For more information, see . + /// IotHubUnauthorizedAccess = 401002, /// - /// The SAS token has expired or IoT hub couldn't authenticate the authentication header, rule, or key. + /// Unused error code. Service does not return it and neither does the SDK. + /// Replaced by /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] IotHubUnauthorized = 401003, // Forbidden - 403 + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] IotHubSuspended = 403001, /// - /// The daily message quota for the IoT hub is exceeded. + /// Total number of messages on the hub exceeded the allocated quota. + /// + /// Increase units for this hub to increase the quota. + /// For more information on quota, please refer to . + /// /// IotHubQuotaExceeded = 403002, + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] JobQuotaExceeded = 403003, /// - /// The underlying cause is that the number of messages enqueued for the device exceeds the queue limit (50). - /// The most likely reason that you're running into this limit is because you're using HTTPS to receive the message, - /// which leads to continuous polling using ReceiveAsync, resulting in IoT hub throttling the request. + /// The underlying cause is that the number of cloud-to-device messages enqueued for the device exceeds the queue limit. + /// + /// You will need to receive and complete/reject the messages from the device-side before you can enqueue any additional messages. + /// If you want to discard the currently enqueued messages, you can + /// purge your device message queue. + /// For more information on cloud-to-device message operations, see . + /// /// DeviceMaximumQueueDepthExceeded = 403004, + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] IotHubMaxCbsTokenExceeded = 403005, // NotFound - 404 /// - /// The operation failed because the device cannot be found by IoT Hub. The device is either not registered or disabled. + /// The operation failed because the device cannot be found by IoT hub. + /// + /// The device is either not registered or disabled. May be thrown by operations such as + /// . + /// /// DeviceNotFound = 404001, + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] JobNotFound = 404002, - PartitionNotFound = 404003, + + /// + /// The error is internal to IoT hub and is likely transient. + /// + /// For more information, see 503003 PartitionNotFound. + /// + /// + [Obsolete("This error does should not be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] + PartitionNotFound = 503003, + + /// + /// The operation failed because the module cannot be found by IoT hub. + /// + /// The module is either not registered or disabled. May be thrown by operations such as + /// . + /// + /// ModuleNotFound = 404010, // Conflict - 409 /// /// There's already a device with the same device Id in the IoT hub. + /// + /// This can be returned on calling + /// with a device that already exists in the IoT hub. + /// /// DeviceAlreadyExists = 409001, + /// + /// The operation failed because it attempted to add a module to a device when that device already has a module registered to it with the same Id. This issue can be + /// fixed by removing the existing module from the device first with . This error code is only returned from + /// methods like . + /// ModuleAlreadyExistsOnDevice = 409301, - // PreconditionFailed - 412 - PreconditionFailed = 412001, + /// + /// The ETag in the request does not match the ETag of the existing resource, as per RFC7232. + /// + /// The ETag is a mechanism for protecting against the race conditions of multiple clients updating the same resource and overwriting each other. + /// In order to get the up-to-date ETag for a twin, see or + /// . + /// + /// + PreconditionFailed = 412001, // PreconditionFailed - 412 /// + /// If the device tries to complete the message after the lock timeout expires, IoT hub throws this exception. + /// /// When a device receives a cloud-to-device message from the queue (for example, using ReceiveAsync()) /// the message is locked by IoT hub for a lock timeout duration of one minute. - /// If the device tries to complete the message after the lock timeout expires, IoT hub throws this exception. + /// /// + [Obsolete("This error should not be returned to a service application. This is relevant only for a device application.")] + [EditorBrowsable(EditorBrowsableState.Never)] DeviceMessageLockLost = 412002, // RequestEntityTooLarge - 413 + + /// + /// When the message is too large for IoT hub you will receive this error.' + /// + /// You should attempt to reduce your message size and send again. + /// For more information on message sizes, see IoT hub quotas and throttling | Other limits + /// + /// MessageTooLarge = 413001, + /// + /// Too many devices were included in the bulk operation. + /// + /// Check the response for details. + /// For more information, see . + /// + /// TooManyDevices = 413002, + + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] TooManyModulesOnDevice = 413003, // Throttling Exception /// /// IoT hub throttling limits have been exceeded for the requested operation. - /// For more information, + /// For more information, IoT hub quotas and throttling. /// ThrottlingException = 429001, + /// + /// IoT hub throttling limits have been exceeded for the requested operation. + /// + /// For more information, see IoT hub quotas and throttling. + /// + /// ThrottleBacklogLimitExceeded = 429002, - InvalidThrottleParameter = 429003, + + /// + /// IoT hub ran into a server side issue when attempting to throttle. + /// + /// For more information, see 500xxx Internal errors. + /// + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] + InvalidThrottleParameter = 500009, // InternalServerError - 500 /// /// IoT hub ran into a server side issue. + /// /// There can be a number of causes for a 500xxx error response. In all cases, the issue is most likely transient. - /// IoT hub nodes can occasionally experience transient faults. When your device tries to connect to a node that is - /// having issues, you receive this error. To mitigate 500xxx errors, issue a retry from the device. + /// IoT hub nodes can occasionally experience transient faults. When your application tries to connect to a node that is + /// having issues, you receive this error. To mitigate 500xxx errors, issue a retry from your application. + /// /// ServerError = 500001, + /// + /// Unused error code. Service does not return it and neither does the SDK. + /// + [Obsolete("This error does not appear to be returned by the service.")] + [EditorBrowsable(EditorBrowsableState.Never)] JobCancelled = 500002, // ServiceUnavailable /// - /// IoT hub encountered an internal error. + /// IoT hub is currently unable to process the request. This is a transient, retryable error. /// ServiceUnavailable = 503001, - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } diff --git a/iothub/service/src/Common/Exceptions/IotHubAmqpErrorCode.cs b/iothub/service/src/Common/Exceptions/IotHubAmqpErrorCode.cs index 419567a58d..1ef9db4f6d 100644 --- a/iothub/service/src/Common/Exceptions/IotHubAmqpErrorCode.cs +++ b/iothub/service/src/Common/Exceptions/IotHubAmqpErrorCode.cs @@ -26,7 +26,6 @@ internal static class IotHubAmqpErrorCode public static readonly AmqpSymbol DeviceAlreadyExists = AmqpConstants.Vendor + ":device-already-exists"; public static readonly AmqpSymbol DeviceContainerThrottled = AmqpConstants.Vendor + ":device-container-throttled"; public static readonly AmqpSymbol QuotaExceeded = AmqpConstants.Vendor + ":quota-exceeded"; - public static readonly AmqpSymbol PartitionNotFound = AmqpConstants.Vendor + ":partition-not-found"; public static readonly AmqpSymbol PreconditionFailed = AmqpConstants.Vendor + ":precondition-failed"; public static readonly AmqpSymbol IotHubSuspended = AmqpConstants.Vendor + ":iot-hub-suspended"; } diff --git a/iothub/service/src/Common/Exceptions/IotHubCommunicationException.cs b/iothub/service/src/Common/Exceptions/IotHubCommunicationException.cs index 82874f6dd4..8b6c88de2f 100644 --- a/iothub/service/src/Common/Exceptions/IotHubCommunicationException.cs +++ b/iothub/service/src/Common/Exceptions/IotHubCommunicationException.cs @@ -7,7 +7,8 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when an attempt to communicate with the IoT Hub fails. + /// This exception is thrown when an attempt to communicate with the IoT hub service fails due to transient + /// network issues or operation timeouts. Retrying failed operations could resolve the error. /// [Serializable] public sealed class IotHubCommunicationException : IotHubException diff --git a/iothub/service/src/Common/Exceptions/IotHubSuspendedException.cs b/iothub/service/src/Common/Exceptions/IotHubSuspendedException.cs index 190f79b886..43fb7c0b29 100644 --- a/iothub/service/src/Common/Exceptions/IotHubSuspendedException.cs +++ b/iothub/service/src/Common/Exceptions/IotHubSuspendedException.cs @@ -7,7 +7,8 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when a request is made against an IoT Hub that has been suspended. + /// This exception is thrown when the IoT hub has been suspended. This is likely due to exceeding Azure + /// spending limits. To resolve the error, check the Azure bill and ensure there are enough credits. /// [Serializable] public class IotHubSuspendedException : IotHubException diff --git a/iothub/service/src/Common/Exceptions/IotHubThrottledException.cs b/iothub/service/src/Common/Exceptions/IotHubThrottledException.cs index 91642d653c..5f4b90def0 100644 --- a/iothub/service/src/Common/Exceptions/IotHubThrottledException.cs +++ b/iothub/service/src/Common/Exceptions/IotHubThrottledException.cs @@ -7,8 +7,12 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when the rate of incoming requests exceeds the throttling limit set by IoT Hub. + /// This exception is thrown when the requests to the IoT hub exceed the limits based on the tier of the hub. + /// Retrying with exponential back-off could resolve this error. /// + /// + /// For information on the IoT hub quotas and throttling, see . + /// [Serializable] public sealed class IotHubThrottledException : IotHubException { diff --git a/iothub/service/src/Common/Exceptions/MessageTooLargeException.cs b/iothub/service/src/Common/Exceptions/MessageTooLargeException.cs index 9f61742049..6d3ebcdf42 100644 --- a/iothub/service/src/Common/Exceptions/MessageTooLargeException.cs +++ b/iothub/service/src/Common/Exceptions/MessageTooLargeException.cs @@ -7,8 +7,11 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when a message is sent to IoT Hub that exceeds the maximum allowed bytes in size. + /// The exception that is thrown when an attempt to send a message fails because the length of the message exceeds the maximum size allowed. /// + /// + /// When the message is too large for IoT Hub you will receive this exception. You should attempt to reduce your message size and send again. For more information on message sizes, see IoT Hub quotas and throttling | Other limits + /// [Serializable] public sealed class MessageTooLargeException : IotHubException { diff --git a/iothub/service/src/Common/Exceptions/QuotaExceededException.cs b/iothub/service/src/Common/Exceptions/QuotaExceededException.cs index 8b005c8407..09032c2172 100644 --- a/iothub/service/src/Common/Exceptions/QuotaExceededException.cs +++ b/iothub/service/src/Common/Exceptions/QuotaExceededException.cs @@ -7,8 +7,11 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when the allocated quota set by IoT Hub is exceeded. + /// The exception that is thrown by the service client when the daily message quota for the IoT hub is exceeded. /// + /// + /// To resolve this exception please review the Troubleshoot Quota Exceeded guide. + /// [Serializable] public sealed class QuotaExceededException : IotHubException { diff --git a/iothub/service/src/Common/Exceptions/ServerBusyException.cs b/iothub/service/src/Common/Exceptions/ServerBusyException.cs index 5f06accc1c..940c8f43c0 100644 --- a/iothub/service/src/Common/Exceptions/ServerBusyException.cs +++ b/iothub/service/src/Common/Exceptions/ServerBusyException.cs @@ -7,9 +7,12 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when IoT Hub is busy with previous requests. - /// Callers should wait a while and retry the operation. + /// The exception that is thrown when the IoT Hub is busy. /// + /// + /// This exception typically means the service is unavailable due to high load or an unexpected error and is usually transient. + /// The best course of action is to retry your operation after some time. + /// [Serializable] public sealed class ServerBusyException : IotHubException { diff --git a/iothub/service/src/Common/Exceptions/ServerErrorException.cs b/iothub/service/src/Common/Exceptions/ServerErrorException.cs index 5ce7c011eb..4cf3c31e1f 100644 --- a/iothub/service/src/Common/Exceptions/ServerErrorException.cs +++ b/iothub/service/src/Common/Exceptions/ServerErrorException.cs @@ -7,8 +7,13 @@ namespace Microsoft.Azure.Devices.Common.Exceptions { /// - /// The exception that is thrown when IoT Hub encounters an error while processing a request. + /// The exception that is thrown when the IoT Hub returned an internal service error. /// + /// + /// This exception typically means the IoT Hub service has encountered an unexpected error and is usually transient. + /// Please review the 500xxx Internal errors + /// guide for more information. The best course of action is to retry your operation after some time. + /// [Serializable] public sealed class ServerErrorException : IotHubException { diff --git a/iothub/service/src/Common/Exceptions/UnauthorizedException.cs b/iothub/service/src/Common/Exceptions/UnauthorizedException.cs index 6eba26f540..38f83ca834 100644 --- a/iothub/service/src/Common/Exceptions/UnauthorizedException.cs +++ b/iothub/service/src/Common/Exceptions/UnauthorizedException.cs @@ -9,6 +9,11 @@ namespace Microsoft.Azure.Devices.Common.Exceptions /// /// The exception that is thrown when there is an authorization error. /// + /// + /// This exception means the client is not authorized to use the specified IoT hub. + /// Please review the 401003 IoTHubUnauthorized + /// guide for more information. + /// [Serializable] public sealed class UnauthorizedException : IotHubException { diff --git a/iothub/service/src/Common/TrackingHelper.cs b/iothub/service/src/Common/TrackingHelper.cs index cbc4e77d4b..e6f6c5f875 100644 --- a/iothub/service/src/Common/TrackingHelper.cs +++ b/iothub/service/src/Common/TrackingHelper.cs @@ -220,10 +220,6 @@ public static ErrorCode GetErrorCodeFromAmqpError(Error ex) { return ErrorCode.DeviceNotFound; } - if (ex.Condition.Equals(IotHubAmqpErrorCode.MessageLockLostError)) - { - return ErrorCode.DeviceMessageLockLost; - } if (ex.Condition.Equals(IotHubAmqpErrorCode.IotHubSuspended)) { return ErrorCode.IotHubSuspended; diff --git a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs index a1289d8719..9f434063d8 100644 --- a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs +++ b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs @@ -27,60 +27,12 @@ public class DigitalTwinClient : IDisposable private readonly IotHubGatewayServiceAPIs _client; private readonly PnpDigitalTwin _protocolLayer; - private DigitalTwinClient(string hostName, DigitalTwinServiceClientCredentials credentials, params DelegatingHandler[] handlers) - { - var httpsEndpoint = new UriBuilder(HttpsEndpointPrefix, hostName).Uri; - var httpMessageHandler = HttpClientHelper.CreateDefaultHttpMessageHandler(null, httpsEndpoint, ServicePointHelpers.DefaultConnectionLeaseTimeout); -#pragma warning disable CA2000 // Dispose objects before losing scope (httpMessageHandlerWithDelegatingHandlers is disposed when the http client owning it is disposed) - HttpMessageHandler httpMessageHandlerWithDelegatingHandlers = CreateHttpHandlerPipeline(httpMessageHandler, handlers); -#pragma warning restore CA2000 // Dispose objects before losing scope - -#pragma warning disable CA2000 // Dispose objects before losing scope (httpClient is disposed when the protocol layer client owning it is disposed) - var httpClient = new HttpClient(httpMessageHandlerWithDelegatingHandlers, true) - { - BaseAddress = httpsEndpoint - }; -#pragma warning restore CA2000 // Dispose objects before losing scope - -#pragma warning restore CA2000 // Dispose objects before losing scope - - // When this client is disposed, all the http message handlers and delegating handlers will be disposed automatically - _client = new IotHubGatewayServiceAPIs(credentials, httpClient, true); - _client.BaseUri = httpsEndpoint; - _protocolLayer = new PnpDigitalTwin(_client); - } - - // Creates a single HttpMessageHandler to construct a HttpClient with from a base httpMessageHandler and some number of custom delegating handlers - // This is almost a copy of the Microsoft.Rest.ClientRuntime library's implementation, but with the return and parameter type HttpClientHandler replaced - // with the more abstract HttpMessageHandler in order for us to set the base handler as either a SocketsHttpHandler for .net core or an HttpClientHandler otherwise - // https://github.com/Azure/azure-sdk-for-net/blob/99f4da88ab0aa01c79aa291c6c101ab94c4ac940/sdk/mgmtcommon/ClientRuntime/ClientRuntime/ServiceClient.cs#L376 - private static HttpMessageHandler CreateHttpHandlerPipeline(HttpMessageHandler httpMessageHandler, params DelegatingHandler[] handlers) + /// + /// Creates an instance of , provided for unit testing purposes only. + /// Use the CreateFromConnectionString or Create method to create an instance to use the client. + /// + public DigitalTwinClient() { - // The RetryAfterDelegatingHandler should be the absolute outermost handler - // because it's extremely lightweight and non-interfering - HttpMessageHandler currentHandler = -#pragma warning disable CA2000 // Dispose objects before losing scope (delegating handler is disposed when the http client that uses it is disposed) - new RetryDelegatingHandler(new RetryAfterDelegatingHandler { InnerHandler = httpMessageHandler }); -#pragma warning restore CA2000 // Dispose objects before losing scope - - if (handlers != null) - { - for (int i = handlers.Length - 1; i >= 0; --i) - { - DelegatingHandler handler = handlers[i]; - // Non-delegating handlers are ignored since we always - // have RetryDelegatingHandler as the outer-most handler - while (handler.InnerHandler is DelegatingHandler) - { - handler = handler.InnerHandler as DelegatingHandler; - } - - handler.InnerHandler = currentHandler; - currentHandler = handlers[i]; - } - } - - return currentHandler; } /// @@ -157,7 +109,7 @@ public static DigitalTwinClient Create( /// The Id of the digital twin. /// The cancellation token. /// The application/json digital twin and the http response. - public async Task> GetDigitalTwinAsync(string digitalTwinId, CancellationToken cancellationToken = default) + public virtual async Task> GetDigitalTwinAsync(string digitalTwinId, CancellationToken cancellationToken = default) { using HttpOperationResponse response = await _protocolLayer.GetDigitalTwinWithHttpMessagesAsync(digitalTwinId, null, cancellationToken) .ConfigureAwait(false); @@ -179,7 +131,7 @@ public async Task> GetDigitalTwi /// The optional settings for this request. /// The cancellationToken. /// The http response. - public Task> UpdateDigitalTwinAsync( + public virtual Task> UpdateDigitalTwinAsync( string digitalTwinId, string digitalTwinUpdateOperations, DigitalTwinUpdateRequestOptions requestOptions = default, @@ -197,7 +149,7 @@ public Task> UpdateDigital /// The optional settings for this request. /// The cancellationToken. /// The application/json command invocation response and the http response. - public async Task> InvokeCommandAsync( + public virtual async Task> InvokeCommandAsync( string digitalTwinId, string commandName, string payload = default, @@ -232,7 +184,7 @@ public async TaskThe optional settings for this request. /// The cancellationToken. /// The application/json command invocation response and the http response. - public async Task> InvokeComponentCommandAsync( + public virtual async Task> InvokeComponentCommandAsync( string digitalTwinId, string componentName, string commandName, @@ -274,5 +226,61 @@ protected virtual void Dispose(bool disposing) { _client?.Dispose(); } + + private DigitalTwinClient(string hostName, DigitalTwinServiceClientCredentials credentials, params DelegatingHandler[] handlers) + { + var httpsEndpoint = new UriBuilder(HttpsEndpointPrefix, hostName).Uri; + var httpMessageHandler = HttpClientHelper.CreateDefaultHttpMessageHandler(null, httpsEndpoint, ServicePointHelpers.DefaultConnectionLeaseTimeout); +#pragma warning disable CA2000 // Dispose objects before losing scope (httpMessageHandlerWithDelegatingHandlers is disposed when the http client owning it is disposed) + HttpMessageHandler httpMessageHandlerWithDelegatingHandlers = CreateHttpHandlerPipeline(httpMessageHandler, handlers); +#pragma warning restore CA2000 // Dispose objects before losing scope + +#pragma warning disable CA2000 // Dispose objects before losing scope (httpClient is disposed when the protocol layer client owning it is disposed) + var httpClient = new HttpClient(httpMessageHandlerWithDelegatingHandlers, true) + { + BaseAddress = httpsEndpoint + }; +#pragma warning restore CA2000 // Dispose objects before losing scope + +#pragma warning restore CA2000 // Dispose objects before losing scope + + // When this client is disposed, all the http message handlers and delegating handlers will be disposed automatically + _client = new IotHubGatewayServiceAPIs(credentials, httpClient, true); + _client.BaseUri = httpsEndpoint; + _protocolLayer = new PnpDigitalTwin(_client); + } + + // Creates a single HttpMessageHandler to construct a HttpClient with from a base httpMessageHandler and some number of custom delegating handlers + // This is almost a copy of the Microsoft.Rest.ClientRuntime library's implementation, but with the return and parameter type HttpClientHandler replaced + // with the more abstract HttpMessageHandler in order for us to set the base handler as either a SocketsHttpHandler for .net core or an HttpClientHandler otherwise + // https://github.com/Azure/azure-sdk-for-net/blob/99f4da88ab0aa01c79aa291c6c101ab94c4ac940/sdk/mgmtcommon/ClientRuntime/ClientRuntime/ServiceClient.cs#L376 + private static HttpMessageHandler CreateHttpHandlerPipeline(HttpMessageHandler httpMessageHandler, params DelegatingHandler[] handlers) + { + // The RetryAfterDelegatingHandler should be the absolute outermost handler + // because it's extremely lightweight and non-interfering + HttpMessageHandler currentHandler = +#pragma warning disable CA2000 // Dispose objects before losing scope (delegating handler is disposed when the http client that uses it is disposed) + new RetryDelegatingHandler(new RetryAfterDelegatingHandler { InnerHandler = httpMessageHandler }); +#pragma warning restore CA2000 // Dispose objects before losing scope + + if (handlers != null) + { + for (int i = handlers.Length - 1; i >= 0; --i) + { + DelegatingHandler handler = handlers[i]; + // Non-delegating handlers are ignored since we always + // have RetryDelegatingHandler as the outer-most handler + while (handler.InnerHandler is DelegatingHandler) + { + handler = handler.InnerHandler as DelegatingHandler; + } + + handler.InnerHandler = currentHandler; + currentHandler = handlers[i]; + } + } + + return currentHandler; + } } } diff --git a/iothub/service/src/HttpRegistryManager.cs b/iothub/service/src/HttpRegistryManager.cs deleted file mode 100644 index 448e738784..0000000000 --- a/iothub/service/src/HttpRegistryManager.cs +++ /dev/null @@ -1,2235 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Devices.Common; -using Microsoft.Azure.Devices.Common.Exceptions; -using Microsoft.Azure.Devices.Shared; -using Newtonsoft.Json; - -namespace Microsoft.Azure.Devices -{ - internal class HttpRegistryManager : RegistryManager - { - private const string AdminUriFormat = "/$admin/{0}?{1}"; - private const string RequestUriFormat = "/devices/{0}?{1}"; - private const string JobsUriFormat = "/jobs{0}?{1}"; - private const string StatisticsUriFormat = "/statistics/devices?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string DevicesRequestUriFormat = "/devices/?top={0}&{1}"; - private const string DevicesQueryUriFormat = "/devices/query?" + ClientApiVersionHelper.ApiVersionQueryString; - private const string WildcardEtag = "*"; - - private const string ContinuationTokenHeader = "x-ms-continuation"; - private const string PageSizeHeader = "x-ms-max-item-count"; - - private const string TwinUriFormat = "/twins/{0}?{1}"; - - private const string ModulesRequestUriFormat = "/devices/{0}/modules/{1}?{2}"; - private const string ModulesOnDeviceRequestUriFormat = "/devices/{0}/modules?{1}"; - private const string ModuleTwinUriFormat = "/twins/{0}/modules/{1}?{2}"; - - private const string ConfigurationRequestUriFormat = "/configurations/{0}?{1}"; - private const string ConfigurationsRequestUriFormat = "/configurations/?top={0}&{1}"; - - private const string ApplyConfigurationOnDeviceUriFormat = "/devices/{0}/applyConfigurationContent?" + ClientApiVersionHelper.ApiVersionQueryString; - - private static readonly TimeSpan s_regexTimeoutMilliseconds = TimeSpan.FromMilliseconds(500); - - private static readonly Regex s_deviceIdRegex = new Regex( - @"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$", - RegexOptions.Compiled | RegexOptions.IgnoreCase, - s_regexTimeoutMilliseconds); - - private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); - private static readonly TimeSpan s_defaultGetDevicesOperationTimeout = TimeSpan.FromSeconds(120); - - private IHttpClientHelper _httpClientHelper; - private readonly string _iotHubName; - - internal HttpRegistryManager(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) - { - _iotHubName = connectionProperties.IotHubName; - _httpClientHelper = new HttpClientHelper( - connectionProperties.HttpsEndpoint, - connectionProperties, - ExceptionHandlingHelper.GetDefaultErrorMapping(), - s_defaultOperationTimeout, - transportSettings.Proxy, - transportSettings.ConnectionLeaseTimeoutMilliseconds); - } - - // internal test helper - internal HttpRegistryManager(IHttpClientHelper httpClientHelper, string iotHubName) - { - _httpClientHelper = httpClientHelper ?? throw new ArgumentNullException(nameof(httpClientHelper)); - _iotHubName = iotHubName; - } - - public override Task OpenAsync() - { - return TaskHelpers.CompletedTask; - } - - public override Task CloseAsync() - { - return TaskHelpers.CompletedTask; - } - - public override Task AddDeviceAsync(Device device) - { - return AddDeviceAsync(device, CancellationToken.None); - } - - public override Task AddDeviceAsync(Device device, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding device: {device?.Id}", nameof(AddDeviceAsync)); - - try - { - EnsureInstanceNotClosed(); - - ValidateDeviceId(device); - - if (!string.IsNullOrEmpty(device.ETag)) - { - throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); - } - - ValidateDeviceAuthentication(device.Authentication, device.Id); - - NormalizeDevice(device); - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - } - }; - - return _httpClientHelper.PutAsync(GetRequestUri(device.Id), device, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddDeviceAsync)} threw an exception: {ex}", nameof(AddDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Adding device: {device?.Id}", nameof(AddDeviceAsync)); - } - } - - public override Task AddModuleAsync(Module module) - { - return AddModuleAsync(module, CancellationToken.None); - } - - public override Task AddModuleAsync(Module module, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding module: {module?.Id}", nameof(AddModuleAsync)); - - try - { - EnsureInstanceNotClosed(); - - ValidateModuleId(module); - - if (!string.IsNullOrEmpty(module.ETag)) - { - throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); - } - - ValidateDeviceAuthentication(module.Authentication, module.DeviceId); - - // auto generate keys if not specified - if (module.Authentication == null) - { - module.Authentication = new AuthenticationMechanism(); - } - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - { - HttpStatusCode.Conflict, - async responseMessage => new ModuleAlreadyExistsException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - { - HttpStatusCode.RequestEntityTooLarge, - async responseMessage => new TooManyModulesOnDeviceException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - } - }; - - return _httpClientHelper.PutAsync(GetModulesRequestUri(module.DeviceId, module.Id), module, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddModuleAsync)} threw an exception: {ex}", nameof(AddModuleAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Adding module: {module?.Id}", nameof(AddModuleAsync)); - } - } - - public override Task AddDeviceWithTwinAsync(Device device, Twin twin) - { - return AddDeviceWithTwinAsync(device, twin, CancellationToken.None); - } - - public override Task AddDeviceWithTwinAsync(Device device, Twin twin, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding device with twin: {device?.Id}", nameof(AddDeviceWithTwinAsync)); - - try - { - ValidateDeviceId(device); - if (!string.IsNullOrWhiteSpace(device.ETag)) - { - throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); - } - var exportImportDeviceList = new List(1); - - var exportImportDevice = new ExportImportDevice(device, ImportMode.Create) - { - Tags = twin?.Tags, - Properties = new ExportImportDevice.PropertyContainer - { - DesiredProperties = twin?.Properties.Desired, - ReportedProperties = twin?.Properties.Reported, - } - }; - - exportImportDeviceList.Add(exportImportDevice); - - return BulkDeviceOperationsAsync( - exportImportDeviceList, - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddDeviceWithTwinAsync)} threw an exception: {ex}", nameof(AddDeviceWithTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Adding device with twin: {device?.Id}", nameof(AddDeviceWithTwinAsync)); - } - } - - [Obsolete("Use AddDevices2Async")] - public override Task AddDevicesAsync(IEnumerable devices) - { - return AddDevicesAsync(devices, CancellationToken.None); - } - - [Obsolete("Use AddDevices2Async")] - public override Task AddDevicesAsync(IEnumerable devices, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding {devices?.Count()} devices", nameof(AddDevicesAsync)); - - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, ImportMode.Create), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddDevicesAsync)} threw an exception: {ex}", nameof(AddDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Adding {devices?.Count()} devices", nameof(AddDevicesAsync)); - } - } - - public override Task AddDevices2Async(IEnumerable devices) - { - return AddDevices2Async(devices, CancellationToken.None); - } - - public override Task AddDevices2Async(IEnumerable devices, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding {devices?.Count()} devices", nameof(AddDevices2Async)); - - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, ImportMode.Create), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddDevices2Async)} threw an exception: {ex}", nameof(AddDevices2Async)); - throw; - } - finally - { - Logging.Exit(this, $"Adding {devices?.Count()} devices", nameof(AddDevices2Async)); - } - } - - public override Task UpdateDeviceAsync(Device device) - { - return UpdateDeviceAsync(device, CancellationToken.None); - } - - public override Task UpdateDeviceAsync(Device device, bool forceUpdate) - { - return UpdateDeviceAsync(device, forceUpdate, CancellationToken.None); - } - - public override Task UpdateDeviceAsync(Device device, CancellationToken cancellationToken) - { - return UpdateDeviceAsync(device, false, cancellationToken); - } - - public override Task UpdateDeviceAsync(Device device, bool forceUpdate, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating device: {device?.Id}", nameof(UpdateDeviceAsync)); - try - { - EnsureInstanceNotClosed(); - - ValidateDeviceId(device); - - if (string.IsNullOrWhiteSpace(device.ETag) && !forceUpdate) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); - } - - ValidateDeviceAuthentication(device.Authentication, device.Id); - - NormalizeDevice(device); - - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, - { - HttpStatusCode.NotFound, async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return (Exception)new DeviceNotFoundException(responseContent, (Exception)null); - } - } - }; - - PutOperationType operationType = forceUpdate ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity; - - return _httpClientHelper.PutAsync(GetRequestUri(device.Id), device, operationType, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateDeviceAsync)} threw an exception: {ex}", nameof(UpdateDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating device: {device?.Id}", nameof(UpdateDeviceAsync)); - } - } - - public override Task UpdateModuleAsync(Module module) - { - return UpdateModuleAsync(module, CancellationToken.None); - } - - public override Task UpdateModuleAsync(Module module, bool forceUpdate) - { - return UpdateModuleAsync(module, forceUpdate, CancellationToken.None); - } - - public override Task UpdateModuleAsync(Module module, CancellationToken cancellationToken) - { - return UpdateModuleAsync(module, false, CancellationToken.None); - } - - public override Task UpdateModuleAsync(Module module, bool forceUpdate, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating module: {module?.Id}", nameof(UpdateModuleAsync)); - - try - { - EnsureInstanceNotClosed(); - - ValidateModuleId(module); - - if (string.IsNullOrWhiteSpace(module.ETag) && !forceUpdate) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); - } - - ValidateDeviceAuthentication(module.Authentication, module.DeviceId); - - // auto generate keys if not specified - if (module.Authentication == null) - { - module.Authentication = new AuthenticationMechanism(); - } - - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, - { - HttpStatusCode.NotFound, async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return new ModuleNotFoundException(responseContent, (Exception)null); - } - } - }; - - PutOperationType operationType = forceUpdate ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity; - - return _httpClientHelper.PutAsync(GetModulesRequestUri(module.DeviceId, module.Id), module, operationType, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateModuleAsync)} threw an exception: {ex}", nameof(UpdateModuleAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating module: {module?.Id}", nameof(UpdateModuleAsync)); - } - } - - public override Task AddConfigurationAsync(Configuration configuration) - { - return AddConfigurationAsync(configuration, CancellationToken.None); - } - - public override Task AddConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Adding configuration: {configuration?.Id}", nameof(AddConfigurationAsync)); - - try - { - EnsureInstanceNotClosed(); - - if (!string.IsNullOrEmpty(configuration.ETag)) - { - throw new ArgumentException(ApiResources.ETagSetWhileCreatingConfiguration); - } - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - } - }; - - return _httpClientHelper.PutAsync(GetConfigurationRequestUri(configuration.Id), configuration, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(AddConfigurationAsync)} threw an exception: {ex}", nameof(AddConfigurationAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Adding configuration: {configuration?.Id}", nameof(AddConfigurationAsync)); - } - } - - public override Task GetConfigurationAsync(string configurationId) - { - return GetConfigurationAsync(configurationId, CancellationToken.None); - } - - public override Task GetConfigurationAsync(string configurationId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting configuration: {configurationId}", nameof(GetConfigurationAsync)); - try - { - if (string.IsNullOrWhiteSpace(configurationId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "configurationId")); - } - - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.NotFound, async responseMessage => new ConfigurationNotFoundException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } - }; - - return _httpClientHelper.GetAsync(GetConfigurationRequestUri(configurationId), errorMappingOverrides, null, false, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetConfigurationAsync)} threw an exception: {ex}", nameof(GetConfigurationAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Get configuration: {configurationId}", nameof(GetConfigurationAsync)); - } - } - - public override Task> GetConfigurationsAsync(int maxCount) - { - return GetConfigurationsAsync(maxCount, CancellationToken.None); - } - - public override Task> GetConfigurationsAsync(int maxCount, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting configuration: max count: {maxCount}", nameof(GetConfigurationsAsync)); - try - { - EnsureInstanceNotClosed(); - - return _httpClientHelper.GetAsync>( - GetConfigurationsRequestUri(maxCount), - null, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetConfigurationsAsync)} threw an exception: {ex}", nameof(GetConfigurationsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting configuration: max count: {maxCount}", nameof(GetConfigurationsAsync)); - } - } - - public override Task UpdateConfigurationAsync(Configuration configuration) - { - return UpdateConfigurationAsync(configuration, CancellationToken.None); - } - - public override Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate) - { - return UpdateConfigurationAsync(configuration, forceUpdate, CancellationToken.None); - } - - public override Task UpdateConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) - { - return UpdateConfigurationAsync(configuration, false, cancellationToken); - } - - public override Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating configuration: {configuration?.Id} - Force update: {forceUpdate}", nameof(UpdateConfigurationAsync)); - - try - { - EnsureInstanceNotClosed(); - - if (string.IsNullOrWhiteSpace(configuration.ETag) && !forceUpdate) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingConfiguration); - } - - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, - { - HttpStatusCode.NotFound, async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return new ConfigurationNotFoundException(responseContent, (Exception)null); - } - } - }; - - PutOperationType operationType = forceUpdate - ? PutOperationType.ForceUpdateEntity - : PutOperationType.UpdateEntity; - - return _httpClientHelper.PutAsync(GetConfigurationRequestUri(configuration.Id), configuration, operationType, errorMappingOverrides, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateConfigurationAsync)} threw an exception: {ex}", nameof(UpdateConfigurationAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating configuration: {configuration?.Id} - Force update: {forceUpdate}", nameof(UpdateConfigurationAsync)); - } - } - - public override Task RemoveConfigurationAsync(string configurationId) - { - return RemoveConfigurationAsync(configurationId, CancellationToken.None); - } - - public override Task RemoveConfigurationAsync(string configurationId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing configuration: {configurationId}", nameof(RemoveConfigurationAsync)); - - try - { - EnsureInstanceNotClosed(); - - if (string.IsNullOrWhiteSpace(configurationId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "configurationId")); - } - - // use wild-card ETag - var eTag = new ETagHolder { ETag = "*" }; - return RemoveConfigurationAsync(configurationId, eTag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveConfigurationAsync)} threw an exception: {ex}", nameof(RemoveConfigurationAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing configuration: {configurationId}", nameof(RemoveConfigurationAsync)); - } - } - - public override Task RemoveConfigurationAsync(Configuration configuration) - { - return RemoveConfigurationAsync(configuration, CancellationToken.None); - } - - public override Task RemoveConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing configuration: {configuration?.Id}", nameof(RemoveConfigurationAsync)); - try - { - EnsureInstanceNotClosed(); - - return string.IsNullOrWhiteSpace(configuration.ETag) - ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingConfiguration) - : RemoveConfigurationAsync(configuration.Id, configuration, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveConfigurationAsync)} threw an exception: {ex}", nameof(RemoveConfigurationAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing configuration: {configuration?.Id}", nameof(RemoveConfigurationAsync)); - } - } - - public override Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content) - { - return ApplyConfigurationContentOnDeviceAsync(deviceId, content, CancellationToken.None); - } - - public override Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Applying configuration content on device: {deviceId}", nameof(ApplyConfigurationContentOnDeviceAsync)); - - try - { - return _httpClientHelper.PostAsync(GetApplyConfigurationOnDeviceRequestUri(deviceId), content, null, null, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ApplyConfigurationContentOnDeviceAsync)} threw an exception: {ex}", nameof(ApplyConfigurationContentOnDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Applying configuration content on device: {deviceId}", nameof(ApplyConfigurationContentOnDeviceAsync)); - } - } - - private Task RemoveConfigurationAsync(string configurationId, IETagHolder eTagHolder, CancellationToken cancellationToken) - { - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.NotFound, - async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return new ConfigurationNotFoundException(responseContent, (Exception) null); - } - }, - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - } - }; - - return _httpClientHelper.DeleteAsync(GetConfigurationRequestUri(configurationId), eTagHolder, errorMappingOverrides, null, cancellationToken); - } - - [Obsolete("Use UpdateDevices2Async")] - public override Task UpdateDevicesAsync(IEnumerable devices) - { - return UpdateDevicesAsync(devices, false, CancellationToken.None); - } - - [Obsolete("Use UpdateDevices2Async")] - public override Task UpdateDevicesAsync(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating multiple devices: count: {devices?.Count()}", nameof(UpdateDevicesAsync)); - - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, forceUpdate ? ImportMode.Update : ImportMode.UpdateIfMatchETag), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateDevicesAsync)} threw an exception: {ex}", nameof(UpdateDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating multiple devices: count: {devices?.Count()}", nameof(UpdateDevicesAsync)); - } - } - - public override Task UpdateDevices2Async(IEnumerable devices) - { - return UpdateDevices2Async(devices, false, CancellationToken.None); - } - - public override Task UpdateDevices2Async(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating multiple devices: count: {devices?.Count()} - Force update: {forceUpdate}", nameof(UpdateDevices2Async)); - - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, forceUpdate ? ImportMode.Update : ImportMode.UpdateIfMatchETag), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateDevices2Async)} threw an exception: {ex}", nameof(UpdateDevices2Async)); - throw; - } - finally - { - Logging.Exit(this, $"Updating multiple devices: count: {devices?.Count()} - Force update: {forceUpdate}", nameof(UpdateDevices2Async)); - } - } - - public override Task RemoveDeviceAsync(string deviceId) - { - return RemoveDeviceAsync(deviceId, CancellationToken.None); - } - - public override Task RemoveDeviceAsync(string deviceId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing device: {deviceId}", nameof(RemoveDeviceAsync)); - try - { - EnsureInstanceNotClosed(); - - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - // use wild-card ETag - var eTag = new ETagHolder { ETag = "*" }; - return RemoveDeviceAsync(deviceId, eTag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveDeviceAsync)} threw an exception: {ex}", nameof(RemoveDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing device: {deviceId}", nameof(RemoveDeviceAsync)); - } - } - - public override Task RemoveDeviceAsync(Device device) - { - return RemoveDeviceAsync(device, CancellationToken.None); - } - - public override Task RemoveDeviceAsync(Device device, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing device: {device?.Id}", nameof(RemoveDeviceAsync)); - - try - { - EnsureInstanceNotClosed(); - - ValidateDeviceId(device); - - return string.IsNullOrWhiteSpace(device.ETag) - ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice) - : RemoveDeviceAsync(device.Id, device, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveDeviceAsync)} threw an exception: {ex}", nameof(RemoveDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing device: {device?.Id}", nameof(RemoveDeviceAsync)); - } - } - - public override Task RemoveModuleAsync(string deviceId, string moduleId) - { - return RemoveModuleAsync(deviceId, moduleId, CancellationToken.None); - } - - public override Task RemoveModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing module: device Id:{deviceId} moduleId: {moduleId}", nameof(RemoveDeviceAsync)); - - try - { - EnsureInstanceNotClosed(); - - if (string.IsNullOrWhiteSpace(deviceId) || string.IsNullOrEmpty(moduleId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - // use wild-card ETag - var eTag = new ETagHolder { ETag = "*" }; - return RemoveDeviceModuleAsync(deviceId, moduleId, eTag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveModuleAsync)} threw an exception: {ex}", nameof(RemoveModuleAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing module: device Id:{deviceId} moduleId: {moduleId}", nameof(RemoveModuleAsync)); - } - } - - public override Task RemoveModuleAsync(Module module) - { - return RemoveModuleAsync(module, CancellationToken.None); - } - - public override Task RemoveModuleAsync(Module module, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing module: device Id:{module?.DeviceId} moduleId: {module?.Id}", nameof(RemoveModuleAsync)); - - try - { - EnsureInstanceNotClosed(); - - ValidateModuleId(module); - - return string.IsNullOrWhiteSpace(module.ETag) - ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice) - : RemoveDeviceModuleAsync(module.DeviceId, module.Id, module, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveModuleAsync)} threw an exception: {ex}", nameof(RemoveModuleAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing module: device Id:{module?.DeviceId} moduleId: {module?.Id}", nameof(RemoveModuleAsync)); - } - } - - [Obsolete("Use RemoveDevices2Async")] - public override Task RemoveDevicesAsync(IEnumerable devices) - { - return RemoveDevicesAsync(devices, false, CancellationToken.None); - } - - [Obsolete("Use RemoveDevices2Async")] - public override Task RemoveDevicesAsync(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, forceRemove ? ImportMode.Delete : ImportMode.DeleteIfMatchETag), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveDevicesAsync)} threw an exception: {ex}", nameof(RemoveDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); - } - } - - public override Task RemoveDevices2Async(IEnumerable devices) - { - return RemoveDevices2Async(devices, false, CancellationToken.None); - } - - public override Task RemoveDevices2Async(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevices2Async)); - - try - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForBulkOperations(devices, forceRemove ? ImportMode.Delete : ImportMode.DeleteIfMatchETag), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(RemoveDevicesAsync)} threw an exception: {ex}", nameof(RemoveDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); - } - } - - public override Task GetRegistryStatisticsAsync() - { - return GetRegistryStatisticsAsync(CancellationToken.None); - } - - public override Task GetRegistryStatisticsAsync(CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting registry statistics", nameof(GetRegistryStatisticsAsync)); - - try - { - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } - }; - - return _httpClientHelper.GetAsync(GetStatisticsUri(), errorMappingOverrides, null, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetRegistryStatisticsAsync)} threw an exception: {ex}", nameof(GetRegistryStatisticsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting registry statistics", nameof(GetRegistryStatisticsAsync)); - } - } - - public override Task GetDeviceAsync(string deviceId) - { - return GetDeviceAsync(deviceId, CancellationToken.None); - } - - public override Task GetDeviceAsync(string deviceId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting device: {deviceId}", nameof(GetDeviceAsync)); - try - { - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.NotFound, async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } - }; - - return _httpClientHelper.GetAsync(GetRequestUri(deviceId), errorMappingOverrides, null, false, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetDeviceAsync)} threw an exception: {ex}", nameof(GetDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting device: {deviceId}", nameof(GetDeviceAsync)); - } - } - - public override Task GetModuleAsync(string deviceId, string moduleId) - { - return GetModuleAsync(deviceId, moduleId, CancellationToken.None); - } - - public override Task GetModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting module: device Id: {deviceId} - module Id: {moduleId}", nameof(GetModuleAsync)); - - try - { - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - if (string.IsNullOrWhiteSpace(moduleId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "moduleId")); - } - - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.NotFound, - responseMessage => Task.FromResult(new ModuleNotFoundException(deviceId, moduleId)) - }, - }; - - return _httpClientHelper.GetAsync(GetModulesRequestUri(deviceId, moduleId), errorMappingOverrides, null, false, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetModuleAsync)} threw an exception: {ex}", nameof(GetModuleAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting module: device Id: {deviceId} - module Id: {moduleId}", nameof(GetModuleAsync)); - } - } - - public override Task> GetModulesOnDeviceAsync(string deviceId) - { - return GetModulesOnDeviceAsync(deviceId, CancellationToken.None); - } - - public override Task> GetModulesOnDeviceAsync(string deviceId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting module on device: {deviceId}", nameof(GetModulesOnDeviceAsync)); - - try - { - EnsureInstanceNotClosed(); - - return _httpClientHelper.GetAsync>( - GetModulesOnDeviceRequestUri(deviceId), - null, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetModulesOnDeviceAsync)} threw an exception: {ex}", nameof(GetModulesOnDeviceAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting module on device: {deviceId}", nameof(GetModulesOnDeviceAsync)); - } - } - - [Obsolete("Use CreateQuery(\"select * from devices\", pageSize);")] - public override Task> GetDevicesAsync(int maxCount) - { - return GetDevicesAsync(maxCount, CancellationToken.None); - } - - [Obsolete("Use CreateQuery(\"select * from devices\", pageSize);")] - public override Task> GetDevicesAsync(int maxCount, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting devices - max count: {maxCount}", nameof(GetDevicesAsync)); - - try - { - EnsureInstanceNotClosed(); - - return _httpClientHelper.GetAsync>( - GetDevicesRequestUri(maxCount), - s_defaultGetDevicesOperationTimeout, - null, - null, - true, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetDevicesAsync)} threw an exception: {ex}", nameof(GetDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting devices - max count: {maxCount}", nameof(GetDevicesAsync)); - } - } - - public override IQuery CreateQuery(string sqlQueryString) - { - return CreateQuery(sqlQueryString, null); - } - - public override IQuery CreateQuery(string sqlQueryString, int? pageSize) - { - Logging.Enter(this, $"Creating query", nameof(CreateQuery)); - try - { - return new Query((token) => ExecuteQueryAsync( - sqlQueryString, - pageSize, - token, - CancellationToken.None)); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(CreateQuery)} threw an exception: {ex}", nameof(CreateQuery)); - throw; - } - finally - { - Logging.Exit(this, $"Creating query", nameof(CreateQuery)); - } - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing && _httpClientHelper != null) - { - _httpClientHelper.Dispose(); - _httpClientHelper = null; - } - } - - private static IEnumerable GenerateExportImportDeviceListForBulkOperations(IEnumerable devices, ImportMode importMode) - { - if (devices == null) - { - throw new ArgumentNullException(nameof(devices)); - } - - if (!devices.Any()) - { - throw new ArgumentException($"Parameter {nameof(devices)} cannot be empty."); - } - - var exportImportDeviceList = new List(devices.Count()); - foreach (Device device in devices) - { - ValidateDeviceId(device); - - switch (importMode) - { - case ImportMode.Create: - if (!string.IsNullOrWhiteSpace(device.ETag)) - { - throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); - } - break; - - case ImportMode.Update: - // No preconditions - break; - - case ImportMode.UpdateIfMatchETag: - if (string.IsNullOrWhiteSpace(device.ETag)) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); - } - break; - - case ImportMode.Delete: - // No preconditions - break; - - case ImportMode.DeleteIfMatchETag: - if (string.IsNullOrWhiteSpace(device.ETag)) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice); - } - break; - - default: - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.InvalidImportMode, importMode)); - } - - var exportImportDevice = new ExportImportDevice(device, importMode); - exportImportDeviceList.Add(exportImportDevice); - } - - return exportImportDeviceList; - } - - private static IEnumerable GenerateExportImportDeviceListForTwinBulkOperations(IEnumerable twins, ImportMode importMode) - { - if (twins == null) - { - throw new ArgumentNullException(nameof(twins)); - } - - if (!twins.Any()) - { - throw new ArgumentException($"Parameter {nameof(twins)} cannot be empty"); - } - - var exportImportDeviceList = new List(twins.Count()); - foreach (Twin twin in twins) - { - ValidateTwinId(twin); - - switch (importMode) - { - case ImportMode.UpdateTwin: - // No preconditions - break; - - case ImportMode.UpdateTwinIfMatchETag: - if (string.IsNullOrWhiteSpace(twin.ETag)) - { - throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingTwin); - } - break; - - default: - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.InvalidImportMode, importMode)); - } - - var exportImportDevice = new ExportImportDevice - { - Id = twin.DeviceId, - ModuleId = twin.ModuleId, - ImportMode = importMode, - TwinETag = importMode == ImportMode.UpdateTwinIfMatchETag ? twin.ETag : null, - Tags = twin.Tags, - Properties = new ExportImportDevice.PropertyContainer(), - }; - exportImportDevice.Properties.DesiredProperties = twin.Properties?.Desired; - - exportImportDeviceList.Add(exportImportDevice); - } - - return exportImportDeviceList; - } - - private Task BulkDeviceOperationsAsync(IEnumerable devices, string version, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Performing bulk device operation on : {devices?.Count()} devices. version: {version}", nameof(BulkDeviceOperationsAsync)); - try - { - BulkDeviceOperationSetup(devices); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.PreconditionFailed, async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, - { HttpStatusCode.RequestEntityTooLarge, async responseMessage => new TooManyDevicesException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, - { HttpStatusCode.BadRequest, async responseMessage => new ArgumentException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } - }; - - return _httpClientHelper.PostAsync, T>(GetBulkRequestUri(version), devices, errorMappingOverrides, null, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(BulkDeviceOperationsAsync)} threw an exception: {ex}", nameof(BulkDeviceOperationsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Performing bulk device operation on : {devices?.Count()} devices. version: {version}", nameof(BulkDeviceOperationsAsync)); - } - } - - private void BulkDeviceOperationSetup(IEnumerable devices) - { - EnsureInstanceNotClosed(); - - if (devices == null) - { - throw new ArgumentNullException(nameof(devices)); - } - - foreach (ExportImportDevice device in devices) - { - ValidateDeviceAuthentication(device.Authentication, device.Id); - - NormalizeExportImportDevice(device); - } - } - - public override Task ExportRegistryAsync(string storageAccountConnectionString, string containerName) - { - return ExportRegistryAsync(storageAccountConnectionString, containerName, CancellationToken.None); - } - - public override Task ExportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Exporting registry", nameof(ExportRegistryAsync)); - try - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } - }; - - return _httpClientHelper.PostAsync( - GetAdminUri("exportRegistry"), - new ExportImportRequest - { - ContainerName = containerName, - StorageConnectionString = storageAccountConnectionString, - }, - errorMappingOverrides, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ExportRegistryAsync)} threw an exception: {ex}", nameof(ExportRegistryAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Exporting registry", nameof(ExportRegistryAsync)); - } - } - - public override Task ImportRegistryAsync(string storageAccountConnectionString, string containerName) - { - return ImportRegistryAsync(storageAccountConnectionString, containerName, CancellationToken.None); - } - - public override Task ImportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Importing registry", nameof(ImportRegistryAsync)); - - try - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } - }; - - return _httpClientHelper.PostAsync( - GetAdminUri("importRegistry"), - new ExportImportRequest - { - ContainerName = containerName, - StorageConnectionString = storageAccountConnectionString, - }, - errorMappingOverrides, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ImportRegistryAsync)} threw an exception: {ex}", nameof(ImportRegistryAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Importing registry", nameof(ImportRegistryAsync)); - } - } - - public override Task GetJobAsync(string jobId) - { - return GetJobAsync(jobId, CancellationToken.None); - } - - public override Task> GetJobsAsync() - { - return GetJobsAsync(CancellationToken.None); - } - - public override Task CancelJobAsync(string jobId) - { - return CancelJobAsync(jobId, CancellationToken.None); - } - - public override Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys) - { - return ExportDevicesAsync( - JobProperties.CreateForExportJob( - exportBlobContainerUri, - excludeKeys)); - } - - public override Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys, CancellationToken ct) - { - return ExportDevicesAsync( - JobProperties.CreateForExportJob( - exportBlobContainerUri, - excludeKeys), - ct); - } - - public override Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys) - { - return ExportDevicesAsync( - JobProperties.CreateForExportJob( - exportBlobContainerUri, - excludeKeys, - outputBlobName)); - } - - public override Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys, CancellationToken ct) - { - return ExportDevicesAsync( - JobProperties.CreateForExportJob( - exportBlobContainerUri, - excludeKeys, - outputBlobName), - ct); - } - - public override Task ExportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default) - { - Logging.Enter(this, $"Export Job running with {jobParameters}", nameof(ExportDevicesAsync)); - - try - { - jobParameters.Type = JobType.ExportDevices; - return CreateJobAsync(jobParameters, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ExportDevicesAsync)} threw an exception: {ex}", nameof(ExportDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Export Job running with {jobParameters}", nameof(ExportDevicesAsync)); - } - } - - public override Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri) - { - return ImportDevicesAsync( - JobProperties.CreateForImportJob( - importBlobContainerUri, - outputBlobContainerUri)); - } - - public override Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, CancellationToken ct) - { - return ImportDevicesAsync( - JobProperties.CreateForImportJob( - importBlobContainerUri, - outputBlobContainerUri), - ct); - } - - public override Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName) - { - return ImportDevicesAsync( - JobProperties.CreateForImportJob( - importBlobContainerUri, - outputBlobContainerUri, - inputBlobName)); - } - - public override Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName, CancellationToken ct) - { - return ImportDevicesAsync( - JobProperties.CreateForImportJob( - importBlobContainerUri, - outputBlobContainerUri, - inputBlobName), - ct); - } - - public override Task ImportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default) - { - Logging.Enter(this, $"Import Job running with {jobParameters}", nameof(ImportDevicesAsync)); - try - { - jobParameters.Type = JobType.ImportDevices; - return CreateJobAsync(jobParameters, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ExportDevicesAsync)} threw an exception: {ex}", nameof(ImportDevicesAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Import Job running with {jobParameters}", nameof(ImportDevicesAsync)); - } - } - - private Task CreateJobAsync(JobProperties jobProperties, CancellationToken ct) - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.Forbidden, async (responseMessage) => new JobQuotaExceededException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false))} - }; - - string clientApiVersion = ClientApiVersionHelper.ApiVersionQueryString; - - return _httpClientHelper.PostAsync( - GetJobUri("/create", clientApiVersion), - jobProperties, - errorMappingOverrides, - null, - ct); - } - - public override Task GetJobAsync(string jobId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting job {jobId}", nameof(GetJobsAsync)); - try - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new JobNotFoundException(jobId)) } - }; - - return _httpClientHelper.GetAsync( - GetJobUri("/{0}".FormatInvariant(jobId)), - errorMappingOverrides, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting job {jobId}", nameof(GetJobsAsync)); - } - } - - public override Task> GetJobsAsync(CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting job", nameof(GetJobsAsync)); - try - { - EnsureInstanceNotClosed(); - - return _httpClientHelper.GetAsync>( - GetJobUri(string.Empty), - null, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting job", nameof(GetJobsAsync)); - } - } - - public override Task CancelJobAsync(string jobId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Canceling job: {jobId}", nameof(CancelJobAsync)); - try - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new JobNotFoundException(jobId)) } - }; - - IETagHolder jobETag = new ETagHolder - { - ETag = jobId, - }; - - return _httpClientHelper.DeleteAsync( - GetJobUri("/{0}".FormatInvariant(jobId)), - jobETag, - errorMappingOverrides, - null, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting job {jobId}", nameof(GetJobsAsync)); - } - } - - public override Task GetTwinAsync(string deviceId) - { - return GetTwinAsync(deviceId, CancellationToken.None); - } - - public override Task GetTwinAsync(string deviceId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting device twin on device: {deviceId}", nameof(GetTwinAsync)); - try - { - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.NotFound, async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } - }; - - return _httpClientHelper.GetAsync(GetTwinUri(deviceId), errorMappingOverrides, null, false, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetTwinAsync)} threw an exception: {ex}", nameof(GetTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting device twin on device: {deviceId}", nameof(GetTwinAsync)); - } - } - - public override Task GetTwinAsync(string deviceId, string moduleId) - { - return GetTwinAsync(deviceId, moduleId, CancellationToken.None); - } - - public override Task GetTwinAsync(string deviceId, string moduleId, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Getting device twin on device: {deviceId} and module: {moduleId}", nameof(GetTwinAsync)); - - try - { - if (string.IsNullOrWhiteSpace(deviceId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); - } - - if (string.IsNullOrWhiteSpace(moduleId)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "moduleId")); - } - - EnsureInstanceNotClosed(); - var errorMappingOverrides = new Dictionary>>() - { - { HttpStatusCode.NotFound, async responseMessage => new ModuleNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), (Exception)null) } - }; - - return _httpClientHelper.GetAsync(GetModuleTwinRequestUri(deviceId, moduleId), errorMappingOverrides, null, false, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(GetTwinAsync)} threw an exception: {ex}", nameof(GetTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Getting device twin on device: {deviceId} and module: {moduleId}", nameof(GetTwinAsync)); - } - } - - public override Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag) - { - return UpdateTwinAsync(deviceId, jsonTwinPatch, etag, CancellationToken.None); - } - - public override Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating device twin on device: {deviceId}", nameof(UpdateTwinAsync)); - - try - { - if (string.IsNullOrWhiteSpace(jsonTwinPatch)) - { - throw new ArgumentNullException(nameof(jsonTwinPatch)); - } - - // TODO: Do we need to deserialize Twin, only to serialize it again? - Twin twin = JsonConvert.DeserializeObject(jsonTwinPatch); - return UpdateTwinAsync(deviceId, twin, etag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating device twin on device: {deviceId}", nameof(UpdateTwinAsync)); - } - } - - public override Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag) - { - return UpdateTwinAsync(deviceId, moduleId, twinPatch, etag, CancellationToken.None); - } - - public override Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag, CancellationToken cancellationToken) - { - return UpdateTwinInternalAsync(deviceId, moduleId, twinPatch, etag, false, cancellationToken); - } - - public override Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag) - { - return UpdateTwinAsync(deviceId, moduleId, jsonTwinPatch, etag, CancellationToken.None); - } - - public override Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Updating device twin on device: {deviceId} and module: {moduleId}", nameof(UpdateTwinAsync)); - try - { - if (string.IsNullOrWhiteSpace(jsonTwinPatch)) - { - throw new ArgumentNullException(nameof(jsonTwinPatch)); - } - - // TODO: Do we need to deserialize Twin, only to serialize it again? - Twin twin = JsonConvert.DeserializeObject(jsonTwinPatch); - return UpdateTwinAsync(deviceId, moduleId, twin, etag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Updating device twin on device: {deviceId} and module: {moduleId}", nameof(UpdateTwinAsync)); - } - } - - public override Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag) - { - return UpdateTwinAsync(deviceId, twinPatch, etag, CancellationToken.None); - } - - public override Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag, CancellationToken cancellationToken) - { - return UpdateTwinInternalAsync(deviceId, twinPatch, etag, false, cancellationToken); - } - - public override Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag) - { - return ReplaceTwinAsync(deviceId, newTwinJson, etag, CancellationToken.None); - } - - public override Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Replacing device twin on device: {deviceId}", nameof(ReplaceTwinAsync)); - try - { - if (string.IsNullOrWhiteSpace(newTwinJson)) - { - throw new ArgumentNullException(nameof(newTwinJson)); - } - - // TODO: Do we need to deserialize Twin, only to serialize it again? - Twin twin = JsonConvert.DeserializeObject(newTwinJson); - return ReplaceTwinAsync(deviceId, twin, etag, cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(ReplaceTwinAsync)} threw an exception: {ex}", nameof(ReplaceTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Replacing device twin on device: {deviceId}", nameof(ReplaceTwinAsync)); - } - } - - public override Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag) - { - return ReplaceTwinAsync(deviceId, moduleId, newTwin, etag, CancellationToken.None); - } - - public override Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag, CancellationToken cancellationToken) - { - return UpdateTwinInternalAsync(deviceId, moduleId, newTwin, etag, true, cancellationToken); - } - - public override Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag) - { - return ReplaceTwinAsync(deviceId, moduleId, newTwinJson, etag, CancellationToken.None); - } - - public override Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(newTwinJson)) - { - throw new ArgumentNullException(nameof(newTwinJson)); - } - - // TODO: Do we need to deserialize Twin, only to serialize it again? - Twin twin = JsonConvert.DeserializeObject(newTwinJson); - return ReplaceTwinAsync(deviceId, moduleId, twin, etag, cancellationToken); - } - - public override Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag) - { - return ReplaceTwinAsync(deviceId, newTwin, etag, CancellationToken.None); - } - - public override Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag, CancellationToken cancellationToken) - { - return UpdateTwinInternalAsync(deviceId, newTwin, etag, true, cancellationToken); - } - - private Task UpdateTwinInternalAsync(string deviceId, Twin twin, string etag, bool isReplace, CancellationToken cancellationToken) - { - EnsureInstanceNotClosed(); - - if (twin != null) - { - twin.DeviceId = deviceId; - } - - ValidateTwinId(twin); - - if (string.IsNullOrEmpty(etag)) - { - throw new ArgumentNullException(nameof(etag)); - } - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - { - HttpStatusCode.NotFound, - async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), (Exception)null) - } - }; - - return isReplace - ? _httpClientHelper.PutAsync( - GetTwinUri(deviceId), - twin, - etag, - etag == WildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, - errorMappingOverrides, - cancellationToken) - : _httpClientHelper.PatchAsync( - GetTwinUri(deviceId), - twin, - etag, - etag == WildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, - errorMappingOverrides, - cancellationToken); - } - - private Task UpdateTwinInternalAsync(string deviceId, string moduleId, Twin twin, string etag, bool isReplace, CancellationToken cancellationToken) - { - Logging.Enter(this, $"Replacing device twin on device: {deviceId} - module: {moduleId} - is replace: {isReplace}", nameof(UpdateTwinAsync)); - try - { - EnsureInstanceNotClosed(); - - if (twin != null) - { - twin.DeviceId = deviceId; - twin.ModuleId = moduleId; - } - - ValidateTwinId(twin); - - if (string.IsNullOrEmpty(etag)) - { - throw new ArgumentNullException(nameof(etag)); - } - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - { - HttpStatusCode.NotFound, - async responseMessage => new ModuleNotFoundException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), - (Exception)null) - } - }; - - return isReplace - ? _httpClientHelper.PutAsync( - GetModuleTwinRequestUri(deviceId, moduleId), - twin, - etag, - etag == WildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, - errorMappingOverrides, - cancellationToken) - : _httpClientHelper.PatchAsync( - GetModuleTwinRequestUri(deviceId, moduleId), - twin, - etag, - etag == WildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, - errorMappingOverrides, - cancellationToken); - } - catch (Exception ex) - { - Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); - throw; - } - finally - { - Logging.Exit(this, $"Replacing device twin on device: {deviceId} - module: {moduleId} - is replace: {isReplace}", nameof(UpdateTwinAsync)); - } - } - - public override Task UpdateTwins2Async(IEnumerable twins) - { - return UpdateTwins2Async(twins, false, CancellationToken.None); - } - - public override Task UpdateTwins2Async(IEnumerable twins, CancellationToken cancellationToken) - { - return UpdateTwins2Async(twins, false, cancellationToken); - } - - public override Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate) - { - return UpdateTwins2Async(twins, forceUpdate, CancellationToken.None); - } - - public override Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate, CancellationToken cancellationToken) - { - return BulkDeviceOperationsAsync( - GenerateExportImportDeviceListForTwinBulkOperations(twins, forceUpdate ? ImportMode.UpdateTwin : ImportMode.UpdateTwinIfMatchETag), - ClientApiVersionHelper.ApiVersionQueryString, - cancellationToken); - } - - private Task RemoveDeviceAsync(string deviceId, IETagHolder eTagHolder, CancellationToken cancellationToken) - { - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.NotFound, - async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return new DeviceNotFoundException(responseContent, (Exception) null); - } - }, - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - }; - - return _httpClientHelper.DeleteAsync(GetRequestUri(deviceId), eTagHolder, errorMappingOverrides, null, cancellationToken); - } - - private Task RemoveDeviceModuleAsync(string deviceId, string moduleId, IETagHolder eTagHolder, CancellationToken cancellationToken) - { - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.NotFound, - async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return new DeviceNotFoundException(responseContent, (Exception) null); - } - }, - { - HttpStatusCode.PreconditionFailed, - async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - }; - - return _httpClientHelper.DeleteAsync(GetModulesRequestUri(deviceId, moduleId), eTagHolder, errorMappingOverrides, null, cancellationToken); - } - - private static Uri GetRequestUri(string deviceId) - { - deviceId = WebUtility.UrlEncode(deviceId); - return new Uri(RequestUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetModulesRequestUri(string deviceId, string moduleId) - { - deviceId = WebUtility.UrlEncode(deviceId); - moduleId = WebUtility.UrlEncode(moduleId); - return new Uri(ModulesRequestUriFormat.FormatInvariant(deviceId, moduleId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetModulesOnDeviceRequestUri(string deviceId) - { - deviceId = WebUtility.UrlEncode(deviceId); - return new Uri(ModulesOnDeviceRequestUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetModuleTwinRequestUri(string deviceId, string moduleId) - { - deviceId = WebUtility.UrlEncode(deviceId); - moduleId = WebUtility.UrlEncode(moduleId); - return new Uri(ModuleTwinUriFormat.FormatInvariant(deviceId, moduleId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetConfigurationRequestUri(string configurationId) - { - configurationId = WebUtility.UrlEncode(configurationId); - return new Uri(ConfigurationRequestUriFormat.FormatInvariant(configurationId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetConfigurationsRequestUri(int maxCount) - { - return new Uri(ConfigurationsRequestUriFormat.FormatInvariant(maxCount, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetApplyConfigurationOnDeviceRequestUri(string deviceId) - { - return new Uri(ApplyConfigurationOnDeviceUriFormat.FormatInvariant(deviceId), UriKind.Relative); - } - - private static Uri GetBulkRequestUri(string apiVersionQueryString) - { - return new Uri(RequestUriFormat.FormatInvariant(string.Empty, apiVersionQueryString), UriKind.Relative); - } - - private static Uri GetJobUri(string jobId, string apiVersion = ClientApiVersionHelper.ApiVersionQueryString) - { - return new Uri(JobsUriFormat.FormatInvariant(jobId, apiVersion), UriKind.Relative); - } - - private static Uri GetDevicesRequestUri(int maxCount) - { - return new Uri(DevicesRequestUriFormat.FormatInvariant(maxCount, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri QueryDevicesRequestUri() - { - return new Uri(DevicesQueryUriFormat, UriKind.Relative); - } - - private static Uri GetAdminUri(string operation) - { - return new Uri(AdminUriFormat.FormatInvariant(operation, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static Uri GetStatisticsUri() - { - return new Uri(StatisticsUriFormat, UriKind.Relative); - } - - private static Uri GetTwinUri(string deviceId) - { - deviceId = WebUtility.UrlEncode(deviceId); - return new Uri(TwinUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - - private static void ValidateDeviceId(Device device) - { - if (device == null) - { - throw new ArgumentNullException(nameof(device)); - } - - if (string.IsNullOrWhiteSpace(device.Id)) - { - throw new ArgumentException("device.Id"); - } - - if (!s_deviceIdRegex.IsMatch(device.Id)) - { - throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(device.Id)); - } - } - - private static void ValidateTwinId(Twin twin) - { - if (twin == null) - { - throw new ArgumentNullException(nameof(twin)); - } - - if (string.IsNullOrWhiteSpace(twin.DeviceId)) - { - throw new ArgumentException("twin.DeviceId"); - } - - if (!s_deviceIdRegex.IsMatch(twin.DeviceId)) - { - throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(twin.DeviceId)); - } - } - - private static void ValidateModuleId(Module module) - { - if (module == null) - { - throw new ArgumentNullException(nameof(module)); - } - - if (string.IsNullOrWhiteSpace(module.DeviceId)) - { - throw new ArgumentException("module.Id"); - } - - if (string.IsNullOrWhiteSpace(module.Id)) - { - throw new ArgumentException("module.ModuleId"); - } - - if (!s_deviceIdRegex.IsMatch(module.DeviceId)) - { - throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(module.DeviceId)); - } - - if (!s_deviceIdRegex.IsMatch(module.Id)) - { - throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(module.Id)); - } - } - - private static void ValidateDeviceAuthentication(AuthenticationMechanism authentication, string deviceId) - { - if (authentication != null) - { - // Both symmetric keys and X.509 cert thumbprints cannot be specified for the same device - bool symmetricKeyIsSet = !authentication.SymmetricKey?.IsEmpty() ?? false; - bool x509ThumbprintIsSet = !authentication.X509Thumbprint?.IsEmpty() ?? false; - - if (symmetricKeyIsSet && x509ThumbprintIsSet) - { - throw new ArgumentException(ApiResources.DeviceAuthenticationInvalid.FormatInvariant(deviceId ?? string.Empty)); - } - - // Validate X.509 thumbprints or SymmetricKeys since we should not have both at the same time - if (x509ThumbprintIsSet) - { - authentication.X509Thumbprint.IsValid(true); - } - else if (symmetricKeyIsSet) - { - authentication.SymmetricKey.IsValid(true); - } - } - } - - private void EnsureInstanceNotClosed() - { - if (_httpClientHelper == null) - { - throw new ObjectDisposedException("RegistryManager", ApiResources.RegistryManagerInstanceAlreadyClosed); - } - } - - private async Task ExecuteQueryAsync(string sqlQueryString, int? pageSize, string continuationToken, CancellationToken cancellationToken) - { - EnsureInstanceNotClosed(); - - if (string.IsNullOrWhiteSpace(sqlQueryString)) - { - throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrEmpty, nameof(sqlQueryString))); - } - - var customHeaders = new Dictionary(); - if (!string.IsNullOrWhiteSpace(continuationToken)) - { - customHeaders.Add(ContinuationTokenHeader, continuationToken); - } - - if (pageSize != null) - { - customHeaders.Add(PageSizeHeader, pageSize.ToString()); - } - - HttpResponseMessage response = await _httpClientHelper - .PostAsync( - QueryDevicesRequestUri(), - new QuerySpecification { Sql = sqlQueryString }, - null, - customHeaders, - new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }, - null, - cancellationToken) - .ConfigureAwait(false); - - return await QueryResult.FromHttpResponseAsync(response).ConfigureAwait(false); - } - - private static void NormalizeExportImportDevice(ExportImportDevice device) - { - // auto generate keys if not specified - if (device.Authentication == null) - { - device.Authentication = new AuthenticationMechanism(); - } - - NormalizeAuthenticationInfo(device.Authentication); - } - - private static void NormalizeDevice(Device device) - { - // auto generate keys if not specified - if (device.Authentication == null) - { - device.Authentication = new AuthenticationMechanism(); - } - - NormalizeAuthenticationInfo(device.Authentication); - } - - private static void NormalizeAuthenticationInfo(AuthenticationMechanism authenticationInfo) - { - //to make it backward compatible we set the type according to the values - //we don't set CA type - that has to be explicit - if (authenticationInfo.SymmetricKey != null && !authenticationInfo.SymmetricKey.IsEmpty()) - { - authenticationInfo.Type = AuthenticationType.Sas; - } - - if (authenticationInfo.X509Thumbprint != null && !authenticationInfo.X509Thumbprint.IsEmpty()) - { - authenticationInfo.Type = AuthenticationType.SelfSigned; - } - } - } -} diff --git a/iothub/service/src/JobClient/HttpJobClient.cs b/iothub/service/src/JobClient/HttpJobClient.cs deleted file mode 100644 index 63fe9f3683..0000000000 --- a/iothub/service/src/JobClient/HttpJobClient.cs +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.Devices.Shared; -using Microsoft.Azure.Devices.Common; -using Microsoft.Azure.Devices.Common.Exceptions; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Azure.Devices -{ - internal class HttpJobClient : JobClient - { - private const string JobsUriFormat = "/jobs/v2/{0}?{1}"; - private const string JobsQueryFormat = "/jobs/v2/query?{0}"; - private const string CancelJobUriFormat = "/jobs/v2/{0}/cancel?{1}"; - - private const string ContinuationTokenHeader = "x-ms-continuation"; - private const string PageSizeHeader = "x-ms-max-item-count"; - - private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); - - private IHttpClientHelper _httpClientHelper; - - internal HttpJobClient(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) - { - _httpClientHelper = new HttpClientHelper( - connectionProperties.HttpsEndpoint, - connectionProperties, - ExceptionHandlingHelper.GetDefaultErrorMapping(), - s_defaultOperationTimeout, - transportSettings.Proxy, - transportSettings.ConnectionLeaseTimeoutMilliseconds); - } - - // internal test helper - internal HttpJobClient(IHttpClientHelper httpClientHelper) - { - _httpClientHelper = httpClientHelper ?? throw new ArgumentNullException(nameof(httpClientHelper)); - } - - public override Task OpenAsync() - { - return TaskHelpers.CompletedTask; - } - - public override Task CloseAsync() - { - return TaskHelpers.CompletedTask; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - if (disposing) - { - if (_httpClientHelper != null) - { - _httpClientHelper.Dispose(); - _httpClientHelper = null; - } - } - } - - public override Task GetJobAsync(string jobId) - { - return GetJobAsync(jobId, CancellationToken.None); - } - - public override IQuery CreateQuery() - { - return CreateQuery(null, null, null); - } - - public override IQuery CreateQuery(int? pageSize) - { - return CreateQuery(null, null, pageSize); - } - - public override IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus) - { - return CreateQuery(jobType, jobStatus, null); - } - - public override IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus, int? pageSize) - { - return new Query((token) => GetJobsAsync(jobType, jobStatus, pageSize, token, CancellationToken.None)); - } - - public override Task CancelJobAsync(string jobId) - { - return CancelJobAsync(jobId, CancellationToken.None); - } - - public override Task GetJobAsync(string jobId, CancellationToken cancellationToken) - { - Logging.Enter(this, jobId, nameof(GetJobAsync)); - - try - { - EnsureInstanceNotClosed(); - - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.NotFound, - responseMessage => Task.FromResult((Exception) new JobNotFoundException(jobId)) - } - }; - - return _httpClientHelper.GetAsync( - GetJobUri(jobId), - errorMappingOverrides, - null, - cancellationToken); - } - finally - { - Logging.Exit(this, jobId, nameof(GetJobAsync)); - } - } - - public override Task CancelJobAsync(string jobId, CancellationToken cancellationToken) - { - Logging.Enter(this, jobId, nameof(CancelJobAsync)); - - try - { - EnsureInstanceNotClosed(); - - return _httpClientHelper.PostAsync( - new Uri(CancelJobUriFormat.FormatInvariant(jobId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative), - null, - null, - null, - cancellationToken); - } - finally - { - Logging.Exit(this, jobId, nameof(CancelJobAsync)); - } - } - - /// - public override Task ScheduleDeviceMethodAsync( - string jobId, - string queryCondition, - CloudToDeviceMethod methodCall, - DateTime startTimeUtc, - long maxExecutionTimeInSeconds) - { - return ScheduleDeviceMethodAsync(jobId, queryCondition, methodCall, startTimeUtc, maxExecutionTimeInSeconds, CancellationToken.None); - } - - /// - public override Task ScheduleDeviceMethodAsync( - string jobId, - string queryCondition, - CloudToDeviceMethod cloudToDeviceMethod, - DateTime startTimeUtc, - long maxExecutionTimeInSeconds, - CancellationToken cancellationToken) - { - EnsureInstanceNotClosed(); - - var jobRequest = new JobRequest - { - JobId = jobId, - JobType = JobType.ScheduleDeviceMethod, - CloudToDeviceMethod = cloudToDeviceMethod, - QueryCondition = queryCondition, - StartTime = startTimeUtc, - MaxExecutionTimeInSeconds = maxExecutionTimeInSeconds - }; - - return CreateJobAsync(jobRequest, cancellationToken); - } - - public override Task ScheduleTwinUpdateAsync( - string jobId, - string queryCondition, - Twin twin, - DateTime startTimeUtc, - long maxExecutionTimeInSeconds) - { - return ScheduleTwinUpdateAsync(jobId, queryCondition, twin, startTimeUtc, maxExecutionTimeInSeconds, CancellationToken.None); - } - - public override Task ScheduleTwinUpdateAsync( - string jobId, - string queryCondition, - Twin twin, - DateTime startTimeUtc, - long maxExecutionTimeInSeconds, - CancellationToken cancellationToken) - { - EnsureInstanceNotClosed(); - - var jobRequest = new JobRequest - { - JobId = jobId, - JobType = JobType.ScheduleUpdateTwin, - UpdateTwin = twin, - QueryCondition = queryCondition, - StartTime = startTimeUtc, - MaxExecutionTimeInSeconds = maxExecutionTimeInSeconds - }; - - return CreateJobAsync(jobRequest, cancellationToken); - } - - private Task CreateJobAsync(JobRequest jobRequest, CancellationToken cancellationToken) - { - Logging.Enter(this, $"jobId=[{jobRequest?.JobId}], jobType=[{jobRequest?.JobType}]", nameof(CreateJobAsync)); - - try - { - var errorMappingOverrides = new Dictionary>> - { - { - HttpStatusCode.PreconditionFailed, - async (responseMessage) => - new PreconditionFailedException( - await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) - }, - { - HttpStatusCode.NotFound, async responseMessage => - { - string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); - return (Exception) new DeviceNotFoundException(responseContent, (Exception) null); - } - } - }; - - return _httpClientHelper.PutAsync( - GetJobUri(jobRequest.JobId), - jobRequest, - errorMappingOverrides, - cancellationToken); - } - finally - { - Logging.Exit(this, $"jobId=[{jobRequest?.JobId}], jobType=[{jobRequest?.JobType}]", nameof(CreateJobAsync)); - } - } - - private void EnsureInstanceNotClosed() - { - if (_httpClientHelper == null) - { - throw new ObjectDisposedException("JobClient", ApiResources.JobClientInstanceAlreadyClosed); - } - } - - private async Task GetJobsAsync(JobType? jobType, JobStatus? jobStatus, int? pageSize, string continuationToken, CancellationToken cancellationToken) - { - Logging.Enter(this, $"jobType=[{jobType}], jobStatus=[{jobStatus}], pageSize=[{pageSize}]", nameof(GetJobsAsync)); - - try - { - EnsureInstanceNotClosed(); - - var customHeaders = new Dictionary(); - if (!string.IsNullOrWhiteSpace(continuationToken)) - { - customHeaders.Add(ContinuationTokenHeader, continuationToken); - } - - if (pageSize != null) - { - customHeaders.Add(PageSizeHeader, pageSize.ToString()); - } - - HttpResponseMessage response = await _httpClientHelper.GetAsync( - BuildQueryJobUri(jobType, jobStatus), - null, - customHeaders, - cancellationToken).ConfigureAwait(false); - - return await QueryResult.FromHttpResponseAsync(response).ConfigureAwait(false); - } - finally - { - Logging.Exit(this, $"jobType=[{jobType}], jobStatus=[{jobStatus}], pageSize=[{pageSize}]", nameof(GetJobsAsync)); - } - } - - private static Uri BuildQueryJobUri(JobType? jobType, JobStatus? jobStatus) - { - var stringBuilder = new StringBuilder(JobsQueryFormat.FormatInvariant(ClientApiVersionHelper.ApiVersionQueryString)); - - if (jobType != null) - { - stringBuilder.Append("&jobType={0}".FormatInvariant(WebUtility.UrlEncode(jobType.ToString()))); - } - - if (jobStatus != null) - { - stringBuilder.Append("&jobStatus={0}".FormatInvariant(WebUtility.UrlEncode(jobStatus.ToString()))); - } - - return new Uri(stringBuilder.ToString(), UriKind.Relative); - } - - private static Uri GetJobUri(string jobId) - { - return new Uri(JobsUriFormat.FormatInvariant(jobId ?? string.Empty, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); - } - } -} diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index c5dca84b67..32421512b4 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -4,6 +4,12 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Devices.Common; +using System.Collections.Generic; +using System.Net; +using Microsoft.Azure.Devices.Common.Exceptions; +using System.Net.Http; +using System.Text; #if !NET451 @@ -17,13 +23,49 @@ namespace Microsoft.Azure.Devices /// /// Job management /// - public abstract class JobClient : IDisposable + public class JobClient : IDisposable { + private const string _jobsUriFormat = "/jobs/v2/{0}?{1}"; + private const string _jobsQueryFormat = "/jobs/v2/query?{0}"; + private const string _CancelJobUriFormat = "/jobs/v2/{0}/cancel?{1}"; + + private const string _continuationTokenHeader = "x-ms-continuation"; + private const string _pageSizeHeader = "x-ms-max-item-count"; + + private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); + + private IHttpClientHelper _httpClientHelper; + /// - /// Creates a JobClient from the Iot Hub connection string. + /// Creates an instance of , provided for unit testing purposes only. + /// Use the CreateFromConnectionString method to create an instance to use the client. + /// + public JobClient() + { + } + + // internal test helper + internal JobClient(IHttpClientHelper httpClientHelper) + { + _httpClientHelper = httpClientHelper; + } + + internal JobClient(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) + { + _httpClientHelper = new HttpClientHelper( + connectionProperties.HttpsEndpoint, + connectionProperties, + ExceptionHandlingHelper.GetDefaultErrorMapping(), + s_defaultOperationTimeout, + transportSettings.Proxy, + transportSettings.ConnectionLeaseTimeoutMilliseconds); + } + + /// + /// Creates a JobClient from the IoT Hub connection string. /// For more information, see /// - /// The Iot Hub connection string. + /// The IoT Hub connection string. /// A JobClient instance. public static JobClient CreateFromConnectionString(string connectionString) { @@ -31,9 +73,9 @@ public static JobClient CreateFromConnectionString(string connectionString) } /// - /// Creates a JobClient from the Iot Hub connection string and HTTP transport settings + /// Creates a JobClient from the IoT Hub connection string and HTTP transport settings /// - /// The Iot Hub connection string. + /// The IoT Hub connection string. /// The HTTP transport settings. /// A JobClient instance. public static JobClient CreateFromConnectionString(string connectionString, HttpTransportSettings transportSettings) @@ -45,7 +87,7 @@ public static JobClient CreateFromConnectionString(string connectionString, Http TlsVersions.Instance.SetLegacyAcceptableVersions(); var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); - return new HttpJobClient(iotHubConnectionString, transportSettings); + return new JobClient(iotHubConnectionString, transportSettings); } #if !NET451 @@ -76,7 +118,7 @@ public static JobClient Create( } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); - return new HttpJobClient(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); + return new JobClient(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); } /// @@ -102,7 +144,7 @@ public static JobClient Create( } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); - return new HttpJobClient(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); + return new JobClient(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); } #endif @@ -110,7 +152,7 @@ public static JobClient Create( /// public void Dispose() { - this.Dispose(true); + Dispose(true); GC.SuppressFinalize(this); } @@ -118,24 +160,43 @@ public void Dispose() /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_httpClientHelper != null) + { + _httpClientHelper.Dispose(); + _httpClientHelper = null; + } + } + } /// /// Explicitly open the JobClient instance. /// - public abstract Task OpenAsync(); + public virtual Task OpenAsync() + { + return TaskHelpers.CompletedTask; + } /// /// Closes the JobClient instance and disposes its resources. /// - public abstract Task CloseAsync(); + public virtual Task CloseAsync() + { + return TaskHelpers.CompletedTask; + } /// /// Gets the job with the specified Id. /// /// Id of the Job to retrieve /// The matching JobResponse object - public abstract Task GetJobAsync(string jobId); + public virtual Task GetJobAsync(string jobId) + { + return GetJobAsync(jobId, CancellationToken.None); + } /// /// Gets the job with the specified Id. @@ -143,20 +204,52 @@ protected virtual void Dispose(bool disposing) { } /// Id of the job to retrieve /// Task cancellation token /// The matching JobResponse object - public abstract Task GetJobAsync(string jobId, CancellationToken cancellationToken); + public virtual Task GetJobAsync(string jobId, CancellationToken cancellationToken) + { + Logging.Enter(this, jobId, nameof(GetJobAsync)); + + try + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.NotFound, + responseMessage => Task.FromResult((Exception) new JobNotFoundException(jobId)) + } + }; + + return _httpClientHelper.GetAsync( + GetJobUri(jobId), + errorMappingOverrides, + null, + cancellationToken); + } + finally + { + Logging.Exit(this, jobId, nameof(GetJobAsync)); + } + } /// /// Get IQuery through which job responses for all job types and statuses are retrieved page by page /// /// IQuery - public abstract IQuery CreateQuery(); + public virtual IQuery CreateQuery() + { + return CreateQuery(null, null, null); + } /// /// Get IQuery through which job responses are retrieved page by page and specify page size /// /// Number of job responses in a page /// - public abstract IQuery CreateQuery(int? pageSize); + public virtual IQuery CreateQuery(int? pageSize) + { + return CreateQuery(null, null, pageSize); + } /// /// Get IQuery through which job responses for specified jobType and jobStatus are retrieved page by page @@ -164,7 +257,10 @@ protected virtual void Dispose(bool disposing) { } /// The job type to query. Could be null if not querying. /// The job status to query. Could be null if not querying. /// - public abstract IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus); + public virtual IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus) + { + return CreateQuery(jobType, jobStatus, null); + } /// /// Get IQuery through which job responses for specified jobType and jobStatus are retrieved page by page, @@ -174,20 +270,45 @@ protected virtual void Dispose(bool disposing) { } /// The job status to query. Could be null if not querying. /// Number of job responses in a page /// - public abstract IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus, int? pageSize); + public virtual IQuery CreateQuery(JobType? jobType, JobStatus? jobStatus, int? pageSize) + { + return new Query((token) => GetJobsAsync(jobType, jobStatus, pageSize, token, CancellationToken.None)); + } /// /// Cancels/Deletes the job with the specified Id. /// /// Id of the Job to cancel - public abstract Task CancelJobAsync(string jobId); + public virtual Task CancelJobAsync(string jobId) + { + return CancelJobAsync(jobId, CancellationToken.None); + } /// /// Cancels/Deletes the job with the specified Id. /// /// Id of the job to cancel /// Task cancellation token - public abstract Task CancelJobAsync(string jobId, CancellationToken cancellationToken); + public virtual Task CancelJobAsync(string jobId, CancellationToken cancellationToken) + { + Logging.Enter(this, jobId, nameof(CancelJobAsync)); + + try + { + EnsureInstanceNotClosed(); + + return _httpClientHelper.PostAsync( + new Uri(_CancelJobUriFormat.FormatInvariant(jobId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative), + null, + null, + null, + cancellationToken); + } + finally + { + Logging.Exit(this, jobId, nameof(CancelJobAsync)); + } + } /// /// Creates a new Job to run a device method on one or multiple devices @@ -198,7 +319,10 @@ protected virtual void Dispose(bool disposing) { } /// Date time in Utc to start the job /// Max execution time in seconds, i.e., ttl duration the job can run /// A JobResponse object - public abstract Task ScheduleDeviceMethodAsync(string jobId, string queryCondition, CloudToDeviceMethod cloudToDeviceMethod, DateTime startTimeUtc, long maxExecutionTimeInSeconds); + public virtual Task ScheduleDeviceMethodAsync(string jobId, string queryCondition, CloudToDeviceMethod cloudToDeviceMethod, DateTime startTimeUtc, long maxExecutionTimeInSeconds) + { + return ScheduleDeviceMethodAsync(jobId, queryCondition, cloudToDeviceMethod, startTimeUtc, maxExecutionTimeInSeconds, CancellationToken.None); + } /// /// Creates a new Job to run a device method on one or multiple devices @@ -210,7 +334,22 @@ protected virtual void Dispose(bool disposing) { } /// Max execution time in seconds, i.e., ttl duration the job can run /// Task cancellation token /// A JobResponse object - public abstract Task ScheduleDeviceMethodAsync(string jobId, string queryCondition, CloudToDeviceMethod cloudToDeviceMethod, DateTime startTimeUtc, long maxExecutionTimeInSeconds, CancellationToken cancellationToken); + public virtual Task ScheduleDeviceMethodAsync(string jobId, string queryCondition, CloudToDeviceMethod cloudToDeviceMethod, DateTime startTimeUtc, long maxExecutionTimeInSeconds, CancellationToken cancellationToken) + { + EnsureInstanceNotClosed(); + + var jobRequest = new JobRequest + { + JobId = jobId, + JobType = JobType.ScheduleDeviceMethod, + CloudToDeviceMethod = cloudToDeviceMethod, + QueryCondition = queryCondition, + StartTime = startTimeUtc, + MaxExecutionTimeInSeconds = maxExecutionTimeInSeconds + }; + + return CreateJobAsync(jobRequest, cancellationToken); + } /// /// Creates a new Job to update twin tags and desired properties on one or multiple devices @@ -221,7 +360,10 @@ protected virtual void Dispose(bool disposing) { } /// Date time in Utc to start the job /// Max execution time in seconds, i.e., ttl duration the job can run /// A JobResponse object - public abstract Task ScheduleTwinUpdateAsync(string jobId, string queryCondition, Twin twin, DateTime startTimeUtc, long maxExecutionTimeInSeconds); + public virtual Task ScheduleTwinUpdateAsync(string jobId, string queryCondition, Twin twin, DateTime startTimeUtc, long maxExecutionTimeInSeconds) + { + return ScheduleTwinUpdateAsync(jobId, queryCondition, twin, startTimeUtc, maxExecutionTimeInSeconds, CancellationToken.None); + } /// /// Creates a new Job to update twin tags and desired properties on one or multiple devices @@ -233,6 +375,119 @@ protected virtual void Dispose(bool disposing) { } /// Max execution time in seconds, i.e., ttl duration the job can run /// Task cancellation token /// A JobResponse object - public abstract Task ScheduleTwinUpdateAsync(string jobId, string queryCondition, Twin twin, DateTime startTimeUtc, long maxExecutionTimeInSeconds, CancellationToken cancellationToken); + public virtual Task ScheduleTwinUpdateAsync(string jobId, string queryCondition, Twin twin, DateTime startTimeUtc, long maxExecutionTimeInSeconds, CancellationToken cancellationToken) + { + EnsureInstanceNotClosed(); + + var jobRequest = new JobRequest + { + JobId = jobId, + JobType = JobType.ScheduleUpdateTwin, + UpdateTwin = twin, + QueryCondition = queryCondition, + StartTime = startTimeUtc, + MaxExecutionTimeInSeconds = maxExecutionTimeInSeconds + }; + + return CreateJobAsync(jobRequest, cancellationToken); + } + + private void EnsureInstanceNotClosed() + { + if (_httpClientHelper == null) + { + throw new ObjectDisposedException("JobClient", ApiResources.JobClientInstanceAlreadyClosed); + } + } + + private static Uri GetJobUri(string jobId) + { + return new Uri(_jobsUriFormat.FormatInvariant(jobId ?? string.Empty, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private Task CreateJobAsync(JobRequest jobRequest, CancellationToken cancellationToken) + { + Logging.Enter(this, $"jobId=[{jobRequest?.JobId}], jobType=[{jobRequest?.JobType}]", nameof(CreateJobAsync)); + + try + { + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async (responseMessage) => + new PreconditionFailedException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + { + HttpStatusCode.NotFound, async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return (Exception) new DeviceNotFoundException(responseContent, (Exception) null); + } + } + }; + + return _httpClientHelper.PutAsync( + GetJobUri(jobRequest.JobId), + jobRequest, + errorMappingOverrides, + cancellationToken); + } + finally + { + Logging.Exit(this, $"jobId=[{jobRequest?.JobId}], jobType=[{jobRequest?.JobType}]", nameof(CreateJobAsync)); + } + } + + private static Uri BuildQueryJobUri(JobType? jobType, JobStatus? jobStatus) + { + var stringBuilder = new StringBuilder(_jobsQueryFormat.FormatInvariant(ClientApiVersionHelper.ApiVersionQueryString)); + + if (jobType != null) + { + stringBuilder.Append("&jobType={0}".FormatInvariant(WebUtility.UrlEncode(jobType.ToString()))); + } + + if (jobStatus != null) + { + stringBuilder.Append("&jobStatus={0}".FormatInvariant(WebUtility.UrlEncode(jobStatus.ToString()))); + } + + return new Uri(stringBuilder.ToString(), UriKind.Relative); + } + + private async Task GetJobsAsync(JobType? jobType, JobStatus? jobStatus, int? pageSize, string continuationToken, CancellationToken cancellationToken) + { + Logging.Enter(this, $"jobType=[{jobType}], jobStatus=[{jobStatus}], pageSize=[{pageSize}]", nameof(GetJobsAsync)); + + try + { + EnsureInstanceNotClosed(); + + var customHeaders = new Dictionary(); + if (!string.IsNullOrWhiteSpace(continuationToken)) + { + customHeaders.Add(_continuationTokenHeader, continuationToken); + } + + if (pageSize != null) + { + customHeaders.Add(_pageSizeHeader, pageSize.ToString()); + } + + HttpResponseMessage response = await _httpClientHelper.GetAsync( + BuildQueryJobUri(jobType, jobStatus), + null, + customHeaders, + cancellationToken).ConfigureAwait(false); + + return await QueryResult.FromHttpResponseAsync(response).ConfigureAwait(false); + } + finally + { + Logging.Exit(this, $"jobType=[{jobType}], jobStatus=[{jobStatus}], pageSize=[{pageSize}]", nameof(GetJobsAsync)); + } + } } } diff --git a/iothub/service/src/JobStatus.cs b/iothub/service/src/JobStatus.cs index aacc7140e5..11a46cbd6b 100644 --- a/iothub/service/src/JobStatus.cs +++ b/iothub/service/src/JobStatus.cs @@ -56,7 +56,7 @@ public enum JobStatus Scheduled, /// - /// Indicates that a Job is in the queue for execution (synonym for enqueued to be depricated) + /// Indicates that a Job is in the queue for execution (synonym for enqueued to be deprecated) /// [EnumMember(Value = "queued")] Queued diff --git a/iothub/service/src/Microsoft.Azure.Devices.csproj b/iothub/service/src/Microsoft.Azure.Devices.csproj index e6b6bea77b..369761f9a5 100644 --- a/iothub/service/src/Microsoft.Azure.Devices.csproj +++ b/iothub/service/src/Microsoft.Azure.Devices.csproj @@ -25,7 +25,7 @@ - 1.34.0 + 1.35.0 Microsoft Azure IoT Service Client SDK True True diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index ef7ce5c51b..ed6882f3d1 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -3,9 +3,17 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Devices.Common; +using Microsoft.Azure.Devices.Common.Exceptions; using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; #if !NET451 @@ -24,8 +32,70 @@ namespace Microsoft.Azure.Devices "Naming", "CA1716:Identifiers should not match keywords", Justification = "Cannot change parameter names as it is considered a breaking change.")] - public abstract class RegistryManager : IDisposable + public class RegistryManager : IDisposable { + private const string _adminUriFormat = "/$admin/{0}?{1}"; + private const string _requestUriFormat = "/devices/{0}?{1}"; + private const string _jobsUriFormat = "/jobs{0}?{1}"; + private const string _statisticsUriFormat = "/statistics/devices?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _devicesRequestUriFormat = "/devices/?top={0}&{1}"; + private const string _devicesQueryUriFormat = "/devices/query?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _wildcardEtag = "*"; + + private const string _continuationTokenHeader = "x-ms-continuation"; + private const string _pageSizeHeader = "x-ms-max-item-count"; + + private const string _twinUriFormat = "/twins/{0}?{1}"; + + private const string _modulesRequestUriFormat = "/devices/{0}/modules/{1}?{2}"; + private const string _modulesOnDeviceRequestUriFormat = "/devices/{0}/modules?{1}"; + private const string _moduleTwinUriFormat = "/twins/{0}/modules/{1}?{2}"; + + private const string _configurationRequestUriFormat = "/configurations/{0}?{1}"; + private const string _configurationsRequestUriFormat = "/configurations/?top={0}&{1}"; + + private const string _applyConfigurationOnDeviceUriFormat = "/devices/{0}/applyConfigurationContent?" + ClientApiVersionHelper.ApiVersionQueryString; + + private static readonly TimeSpan s_regexTimeoutMilliseconds = TimeSpan.FromMilliseconds(500); + + private static readonly Regex s_deviceIdRegex = new Regex( + @"^[A-Za-z0-9\-:.+%_#*?!(),=@;$']{1,128}$", + RegexOptions.Compiled | RegexOptions.IgnoreCase, + s_regexTimeoutMilliseconds); + + private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); + private static readonly TimeSpan s_defaultGetDevicesOperationTimeout = TimeSpan.FromSeconds(120); + + private readonly string _iotHubName; + private IHttpClientHelper _httpClientHelper; + + /// + /// Creates an instance of , provided for unit testing purposes only. + /// Use the CreateFromConnectionString method to create an instance to use the client. + /// + public RegistryManager() + { + } + + internal RegistryManager(IotHubConnectionProperties connectionProperties, HttpTransportSettings transportSettings) + { + _iotHubName = connectionProperties.IotHubName; + _httpClientHelper = new HttpClientHelper( + connectionProperties.HttpsEndpoint, + connectionProperties, + ExceptionHandlingHelper.GetDefaultErrorMapping(), + s_defaultOperationTimeout, + transportSettings.Proxy, + transportSettings.ConnectionLeaseTimeoutMilliseconds); + } + + // internal test helper + internal RegistryManager(string iotHubName, IHttpClientHelper httpClientHelper) + { + _iotHubName = iotHubName; + _httpClientHelper = httpClientHelper ?? throw new ArgumentNullException(nameof(httpClientHelper)); + } + /// /// Creates a RegistryManager from the IoT Hub connection string. /// @@ -51,7 +121,7 @@ public static RegistryManager CreateFromConnectionString(string connectionString TlsVersions.Instance.SetLegacyAcceptableVersions(); var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); - return new HttpRegistryManager(iotHubConnectionString, transportSettings); + return new RegistryManager(iotHubConnectionString, transportSettings); } #if !NET451 @@ -82,7 +152,7 @@ public static RegistryManager Create( } var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); - return new HttpRegistryManager(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); + return new RegistryManager(tokenCredentialProperties, transportSettings ?? new HttpTransportSettings()); } /// @@ -108,7 +178,7 @@ public static RegistryManager Create( } var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); - return new HttpRegistryManager(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); + return new RegistryManager(sasCredentialProperties, transportSettings ?? new HttpTransportSettings()); } #endif @@ -124,24 +194,40 @@ public void Dispose() /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { } + protected virtual void Dispose(bool disposing) + { + if (disposing && _httpClientHelper != null) + { + _httpClientHelper.Dispose(); + _httpClientHelper = null; + } + } /// /// Explicitly open the RegistryManager instance. /// - public abstract Task OpenAsync(); + public virtual Task OpenAsync() + { + return TaskHelpers.CompletedTask; + } /// /// Closes the RegistryManager instance and disposes its resources. /// - public abstract Task CloseAsync(); + public virtual Task CloseAsync() + { + return TaskHelpers.CompletedTask; + } /// /// Register a new device with the system /// /// The Device object being registered. /// The Device object with the generated keys and ETags. - public abstract Task AddDeviceAsync(Device device); + public virtual Task AddDeviceAsync(Device device) + { + return AddDeviceAsync(device, CancellationToken.None); + } /// /// Register a new device with the system @@ -149,14 +235,55 @@ protected virtual void Dispose(bool disposing) { } /// The Device object being registered. /// The token which allows the operation to be canceled. /// The Device object with the generated keys and ETags. - public abstract Task AddDeviceAsync(Device device, CancellationToken cancellationToken); + public virtual Task AddDeviceAsync(Device device, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding device: {device?.Id}", nameof(AddDeviceAsync)); + + try + { + EnsureInstanceNotClosed(); + + ValidateDeviceId(device); + + if (!string.IsNullOrEmpty(device.ETag)) + { + throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); + } + + ValidateDeviceAuthentication(device.Authentication, device.Id); + + NormalizeDevice(device); + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + } + }; + + return _httpClientHelper.PutAsync(GetRequestUri(device.Id), device, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddDeviceAsync)} threw an exception: {ex}", nameof(AddDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Adding device: {device?.Id}", nameof(AddDeviceAsync)); + } + } /// /// Register a new module with device in the system /// /// The Module object being registered. /// The Module object with the generated keys and ETags. - public abstract Task AddModuleAsync(Module module); + public virtual Task AddModuleAsync(Module module) + { + return AddModuleAsync(module, CancellationToken.None); + } /// /// Register a new module with device in the system @@ -164,7 +291,60 @@ protected virtual void Dispose(bool disposing) { } /// The Module object being registered. /// The token which allows the operation to be canceled. /// The Module object with the generated keys and ETags. - public abstract Task AddModuleAsync(Module module, CancellationToken cancellationToken); + public virtual Task AddModuleAsync(Module module, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding module: {module?.Id}", nameof(AddModuleAsync)); + + try + { + EnsureInstanceNotClosed(); + + ValidateModuleId(module); + + if (!string.IsNullOrEmpty(module.ETag)) + { + throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); + } + + ValidateDeviceAuthentication(module.Authentication, module.DeviceId); + + // auto generate keys if not specified + if (module.Authentication == null) + { + module.Authentication = new AuthenticationMechanism(); + } + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + { + HttpStatusCode.Conflict, + async responseMessage => new ModuleAlreadyExistsException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + { + HttpStatusCode.RequestEntityTooLarge, + async responseMessage => new TooManyModulesOnDeviceException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + } + }; + + return _httpClientHelper.PutAsync(GetModulesRequestUri(module.DeviceId, module.Id), module, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddModuleAsync)} threw an exception: {ex}", nameof(AddModuleAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Adding module: {module?.Id}", nameof(AddModuleAsync)); + } + } /// /// Adds a Device with Twin information @@ -172,7 +352,10 @@ protected virtual void Dispose(bool disposing) { } /// The device to add. /// The twin information for the device being added. /// The result of the add operation. - public abstract Task AddDeviceWithTwinAsync(Device device, Twin twin); + public virtual Task AddDeviceWithTwinAsync(Device device, Twin twin) + { + return AddDeviceWithTwinAsync(device, twin, CancellationToken.None); + } /// /// Adds a Device with Twin information @@ -181,7 +364,46 @@ protected virtual void Dispose(bool disposing) { } /// The twin information for the device being added. /// A cancellation token to cancel the operation. /// The result of the add operation. - public abstract Task AddDeviceWithTwinAsync(Device device, Twin twin, CancellationToken cancellationToken); + public virtual Task AddDeviceWithTwinAsync(Device device, Twin twin, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding device with twin: {device?.Id}", nameof(AddDeviceWithTwinAsync)); + + try + { + ValidateDeviceId(device); + if (!string.IsNullOrWhiteSpace(device.ETag)) + { + throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); + } + var exportImportDeviceList = new List(1); + + var exportImportDevice = new ExportImportDevice(device, ImportMode.Create) + { + Tags = twin?.Tags, + Properties = new ExportImportDevice.PropertyContainer + { + DesiredProperties = twin?.Properties.Desired, + ReportedProperties = twin?.Properties.Reported, + } + }; + + exportImportDeviceList.Add(exportImportDevice); + + return BulkDeviceOperationsAsync( + exportImportDeviceList, + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddDeviceWithTwinAsync)} threw an exception: {ex}", nameof(AddDeviceWithTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Adding device with twin: {device?.Id}", nameof(AddDeviceWithTwinAsync)); + } + } /// /// Register a list of new devices with the system @@ -189,7 +411,10 @@ protected virtual void Dispose(bool disposing) { } /// The Device objects being registered. /// Returns a string array of error messages. [Obsolete("Use AddDevices2Async")] - public abstract Task AddDevicesAsync(IEnumerable devices); + public virtual Task AddDevicesAsync(IEnumerable devices) + { + return AddDevicesAsync(devices, CancellationToken.None); + } /// /// Register a list of new devices with the system @@ -198,14 +423,37 @@ protected virtual void Dispose(bool disposing) { } /// The token which allows the operation to be canceled. /// Returns a string array of error messages. [Obsolete("Use AddDevices2Async")] - public abstract Task AddDevicesAsync(IEnumerable devices, CancellationToken cancellationToken); + public virtual Task AddDevicesAsync(IEnumerable devices, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding {devices?.Count()} devices", nameof(AddDevicesAsync)); + + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, ImportMode.Create), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddDevicesAsync)} threw an exception: {ex}", nameof(AddDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Adding {devices?.Count()} devices", nameof(AddDevicesAsync)); + } + } /// /// Register a list of new devices with the system /// /// The Device objects being registered. /// Returns a BulkRegistryOperationResult object. - public abstract Task AddDevices2Async(IEnumerable devices); + public virtual Task AddDevices2Async(IEnumerable devices) + { + return AddDevices2Async(devices, CancellationToken.None); + } /// /// Register a list of new devices with the system @@ -213,14 +461,37 @@ protected virtual void Dispose(bool disposing) { } /// The Device objects being registered. /// The token which allows the operation to be canceled. /// Returns a BulkRegistryOperationResult object. - public abstract Task AddDevices2Async(IEnumerable devices, CancellationToken cancellationToken); + public virtual Task AddDevices2Async(IEnumerable devices, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding {devices?.Count()} devices", nameof(AddDevices2Async)); + + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, ImportMode.Create), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddDevices2Async)} threw an exception: {ex}", nameof(AddDevices2Async)); + throw; + } + finally + { + Logging.Exit(this, $"Adding {devices?.Count()} devices", nameof(AddDevices2Async)); + } + } /// /// Update the mutable fields of the device registration /// /// The Device object with updated fields. /// The Device object with updated ETag. - public abstract Task UpdateDeviceAsync(Device device); + public virtual Task UpdateDeviceAsync(Device device) + { + return UpdateDeviceAsync(device, CancellationToken.None); + } /// /// Update the mutable fields of the device registration @@ -228,7 +499,10 @@ protected virtual void Dispose(bool disposing) { } /// The Device object with updated fields. /// Forces the device object to be replaced without regard for an ETag match. /// The Device object with updated ETag. - public abstract Task UpdateDeviceAsync(Device device, bool forceUpdate); + public virtual Task UpdateDeviceAsync(Device device, bool forceUpdate) + { + return UpdateDeviceAsync(device, forceUpdate, CancellationToken.None); + } /// /// Update the mutable fields of the device registration @@ -236,7 +510,10 @@ protected virtual void Dispose(bool disposing) { } /// The Device object with updated fields. /// The token which allows the operation to be canceled. /// The Device object with updated ETag. - public abstract Task UpdateDeviceAsync(Device device, CancellationToken cancellationToken); + public virtual Task UpdateDeviceAsync(Device device, CancellationToken cancellationToken) + { + return UpdateDeviceAsync(device, false, cancellationToken); + } /// /// Update the mutable fields of the device registration @@ -245,14 +522,60 @@ protected virtual void Dispose(bool disposing) { } /// Forces the device object to be replaced even if it was updated since it was retrieved last time. /// The token which allows the operation to be canceled. /// The Device object with updated ETags. - public abstract Task UpdateDeviceAsync(Device device, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateDeviceAsync(Device device, bool forceUpdate, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating device: {device?.Id}", nameof(UpdateDeviceAsync)); + try + { + EnsureInstanceNotClosed(); + + ValidateDeviceId(device); + + if (string.IsNullOrWhiteSpace(device.ETag) && !forceUpdate) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); + } + + ValidateDeviceAuthentication(device.Authentication, device.Id); + + NormalizeDevice(device); + + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, + { + HttpStatusCode.NotFound, async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return (Exception)new DeviceNotFoundException(responseContent, (Exception)null); + } + } + }; + + PutOperationType operationType = forceUpdate ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity; + + return _httpClientHelper.PutAsync(GetRequestUri(device.Id), device, operationType, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateDeviceAsync)} threw an exception: {ex}", nameof(UpdateDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating device: {device?.Id}", nameof(UpdateDeviceAsync)); + } + } /// /// Update the mutable fields of the module registration /// /// The Module object with updated fields. /// The Module object with updated ETags. - public abstract Task UpdateModuleAsync(Module module); + public virtual Task UpdateModuleAsync(Module module) + { + return UpdateModuleAsync(module, CancellationToken.None); + } /// /// Update the mutable fields of the module registration @@ -260,7 +583,10 @@ protected virtual void Dispose(bool disposing) { } /// The Module object with updated fields. /// Forces the device object to be replaced without regard for an ETag match. /// The Module object with updated ETags. - public abstract Task UpdateModuleAsync(Module module, bool forceUpdate); + public virtual Task UpdateModuleAsync(Module module, bool forceUpdate) + { + return UpdateModuleAsync(module, forceUpdate, CancellationToken.None); + } /// /// Update the mutable fields of the module registration @@ -268,7 +594,10 @@ protected virtual void Dispose(bool disposing) { } /// The Module object with updated fields. /// The token which allows the operation to be canceled. /// The Module object with updated ETags. - public abstract Task UpdateModuleAsync(Module module, CancellationToken cancellationToken); + public virtual Task UpdateModuleAsync(Module module, CancellationToken cancellationToken) + { + return UpdateModuleAsync(module, false, CancellationToken.None); + } /// /// Update the mutable fields of the module registration @@ -277,7 +606,55 @@ protected virtual void Dispose(bool disposing) { } /// Forces the module object to be replaced even if it was updated since it was retrieved last time. /// The token which allows the operation to be canceled. /// The Module object with updated ETags. - public abstract Task UpdateModuleAsync(Module module, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateModuleAsync(Module module, bool forceUpdate, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating module: {module?.Id}", nameof(UpdateModuleAsync)); + + try + { + EnsureInstanceNotClosed(); + + ValidateModuleId(module); + + if (string.IsNullOrWhiteSpace(module.ETag) && !forceUpdate) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); + } + + ValidateDeviceAuthentication(module.Authentication, module.DeviceId); + + // auto generate keys if not specified + if (module.Authentication == null) + { + module.Authentication = new AuthenticationMechanism(); + } + + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, + { + HttpStatusCode.NotFound, async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return new ModuleNotFoundException(responseContent, (Exception)null); + } + } + }; + + PutOperationType operationType = forceUpdate ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity; + + return _httpClientHelper.PutAsync(GetModulesRequestUri(module.DeviceId, module.Id), module, operationType, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateModuleAsync)} threw an exception: {ex}", nameof(UpdateModuleAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating module: {module?.Id}", nameof(UpdateModuleAsync)); + } + } /// /// Update a list of devices with the system @@ -285,7 +662,10 @@ protected virtual void Dispose(bool disposing) { } /// The Device objects being updated. /// Returns a string array of error messages. [Obsolete("Use UpdateDevices2Async")] - public abstract Task UpdateDevicesAsync(IEnumerable devices); + public virtual Task UpdateDevicesAsync(IEnumerable devices) + { + return UpdateDevicesAsync(devices, false, CancellationToken.None); + } /// /// Update a list of devices with the system @@ -295,14 +675,37 @@ protected virtual void Dispose(bool disposing) { } /// The token which allows the operation to be canceled. /// Returns a string array of error messages. [Obsolete("Use UpdateDevices2Async")] - public abstract Task UpdateDevicesAsync(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateDevicesAsync(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating multiple devices: count: {devices?.Count()}", nameof(UpdateDevicesAsync)); + + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, forceUpdate ? ImportMode.Update : ImportMode.UpdateIfMatchETag), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateDevicesAsync)} threw an exception: {ex}", nameof(UpdateDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating multiple devices: count: {devices?.Count()}", nameof(UpdateDevicesAsync)); + } + } /// /// Update a list of devices with the system /// /// The Device objects being updated. /// Returns a BulkRegistryOperationResult object. - public abstract Task UpdateDevices2Async(IEnumerable devices); + public virtual Task UpdateDevices2Async(IEnumerable devices) + { + return UpdateDevices2Async(devices, false, CancellationToken.None); + } /// /// Update a list of devices with the system @@ -311,40 +714,117 @@ protected virtual void Dispose(bool disposing) { } /// Forces the device object to be replaced even if it was updated since it was retrieved last time. /// The token which allows the operation to be canceled. /// Returns a BulkRegistryOperationResult object. - public abstract Task UpdateDevices2Async(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateDevices2Async(IEnumerable devices, bool forceUpdate, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating multiple devices: count: {devices?.Count()} - Force update: {forceUpdate}", nameof(UpdateDevices2Async)); + + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, forceUpdate ? ImportMode.Update : ImportMode.UpdateIfMatchETag), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateDevices2Async)} threw an exception: {ex}", nameof(UpdateDevices2Async)); + throw; + } + finally + { + Logging.Exit(this, $"Updating multiple devices: count: {devices?.Count()} - Force update: {forceUpdate}", nameof(UpdateDevices2Async)); + } + } /// /// Deletes a previously registered device from the system. /// /// The id of the device being deleted. - public abstract Task RemoveDeviceAsync(string deviceId); + public virtual Task RemoveDeviceAsync(string deviceId) + { + return RemoveDeviceAsync(deviceId, CancellationToken.None); + } /// /// Deletes a previously registered device from the system. /// /// The id of the device being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveDeviceAsync(string deviceId, CancellationToken cancellationToken); + public virtual Task RemoveDeviceAsync(string deviceId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing device: {deviceId}", nameof(RemoveDeviceAsync)); + try + { + EnsureInstanceNotClosed(); + + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + // use wild-card ETag + var eTag = new ETagHolder { ETag = "*" }; + return RemoveDeviceAsync(deviceId, eTag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveDeviceAsync)} threw an exception: {ex}", nameof(RemoveDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing device: {deviceId}", nameof(RemoveDeviceAsync)); + } + } /// /// Deletes a previously registered device from the system. /// /// The device being deleted. - public abstract Task RemoveDeviceAsync(Device device); + public virtual Task RemoveDeviceAsync(Device device) + { + return RemoveDeviceAsync(device, CancellationToken.None); + } /// /// Deletes a previously registered device from the system. /// /// The device being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveDeviceAsync(Device device, CancellationToken cancellationToken); + public virtual Task RemoveDeviceAsync(Device device, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing device: {device?.Id}", nameof(RemoveDeviceAsync)); + + try + { + EnsureInstanceNotClosed(); + + ValidateDeviceId(device); + + return string.IsNullOrWhiteSpace(device.ETag) + ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice) + : RemoveDeviceAsync(device.Id, device, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveDeviceAsync)} threw an exception: {ex}", nameof(RemoveDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing device: {device?.Id}", nameof(RemoveDeviceAsync)); + } + } /// /// Deletes a previously registered module from device in the system. /// /// The id of the device being deleted. /// The id of the moduleId being deleted. - public abstract Task RemoveModuleAsync(string deviceId, string moduleId); + public virtual Task RemoveModuleAsync(string deviceId, string moduleId) + { + return RemoveModuleAsync(deviceId, moduleId, CancellationToken.None); + } /// /// Deletes a previously registered module from device in the system. @@ -352,27 +832,82 @@ protected virtual void Dispose(bool disposing) { } /// The id of the device being deleted. /// The id of the moduleId being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken); + public virtual Task RemoveModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing module: device Id:{deviceId} moduleId: {moduleId}", nameof(RemoveDeviceAsync)); + + try + { + EnsureInstanceNotClosed(); + + if (string.IsNullOrWhiteSpace(deviceId) || string.IsNullOrEmpty(moduleId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + // use wild-card ETag + var eTag = new ETagHolder { ETag = "*" }; + return RemoveDeviceModuleAsync(deviceId, moduleId, eTag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveModuleAsync)} threw an exception: {ex}", nameof(RemoveModuleAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing module: device Id:{deviceId} moduleId: {moduleId}", nameof(RemoveModuleAsync)); + } + } /// /// Deletes a previously registered module from device in the system. /// /// The module being deleted. - public abstract Task RemoveModuleAsync(Module module); + public virtual Task RemoveModuleAsync(Module module) + { + return RemoveModuleAsync(module, CancellationToken.None); + } /// /// Deletes a previously registered module from device in the system. /// /// The module being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveModuleAsync(Module module, CancellationToken cancellationToken); + public virtual Task RemoveModuleAsync(Module module, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing module: device Id:{module?.DeviceId} moduleId: {module?.Id}", nameof(RemoveModuleAsync)); + + try + { + EnsureInstanceNotClosed(); + + ValidateModuleId(module); + + return string.IsNullOrWhiteSpace(module.ETag) + ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice) + : RemoveDeviceModuleAsync(module.DeviceId, module.Id, module, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveModuleAsync)} threw an exception: {ex}", nameof(RemoveModuleAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing module: device Id:{module?.DeviceId} moduleId: {module?.Id}", nameof(RemoveModuleAsync)); + } + } /// /// Deletes a list of previously registered devices from the system. /// /// The devices being deleted. [Obsolete("Use RemoveDevices2Async")] - public abstract Task RemoveDevicesAsync(IEnumerable devices); + public virtual Task RemoveDevicesAsync(IEnumerable devices) + { + return RemoveDevicesAsync(devices, false, CancellationToken.None); + } /// /// Deletes a list of previously registered devices from the system. @@ -381,14 +916,36 @@ protected virtual void Dispose(bool disposing) { } /// Forces the device object to be removed without regard for an ETag match. /// The token which allows the operation to be canceled. [Obsolete("Use RemoveDevices2Async")] - public abstract Task RemoveDevicesAsync(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken); + public virtual Task RemoveDevicesAsync(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, forceRemove ? ImportMode.Delete : ImportMode.DeleteIfMatchETag), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveDevicesAsync)} threw an exception: {ex}", nameof(RemoveDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); + } + } /// /// Deletes a list of previously registered devices from the system. /// /// The devices being deleted. /// Returns a BulkRegistryOperationResult object. - public abstract Task RemoveDevices2Async(IEnumerable devices); + public virtual Task RemoveDevices2Async(IEnumerable devices) + { + return RemoveDevices2Async(devices, false, CancellationToken.None); + } /// /// Deletes a list of previously registered devices from the system. @@ -397,25 +954,74 @@ protected virtual void Dispose(bool disposing) { } /// Forces the device object to be removed even if it was updated since it was retrieved last time. /// The token which allows the operation to be canceled. /// Returns a BulkRegistryOperationResult object. - public abstract Task RemoveDevices2Async(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken); + public virtual Task RemoveDevices2Async(IEnumerable devices, bool forceRemove, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevices2Async)); + + try + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForBulkOperations(devices, forceRemove ? ImportMode.Delete : ImportMode.DeleteIfMatchETag), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveDevicesAsync)} threw an exception: {ex}", nameof(RemoveDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing devices : count: {devices?.Count()} - Force remove: {forceRemove}", nameof(RemoveDevicesAsync)); + } + } /// /// Gets usage statistics for the IoT Hub. /// - public abstract Task GetRegistryStatisticsAsync(); + public virtual Task GetRegistryStatisticsAsync() + { + return GetRegistryStatisticsAsync(CancellationToken.None); + } /// /// Gets usage statistics for the IoT Hub. /// /// The token which allows the operation to be canceled. - public abstract Task GetRegistryStatisticsAsync(CancellationToken cancellationToken); + public virtual Task GetRegistryStatisticsAsync(CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting registry statistics", nameof(GetRegistryStatisticsAsync)); + + try + { + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } + }; + + return _httpClientHelper.GetAsync(GetStatisticsUri(), errorMappingOverrides, null, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetRegistryStatisticsAsync)} threw an exception: {ex}", nameof(GetRegistryStatisticsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting registry statistics", nameof(GetRegistryStatisticsAsync)); + } + } /// /// Retrieves the specified Device object. /// /// The id of the device being retrieved. /// The Device object. - public abstract Task GetDeviceAsync(string deviceId); + public virtual Task GetDeviceAsync(string deviceId) + { + return GetDeviceAsync(deviceId, CancellationToken.None); + } /// /// Retrieves the specified Device object. @@ -423,7 +1029,34 @@ protected virtual void Dispose(bool disposing) { } /// The id of the device being retrieved. /// The token which allows the operation to be canceled. /// The Device object. - public abstract Task GetDeviceAsync(string deviceId, CancellationToken cancellationToken); + public virtual Task GetDeviceAsync(string deviceId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting device: {deviceId}", nameof(GetDeviceAsync)); + try + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.NotFound, async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } + }; + + return _httpClientHelper.GetAsync(GetRequestUri(deviceId), errorMappingOverrides, null, false, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetDeviceAsync)} threw an exception: {ex}", nameof(GetDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting device: {deviceId}", nameof(GetDeviceAsync)); + } + } /// /// Retrieves the specified Module object. @@ -431,7 +1064,10 @@ protected virtual void Dispose(bool disposing) { } /// The id of the device being retrieved. /// The id of the module being retrieved. /// The Module object. - public abstract Task GetModuleAsync(string deviceId, string moduleId); + public virtual Task GetModuleAsync(string deviceId, string moduleId) + { + return GetModuleAsync(deviceId, moduleId, CancellationToken.None); + } /// /// Retrieves the specified Module object. @@ -440,14 +1076,53 @@ protected virtual void Dispose(bool disposing) { } /// The id of the module being retrieved. /// The token which allows the operation to be canceled. /// The Module object. - public abstract Task GetModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken); + public virtual Task GetModuleAsync(string deviceId, string moduleId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting module: device Id: {deviceId} - module Id: {moduleId}", nameof(GetModuleAsync)); + + try + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + if (string.IsNullOrWhiteSpace(moduleId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "moduleId")); + } + + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.NotFound, + responseMessage => Task.FromResult(new ModuleNotFoundException(deviceId, moduleId)) + }, + }; + + return _httpClientHelper.GetAsync(GetModulesRequestUri(deviceId, moduleId), errorMappingOverrides, null, false, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetModuleAsync)} threw an exception: {ex}", nameof(GetModuleAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting module: device Id: {deviceId} - module Id: {moduleId}", nameof(GetModuleAsync)); + } + } /// /// Retrieves the module identities on device /// /// The device Id. /// List of modules on device. - public abstract Task> GetModulesOnDeviceAsync(string deviceId); + public virtual Task> GetModulesOnDeviceAsync(string deviceId) + { + return GetModulesOnDeviceAsync(deviceId, CancellationToken.None); + } /// /// Retrieves the module identities on device @@ -455,7 +1130,30 @@ protected virtual void Dispose(bool disposing) { } /// The device Id. /// The token which allows the operation to be canceled. /// List of modules on device. - public abstract Task> GetModulesOnDeviceAsync(string deviceId, CancellationToken cancellationToken); + public virtual Task> GetModulesOnDeviceAsync(string deviceId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting module on device: {deviceId}", nameof(GetModulesOnDeviceAsync)); + + try + { + EnsureInstanceNotClosed(); + + return _httpClientHelper.GetAsync>( + GetModulesOnDeviceRequestUri(deviceId), + null, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetModulesOnDeviceAsync)} threw an exception: {ex}", nameof(GetModulesOnDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting module on device: {deviceId}", nameof(GetModulesOnDeviceAsync)); + } + } /// /// Retrieves specified number of devices from every IoT Hub partition. @@ -463,7 +1161,10 @@ protected virtual void Dispose(bool disposing) { } /// /// The list of devices. [Obsolete("Use CreateQuery(\"select * from devices\", pageSize);")] - public abstract Task> GetDevicesAsync(int maxCount); + public virtual Task> GetDevicesAsync(int maxCount) + { + return GetDevicesAsync(maxCount, CancellationToken.None); + } /// /// Retrieves specified number of devices from every IoT hub partition. @@ -471,14 +1172,42 @@ protected virtual void Dispose(bool disposing) { } /// /// The list of devices. [Obsolete("Use CreateQuery(\"select * from devices\", pageSize);")] - public abstract Task> GetDevicesAsync(int maxCount, CancellationToken cancellationToken); + public virtual Task> GetDevicesAsync(int maxCount, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting devices - max count: {maxCount}", nameof(GetDevicesAsync)); + + try + { + EnsureInstanceNotClosed(); + + return _httpClientHelper.GetAsync>( + GetDevicesRequestUri(maxCount), + s_defaultGetDevicesOperationTimeout, + null, + null, + true, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetDevicesAsync)} threw an exception: {ex}", nameof(GetDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting devices - max count: {maxCount}", nameof(GetDevicesAsync)); + } + } /// /// Retrieves a handle through which a result for a given query can be fetched. /// /// The SQL query. /// A handle used to fetch results for a SQL query. - public abstract IQuery CreateQuery(string sqlQueryString); + public virtual IQuery CreateQuery(string sqlQueryString) + { + return CreateQuery(sqlQueryString, null); + } /// /// Retrieves a handle through which a result for a given query can be fetched. @@ -486,14 +1215,37 @@ protected virtual void Dispose(bool disposing) { } /// The SQL query. /// The maximum number of items per page. /// A handle used to fetch results for a SQL query. - public abstract IQuery CreateQuery(string sqlQueryString, int? pageSize); + public virtual IQuery CreateQuery(string sqlQueryString, int? pageSize) + { + Logging.Enter(this, $"Creating query", nameof(CreateQuery)); + try + { + return new Query((token) => ExecuteQueryAsync( + sqlQueryString, + pageSize, + token, + CancellationToken.None)); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(CreateQuery)} threw an exception: {ex}", nameof(CreateQuery)); + throw; + } + finally + { + Logging.Exit(this, $"Creating query", nameof(CreateQuery)); + } + } /// /// Copies registered device data to a set of blobs in a specific container in a storage account. /// /// ConnectionString to the destination StorageAccount. /// Destination blob container name. - public abstract Task ExportRegistryAsync(string storageAccountConnectionString, string containerName); + public virtual Task ExportRegistryAsync(string storageAccountConnectionString, string containerName) + { + return ExportRegistryAsync(storageAccountConnectionString, containerName, CancellationToken.None); + } /// /// Copies registered device data to a set of blobs in a specific container in a storage account. @@ -501,14 +1253,49 @@ protected virtual void Dispose(bool disposing) { } /// ConnectionString to the destination StorageAccount. /// Destination blob container name. /// Task cancellation token. - public abstract Task ExportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken); + public virtual Task ExportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Exporting registry", nameof(ExportRegistryAsync)); + try + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } + }; + + return _httpClientHelper.PostAsync( + GetAdminUri("exportRegistry"), + new ExportImportRequest + { + ContainerName = containerName, + StorageConnectionString = storageAccountConnectionString, + }, + errorMappingOverrides, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ExportRegistryAsync)} threw an exception: {ex}", nameof(ExportRegistryAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Exporting registry", nameof(ExportRegistryAsync)); + } + } /// /// Imports registered device data from a set of blobs in a specific container in a storage account. /// /// ConnectionString to the source StorageAccount. /// Source blob container name. - public abstract Task ImportRegistryAsync(string storageAccountConnectionString, string containerName); + public virtual Task ImportRegistryAsync(string storageAccountConnectionString, string containerName) + { + return ImportRegistryAsync(storageAccountConnectionString, containerName, CancellationToken.None); + } /// /// Imports registered device data from a set of blobs in a specific container in a storage account. @@ -516,7 +1303,40 @@ protected virtual void Dispose(bool disposing) { } /// ConnectionString to the source StorageAccount. /// Source blob container name. /// Task cancellation token. - public abstract Task ImportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken); + public virtual Task ImportRegistryAsync(string storageAccountConnectionString, string containerName, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Importing registry", nameof(ImportRegistryAsync)); + + try + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } + }; + + return _httpClientHelper.PostAsync( + GetAdminUri("importRegistry"), + new ExportImportRequest + { + ContainerName = containerName, + StorageConnectionString = storageAccountConnectionString, + }, + errorMappingOverrides, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ImportRegistryAsync)} threw an exception: {ex}", nameof(ImportRegistryAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Importing registry", nameof(ImportRegistryAsync)); + } + } #pragma warning disable CA1054 // Uri parameters should not be strings @@ -527,7 +1347,13 @@ protected virtual void Dispose(bool disposing) { } /// Specifies whether to exclude the Device's Keys during the export. /// JobProperties of the newly created job. - public abstract Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys); + public virtual Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys) + { + return ExportDevicesAsync( + JobProperties.CreateForExportJob( + exportBlobContainerUri, + excludeKeys)); + } /// /// Creates a new bulk job to export device registrations to the container specified by the provided URI. @@ -536,7 +1362,14 @@ protected virtual void Dispose(bool disposing) { } /// Specifies whether to exclude the Device's Keys during the export. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys, CancellationToken cancellationToken); + public virtual Task ExportDevicesAsync(string exportBlobContainerUri, bool excludeKeys, CancellationToken cancellationToken) + { + return ExportDevicesAsync( + JobProperties.CreateForExportJob( + exportBlobContainerUri, + excludeKeys), + cancellationToken); + } /// /// Creates a new bulk job to export device registrations to the container specified by the provided URI. @@ -545,7 +1378,14 @@ protected virtual void Dispose(bool disposing) { } /// The name of the blob that will be created in the provided output blob container. /// Specifies whether to exclude the Device's Keys during the export. /// JobProperties of the newly created job. - public abstract Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys); + public virtual Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys) + { + return ExportDevicesAsync( + JobProperties.CreateForExportJob( + exportBlobContainerUri, + excludeKeys, + outputBlobName)); + } /// /// Creates a new bulk job to export device registrations to the container specified by the provided URI. @@ -555,7 +1395,15 @@ protected virtual void Dispose(bool disposing) { } /// Specifies whether to exclude the Device's Keys during the export. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys, CancellationToken cancellationToken); + public virtual Task ExportDevicesAsync(string exportBlobContainerUri, string outputBlobName, bool excludeKeys, CancellationToken cancellationToken) + { + return ExportDevicesAsync( + JobProperties.CreateForExportJob( + exportBlobContainerUri, + excludeKeys, + outputBlobName), + cancellationToken); + } /// /// Creates a new bulk job to export device registrations to the container specified by the provided URI. @@ -563,7 +1411,30 @@ protected virtual void Dispose(bool disposing) { } /// Parameters for the job. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ExportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default); + public virtual Task ExportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default) + { + if (jobParameters == null) + { + throw new ArgumentNullException(nameof(jobParameters)); + } + + Logging.Enter(this, $"Export Job running with {jobParameters}", nameof(ExportDevicesAsync)); + + try + { + jobParameters.Type = JobType.ExportDevices; + return CreateJobAsync(jobParameters, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ExportDevicesAsync)} threw an exception: {ex}", nameof(ExportDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Export Job running with {jobParameters}", nameof(ExportDevicesAsync)); + } + } /// /// Creates a new bulk job to import device registrations into the IoT Hub. @@ -571,7 +1442,13 @@ protected virtual void Dispose(bool disposing) { } /// Source blob container URI. /// Destination blob container URI. /// JobProperties of the newly created job. - public abstract Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri); + public virtual Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri) + { + return ImportDevicesAsync( + JobProperties.CreateForImportJob( + importBlobContainerUri, + outputBlobContainerUri)); + } /// /// Creates a new bulk job to import device registrations into the IoT Hub. @@ -580,7 +1457,14 @@ protected virtual void Dispose(bool disposing) { } /// Destination blob container URI. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, CancellationToken cancellationToken); + public virtual Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, CancellationToken cancellationToken) + { + return ImportDevicesAsync( + JobProperties.CreateForImportJob( + importBlobContainerUri, + outputBlobContainerUri), + cancellationToken); + } /// /// Creates a new bulk job to import device registrations into the IoT Hub. @@ -589,7 +1473,14 @@ protected virtual void Dispose(bool disposing) { } /// Destination blob container URI. /// The blob name to be used when importing from the provided input blob container. /// JobProperties of the newly created job. - public abstract Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName); + public virtual Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName) + { + return ImportDevicesAsync( + JobProperties.CreateForImportJob( + importBlobContainerUri, + outputBlobContainerUri, + inputBlobName)); + } /// /// Creates a new bulk job to import device registrations into the IoT Hub. @@ -599,7 +1490,15 @@ protected virtual void Dispose(bool disposing) { } /// The blob name to be used when importing from the provided input blob container. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName, CancellationToken cancellationToken); + public virtual Task ImportDevicesAsync(string importBlobContainerUri, string outputBlobContainerUri, string inputBlobName, CancellationToken cancellationToken) + { + return ImportDevicesAsync( + JobProperties.CreateForImportJob( + importBlobContainerUri, + outputBlobContainerUri, + inputBlobName), + cancellationToken); + } #pragma warning restore CA1054 // Uri parameters should not be strings @@ -609,14 +1508,39 @@ protected virtual void Dispose(bool disposing) { } /// Parameters for the job. /// Task cancellation token. /// JobProperties of the newly created job. - public abstract Task ImportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default); + public virtual Task ImportDevicesAsync(JobProperties jobParameters, CancellationToken cancellationToken = default) + { + if (jobParameters == null) + { + throw new ArgumentNullException(nameof(jobParameters)); + } + + Logging.Enter(this, $"Import Job running with {jobParameters}", nameof(ImportDevicesAsync)); + try + { + jobParameters.Type = JobType.ImportDevices; + return CreateJobAsync(jobParameters, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ExportDevicesAsync)} threw an exception: {ex}", nameof(ImportDevicesAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Import Job running with {jobParameters}", nameof(ImportDevicesAsync)); + } + } /// /// Gets the job with the specified Id. /// /// Id of the Job object to retrieve. /// JobProperties of the job specified by the provided jobId. - public abstract Task GetJobAsync(string jobId); + public virtual Task GetJobAsync(string jobId) + { + return GetJobAsync(jobId, CancellationToken.None); + } /// /// Gets the job with the specified Id. @@ -624,40 +1548,131 @@ protected virtual void Dispose(bool disposing) { } /// Id of the Job object to retrieve. /// Task cancellation token. /// JobProperties of the job specified by the provided jobId. - public abstract Task GetJobAsync(string jobId, CancellationToken cancellationToken); + public virtual Task GetJobAsync(string jobId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting job {jobId}", nameof(GetJobsAsync)); + try + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new JobNotFoundException(jobId)) } + }; + + return _httpClientHelper.GetAsync( + GetJobUri("/{0}".FormatInvariant(jobId)), + errorMappingOverrides, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting job {jobId}", nameof(GetJobsAsync)); + } + } /// /// List all jobs for the IoT Hub. /// /// IEnumerable of JobProperties of all jobs for this IoT Hub. - public abstract Task> GetJobsAsync(); + public virtual Task> GetJobsAsync() + { + return GetJobsAsync(CancellationToken.None); + } /// /// List all jobs for the IoT Hub. /// /// Task cancellation token. /// IEnumerable of JobProperties of all jobs for this IoT Hub. - public abstract Task> GetJobsAsync(CancellationToken cancellationToken); + public virtual Task> GetJobsAsync(CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting job", nameof(GetJobsAsync)); + try + { + EnsureInstanceNotClosed(); + + return _httpClientHelper.GetAsync>( + GetJobUri(string.Empty), + null, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting job", nameof(GetJobsAsync)); + } + } /// /// Cancels/Deletes the job with the specified Id. /// /// Id of the job to cancel. - public abstract Task CancelJobAsync(string jobId); + public virtual Task CancelJobAsync(string jobId) + { + return CancelJobAsync(jobId, CancellationToken.None); + } /// /// Cancels/Deletes the job with the specified Id. /// /// Id of the job to cancel. /// Task cancellation token. - public abstract Task CancelJobAsync(string jobId, CancellationToken cancellationToken); + public virtual Task CancelJobAsync(string jobId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Canceling job: {jobId}", nameof(CancelJobAsync)); + try + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new JobNotFoundException(jobId)) } + }; + + IETagHolder jobETag = new ETagHolder + { + ETag = jobId, + }; + + return _httpClientHelper.DeleteAsync( + GetJobUri("/{0}".FormatInvariant(jobId)), + jobETag, + errorMappingOverrides, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetJobsAsync)} threw an exception: {ex}", nameof(GetJobsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting job {jobId}", nameof(GetJobsAsync)); + } + } /// /// Gets from IotHub /// /// The device Id. /// Twin instance. - public abstract Task GetTwinAsync(string deviceId); + public virtual Task GetTwinAsync(string deviceId) + { + return GetTwinAsync(deviceId, CancellationToken.None); + } /// /// Gets from IotHub @@ -665,7 +1680,34 @@ protected virtual void Dispose(bool disposing) { } /// The device Id. /// Task cancellation token. /// Twin instance. - public abstract Task GetTwinAsync(string deviceId, CancellationToken cancellationToken); + public virtual Task GetTwinAsync(string deviceId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting device twin on device: {deviceId}", nameof(GetTwinAsync)); + try + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.NotFound, async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } + }; + + return _httpClientHelper.GetAsync(GetTwinUri(deviceId), errorMappingOverrides, null, false, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetTwinAsync)} threw an exception: {ex}", nameof(GetTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting device twin on device: {deviceId}", nameof(GetTwinAsync)); + } + } /// /// Gets Module's from IotHub @@ -673,7 +1715,10 @@ protected virtual void Dispose(bool disposing) { } /// The device Id. /// The module Id. /// Twin instance. - public abstract Task GetTwinAsync(string deviceId, string moduleId); + public virtual Task GetTwinAsync(string deviceId, string moduleId) + { + return GetTwinAsync(deviceId, moduleId, CancellationToken.None); + } /// /// Gets Module's from IotHub @@ -682,7 +1727,40 @@ protected virtual void Dispose(bool disposing) { } /// The module Id. /// Task cancellation token. /// Twin instance. - public abstract Task GetTwinAsync(string deviceId, string moduleId, CancellationToken cancellationToken); + public virtual Task GetTwinAsync(string deviceId, string moduleId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting device twin on device: {deviceId} and module: {moduleId}", nameof(GetTwinAsync)); + + try + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "deviceId")); + } + + if (string.IsNullOrWhiteSpace(moduleId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "moduleId")); + } + + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.NotFound, async responseMessage => new ModuleNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), (Exception)null) } + }; + + return _httpClientHelper.GetAsync(GetModuleTwinRequestUri(deviceId, moduleId), errorMappingOverrides, null, false, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetTwinAsync)} threw an exception: {ex}", nameof(GetTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting device twin on device: {deviceId} and module: {moduleId}", nameof(GetTwinAsync)); + } + } /// /// Updates the mutable fields of @@ -691,7 +1769,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin with updated fields. /// Twin's ETag. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag); + public virtual Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag) + { + return UpdateTwinAsync(deviceId, twinPatch, etag, CancellationToken.None); + } /// /// Updates the mutable fields of @@ -701,7 +1782,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag, CancellationToken cancellationToken); + public virtual Task UpdateTwinAsync(string deviceId, Twin twinPatch, string etag, CancellationToken cancellationToken) + { + return UpdateTwinInternalAsync(deviceId, twinPatch, etag, false, cancellationToken); + } /// /// Updates the mutable fields of @@ -710,7 +1794,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin json with updated fields. /// Twin's ETag. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag); + public virtual Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag) + { + return UpdateTwinAsync(deviceId, jsonTwinPatch, etag, CancellationToken.None); + } /// /// Updates the mutable fields of @@ -720,7 +1807,31 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag, CancellationToken cancellationToken); + public virtual Task UpdateTwinAsync(string deviceId, string jsonTwinPatch, string etag, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating device twin on device: {deviceId}", nameof(UpdateTwinAsync)); + + try + { + if (string.IsNullOrWhiteSpace(jsonTwinPatch)) + { + throw new ArgumentNullException(nameof(jsonTwinPatch)); + } + + // TODO: Do we need to deserialize Twin, only to serialize it again? + Twin twin = JsonConvert.DeserializeObject(jsonTwinPatch); + return UpdateTwinAsync(deviceId, twin, etag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating device twin on device: {deviceId}", nameof(UpdateTwinAsync)); + } + } /// /// Updates the mutable fields of Module's @@ -730,7 +1841,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin with updated fields. /// Twin's ETag. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag); + public virtual Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag) + { + return UpdateTwinAsync(deviceId, moduleId, twinPatch, etag, CancellationToken.None); + } /// /// Updates the mutable fields of Module's @@ -741,7 +1855,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag, CancellationToken cancellationToken); + public virtual Task UpdateTwinAsync(string deviceId, string moduleId, Twin twinPatch, string etag, CancellationToken cancellationToken) + { + return UpdateTwinInternalAsync(deviceId, moduleId, twinPatch, etag, false, cancellationToken); + } /// /// Updates the mutable fields of Module's @@ -751,7 +1868,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin json with updated fields. /// Twin's ETag. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag); + public virtual Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag) + { + return UpdateTwinAsync(deviceId, moduleId, jsonTwinPatch, etag, CancellationToken.None); + } /// /// Updates the mutable fields of Module's @@ -762,14 +1882,40 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag, CancellationToken cancellationToken); + public virtual Task UpdateTwinAsync(string deviceId, string moduleId, string jsonTwinPatch, string etag, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating device twin on device: {deviceId} and module: {moduleId}", nameof(UpdateTwinAsync)); + try + { + if (string.IsNullOrWhiteSpace(jsonTwinPatch)) + { + throw new ArgumentNullException(nameof(jsonTwinPatch)); + } + + // TODO: Do we need to deserialize Twin, only to serialize it again? + Twin twin = JsonConvert.DeserializeObject(jsonTwinPatch); + return UpdateTwinAsync(deviceId, moduleId, twin, etag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating device twin on device: {deviceId} and module: {moduleId}", nameof(UpdateTwinAsync)); + } + } /// /// Update the mutable fields for a list of s previously created within the system /// /// List of s with updated fields. /// Result of the bulk update operation. - public abstract Task UpdateTwins2Async(IEnumerable twins); + public virtual Task UpdateTwins2Async(IEnumerable twins) + { + return UpdateTwins2Async(twins, false, CancellationToken.None); + } /// /// Update the mutable fields for a list of s previously created within the system @@ -777,7 +1923,10 @@ protected virtual void Dispose(bool disposing) { } /// List of s with updated fields. /// Task cancellation token. /// Result of the bulk update operation. - public abstract Task UpdateTwins2Async(IEnumerable twins, CancellationToken cancellationToken); + public virtual Task UpdateTwins2Async(IEnumerable twins, CancellationToken cancellationToken) + { + return UpdateTwins2Async(twins, false, cancellationToken); + } /// /// Update the mutable fields for a list of s previously created within the system @@ -785,7 +1934,10 @@ protected virtual void Dispose(bool disposing) { } /// List of s with updated fields. /// Forces the object to be updated even if it has changed since it was retrieved last time. /// Result of the bulk update operation. - public abstract Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate); + public virtual Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate) + { + return UpdateTwins2Async(twins, forceUpdate, CancellationToken.None); + } /// /// Update the mutable fields for a list of s previously created within the system @@ -794,7 +1946,13 @@ protected virtual void Dispose(bool disposing) { } /// Forces the object to be updated even if it has changed since it was retrieved last time. /// Task cancellation token. /// Result of the bulk update operation. - public abstract Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateTwins2Async(IEnumerable twins, bool forceUpdate, CancellationToken cancellationToken) + { + return BulkDeviceOperationsAsync( + GenerateExportImportDeviceListForTwinBulkOperations(twins, forceUpdate ? ImportMode.UpdateTwin : ImportMode.UpdateTwinIfMatchETag), + ClientApiVersionHelper.ApiVersionQueryString, + cancellationToken); + } /// /// Updates the mutable fields of @@ -803,7 +1961,10 @@ protected virtual void Dispose(bool disposing) { } /// New Twin object to replace with. /// Twin's ETag. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag); + public virtual Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag) + { + return ReplaceTwinAsync(deviceId, newTwin, etag, CancellationToken.None); + } /// /// Updates the mutable fields of @@ -813,7 +1974,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag, CancellationToken cancellationToken); + public virtual Task ReplaceTwinAsync(string deviceId, Twin newTwin, string etag, CancellationToken cancellationToken) + { + return UpdateTwinInternalAsync(deviceId, newTwin, etag, true, cancellationToken); + } /// /// Updates the mutable fields of @@ -822,7 +1986,10 @@ protected virtual void Dispose(bool disposing) { } /// New Twin json to replace with. /// Twin's ETag. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag); + public virtual Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag) + { + return ReplaceTwinAsync(deviceId, newTwinJson, etag, CancellationToken.None); + } /// /// Updates the mutable fields of @@ -832,7 +1999,30 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag, CancellationToken cancellationToken); + public virtual Task ReplaceTwinAsync(string deviceId, string newTwinJson, string etag, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Replacing device twin on device: {deviceId}", nameof(ReplaceTwinAsync)); + try + { + if (string.IsNullOrWhiteSpace(newTwinJson)) + { + throw new ArgumentNullException(nameof(newTwinJson)); + } + + // TODO: Do we need to deserialize Twin, only to serialize it again? + Twin twin = JsonConvert.DeserializeObject(newTwinJson); + return ReplaceTwinAsync(deviceId, twin, etag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ReplaceTwinAsync)} threw an exception: {ex}", nameof(ReplaceTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Replacing device twin on device: {deviceId}", nameof(ReplaceTwinAsync)); + } + } /// /// Updates the mutable fields of Module's @@ -842,7 +2032,10 @@ protected virtual void Dispose(bool disposing) { } /// New Twin object to replace with. /// Twin's ETag. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag); + public virtual Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag) + { + return ReplaceTwinAsync(deviceId, moduleId, newTwin, etag, CancellationToken.None); + } /// /// Updates the mutable fields of Module's @@ -853,7 +2046,10 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag, CancellationToken cancellationToken); + public virtual Task ReplaceTwinAsync(string deviceId, string moduleId, Twin newTwin, string etag, CancellationToken cancellationToken) + { + return UpdateTwinInternalAsync(deviceId, moduleId, newTwin, etag, true, cancellationToken); + } /// /// Updates the mutable fields of Module's @@ -863,7 +2059,10 @@ protected virtual void Dispose(bool disposing) { } /// New Twin json to replace with. /// Twin's ETag. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag); + public virtual Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag) + { + return ReplaceTwinAsync(deviceId, moduleId, newTwinJson, etag, CancellationToken.None); + } /// /// Updates the mutable fields of Module's @@ -874,14 +2073,27 @@ protected virtual void Dispose(bool disposing) { } /// Twin's ETag. /// Task cancellation token. /// Updated Twin instance. - public abstract Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag, CancellationToken cancellationToken); + public virtual Task ReplaceTwinAsync(string deviceId, string moduleId, string newTwinJson, string etag, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(newTwinJson)) + { + throw new ArgumentNullException(nameof(newTwinJson)); + } + + // TODO: Do we need to deserialize Twin, only to serialize it again? + Twin twin = JsonConvert.DeserializeObject(newTwinJson); + return ReplaceTwinAsync(deviceId, moduleId, twin, etag, cancellationToken); + } /// /// Register a new Configuration for Azure IOT Edge in IotHub /// /// The Configuration object being registered. /// The Configuration object. - public abstract Task AddConfigurationAsync(Configuration configuration); + public virtual Task AddConfigurationAsync(Configuration configuration) + { + return AddConfigurationAsync(configuration, CancellationToken.None); + } /// /// Register a new Configuration for Azure IOT Edge in IotHub @@ -889,14 +2101,50 @@ protected virtual void Dispose(bool disposing) { } /// The Configuration object being registered. /// The token which allows the operation to be canceled. /// The Configuration object. - public abstract Task AddConfigurationAsync(Configuration configuration, CancellationToken cancellationToken); + public virtual Task AddConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Adding configuration: {configuration?.Id}", nameof(AddConfigurationAsync)); + + try + { + EnsureInstanceNotClosed(); + + if (!string.IsNullOrEmpty(configuration.ETag)) + { + throw new ArgumentException(ApiResources.ETagSetWhileCreatingConfiguration); + } + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + } + }; + + return _httpClientHelper.PutAsync(GetConfigurationRequestUri(configuration.Id), configuration, PutOperationType.CreateEntity, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(AddConfigurationAsync)} threw an exception: {ex}", nameof(AddConfigurationAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Adding configuration: {configuration?.Id}", nameof(AddConfigurationAsync)); + } + } /// /// Retrieves the specified Configuration object. /// /// The id of the Configuration being retrieved. /// The Configuration object. - public abstract Task GetConfigurationAsync(string configurationId); + public virtual Task GetConfigurationAsync(string configurationId) + { + return GetConfigurationAsync(configurationId, CancellationToken.None); + } /// /// Retrieves the specified Configuration object. @@ -904,28 +2152,84 @@ protected virtual void Dispose(bool disposing) { } /// The id of the Configuration being retrieved. /// The token which allows the operation to be canceled. /// The Configuration object. - public abstract Task GetConfigurationAsync(string configurationId, CancellationToken cancellationToken); + public virtual Task GetConfigurationAsync(string configurationId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting configuration: {configurationId}", nameof(GetConfigurationAsync)); + try + { + if (string.IsNullOrWhiteSpace(configurationId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "configurationId")); + } + + EnsureInstanceNotClosed(); + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.NotFound, async responseMessage => new ConfigurationNotFoundException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } + }; + + return _httpClientHelper.GetAsync(GetConfigurationRequestUri(configurationId), errorMappingOverrides, null, false, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetConfigurationAsync)} threw an exception: {ex}", nameof(GetConfigurationAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Get configuration: {configurationId}", nameof(GetConfigurationAsync)); + } + } /// /// Retrieves specified number of configurations from every IoT Hub partition. /// Results are not ordered. /// /// The list of configurations. - public abstract Task> GetConfigurationsAsync(int maxCount); + public virtual Task> GetConfigurationsAsync(int maxCount) + { + return GetConfigurationsAsync(maxCount, CancellationToken.None); + } /// /// Retrieves specified number of configurations from every IoT hub partition. /// Results are not ordered. /// /// The list of configurations. - public abstract Task> GetConfigurationsAsync(int maxCount, CancellationToken cancellationToken); + public virtual Task> GetConfigurationsAsync(int maxCount, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Getting configuration: max count: {maxCount}", nameof(GetConfigurationsAsync)); + try + { + EnsureInstanceNotClosed(); + + return _httpClientHelper.GetAsync>( + GetConfigurationsRequestUri(maxCount), + null, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetConfigurationsAsync)} threw an exception: {ex}", nameof(GetConfigurationsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting configuration: max count: {maxCount}", nameof(GetConfigurationsAsync)); + } + } /// /// Update the mutable fields of the Configuration registration /// /// The Configuration object with updated fields. /// The Configuration object with updated ETag. - public abstract Task UpdateConfigurationAsync(Configuration configuration); + public virtual Task UpdateConfigurationAsync(Configuration configuration) + { + return UpdateConfigurationAsync(configuration, CancellationToken.None); + } /// /// Update the mutable fields of the Configuration registration @@ -933,7 +2237,10 @@ protected virtual void Dispose(bool disposing) { } /// The Configuration object with updated fields. /// Forces the device object to be replaced without regard for an ETag match. /// The Configuration object with updated ETags. - public abstract Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate); + public virtual Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate) + { + return UpdateConfigurationAsync(configuration, forceUpdate, CancellationToken.None); + } /// /// Update the mutable fields of the Configuration registration @@ -941,7 +2248,10 @@ protected virtual void Dispose(bool disposing) { } /// The Configuration object with updated fields. /// The token which allows the operation to be canceled. /// The Configuration object with updated ETags. - public abstract Task UpdateConfigurationAsync(Configuration configuration, CancellationToken cancellationToken); + public virtual Task UpdateConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) + { + return UpdateConfigurationAsync(configuration, false, cancellationToken); + } /// /// Update the mutable fields of the Configuration registration @@ -950,40 +2260,136 @@ protected virtual void Dispose(bool disposing) { } /// Forces the Configuration object to be replaced even if it was updated since it was retrieved last time. /// The token which allows the operation to be canceled. /// The Configuration object with updated ETags. - public abstract Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate, CancellationToken cancellationToken); + public virtual Task UpdateConfigurationAsync(Configuration configuration, bool forceUpdate, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Updating configuration: {configuration?.Id} - Force update: {forceUpdate}", nameof(UpdateConfigurationAsync)); + + try + { + EnsureInstanceNotClosed(); + + if (string.IsNullOrWhiteSpace(configuration.ETag) && !forceUpdate) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingConfiguration); + } + + var errorMappingOverrides = new Dictionary>>() + { + { HttpStatusCode.PreconditionFailed, async (responseMessage) => new PreconditionFailedException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, + { + HttpStatusCode.NotFound, async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return new ConfigurationNotFoundException(responseContent, (Exception)null); + } + } + }; + + PutOperationType operationType = forceUpdate + ? PutOperationType.ForceUpdateEntity + : PutOperationType.UpdateEntity; + + return _httpClientHelper.PutAsync(GetConfigurationRequestUri(configuration.Id), configuration, operationType, errorMappingOverrides, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateConfigurationAsync)} threw an exception: {ex}", nameof(UpdateConfigurationAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Updating configuration: {configuration?.Id} - Force update: {forceUpdate}", nameof(UpdateConfigurationAsync)); + } + } /// /// Deletes a previously registered device from the system. /// /// The id of the Configuration being deleted. - public abstract Task RemoveConfigurationAsync(string configurationId); + public virtual Task RemoveConfigurationAsync(string configurationId) + { + return RemoveConfigurationAsync(configurationId, CancellationToken.None); + } /// /// Deletes a previously registered device from the system. /// /// The id of the configurationId being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveConfigurationAsync(string configurationId, CancellationToken cancellationToken); + public virtual Task RemoveConfigurationAsync(string configurationId, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing configuration: {configurationId}", nameof(RemoveConfigurationAsync)); + + try + { + EnsureInstanceNotClosed(); + + if (string.IsNullOrWhiteSpace(configurationId)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrWhitespace, "configurationId")); + } + + // use wild-card ETag + var eTag = new ETagHolder { ETag = "*" }; + return RemoveConfigurationAsync(configurationId, eTag, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveConfigurationAsync)} threw an exception: {ex}", nameof(RemoveConfigurationAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing configuration: {configurationId}", nameof(RemoveConfigurationAsync)); + } + } /// /// Deletes a previously registered device from the system. /// /// The Configuration being deleted. - public abstract Task RemoveConfigurationAsync(Configuration configuration); + public virtual Task RemoveConfigurationAsync(Configuration configuration) + { + return RemoveConfigurationAsync(configuration, CancellationToken.None); + } /// /// Deletes a previously registered device from the system. /// /// The Configuration being deleted. /// The token which allows the operation to be canceled. - public abstract Task RemoveConfigurationAsync(Configuration configuration, CancellationToken cancellationToken); + public virtual Task RemoveConfigurationAsync(Configuration configuration, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Removing configuration: {configuration?.Id}", nameof(RemoveConfigurationAsync)); + try + { + EnsureInstanceNotClosed(); + + return string.IsNullOrWhiteSpace(configuration.ETag) + ? throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingConfiguration) + : RemoveConfigurationAsync(configuration.Id, configuration, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(RemoveConfigurationAsync)} threw an exception: {ex}", nameof(RemoveConfigurationAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Removing configuration: {configuration?.Id}", nameof(RemoveConfigurationAsync)); + } + } /// /// Applies configuration content to an IoTEdge device. /// /// The device Id. /// The configuration of an IoTEdge device. - public abstract Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content); + public virtual Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content) + { + return ApplyConfigurationContentOnDeviceAsync(deviceId, content, CancellationToken.None); + } /// /// Applies configuration content to an IoTEdge device. @@ -991,6 +2397,612 @@ protected virtual void Dispose(bool disposing) { } /// The device Id. /// The configuration of an IoTEdge device. /// The token which allows the operation to be canceled. - public abstract Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content, CancellationToken cancellationToken); + public virtual Task ApplyConfigurationContentOnDeviceAsync(string deviceId, ConfigurationContent content, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Applying configuration content on device: {deviceId}", nameof(ApplyConfigurationContentOnDeviceAsync)); + + try + { + return _httpClientHelper.PostAsync(GetApplyConfigurationOnDeviceRequestUri(deviceId), content, null, null, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(ApplyConfigurationContentOnDeviceAsync)} threw an exception: {ex}", nameof(ApplyConfigurationContentOnDeviceAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Applying configuration content on device: {deviceId}", nameof(ApplyConfigurationContentOnDeviceAsync)); + } + } + + private Task RemoveConfigurationAsync(string configurationId, IETagHolder eTagHolder, CancellationToken cancellationToken) + { + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.NotFound, + async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return new ConfigurationNotFoundException(responseContent, (Exception) null); + } + }, + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + } + }; + + return _httpClientHelper.DeleteAsync(GetConfigurationRequestUri(configurationId), eTagHolder, errorMappingOverrides, null, cancellationToken); + } + + private Task UpdateTwinInternalAsync(string deviceId, Twin twin, string etag, bool isReplace, CancellationToken cancellationToken) + { + EnsureInstanceNotClosed(); + + if (twin != null) + { + twin.DeviceId = deviceId; + } + + ValidateTwinId(twin); + + if (string.IsNullOrEmpty(etag)) + { + throw new ArgumentNullException(nameof(etag)); + } + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + { + HttpStatusCode.NotFound, + async responseMessage => new DeviceNotFoundException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), (Exception)null) + } + }; + + return isReplace + ? _httpClientHelper.PutAsync( + GetTwinUri(deviceId), + twin, + etag, + etag == _wildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, + errorMappingOverrides, + cancellationToken) + : _httpClientHelper.PatchAsync( + GetTwinUri(deviceId), + twin, + etag, + etag == _wildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, + errorMappingOverrides, + cancellationToken); + } + + private Task UpdateTwinInternalAsync(string deviceId, string moduleId, Twin twin, string etag, bool isReplace, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Replacing device twin on device: {deviceId} - module: {moduleId} - is replace: {isReplace}", nameof(UpdateTwinAsync)); + try + { + EnsureInstanceNotClosed(); + + if (twin != null) + { + twin.DeviceId = deviceId; + twin.ModuleId = moduleId; + } + + ValidateTwinId(twin); + + if (string.IsNullOrEmpty(etag)) + { + throw new ArgumentNullException(nameof(etag)); + } + + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + { + HttpStatusCode.NotFound, + async responseMessage => new ModuleNotFoundException( + await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false), + (Exception)null) + } + }; + + return isReplace + ? _httpClientHelper.PutAsync( + GetModuleTwinRequestUri(deviceId, moduleId), + twin, + etag, + etag == _wildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, + errorMappingOverrides, + cancellationToken) + : _httpClientHelper.PatchAsync( + GetModuleTwinRequestUri(deviceId, moduleId), + twin, + etag, + etag == _wildcardEtag ? PutOperationType.ForceUpdateEntity : PutOperationType.UpdateEntity, + errorMappingOverrides, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(UpdateTwinAsync)} threw an exception: {ex}", nameof(UpdateTwinAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Replacing device twin on device: {deviceId} - module: {moduleId} - is replace: {isReplace}", nameof(UpdateTwinAsync)); + } + } + + private async Task ExecuteQueryAsync(string sqlQueryString, int? pageSize, string continuationToken, CancellationToken cancellationToken) + { + EnsureInstanceNotClosed(); + + if (string.IsNullOrWhiteSpace(sqlQueryString)) + { + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.ParameterCannotBeNullOrEmpty, nameof(sqlQueryString))); + } + + var customHeaders = new Dictionary(); + if (!string.IsNullOrWhiteSpace(continuationToken)) + { + customHeaders.Add(_continuationTokenHeader, continuationToken); + } + + if (pageSize != null) + { + customHeaders.Add(_pageSizeHeader, pageSize.ToString()); + } + + HttpResponseMessage response = await _httpClientHelper + .PostAsync( + QueryDevicesRequestUri(), + new QuerySpecification { Sql = sqlQueryString }, + null, + customHeaders, + new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" }, + null, + cancellationToken) + .ConfigureAwait(false); + + return await QueryResult.FromHttpResponseAsync(response).ConfigureAwait(false); + } + + private Task CreateJobAsync(JobProperties jobProperties, CancellationToken ct) + { + EnsureInstanceNotClosed(); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.Forbidden, async (responseMessage) => new JobQuotaExceededException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false))} + }; + + string clientApiVersion = ClientApiVersionHelper.ApiVersionQueryString; + + return _httpClientHelper.PostAsync( + GetJobUri("/create", clientApiVersion), + jobProperties, + errorMappingOverrides, + null, + ct); + } + + private static Uri GetRequestUri(string deviceId) + { + deviceId = WebUtility.UrlEncode(deviceId); + return new Uri(_requestUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetModulesRequestUri(string deviceId, string moduleId) + { + deviceId = WebUtility.UrlEncode(deviceId); + moduleId = WebUtility.UrlEncode(moduleId); + return new Uri(_modulesRequestUriFormat.FormatInvariant(deviceId, moduleId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetModulesOnDeviceRequestUri(string deviceId) + { + deviceId = WebUtility.UrlEncode(deviceId); + return new Uri(_modulesOnDeviceRequestUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetModuleTwinRequestUri(string deviceId, string moduleId) + { + deviceId = WebUtility.UrlEncode(deviceId); + moduleId = WebUtility.UrlEncode(moduleId); + return new Uri(_moduleTwinUriFormat.FormatInvariant(deviceId, moduleId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetConfigurationRequestUri(string configurationId) + { + configurationId = WebUtility.UrlEncode(configurationId); + return new Uri(_configurationRequestUriFormat.FormatInvariant(configurationId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetConfigurationsRequestUri(int maxCount) + { + return new Uri(_configurationsRequestUriFormat.FormatInvariant(maxCount, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetApplyConfigurationOnDeviceRequestUri(string deviceId) + { + return new Uri(_applyConfigurationOnDeviceUriFormat.FormatInvariant(deviceId), UriKind.Relative); + } + + private static Uri GetBulkRequestUri(string apiVersionQueryString) + { + return new Uri(_requestUriFormat.FormatInvariant(string.Empty, apiVersionQueryString), UriKind.Relative); + } + + private static Uri GetJobUri(string jobId, string apiVersion = ClientApiVersionHelper.ApiVersionQueryString) + { + return new Uri(_jobsUriFormat.FormatInvariant(jobId, apiVersion), UriKind.Relative); + } + + private static Uri GetDevicesRequestUri(int maxCount) + { + return new Uri(_devicesRequestUriFormat.FormatInvariant(maxCount, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri QueryDevicesRequestUri() + { + return new Uri(_devicesQueryUriFormat, UriKind.Relative); + } + + private static Uri GetAdminUri(string operation) + { + return new Uri(_adminUriFormat.FormatInvariant(operation, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static Uri GetStatisticsUri() + { + return new Uri(_statisticsUriFormat, UriKind.Relative); + } + + private static Uri GetTwinUri(string deviceId) + { + deviceId = WebUtility.UrlEncode(deviceId); + return new Uri(_twinUriFormat.FormatInvariant(deviceId, ClientApiVersionHelper.ApiVersionQueryString), UriKind.Relative); + } + + private static void ValidateDeviceId(Device device) + { + if (device == null) + { + throw new ArgumentNullException(nameof(device)); + } + + if (string.IsNullOrWhiteSpace(device.Id)) + { + throw new ArgumentException("device.Id"); + } + + if (!s_deviceIdRegex.IsMatch(device.Id)) + { + throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(device.Id)); + } + } + + private static void ValidateTwinId(Twin twin) + { + if (twin == null) + { + throw new ArgumentNullException(nameof(twin)); + } + + if (string.IsNullOrWhiteSpace(twin.DeviceId)) + { + throw new ArgumentException("twin.DeviceId"); + } + + if (!s_deviceIdRegex.IsMatch(twin.DeviceId)) + { + throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(twin.DeviceId)); + } + } + + private static void ValidateModuleId(Module module) + { + if (module == null) + { + throw new ArgumentNullException(nameof(module)); + } + + if (string.IsNullOrWhiteSpace(module.DeviceId)) + { + throw new ArgumentException("module.Id"); + } + + if (string.IsNullOrWhiteSpace(module.Id)) + { + throw new ArgumentException("module.ModuleId"); + } + + if (!s_deviceIdRegex.IsMatch(module.DeviceId)) + { + throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(module.DeviceId)); + } + + if (!s_deviceIdRegex.IsMatch(module.Id)) + { + throw new ArgumentException(ApiResources.DeviceIdInvalid.FormatInvariant(module.Id)); + } + } + + private static void ValidateDeviceAuthentication(AuthenticationMechanism authentication, string deviceId) + { + if (authentication != null) + { + // Both symmetric keys and X.509 cert thumbprints cannot be specified for the same device + bool symmetricKeyIsSet = !authentication.SymmetricKey?.IsEmpty() ?? false; + bool x509ThumbprintIsSet = !authentication.X509Thumbprint?.IsEmpty() ?? false; + + if (symmetricKeyIsSet && x509ThumbprintIsSet) + { + throw new ArgumentException(ApiResources.DeviceAuthenticationInvalid.FormatInvariant(deviceId ?? string.Empty)); + } + + // Validate X.509 thumbprints or SymmetricKeys since we should not have both at the same time + if (x509ThumbprintIsSet) + { + authentication.X509Thumbprint.IsValid(true); + } + else if (symmetricKeyIsSet) + { + authentication.SymmetricKey.IsValid(true); + } + } + } + + private Task RemoveDeviceModuleAsync(string deviceId, string moduleId, IETagHolder eTagHolder, CancellationToken cancellationToken) + { + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.NotFound, + async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return new DeviceNotFoundException(responseContent, (Exception) null); + } + }, + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + }; + + return _httpClientHelper.DeleteAsync(GetModulesRequestUri(deviceId, moduleId), eTagHolder, errorMappingOverrides, null, cancellationToken); + } + + private void EnsureInstanceNotClosed() + { + if (_httpClientHelper == null) + { + throw new ObjectDisposedException("RegistryManager", ApiResources.RegistryManagerInstanceAlreadyClosed); + } + } + + private static void NormalizeDevice(Device device) + { + // auto generate keys if not specified + if (device.Authentication == null) + { + device.Authentication = new AuthenticationMechanism(); + } + + NormalizeAuthenticationInfo(device.Authentication); + } + + private static void NormalizeAuthenticationInfo(AuthenticationMechanism authenticationInfo) + { + //to make it backward compatible we set the type according to the values + //we don't set CA type - that has to be explicit + if (authenticationInfo.SymmetricKey != null && !authenticationInfo.SymmetricKey.IsEmpty()) + { + authenticationInfo.Type = AuthenticationType.Sas; + } + + if (authenticationInfo.X509Thumbprint != null && !authenticationInfo.X509Thumbprint.IsEmpty()) + { + authenticationInfo.Type = AuthenticationType.SelfSigned; + } + } + + private static void NormalizeExportImportDevice(ExportImportDevice device) + { + // auto generate keys if not specified + if (device.Authentication == null) + { + device.Authentication = new AuthenticationMechanism(); + } + + NormalizeAuthenticationInfo(device.Authentication); + } + + private static IEnumerable GenerateExportImportDeviceListForBulkOperations(IEnumerable devices, ImportMode importMode) + { + if (devices == null) + { + throw new ArgumentNullException(nameof(devices)); + } + + if (!devices.Any()) + { + throw new ArgumentException($"Parameter {nameof(devices)} cannot be empty."); + } + + var exportImportDeviceList = new List(devices.Count()); + foreach (Device device in devices) + { + ValidateDeviceId(device); + + switch (importMode) + { + case ImportMode.Create: + if (!string.IsNullOrWhiteSpace(device.ETag)) + { + throw new ArgumentException(ApiResources.ETagSetWhileRegisteringDevice); + } + break; + + case ImportMode.Update: + // No preconditions + break; + + case ImportMode.UpdateIfMatchETag: + if (string.IsNullOrWhiteSpace(device.ETag)) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingDevice); + } + break; + + case ImportMode.Delete: + // No preconditions + break; + + case ImportMode.DeleteIfMatchETag: + if (string.IsNullOrWhiteSpace(device.ETag)) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileDeletingDevice); + } + break; + + default: + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.InvalidImportMode, importMode)); + } + + var exportImportDevice = new ExportImportDevice(device, importMode); + exportImportDeviceList.Add(exportImportDevice); + } + + return exportImportDeviceList; + } + + private static IEnumerable GenerateExportImportDeviceListForTwinBulkOperations(IEnumerable twins, ImportMode importMode) + { + if (twins == null) + { + throw new ArgumentNullException(nameof(twins)); + } + + if (!twins.Any()) + { + throw new ArgumentException($"Parameter {nameof(twins)} cannot be empty"); + } + + var exportImportDeviceList = new List(twins.Count()); + foreach (Twin twin in twins) + { + ValidateTwinId(twin); + + switch (importMode) + { + case ImportMode.UpdateTwin: + // No preconditions + break; + + case ImportMode.UpdateTwinIfMatchETag: + if (string.IsNullOrWhiteSpace(twin.ETag)) + { + throw new ArgumentException(ApiResources.ETagNotSetWhileUpdatingTwin); + } + break; + + default: + throw new ArgumentException(IotHubApiResources.GetString(ApiResources.InvalidImportMode, importMode)); + } + + var exportImportDevice = new ExportImportDevice + { + Id = twin.DeviceId, + ModuleId = twin.ModuleId, + ImportMode = importMode, + TwinETag = importMode == ImportMode.UpdateTwinIfMatchETag ? twin.ETag : null, + Tags = twin.Tags, + Properties = new ExportImportDevice.PropertyContainer(), + }; + exportImportDevice.Properties.DesiredProperties = twin.Properties?.Desired; + + exportImportDeviceList.Add(exportImportDevice); + } + + return exportImportDeviceList; + } + + private Task BulkDeviceOperationsAsync(IEnumerable devices, string version, CancellationToken cancellationToken) + { + Logging.Enter(this, $"Performing bulk device operation on : {devices?.Count()} devices. version: {version}", nameof(BulkDeviceOperationsAsync)); + try + { + BulkDeviceOperationSetup(devices); + + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.PreconditionFailed, async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, + { HttpStatusCode.RequestEntityTooLarge, async responseMessage => new TooManyDevicesException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) }, + { HttpStatusCode.BadRequest, async responseMessage => new ArgumentException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) } + }; + + return _httpClientHelper.PostAsync, T>(GetBulkRequestUri(version), devices, errorMappingOverrides, null, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(BulkDeviceOperationsAsync)} threw an exception: {ex}", nameof(BulkDeviceOperationsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Performing bulk device operation on : {devices?.Count()} devices. version: {version}", nameof(BulkDeviceOperationsAsync)); + } + } + + private void BulkDeviceOperationSetup(IEnumerable devices) + { + EnsureInstanceNotClosed(); + + if (devices == null) + { + throw new ArgumentNullException(nameof(devices)); + } + + foreach (ExportImportDevice device in devices) + { + ValidateDeviceAuthentication(device.Authentication, device.Id); + + NormalizeExportImportDevice(device); + } + } + + private Task RemoveDeviceAsync(string deviceId, IETagHolder eTagHolder, CancellationToken cancellationToken) + { + var errorMappingOverrides = new Dictionary>> + { + { + HttpStatusCode.NotFound, + async responseMessage => + { + string responseContent = await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false); + return new DeviceNotFoundException(responseContent, (Exception) null); + } + }, + { + HttpStatusCode.PreconditionFailed, + async responseMessage => new PreconditionFailedException(await ExceptionHandlingHelper.GetExceptionMessageAsync(responseMessage).ConfigureAwait(false)) + }, + }; + + return _httpClientHelper.DeleteAsync(GetRequestUri(deviceId), eTagHolder, errorMappingOverrides, null, cancellationToken); + } } } diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index bd2649b1a4..5ee7c3856a 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -2,8 +2,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Amqp; +using Microsoft.Azure.Amqp.Framing; +using Microsoft.Azure.Devices.Common; +using Microsoft.Azure.Devices.Common.Data; +using Microsoft.Azure.Devices.Common.Exceptions; using Microsoft.Azure.Devices.Shared; #if !NET451 @@ -39,14 +47,72 @@ public enum TransportType /// Contains methods that services can use to send messages to devices /// For more information, see /// - public abstract class ServiceClient : IDisposable + public class ServiceClient : IDisposable { + private const string _statisticsUriFormat = "/statistics/service?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _purgeMessageQueueFormat = "/devices/{0}/commands?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _deviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; + private const string _moduleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryString; + + private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); + + private readonly FaultTolerantAmqpObject _faultTolerantSendingLink; + private readonly string _sendingPath; + private readonly AmqpFeedbackReceiver _feedbackReceiver; + private readonly AmqpFileNotificationReceiver _fileNotificationReceiver; + private readonly IHttpClientHelper _httpClientHelper; + private readonly string _iotHubName; + private readonly ServiceClientOptions _clientOptions; + private readonly TimeSpan _openTimeout; + private readonly TimeSpan _operationTimeout; + + private int _sendingDeliveryTag; + + internal readonly IotHubConnection Connection; + /// - /// Make this constructor internal so that only this library may implement this abstract class. + /// Creates an instance of , provided for unit testing purposes only. + /// Use the CreateFromConnectionString method to create an instance to use the client. /// - internal ServiceClient() + public ServiceClient() { - TlsVersions.Instance.SetLegacyAcceptableVersions(); + } + + internal ServiceClient( + IotHubConnectionProperties connectionProperties, + bool useWebSocketOnly, + ServiceClientTransportSettings transportSettings, + ServiceClientOptions options) + { + Connection = new IotHubConnection(connectionProperties, useWebSocketOnly, transportSettings); ; + _openTimeout = IotHubConnection.DefaultOpenTimeout; + _operationTimeout = IotHubConnection.DefaultOperationTimeout; + _faultTolerantSendingLink = new FaultTolerantAmqpObject(CreateSendingLinkAsync, Connection.CloseLink); + _feedbackReceiver = new AmqpFeedbackReceiver(Connection); + _fileNotificationReceiver = new AmqpFileNotificationReceiver(Connection); + _iotHubName = connectionProperties.IotHubName; + _clientOptions = options; + _sendingPath = "/messages/deviceBound"; + _httpClientHelper = new HttpClientHelper( + connectionProperties.HttpsEndpoint, + connectionProperties, + ExceptionHandlingHelper.GetDefaultErrorMapping(), + s_defaultOperationTimeout, + transportSettings.HttpProxy, + transportSettings.ConnectionLeaseTimeoutMilliseconds); + + // Set the trace provider for the AMQP library. + AmqpTrace.Provider = new AmqpTransportLog(); + } + + // internal test helper + internal ServiceClient(IotHubConnection connection, IHttpClientHelper httpClientHelper) + { + Connection = connection; + _httpClientHelper = httpClientHelper; + _feedbackReceiver = new AmqpFeedbackReceiver(Connection); + _fileNotificationReceiver = new AmqpFileNotificationReceiver(Connection); + _faultTolerantSendingLink = new FaultTolerantAmqpObject(CreateSendingLinkAsync, Connection.CloseLink); } /// @@ -94,7 +160,7 @@ public static ServiceClient Create( var tokenCredentialProperties = new IotHubTokenCrendentialProperties(hostName, credential); bool useWebSocketOnly = transportType == TransportType.Amqp_WebSocket_Only; - return new AmqpServiceClient( + return new ServiceClient( tokenCredentialProperties, useWebSocketOnly, transportSettings ?? new ServiceClientTransportSettings(), @@ -130,7 +196,7 @@ public static ServiceClient Create( var sasCredentialProperties = new IotHubSasCredentialProperties(hostName, credential); bool useWebSocketOnly = transportType == TransportType.Amqp_WebSocket_Only; - return new AmqpServiceClient( + return new ServiceClient( sasCredentialProperties, useWebSocketOnly, transportSettings ?? new ServiceClientTransportSettings(), @@ -150,7 +216,17 @@ public void Dispose() /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _faultTolerantSendingLink.Dispose(); + _fileNotificationReceiver.Dispose(); + _feedbackReceiver.Dispose(); + Connection.Dispose(); + _httpClientHelper.Dispose(); + } + } /// /// Create an instance of ServiceClient from the specified IoT Hub connection string using specified Transport Type. @@ -181,55 +257,181 @@ public static ServiceClient CreateFromConnectionString(string connectionString, var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); bool useWebSocketOnly = transportType == TransportType.Amqp_WebSocket_Only; - var serviceClient = new AmqpServiceClient(iotHubConnectionString, useWebSocketOnly, transportSettings, options); - return serviceClient; + + return new ServiceClient( + iotHubConnectionString, + useWebSocketOnly, + transportSettings, + options); } /// - /// Open the ServiceClient instance. + /// Open the ServiceClient instance. This call is made over AMQP. /// - public abstract Task OpenAsync(); + public virtual async Task OpenAsync() + { + Logging.Enter(this, $"Opening AmqpServiceClient", nameof(OpenAsync)); + + await _faultTolerantSendingLink.OpenAsync(_openTimeout).ConfigureAwait(false); + await _feedbackReceiver.OpenAsync().ConfigureAwait(false); + + Logging.Exit(this, $"Opening AmqpServiceClient", nameof(OpenAsync)); + } /// - /// Close the ServiceClient instance. + /// Close the ServiceClient instance. This call is made over AMQP. /// - public abstract Task CloseAsync(); + public virtual async Task CloseAsync() + { + Logging.Enter(this, $"Closing AmqpServiceClient", nameof(CloseAsync)); + + await _faultTolerantSendingLink.CloseAsync().ConfigureAwait(false); + await _feedbackReceiver.CloseAsync().ConfigureAwait(false); + await _fileNotificationReceiver.CloseAsync().ConfigureAwait(false); + await Connection.CloseAsync().ConfigureAwait(false); + + Logging.Exit(this, $"Closing AmqpServiceClient", nameof(CloseAsync)); + } /// - /// Send a cloud-to-device message to the specified device. + /// Send a cloud-to-device message to the specified device. This call is made over AMQP. /// /// The device identifier for the target device. /// The cloud-to-device message. /// The operation timeout, which defaults to 1 minute if unspecified. - public abstract Task SendAsync(string deviceId, Message message, TimeSpan? timeout = null); + public virtual async Task SendAsync(string deviceId, Message message, TimeSpan? timeout = null) + { + Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}", nameof(SendAsync)); + + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException(nameof(deviceId)); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (_clientOptions?.SdkAssignsMessageId == SdkAssignsMessageId.WhenUnset && message.MessageId == null) + { + message.MessageId = Guid.NewGuid().ToString(); + } + + if (message.IsBodyCalled) + { + message.ResetBody(); + } + + timeout ??= _operationTimeout; + + using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); + amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/messages/deviceBound"; + + try + { + SendingAmqpLink sendingLink = await GetSendingLinkAsync().ConfigureAwait(false); + Outcome outcome = await sendingLink + .SendMessageAsync(amqpMessage, IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), AmqpConstants.NullBinary, timeout.Value) + .ConfigureAwait(false); + + Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); + + if (outcome.DescriptorCode != Accepted.Code) + { + throw AmqpErrorMapper.GetExceptionFromOutcome(outcome); + } + } + catch (Exception ex) when (!(ex is TimeoutException) && !ex.IsFatal()) + { + Logging.Error(this, $"{nameof(SendAsync)} threw an exception: {ex}", nameof(SendAsync)); + throw AmqpClientHelper.ToIotHubClientContract(ex); + } + finally + { + Logging.Exit(this, $"Sending message [{message?.MessageId}] for device {deviceId}", nameof(SendAsync)); + } + } /// - /// Removes all cloud-to-device messages from a device's queue. + /// Removes all cloud-to-device messages from a device's queue. This call is made over HTTP. /// /// The device identifier for the target device. /// A cancellation token to cancel the operation. - public abstract Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken = default); + public virtual Task PurgeMessageQueueAsync(string deviceId, CancellationToken cancellationToken = default) + { + Logging.Enter(this, $"Purging message queue for device: {deviceId}", nameof(PurgeMessageQueueAsync)); + + try + { + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new DeviceNotFoundException(deviceId)) } + }; + + return _httpClientHelper.DeleteAsync(GetPurgeMessageQueueAsyncUri(deviceId), errorMappingOverrides, null, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(PurgeMessageQueueAsync)} threw an exception: {ex}", nameof(PurgeMessageQueueAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Purging message queue for device: {deviceId}", nameof(PurgeMessageQueueAsync)); + } + } /// /// Get the which can deliver acknowledgments for messages sent to a device/module from IoT Hub. + /// This call is made over AMQP. /// For more information see . /// /// An instance of . - public abstract FeedbackReceiver GetFeedbackReceiver(); + public virtual FeedbackReceiver GetFeedbackReceiver() + { + return _feedbackReceiver; + } /// /// Get the which can deliver notifications for file upload operations. + /// This call is made over AMQP. /// For more information see . /// /// An instance of . - public abstract FileNotificationReceiver GetFileNotificationReceiver(); + public virtual FileNotificationReceiver GetFileNotificationReceiver() + { + return _fileNotificationReceiver; + } /// - /// Gets service statistics for the IoT Hub. + /// Gets service statistics for the IoT Hub. This call is made over HTTP. /// /// A cancellation token to cancel the operation. /// The service statistics that can be retrieved from IoT Hub, eg. the number of devices connected to the hub. - public abstract Task GetServiceStatisticsAsync(CancellationToken cancellationToken = default); + public virtual Task GetServiceStatisticsAsync(CancellationToken cancellationToken = default) + { + Logging.Enter(this, $"Getting service statistics", nameof(GetServiceStatisticsAsync)); + + try + { + var errorMappingOverrides = new Dictionary>> + { + { HttpStatusCode.NotFound, responseMessage => Task.FromResult((Exception)new IotHubNotFoundException(_iotHubName)) } + }; + + return _httpClientHelper.GetAsync(GetStatisticsUri(), errorMappingOverrides, null, cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(GetServiceStatisticsAsync)} threw an exception: {ex}", nameof(GetServiceStatisticsAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Getting service statistics", nameof(GetServiceStatisticsAsync)); + } + } /// /// Interactively invokes a method on a device. @@ -238,7 +440,10 @@ public static ServiceClient CreateFromConnectionString(string connectionString, /// Parameters to execute a direct method on the device. /// A cancellation token to cancel the operation. /// The . - public abstract Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); + public virtual Task InvokeDeviceMethodAsync(string deviceId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default) + { + return InvokeDeviceMethodAsync(GetDeviceMethodUri(deviceId), cloudToDeviceMethod, cancellationToken); + } /// /// Interactively invokes a method on a module. @@ -248,7 +453,20 @@ public static ServiceClient CreateFromConnectionString(string connectionString, /// Parameters to execute a direct method on the module. /// A cancellation token to cancel the operation. /// The . - public abstract Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default); + public virtual Task InvokeDeviceMethodAsync(string deviceId, string moduleId, CloudToDeviceMethod cloudToDeviceMethod, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException(nameof(deviceId)); + } + + if (string.IsNullOrWhiteSpace(moduleId)) + { + throw new ArgumentNullException(nameof(moduleId)); + } + + return InvokeDeviceMethodAsync(GetModuleMethodUri(deviceId, moduleId), cloudToDeviceMethod, cancellationToken); + } /// /// Send a cloud-to-device message to the specified module. @@ -257,6 +475,155 @@ public static ServiceClient CreateFromConnectionString(string connectionString, /// The module identifier for the target module. /// The cloud-to-module message. /// The operation timeout, which defaults to 1 minute if unspecified. - public abstract Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null); + public virtual async Task SendAsync(string deviceId, string moduleId, Message message, TimeSpan? timeout = null) + { + Logging.Enter(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); + + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException(nameof(deviceId)); + } + + if (string.IsNullOrWhiteSpace(moduleId)) + { + throw new ArgumentNullException(nameof(moduleId)); + } + + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (_clientOptions?.SdkAssignsMessageId == SdkAssignsMessageId.WhenUnset && message.MessageId == null) + { + message.MessageId = Guid.NewGuid().ToString(); + } + + if (message.IsBodyCalled) + { + message.ResetBody(); + } + + timeout ??= _operationTimeout; + + using AmqpMessage amqpMessage = MessageConverter.MessageToAmqpMessage(message); + amqpMessage.Properties.To = "/devices/" + WebUtility.UrlEncode(deviceId) + "/modules/" + WebUtility.UrlEncode(moduleId) + "/messages/deviceBound"; + try + { + SendingAmqpLink sendingLink = await GetSendingLinkAsync().ConfigureAwait(false); + Outcome outcome = await sendingLink + .SendMessageAsync( + amqpMessage, + IotHubConnection.GetNextDeliveryTag(ref _sendingDeliveryTag), + AmqpConstants.NullBinary, + timeout.Value) + .ConfigureAwait(false); + + Logging.Info(this, $"Outcome was: {outcome?.DescriptorName}", nameof(SendAsync)); + + if (outcome.DescriptorCode != Accepted.Code) + { + throw AmqpErrorMapper.GetExceptionFromOutcome(outcome); + } + } + catch (Exception ex) when (!ex.IsFatal()) + { + Logging.Error(this, $"{nameof(SendAsync)} threw an exception: {ex}", nameof(SendAsync)); + throw AmqpClientHelper.ToIotHubClientContract(ex); + } + finally + { + Logging.Exit(this, $"Sending message with Id [{message?.MessageId}] for device {deviceId}, module {moduleId}", nameof(SendAsync)); + } + } + + private Task CreateSendingLinkAsync(TimeSpan timeout) + { + return Connection.CreateSendingLinkAsync(_sendingPath, timeout); + } + + private async Task GetSendingLinkAsync() + { + Logging.Enter(this, $"_faultTolerantSendingLink = {_faultTolerantSendingLink?.GetHashCode()}", nameof(GetSendingLinkAsync)); + + try + { + if (!_faultTolerantSendingLink.TryGetOpenedObject(out SendingAmqpLink sendingLink)) + { + sendingLink = await _faultTolerantSendingLink.GetOrCreateAsync(_openTimeout).ConfigureAwait(false); + } + + Logging.Info(this, $"Retrieved SendingAmqpLink [{sendingLink?.Name}]", nameof(GetSendingLinkAsync)); + + return sendingLink; + } + finally + { + Logging.Exit(this, $"_faultTolerantSendingLink = {_faultTolerantSendingLink?.GetHashCode()}", nameof(GetSendingLinkAsync)); + } + } + + private Task InvokeDeviceMethodAsync(Uri uri, + CloudToDeviceMethod cloudToDeviceMethod, + CancellationToken cancellationToken) + { + Logging.Enter(this, $"Invoking device method for: {uri}", nameof(InvokeDeviceMethodAsync)); + + try + { + TimeSpan timeout = GetInvokeDeviceMethodOperationTimeout(cloudToDeviceMethod); + + return _httpClientHelper.PostAsync( + uri, + cloudToDeviceMethod, + timeout, + null, + null, + cancellationToken); + } + catch (Exception ex) + { + Logging.Error(this, $"{nameof(InvokeDeviceMethodAsync)} threw an exception: {ex}", nameof(InvokeDeviceMethodAsync)); + throw; + } + finally + { + Logging.Exit(this, $"Invoking device method for: {uri}", nameof(InvokeDeviceMethodAsync)); + } + } + + private static TimeSpan GetInvokeDeviceMethodOperationTimeout(CloudToDeviceMethod cloudToDeviceMethod) + { + // For InvokeDeviceMethod, we need to take into account the timeouts specified + // for the Device to connect and send a response. We also need to take into account + // the transmission time for the request send/receive + var timeout = TimeSpan.FromSeconds(15); // For wire time + timeout += TimeSpan.FromSeconds(cloudToDeviceMethod.ConnectionTimeoutInSeconds ?? 0); + timeout += TimeSpan.FromSeconds(cloudToDeviceMethod.ResponseTimeoutInSeconds ?? 0); + return timeout <= s_defaultOperationTimeout ? s_defaultOperationTimeout : timeout; + } + + private static Uri GetStatisticsUri() + { + return new Uri(_statisticsUriFormat, UriKind.Relative); + } + + private static Uri GetPurgeMessageQueueAsyncUri(string deviceId) + { + return new Uri(_purgeMessageQueueFormat.FormatInvariant(deviceId), UriKind.Relative); + } + + private static Uri GetDeviceMethodUri(string deviceId) + { + deviceId = WebUtility.UrlEncode(deviceId); + return new Uri(_deviceMethodUriFormat.FormatInvariant(deviceId), UriKind.Relative); + } + + private static Uri GetModuleMethodUri(string deviceId, string moduleId) + { + deviceId = WebUtility.UrlEncode(deviceId); + moduleId = WebUtility.UrlEncode(moduleId); + return new Uri(_moduleMethodUriFormat.FormatInvariant(deviceId, moduleId), UriKind.Relative); + } } } diff --git a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs index 0feae6597f..c46c6be1da 100644 --- a/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs +++ b/iothub/service/tests/ConnectionString/ServiceClientConnectionStringTests.cs @@ -26,7 +26,7 @@ public virtual IotHubConnectionStringBuilder Populate(IotHubConnectionStringBuil public void ServiceClientConnectionStringDefaultScopeDefaultCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE="; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -36,7 +36,7 @@ public void ServiceClientConnectionStringDefaultScopeDefaultCredentialTypeTest() public void ServiceClientConnectionStringIotHubScopeImplicitSharedAccessSignatureCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessSignature;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE="; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -46,7 +46,7 @@ public void ServiceClientConnectionStringIotHubScopeImplicitSharedAccessSignatur public void ServiceClientConnectionStringIotHubScopeExplicitSharedAccessSignatureCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessSignature;SharedAccessKeyName=AllAccessKey;SharedAccessSignature=SharedAccessSignature sr=dh%3a%2f%2facme.azure-devices.net&sig=dGVzdFN0cmluZzU=&se=87824124985&skn=AllAccessKey"; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -56,7 +56,7 @@ public void ServiceClientConnectionStringIotHubScopeExplicitSharedAccessSignatur public void ServiceClientConnectionStringIotHubScopeSharedAccessKeyCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessKey;SharedAccessKeyName=AllAccessKey;SharedAccessKey=dGVzdFN0cmluZzE="; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -66,7 +66,7 @@ public void ServiceClientConnectionStringIotHubScopeSharedAccessKeyCredentialTyp public void ServiceClientConnectionStringDeviceScopeImplicitSharedAccessSignatureCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessSignature;SharedAccessKeyName=blah;SharedAccessKey=dGVzdFN0cmluZzE="; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -76,7 +76,7 @@ public void ServiceClientConnectionStringDeviceScopeImplicitSharedAccessSignatur public void ServiceClientConnectionStringDeviceScopeExplicitSharedAccessSignatureCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessSignature;SharedAccessKeyName=blah;SharedAccessSignature=SharedAccessSignature sr=dh%3a%2f%2facme.azure-devices.net&sig=dGVzdFN0cmluZzU=&se=87824124985&skn=AllAccessKey"; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -86,7 +86,7 @@ public void ServiceClientConnectionStringDeviceScopeExplicitSharedAccessSignatur public void ServiceClientConnectionStringDeviceScopeSharedAccessKeyCredentialTypeTest() { string connectionString = "HostName=acme.azure-devices.net;CredentialScope=IotHub;CredentialType=SharedAccessKey;SharedAccessKeyName=blah;SharedAccessKey=dGVzdFN0cmluZzE="; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); Assert.IsNotNull(serviceClient.Connection.Credential); @@ -179,7 +179,7 @@ public void ServiceClientIotHubConnectionStringBuilderTest() public void ServiceClient_ConnectionString_ModuleIdentity_SharedAccessKeyCredentialType_Test() { string connectionString = "HostName=testhub.azure-devices-int.net;DeviceId=edgecapabledevice1;ModuleId=testModule;SharedAccessKey=dGVzdFN0cmluZzE=;GatewayHostName=edgehub1.ms.com"; - var serviceClient = (AmqpServiceClient)ServiceClient.CreateFromConnectionString(connectionString); + var serviceClient = ServiceClient.CreateFromConnectionString(connectionString); Assert.IsNotNull(serviceClient.Connection); IotHubConnectionString iotHubConnectionString = (IotHubConnectionString)serviceClient.Connection.Credential; diff --git a/iothub/service/tests/DeviceAuthenticationTests.cs b/iothub/service/tests/DeviceAuthenticationTests.cs index 46db0a3d95..ac933dc3d7 100644 --- a/iothub/service/tests/DeviceAuthenticationTests.cs +++ b/iothub/service/tests/DeviceAuthenticationTests.cs @@ -42,7 +42,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest1() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceGoodAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceGoodAuthConfig).ConfigureAwait(false); } @@ -69,7 +69,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest2() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceGoodAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceGoodAuthConfig).ConfigureAwait(false); } @@ -96,7 +96,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest3() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceGoodAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceGoodAuthConfig).ConfigureAwait(false); } @@ -123,7 +123,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest4() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceGoodAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceGoodAuthConfig).ConfigureAwait(false); } @@ -150,7 +150,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest5() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceGoodAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceGoodAuthConfig).ConfigureAwait(false); } @@ -177,7 +177,7 @@ public async Task DeviceAuthenticationGoodAuthConfigTest6() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -204,7 +204,7 @@ public async Task DeviceAuthenticationGoodAuthSHA256() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -236,7 +236,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest1() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -268,7 +268,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest2() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -300,7 +300,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest3() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -332,7 +332,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest4() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -364,7 +364,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest5() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -392,7 +392,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest6() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -420,7 +420,7 @@ public async Task DeviceAuthenticationBadAuthConfigTest7() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -448,7 +448,7 @@ public async Task DeviceAuthenticationBadThumbprintTest1() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -476,7 +476,7 @@ public async Task DeviceAuthenticationBadThumbprintTest2() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -504,7 +504,7 @@ public async Task DeviceAuthenticationBadThumbprintTest3() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -532,7 +532,7 @@ public async Task DeviceAuthenticationBadThumbprintTest4() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -560,7 +560,7 @@ public async Task DeviceAuthenticationBadThumbprintTest5() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -588,7 +588,7 @@ public async Task DeviceAuthenticationBadThumbprintTest6() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -616,7 +616,7 @@ public async Task DeviceAuthenticationBadThumbprintTest7() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } @@ -644,7 +644,7 @@ public async Task DeviceAuthenticationBadThumbprintSHA256Test() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadAuthConfig); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadAuthConfig).ConfigureAwait(false); } @@ -668,7 +668,7 @@ public async Task DeviceAuthenticationIsCertificateAuthority() restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceBadThumbprint); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceBadThumbprint).ConfigureAwait(false); } } diff --git a/iothub/service/tests/JobClient/DeviceJobParametersTest.cs b/iothub/service/tests/JobClient/DeviceJobParametersTest.cs index fab4516111..4d3db405d1 100644 --- a/iothub/service/tests/JobClient/DeviceJobParametersTest.cs +++ b/iothub/service/tests/JobClient/DeviceJobParametersTest.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.Azure.Devices.Api.Test.JobClient +namespace Microsoft.Azure.Devices.Api.Test { using System; using System.Collections.Generic; @@ -39,4 +39,4 @@ public void ConstructorWithSomeEmptyDeviceIdsTest() new DeviceJobParameters(JobType.ScheduleDeviceMethod, deviceList); } } -} \ No newline at end of file +} diff --git a/iothub/service/tests/JobClient/HttpJobClientTests.cs b/iothub/service/tests/JobClient/JobClientTests.cs similarity index 93% rename from iothub/service/tests/JobClient/HttpJobClientTests.cs rename to iothub/service/tests/JobClient/JobClientTests.cs index b804027803..ff88a02e57 100644 --- a/iothub/service/tests/JobClient/HttpJobClientTests.cs +++ b/iothub/service/tests/JobClient/JobClientTests.cs @@ -3,7 +3,7 @@ using System.Linq; -namespace Microsoft.Azure.Devices.Api.Test.JobClient +namespace Microsoft.Azure.Devices.Api.Test { using System; using System.Collections.Generic; @@ -16,20 +16,20 @@ namespace Microsoft.Azure.Devices.Api.Test.JobClient [TestClass] [TestCategory("Unit")] - public class HttpJobClientTests + public class JobClientTests { private readonly string jobId = "testJobId"; private readonly JobResponse expectedJobResponse = new JobResponse(); private readonly TimeSpan timeout = TimeSpan.FromMinutes(1); private Mock httpClientHelperMock; - private HttpJobClient jobClient; + private JobClient jobClient; [TestInitialize] public void Setup() { httpClientHelperMock = new Mock(); - jobClient = new HttpJobClient(httpClientHelperMock.Object); + jobClient = new JobClient(httpClientHelperMock.Object); } private void NoExtraJobParamTestSetup(JobType jobType, CancellationToken cancellationToken) diff --git a/iothub/service/tests/RegistryManagerTests.cs b/iothub/service/tests/RegistryManagerTests.cs index 12c26329f2..48e374c003 100644 --- a/iothub/service/tests/RegistryManagerTests.cs +++ b/iothub/service/tests/RegistryManagerTests.cs @@ -46,7 +46,7 @@ public async Task GetDeviceAsyncTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.GetAsync(It.IsAny(), It.IsAny>>>(), null, false, It.IsAny())).ReturnsAsync(deviceToReturn); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); var device = await registryManager.GetDeviceAsync(DeviceId).ConfigureAwait(false); Assert.AreSame(deviceToReturn, device); restOpMock.VerifyAll(); @@ -57,7 +57,7 @@ public async Task GetDeviceAsyncTest() public async Task GetDeviceAsyncWithNullDeviceIdTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.GetDeviceAsync(null).ConfigureAwait(false); Assert.Fail("Calling GetDeviceAsync with null device id did not throw an exception."); } @@ -76,7 +76,7 @@ public async Task GetDevicesAsyncTest() true, It.IsAny())).ReturnsAsync(devicesToReturn); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete var returnedDevices = await registryManager.GetDevicesAsync(1).ConfigureAwait(false); @@ -94,7 +94,7 @@ public async Task RegisterDeviceAsyncTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceToReturn); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); var returnedDevice = await registryManager.AddDeviceAsync(deviceToReturn).ConfigureAwait(false); Assert.AreSame(deviceToReturn, returnedDevice); restOpMock.VerifyAll(); @@ -106,7 +106,7 @@ public async Task RegisterDeviceAsyncWithInvalidDeviceIdTest() { var deviceToReturn = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected, ETag = "123" }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceToReturn).ConfigureAwait(false); Assert.Fail("RegisterDevice API did not throw exception when bad deviceid was used."); } @@ -117,7 +117,7 @@ public async Task RegisterDeviceAsyncWithETagSetTest() { var deviceToReturn = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "123" }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(deviceToReturn).ConfigureAwait(false); Assert.Fail("RegisterDevice API did not throw exception when ETag was set."); } @@ -127,7 +127,7 @@ public async Task RegisterDeviceAsyncWithETagSetTest() public async Task RegisterDeviceAsyncWithNullDeviceTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(null).ConfigureAwait(false); Assert.Fail("RegisterDevice API did not throw exception when the device parameter was null."); } @@ -137,7 +137,7 @@ public async Task RegisterDeviceAsyncWithNullDeviceTest() public async Task RegisterDeviceAsyncWithDeviceIdNullTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDeviceAsync(new Device()).ConfigureAwait(false); Assert.Fail("RegisterDevice API did not throw exception when the device's id was not set."); } @@ -150,7 +150,7 @@ public async Task RegisterDevicesAsyncWithInvalidDeviceIdTest() // '/' is not a valid character in DeviceId var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.AddDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -165,7 +165,7 @@ public async Task RegisterDevices2AsyncWithInvalidDeviceIdTest() // '/' is not a valid character in DeviceId var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("RegisterDevices API did not throw exception when bad deviceid was used."); } @@ -177,7 +177,7 @@ public async Task RegisterDevicesAsyncWithETagsSetTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.AddDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -191,7 +191,7 @@ public async Task RegisterDevices2AsyncWithETagsSetTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("RegisterDevices API did not throw exception when ETag was used."); } @@ -203,7 +203,7 @@ public async Task RegisterDevicesAsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.AddDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -217,7 +217,7 @@ public async Task RegisterDevices2AsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("RegisterDevices API did not throw exception when Null device was used."); } @@ -227,7 +227,7 @@ public async Task RegisterDevices2AsyncWithNullDeviceTest() public async Task RegisterDevicesAsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.AddDevicesAsync(new List()).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -239,7 +239,7 @@ public async Task RegisterDevicesAsyncWithNullDeviceListTest() public async Task RegisterDevices2AsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDevices2Async(new List()).ConfigureAwait(false); Assert.Fail("RegisterDevices API did not throw exception when Null device list was used."); } @@ -251,7 +251,7 @@ public async Task RegisterDevicesAsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.AddDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -265,7 +265,7 @@ public async Task RegisterDevices2AsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.AddDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("RegisterDevices API did not throw exception when deviceId was null."); } @@ -277,7 +277,7 @@ public async Task UpdateDeviceAsyncTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>>>(), It.IsAny())).ReturnsAsync(deviceToReturn); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); var returnedDevice = await registryManager.UpdateDeviceAsync(deviceToReturn).ConfigureAwait(false); Assert.AreSame(deviceToReturn, returnedDevice); restOpMock.VerifyAll(); @@ -294,7 +294,7 @@ private Device PrepareTestDevice(int batteryLevel, string firmwareVersion) public async Task UpdateDeviceWithNullDeviceTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDeviceAsync(null).ConfigureAwait(false); Assert.Fail("UpdateDevice api did not throw exception when the device parameter was null."); } @@ -304,7 +304,7 @@ public async Task UpdateDeviceWithNullDeviceTest() public async Task UpdateDeviceWithDeviceIdNullTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDeviceAsync(new Device() { ETag = "*" }).ConfigureAwait(false); Assert.Fail("UpdateDevice api did not throw exception when the device's id was null."); } @@ -314,7 +314,7 @@ public async Task UpdateDeviceWithDeviceIdNullTest() public async Task UpdateDeviceWithInvalidDeviceIdTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); // '/' is not a valid char in DeviceId await registryManager.UpdateDeviceAsync(new Device("/baddevice") { ETag = "*" }).ConfigureAwait(false); Assert.Fail("UpdateDevice api did not throw exception when the deviceid was invalid."); @@ -327,7 +327,7 @@ public async Task UpdateDevicesAsyncWithInvalidDeviceIdTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -341,7 +341,7 @@ public async Task UpdateDevices2AsyncWithInvalidDeviceIdTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("UpdateDevices API did not throw exception when bad deviceid was used."); } @@ -353,7 +353,7 @@ public async Task UpdateDevicesAsyncWithETagMissingTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -367,7 +367,7 @@ public async Task UpdateDevices2AsyncWithETagMissingTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("UpdateDevices API did not throw exception when ETag was null."); } @@ -379,7 +379,7 @@ public async Task UpdateDevicesAsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -393,7 +393,7 @@ public async Task UpdateDevices2AsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("UpdateDevices API did not throw exception when Null device was used."); } @@ -403,7 +403,7 @@ public async Task UpdateDevices2AsyncWithNullDeviceTest() public async Task UpdateDevicesAsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List()).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -415,7 +415,7 @@ public async Task UpdateDevicesAsyncWithNullDeviceListTest() public async Task UpdateDevices2AsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List()).ConfigureAwait(false); Assert.Fail("UpdateDevices API did not throw exception when Null device list was used."); } @@ -427,7 +427,7 @@ public async Task UpdateDevicesAsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -441,7 +441,7 @@ public async Task UpdateDevices2AsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("UpdateDevices API did not throw exception when deviceId was null."); } @@ -453,7 +453,7 @@ public async Task UpdateDevicesAsyncForceUpdateTest() var goodDevice2 = new Device("234") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice1, goodDevice2 }, true, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -467,7 +467,7 @@ public async Task UpdateDevices2AsyncForceUpdateTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice1, goodDevice2 }, true, CancellationToken.None).ConfigureAwait(false); } @@ -480,7 +480,7 @@ public async Task UpdateDevicesAsyncForceUpdateMissingETagTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { badDevice1, badDevice2 }, false, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -495,7 +495,7 @@ public async Task UpdateDevices2AsyncForceUpdateMissingETagTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { badDevice1, badDevice2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -507,7 +507,7 @@ public async Task UpdateDevicesAsyncForceUpdateFalseTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.UpdateDevicesAsync(new List() { goodDevice1, goodDevice2 }, false, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -521,7 +521,7 @@ public async Task UpdateDevices2AsyncForceUpdateFalseTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateDevices2Async(new List() { goodDevice1, goodDevice2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -532,7 +532,7 @@ public async Task DeleteDeviceAsyncTest() var restOpMock = new Mock(); var mockETag = new ETagHolder() { ETag = "*" }; restOpMock.Setup(restOp => restOp.DeleteAsync(It.IsAny(), mockETag, It.IsAny>>>(), null, It.IsAny())).Returns(Task.FromResult(0)); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDeviceAsync(new Device()).ConfigureAwait(false); restOpMock.VerifyAll(); } @@ -542,7 +542,7 @@ public async Task DeleteDeviceAsyncTest() public async Task DeleteDeviceAsyncWithNullIdTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDeviceAsync(string.Empty).ConfigureAwait(false); Assert.Fail("Delete API did not throw exception when the device id was null."); } @@ -554,7 +554,7 @@ public async Task DeleteDevicesAsyncWithInvalidDeviceIdTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -568,7 +568,7 @@ public async Task DeleteDevices2AsyncWithInvalidDeviceIdTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected }; var badDevice = new Device("/baddevice") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("DeleteDevices API did not throw exception when bad deviceid was used."); } @@ -580,7 +580,7 @@ public async Task DeleteDevicesAsyncWithETagMissingTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -594,7 +594,7 @@ public async Task DeleteDevices2AsyncWithETagMissingTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device("234") { ConnectionState = DeviceConnectionState.Connected }; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("DeleteDevices API did not throw exception when ETag was null."); } @@ -606,7 +606,7 @@ public async Task DeleteDevicesAsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -620,7 +620,7 @@ public async Task DeleteDevices2AsyncWithNullDeviceTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; Device badDevice = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("DeleteDevices API did not throw exception when Null device was used."); } @@ -630,7 +630,7 @@ public async Task DeleteDevices2AsyncWithNullDeviceTest() public async Task DeleteDevicesAsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List()).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -642,7 +642,7 @@ public async Task DeleteDevicesAsyncWithNullDeviceListTest() public async Task DeleteDevices2AsyncWithNullDeviceListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List()).ConfigureAwait(false); Assert.Fail("DeleteDevices API did not throw exception when Null device list was used."); } @@ -654,7 +654,7 @@ public async Task DeleteDevicesAsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice, badDevice }).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -668,7 +668,7 @@ public async Task DeleteDevices2AsyncWithDeviceIdNullTest() var goodDevice = new Device("123") { ConnectionState = DeviceConnectionState.Connected, ETag = "234" }; var badDevice = new Device(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice, badDevice }).ConfigureAwait(false); Assert.Fail("DeleteDevices API did not throw exception when deviceId was null."); } @@ -681,7 +681,7 @@ public async Task DeleteDevicesAsyncForceDeleteTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice1, goodDevice2 }, true, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -695,7 +695,7 @@ public async Task DeleteDevices2AsyncForceDeleteTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice1, goodDevice2 }, true, CancellationToken.None).ConfigureAwait(false); } @@ -708,7 +708,7 @@ public async Task DeleteDevicesAsyncForceDeleteFalseMissingETagTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { badDevice1, badDevice2 }, false, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -723,7 +723,7 @@ public async Task DeleteDevices2AsyncForceDeleteFalseMissingETagTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { badDevice1, badDevice2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -735,7 +735,7 @@ public async Task DeleteDevicesAsyncForceDeleteFalseTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); #pragma warning disable CS0618 // Type or member is obsolete await registryManager.RemoveDevicesAsync(new List() { goodDevice1, goodDevice2 }, false, CancellationToken.None).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete @@ -749,7 +749,7 @@ public async Task DeleteDevices2AsyncForceDeleteFalseTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.PostAsync, Task>(It.IsAny(), It.IsAny>(), It.IsAny>>>(), It.IsAny>(), It.IsAny())).ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.RemoveDevices2Async(new List() { goodDevice1, goodDevice2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -760,7 +760,7 @@ public async Task UpdateTwins2AsyncWithInvalidDeviceIdTest() var goodTwin = new Twin("123"); var badTwin = new Twin("/badTwin"); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin, badTwin }).ConfigureAwait(false); Assert.Fail("UpdateTwins API did not throw exception when bad deviceid was used."); } @@ -772,7 +772,7 @@ public async Task UpdateTwins2AsyncWithETagMissingTest() var goodTwin = new Twin("123") { ETag = "234" }; var badTwin = new Twin("234"); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin, badTwin }).ConfigureAwait(false); Assert.Fail("UpdateTwins API did not throw exception when ETag was null."); } @@ -784,7 +784,7 @@ public async Task UpdateTwins2AsyncWithNullTwinTest() var goodTwin = new Twin("123") { ETag = "234" }; Twin badTwin = null; var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin, badTwin }).ConfigureAwait(false); Assert.Fail("UpdateTwins API did not throw exception when Null twin was used."); } @@ -794,7 +794,7 @@ public async Task UpdateTwins2AsyncWithNullTwinTest() public async Task UpdateTwins2AsyncWithNullTwinListTest() { var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List()).ConfigureAwait(false); Assert.Fail("UpdateTwins API did not throw exception when Null twin list was used."); } @@ -806,7 +806,7 @@ public async Task UpdateTwins2AsyncWithDeviceIdNullTest() var goodTwin = new Twin("123") { ETag = "234" }; var badTwin = new Twin(); var restOpMock = new Mock(); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin, badTwin }).ConfigureAwait(false); Assert.Fail("UpdateTwins API did not throw exception when deviceId was null."); } @@ -826,7 +826,7 @@ public async Task UpdateTwins2AsyncForceUpdateTest() It.IsAny())) .ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin1, goodTwin2 }, true, CancellationToken.None).ConfigureAwait(false); } @@ -846,7 +846,7 @@ public async Task UpdateTwins2AsyncForceUpdateMissingETagTest() It.IsAny())) .ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { badTwin1, badTwin2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -865,7 +865,7 @@ public async Task UpdateTwins2AsyncForceUpdateFalseTest() It.IsAny())) .ReturnsAsync((Task)null); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.UpdateTwins2Async(new List() { goodTwin1, goodTwin2 }, false, CancellationToken.None).ConfigureAwait(false); } @@ -875,7 +875,7 @@ public void DisposeTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.Dispose()); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); registryManager.Dispose(); restOpMock.Verify(restOp => restOp.Dispose(), Times.Once()); } @@ -886,7 +886,7 @@ public async Task CloseAsyncTest() var restOpMock = new Mock(); restOpMock.Setup(restOp => restOp.Dispose()); - var registryManager = new HttpRegistryManager(restOpMock.Object, IotHubName); + var registryManager = new RegistryManager(IotHubName, restOpMock.Object); await registryManager.CloseAsync().ConfigureAwait(false); restOpMock.Verify(restOp => restOp.Dispose(), Times.Never()); } diff --git a/iothub/service/tests/ServiceClientTests.cs b/iothub/service/tests/ServiceClientTests.cs index 4ba3b86fd1..bb94fc251a 100644 --- a/iothub/service/tests/ServiceClientTests.cs +++ b/iothub/service/tests/ServiceClientTests.cs @@ -23,9 +23,9 @@ public class ServiceClientTests public async Task PurgeMessageQueueWithCancellationTokenTest() { // Arrange Moq - Tuple, AmqpServiceClient, PurgeMessageQueueResult> setupParameters = this.SetupPurgeMessageQueueTests(); + Tuple, ServiceClient, PurgeMessageQueueResult> setupParameters = this.SetupPurgeMessageQueueTests(); Mock restOpMock = setupParameters.Item1; - AmqpServiceClient serviceClient = setupParameters.Item2; + ServiceClient serviceClient = setupParameters.Item2; PurgeMessageQueueResult expectedResult = setupParameters.Item3; // Execute method under test @@ -50,13 +50,17 @@ public async Task PurgeMessageQueueDeviceNotFoundTest() // Instantiate AmqpServiceClient with Mock IHttpClientHelper var authMethod = new ServiceAuthenticationWithSharedAccessPolicyKey("test", "dGVzdFN0cmluZzE="); var builder = IotHubConnectionStringBuilder.Create("acme.azure-devices.net", authMethod); - var serviceClient = new AmqpServiceClient(restOpMock.Object); + Func> onCreate = _ => Task.FromResult(new AmqpSession(null, new AmqpSessionSettings(), null)); + Action onClose = _ => { }; + // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection + var connection = new IotHubConnection(onCreate, onClose); + var serviceClient = new ServiceClient(connection, restOpMock.Object); // Execute method under test PurgeMessageQueueResult result = await serviceClient.PurgeMessageQueueAsync("TestDevice", CancellationToken.None).ConfigureAwait(false); } - Tuple, AmqpServiceClient, PurgeMessageQueueResult> SetupPurgeMessageQueueTests() + private Tuple, ServiceClient, PurgeMessageQueueResult> SetupPurgeMessageQueueTests() { // Create expected return object var deviceId = "TestDevice"; @@ -76,7 +80,11 @@ Tuple, AmqpServiceClient, PurgeMessageQueueResult> Setup // Instantiate AmqpServiceClient with Mock IHttpClientHelper var authMethod = new ServiceAuthenticationWithSharedAccessPolicyKey("test", "dGVzdFN0cmluZzE="); var builder = IotHubConnectionStringBuilder.Create("acme.azure-devices.net", authMethod); - var serviceClient = new AmqpServiceClient(restOpMock.Object); + Func> onCreate = _ => Task.FromResult(new AmqpSession(null, new AmqpSessionSettings(), null)); + Action onClose = _ => { }; + // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection + var connection = new IotHubConnection(onCreate, onClose); + var serviceClient = new ServiceClient(connection, restOpMock.Object); return Tuple.Create(restOpMock, serviceClient, expectedResult); } @@ -91,7 +99,7 @@ public async Task DisposeTest() Action onClose = _ => { connectionClosed = true; }; // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); - var serviceClient = new AmqpServiceClient(connection, restOpMock.Object); + var serviceClient = new ServiceClient(connection, restOpMock.Object); // This is required to cause onClose callback invocation. await connection.OpenAsync(TimeSpan.FromSeconds(1)).ConfigureAwait(false); serviceClient.Dispose(); @@ -110,7 +118,7 @@ public async Task CloseAsyncTest() // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); - var serviceClient = new AmqpServiceClient(connection, restOpMock.Object); + var serviceClient = new ServiceClient(connection, restOpMock.Object); // This is required to cause onClose callback invocation. await connection.OpenAsync(TimeSpan.FromSeconds(1)).ConfigureAwait(false); await serviceClient.CloseAsync().ConfigureAwait(false); diff --git a/provisioning/device/src/Microsoft.Azure.Devices.Provisioning.Client.csproj b/provisioning/device/src/Microsoft.Azure.Devices.Provisioning.Client.csproj index 3b3dc759df..417e7cbd39 100644 --- a/provisioning/device/src/Microsoft.Azure.Devices.Provisioning.Client.csproj +++ b/provisioning/device/src/Microsoft.Azure.Devices.Provisioning.Client.csproj @@ -18,7 +18,7 @@ - 1.17.1 + 1.18.0 Microsoft Azure IoT Provisioning Device Client SDK True True diff --git a/provisioning/service/src/Config/IndividualEnrollment.cs b/provisioning/service/src/Config/IndividualEnrollment.cs index 6d955e3f70..a45455a655 100644 --- a/provisioning/service/src/Config/IndividualEnrollment.cs +++ b/provisioning/service/src/Config/IndividualEnrollment.cs @@ -156,8 +156,6 @@ internal IndividualEnrollment( string eTag, DeviceCapabilities capabilities) { - /* SRS_INDIVIDUAL_ENROLLMENT_21_003: [The constructor shall throws ProvisioningServiceClientException if one of the - provided parameters in JSON is not valid.] */ if (attestation == null) { throw new ProvisioningServiceClientException("Service respond an individualEnrollment without attestation."); @@ -189,8 +187,7 @@ provided parameters in JSON is not valid.] */ /// The string with the content of this class in a pretty print format. public override string ToString() { - string jsonPrettyPrint = Newtonsoft.Json.JsonConvert.SerializeObject(this, Formatting.Indented); - return jsonPrettyPrint; + return JsonConvert.SerializeObject(this, Formatting.Indented); } /// @@ -201,46 +198,13 @@ public override string ToString() /// /// if the provided string does not fit the registration Id requirements [JsonProperty(PropertyName = "registrationId")] - public string RegistrationId - { - get - { - return _registrationId; - } - - private set - { - _registrationId = value; - } - } - - private string _registrationId; + public string RegistrationId { get; private set; } /// /// Desired IoT Hub device Id (optional). /// [JsonProperty(PropertyName = "deviceId", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string DeviceId - { - get - { - return _deviceId; - } - - set - { - if (value == null) - { - _deviceId = null; - } - else - { - _deviceId = value; - } - } - } - - private string _deviceId; + public string DeviceId { get; set; } /// /// Current registration state. @@ -260,19 +224,16 @@ public string DeviceId [JsonIgnore] public Attestation Attestation { - get - { - return _attestation.GetAttestation(); - } + get => _attestation.GetAttestation(); set { - if (value is X509Attestation) + if (value is X509Attestation attestation) { - if ((((X509Attestation)value ?? throw new ArgumentNullException(nameof(value))).ClientCertificates == null) && - (((X509Attestation)value).CAReferences == null)) + if ((attestation ?? throw new ArgumentNullException(nameof(value))).ClientCertificates == null + && attestation.CAReferences == null) { - throw new ArgumentNullException($"{nameof(value)} do not contains client certificate or CA reference."); + throw new ArgumentNullException(nameof(value), $"Value does not contain client certificate or CA reference."); } } diff --git a/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj b/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj index c503049702..c8a073043c 100644 --- a/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj +++ b/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj @@ -19,7 +19,7 @@ - 1.17.1 + 1.17.2 Microsoft Azure IoT Provisioning Service Client SDK True True diff --git a/provisioning/transport/amqp/src/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj b/provisioning/transport/amqp/src/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj index 8a0f1c3774..c7782439a8 100644 --- a/provisioning/transport/amqp/src/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj +++ b/provisioning/transport/amqp/src/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj @@ -20,7 +20,7 @@ - 1.14.1 + 1.15.0 Microsoft Azure IoT Provisioning Device Client AMQP Transport True True diff --git a/provisioning/transport/amqp/src/ProvisioningTransportHandlerAmqp.cs b/provisioning/transport/amqp/src/ProvisioningTransportHandlerAmqp.cs index 675412e509..79bb3064ad 100644 --- a/provisioning/transport/amqp/src/ProvisioningTransportHandlerAmqp.cs +++ b/provisioning/transport/amqp/src/ProvisioningTransportHandlerAmqp.cs @@ -55,6 +55,11 @@ public override async Task RegisterAsync( ProvisioningTransportRegisterMessage message, TimeSpan timeout) { + if (TimeSpan.Zero.Equals(timeout)) + { + throw new OperationCanceledException(); + } + return await RegisterAsync(message, timeout, CancellationToken.None).ConfigureAwait(false); } diff --git a/provisioning/transport/http/src/Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj b/provisioning/transport/http/src/Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj index af4d317ff4..e07f658276 100644 --- a/provisioning/transport/http/src/Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj +++ b/provisioning/transport/http/src/Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj @@ -20,7 +20,7 @@ - 1.13.1 + 1.14.0 Microsoft Azure IoT Provisioning Device Client HTTP Transport True True diff --git a/provisioning/transport/http/src/ProvisioningTransportHandlerHttp.cs b/provisioning/transport/http/src/ProvisioningTransportHandlerHttp.cs index cdbd2bec00..6014d906a5 100644 --- a/provisioning/transport/http/src/ProvisioningTransportHandlerHttp.cs +++ b/provisioning/transport/http/src/ProvisioningTransportHandlerHttp.cs @@ -42,6 +42,11 @@ public override async Task RegisterAsync( ProvisioningTransportRegisterMessage message, TimeSpan timeout) { + if (TimeSpan.Zero.Equals(timeout)) + { + throw new OperationCanceledException(); + } + using var cts = new CancellationTokenSource(timeout); return await RegisterAsync(message, cts.Token).ConfigureAwait(false); } diff --git a/provisioning/transport/mqtt/src/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj b/provisioning/transport/mqtt/src/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj index c252253120..5f2e0d7c6c 100644 --- a/provisioning/transport/mqtt/src/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj +++ b/provisioning/transport/mqtt/src/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj @@ -20,7 +20,7 @@ - 1.15.1 + 1.16.0 Microsoft Azure IoT Provisioning Device Client MQTT Transport True True @@ -97,8 +97,8 @@ - - + + diff --git a/provisioning/transport/mqtt/src/ProvisioningTransportHandlerMqtt.cs b/provisioning/transport/mqtt/src/ProvisioningTransportHandlerMqtt.cs index 99b596d641..51318675cf 100644 --- a/provisioning/transport/mqtt/src/ProvisioningTransportHandlerMqtt.cs +++ b/provisioning/transport/mqtt/src/ProvisioningTransportHandlerMqtt.cs @@ -73,6 +73,11 @@ public override async Task RegisterAsync( ProvisioningTransportRegisterMessage message, TimeSpan timeout) { + if (TimeSpan.Zero.Equals(timeout)) + { + throw new OperationCanceledException(); + } + using var cts = new CancellationTokenSource(timeout); return await RegisterAsync(message, cts.Token).ConfigureAwait(false); } diff --git a/readme.md b/readme.md index d0ca4c0a96..1436873e22 100644 --- a/readme.md +++ b/readme.md @@ -9,6 +9,22 @@ This repository contains the following: - **Microsoft Azure Provisioning device SDK for C#** to provision devices to Azure IoT Hub with .NET. - **Microsoft Azure Provisioning service SDK for C#** to manage your Provisioning service instance from a back-end .NET application. +## Critical Upcoming Change Notice + +All Azure IoT SDK users are advised to be aware of upcoming TLS certificate changes for Azure IoT Hub and Device Provisioning Service +that will impact the SDK's ability to connect to these services. In October 2022, both services will migrate from the current +[Baltimore CyberTrust CA Root](https://baltimore-cybertrust-root.chain-demos.digicert.com/info/index.html) to the +[DigiCert Global G2 CA root](https://global-root-g2.chain-demos.digicert.com/info/index.html). There will be a +transition period beforehand where your IoT devices must have both the Baltimore and Digicert public certificates +installed in their certificate store in order to prevent connectivity issues. + +**Devices with only the Baltimore public certificate installed will lose the ability to connect to Azure IoT hub and Device Provisioning Service in October 2022.** + +To prepare for this change, make sure your device's certificate store has both of these public certificates installed. + +For a more in depth explanation as to why the IoT services are doing this, please see +[this article](https://techcommunity.microsoft.com/t5/internet-of-things/azure-iot-tls-critical-changes-are-almost-here-and-why-you/ba-p/2393169). + ### Build status Due to security considerations, build logs are not publicly available. @@ -147,6 +163,7 @@ This repository contains [provisioning service client SDK](./provisioning/servic - [Set up your development environment](./doc/devbox_setup.md) to prepare your development environment as well as how to run the samples on Linux, Windows or other platforms. - [API reference documentation for .NET](https://docs.microsoft.com/dotnet/api/overview/azure/devices?view=azure-dotnet) - [Get Started with IoT Hub using .NET](https://docs.microsoft.com/azure/iot-hub/iot-hub-csharp-csharp-getstarted) +- [Device connection and messaging reliability](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/device_connection_and_reliability_readme.md) > Device Explorer is no longer supported. A replacement tool can be found [here](https://github.com/Azure/azure-iot-explorer). @@ -189,12 +206,14 @@ Below is a table showing the mapping of the LTS branches to the packages release | Release | Github Branch | LTS Status | LTS Start Date | Maintenance End Date | LTS End Date | | :-------------------------------------------------------------------------------------------: | :-----------: | :--------: | :------------: | :------------------: | :----------: | -| [2021-6-23](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2021-3-18_patch1) | lts_2021_03 | Active | 2020-03-18 | 2022-03-18 | 2024-03-17 | -| [2021-3-18](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2021-3-18) | lts_2021_03 | Active | 2020-03-18 | 2022-03-18 | 2024-03-17 | -| [2020-9-23](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-8-19_patch1) | lts_2020_08 | Active | 2020-08-19 | 2021-08-19 | 2023-08-19 | -| [2020-8-19](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-8-19) | lts_2020_08 | Active | 2020-08-19 | 2021-08-19 | 2023-08-19 | -| [2020-4-03](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-1-31_patch1) | lts_2020_01 | Depreciated | 2020-01-31 | 2021-01-30 | 2023-01-30 | -| [2020-1-31](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-1-31) | lts_2020_01 | Depreciated | 2020-01-31 | 2021-01-30 | 2023-01-30 | +| [2021-8-12](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2021-3-18_patch2) | [lts_2021_03](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2021_03) | Active | 2021-08-12 | 2022-03-18 | 2024-03-17 | +| [2021-8-10](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-8-19_patch2) | [lts_2020_08](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2020_08) | Active | 2021-08-10 | 2021-08-19 | 2023-08-19 | +| [2021-6-23](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2021-3-18_patch1) | [lts_2021_03](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2021_03) | Active | 2020-06-23 | 2022-03-18 | 2024-03-17 | +| [2021-3-18](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2021-3-18) | [lts_2021_03](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2021_03) | Active | 2020-03-18 | 2022-03-18 | 2024-03-17 | +| [2020-9-23](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-8-19_patch1) | [lts_2020_08](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2020_08) | Active | 2020-09-23 | 2021-08-19 | 2023-08-19 | +| [2020-8-19](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-8-19) | [lts_2020_08](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2020_08) | Active | 2020-08-19 | 2021-08-19 | 2023-08-19 | +| [2020-4-3](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-1-31_patch1) | [lts_2020_01](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2020_01) | Deprecated | 2020-04-03 | 2021-01-30 | 2023-01-30 | +| [2020-1-31](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/lts_2020-1-31) | [lts_2020_01](https://github.com/Azure/azure-iot-sdk-csharp/tree/lts_2020_01) | Deprecated | 2020-01-31 | 2021-01-30 | 2023-01-30 | - 1 All scheduled dates are subject to change by the Azure IoT SDK team. @@ -245,4 +264,5 @@ To learn more, review the [privacy statement](https://go.microsoft.com/fwlink/?L [pnp-device-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.DigitalTwin.Client/ [pnp-service-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.DigitalTwin.Service.svg?style=plastic [pnp-service-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.DigitalTwin.Service/ -[pnp-device-dev-guide]: https://docs.microsoft.com/azure/iot-pnp/concepts-developer-guide-device?pivots=programming-language-csharp \ No newline at end of file +[pnp-device-dev-guide]: https://docs.microsoft.com/azure/iot-pnp/concepts-developer-guide-device?pivots=programming-language-csharp + diff --git a/security/tpm/src/Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj b/security/tpm/src/Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj index 712461b0db..fc4af673a6 100644 --- a/security/tpm/src/Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj +++ b/security/tpm/src/Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj @@ -28,7 +28,7 @@ - 1.13.1 + 1.13.2 Microsoft Azure IoT Provisioning Device Security TPM Client True True diff --git a/shared/src/Microsoft.Azure.Devices.Shared.csproj b/shared/src/Microsoft.Azure.Devices.Shared.csproj index 28b2d2afa3..43e44ef18b 100644 --- a/shared/src/Microsoft.Azure.Devices.Shared.csproj +++ b/shared/src/Microsoft.Azure.Devices.Shared.csproj @@ -18,7 +18,7 @@ - 1.28.1 + 1.29.0 Microsoft Azure IoT Devices Shared True True diff --git a/tools/CaptureLogs/iot_startlog.cmd b/tools/CaptureLogs/iot_startlog.cmd deleted file mode 100644 index 0b79c8a8be..0000000000 --- a/tools/CaptureLogs/iot_startlog.cmd +++ /dev/null @@ -1,2 +0,0 @@ -logman create trace IotTrace -o iot.etl -pf iot_providers.txt -logman start IotTrace diff --git a/tools/CaptureLogs/iot_startlog.ps1 b/tools/CaptureLogs/iot_startlog.ps1 new file mode 100644 index 0000000000..61b921c674 --- /dev/null +++ b/tools/CaptureLogs/iot_startlog.ps1 @@ -0,0 +1,23 @@ +param( + [Parameter(Mandatory)] + [string] $TraceName, + + [Parameter(Mandatory)] + [string] $Output, + + [Parameter(Mandatory)] + [string] $ProviderFile +) + +Function StartLogCapture() +{ + $createTrace = "logman create trace $TraceName -o $Output -pf $ProviderFile" + Write-Host "Invoking: $createTrace." + Invoke-Expression $createTrace + + $startTrace = "logman start $TraceName" + Write-Host "Invoking: $startTrace." + Invoke-Expression $startTrace +} + +StartLogCapture \ No newline at end of file diff --git a/tools/CaptureLogs/iot_stoplog.cmd b/tools/CaptureLogs/iot_stoplog.cmd deleted file mode 100644 index a3e91642cc..0000000000 --- a/tools/CaptureLogs/iot_stoplog.cmd +++ /dev/null @@ -1,2 +0,0 @@ -logman stop IotTrace -logman delete IotTrace diff --git a/tools/CaptureLogs/iot_stoplog.ps1 b/tools/CaptureLogs/iot_stoplog.ps1 new file mode 100644 index 0000000000..eb4db74ebd --- /dev/null +++ b/tools/CaptureLogs/iot_stoplog.ps1 @@ -0,0 +1,17 @@ +param( + [Parameter(Mandatory)] + [string] $TraceName +) + +Function StopLogCapture() +{ + $stopTrace = "logman stop $TraceName" + Write-Host "Invoking: $stopTrace." + Invoke-Expression $stopTrace + + $deleteTrace = "logman delete $TraceName" + Write-Host "Invoking: $deleteTrace." + Invoke-Expression $deleteTrace +} + +StopLogCapture \ No newline at end of file diff --git a/tools/CaptureLogs/readme.md b/tools/CaptureLogs/readme.md index 794c2f4d24..6d0efa77ae 100644 --- a/tools/CaptureLogs/readme.md +++ b/tools/CaptureLogs/readme.md @@ -3,19 +3,50 @@ ## Windows On Windows logman or PerfView can be used to collect traces. For more information please see https://github.com/dotnet/runtime/blob/master/docs/workflow/debugging/libraries/windows-instructions.md#traces +We have provided the following convenience scripts for log collection using `logman`. + +1. Launch Powershell with administrator privileges. +2. To start capturing traces, invoke `iot_startlog.ps1`. + 1. Pass in the following required parameters: + 1. `-TraceName` - the name of the event trace data collector. This can be any name that will be used to identity the collector created. + 2. `-Output` - the output log file that will be created. This should be a `.etl` file. + 3. `-ProviderFile` - The file listing multiple Event Trace providers to enable. The file should be a text file containing one provider per line. + The Azure IoT SDK providers file is present [here](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/tools/CaptureLogs/iot_providers.txt). The providers list with their corresponding package details are present [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/master/tools/CaptureLogs#azure-iot-sdk-providers). + + Sample usage: + + To create an event trace data collector called `IotTrace`, using the file `iot_providers.txt` for the list of event providers to be enabled, putting the results in a file `iot.etl` in the same folder from where the command is invoked, type: + ```powersehll + .\iot_startlog.ps1 -Output iot.etl -ProviderFile .\iot_providers.txt -TraceName IotTrace + ``` + +3. To stop capturing traces, invoke `iot_stoplog.ps1`. + 1. Pass in the following required parameter: + 1. `-TraceName` - the name of the event trace data collector. Same as the one used while starting trace capture. + + Sample usage: + ```powersehll + .\iot_stoplog.ps1 -TraceName IotTrace + ``` + ## Linux On Linux and OSX LTTNG and perfcollect can be used to collect traces. For more information please see https://github.com/dotnet/runtime/blob/master/docs/project/linux-performance-tracing.md ## Console logging Logging can be added to console. Note that this method will substantially slow down execution. - 1. Add `e2e\test\Helpers\ConsoleEventListener.cs` to your project. + 1. Add [`e2e\test\helpers\ConsoleEventListener.cs`](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/e2e/test/helpers/ConsoleEventListener.cs) to your project. 2. Instantiate the listener. Add one or more filters (e.g. `Microsoft-Azure-` or `DotNetty-`): -```C# - private readonly ConsoleEventListener _listener = new ConsoleEventListener("Microsoft-Azure-"); +```csharp + private static readonly ConsoleEventListener _listener = new ConsoleEventListener(); ``` - 3. See the `ConsoleEventListener.cs` file to enable colorized logs within Visual Studio Code. +> NOTE: +> 1. `static` fields are optimized for runtime performance and are initialized prior to their first usage. If `_listener` is the only static field initialized in your class, you'll need to provide a static constructor that initializes them when the class is loaded. +> 2. `ConsoleEventListener.cs` logs the following events by default. If you want to log specific event providers, modify the [event filter](https://github.com/Azure/azure-iot-sdk-csharp/blob/4b5e0147f3768761cacaf4913ab6be707425f9da/e2e/test/helpers/ConsoleEventListener.cs#L20) list to include only your desired event providers. +> ```csharp +> private static readonly string[] s_eventFilter = new string[] { "DotNetty-Default", "Microsoft-Azure-Devices", "Azure-Core", "Azure-Identity" }; +> ``` ## Azure IoT SDK providers diff --git a/versions.csv b/versions.csv index a00e605a91..33d5d6b718 100644 --- a/versions.csv +++ b/versions.csv @@ -1,10 +1,10 @@ AssemblyPath, Version -iothub\device\src\Microsoft.Azure.Devices.Client.csproj, 1.37.2 -iothub\service\src\Microsoft.Azure.Devices.csproj, 1.34.0 -shared\src\Microsoft.Azure.Devices.Shared.csproj, 1.28.1 -provisioning\device\src\Microsoft.Azure.Devices.Provisioning.Client.csproj, 1.17.1 -provisioning\transport\amqp\src\Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj, 1.14.1 -provisioning\transport\http\src\Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj, 1.13.1 -provisioning\transport\mqtt\src\Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj, 1.15.1 -security\tpm\src\Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj, 1.13.1 -provisioning\service\src\Microsoft.Azure.Devices.Provisioning.Service.csproj, 1.17.1 +iothub\device\src\Microsoft.Azure.Devices.Client.csproj, 1.38.0 +iothub\service\src\Microsoft.Azure.Devices.csproj, 1.35.0 +shared\src\Microsoft.Azure.Devices.Shared.csproj, 1.29.0 +provisioning\device\src\Microsoft.Azure.Devices.Provisioning.Client.csproj, 1.18.0 +provisioning\transport\amqp\src\Microsoft.Azure.Devices.Provisioning.Transport.Amqp.csproj, 1.15.0 +provisioning\transport\http\src\Microsoft.Azure.Devices.Provisioning.Transport.Http.csproj, 1.14.0 +provisioning\transport\mqtt\src\Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.csproj, 1.16.0 +security\tpm\src\Microsoft.Azure.Devices.Provisioning.Security.Tpm.csproj, 1.13.2 +provisioning\service\src\Microsoft.Azure.Devices.Provisioning.Service.csproj, 1.17.2 diff --git a/vsts/vsts.yaml b/vsts/vsts.yaml index c6415d2d18..e79cf77d10 100644 --- a/vsts/vsts.yaml +++ b/vsts/vsts.yaml @@ -329,7 +329,7 @@ jobs: notifyAlwaysV2: false instanceUrlForTsaV2: 'MSAZURE' projectNameMSAZURE: 'One' - areaPath: 'One\IoT\Developers and Devices\SDKs\Managed' + areaPath: 'One\IoT\Platform and Devices\IoT Devices\SDKs\Managed' iterationPath: 'One\IoT\Backlog' - task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1