diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 86b6ad4217..2b96ff29d2 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 @azabbasi @jamdavi @andykwong-ms diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c7f7c515ae..e6f4ddf2d8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -27,13 +27,13 @@ That is definitely something we want to hear about. Please open an issue on gith ## Contribute documentation -For simple markdown files, we accept documentation pull requests submitted against the `master` branch, if it's about existing SDK features. +For simple markdown files, we accept documentation pull requests submitted against the `main` branch, if it's about existing SDK features. If your PR is about future changes or has changes to the comments in the code itself, we'll treat is as a code change (see the next section). ## Contribute code Our SDKs are open-source and we do accept pull-requests if you feel like taking a stab at fixing the bug and maybe adding your name to our commit history :) Please mention any relevant issue number in the pull request description, and follow the contributing guidelines [below](#contributing-guidelines). -Pull-requests for code are to be submitted against the `master` branch. We will review the request and once approved we will be running it in our gated build system. We try to maintain a high bar for code quality and maintainability, we insist on having tests associated with the code, and if necessary, additions/modifications to the requirement documents. +Pull-requests for code are to be submitted against the `main` branch. We will review the request and once approved we will be running it in our gated build system. We try to maintain a high bar for code quality and maintainability, we insist on having tests associated with the code, and if necessary, additions/modifications to the requirement documents. Also, have you signed the [Contribution License Agreement](https://cla.microsoft.com/) ([CLA](https://cla.microsoft.com/))? A friendly bot will remind you about it when you submit your pull-request. @@ -44,9 +44,9 @@ sure your plans and ours are in sync :) Just open an issue on github and tag it 1. If the change affects the public API, extract the updated public API surface and submit a PR for review. Make sure you get a signoff before you move to Step 2. 2. Post API surface approval, follow the below guidelines for contributing code: - a) Follow the steps [here](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/doc/devbox_setup.md) for setting up your development environment. + a) Follow the steps [here](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/doc/devbox_setup.md) for setting up your development environment. - b) Follow the [C# Coding Style](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/doc/coding-style.md). + b) Follow the [C# Coding Style](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/doc/coding-style.md). c) Unit Tests: We write unit tests for any new function or block of application code that impacts the existing behavior of the code. @@ -73,9 +73,9 @@ sure your plans and ours are in sync :) Just open an issue on github and tag it ``` d) E2E Tests: Any new feature or functionality added must have associated end-to-end tests. - 1. Update/ Add the E2E tests [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/master/e2e/test). - 2. In case environmental setup required for the application is changed, update the pre-requisites [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/master/e2e/test/prerequisites). - 3. Run the E2E test suite and ensure that all the tests pass successfully. You can also test against the [CI script](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/jenkins/windows_csharp.cmd) that is used in our gated build system. + 1. Update/ Add the E2E tests [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/main/e2e/test). + 2. In case environmental setup required for the application is changed, update the pre-requisites [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/main/e2e/test/prerequisites). + 3. Run the E2E test suite and ensure that all the tests pass successfully. You can also test against the [CI script](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/jenkins/windows_csharp.cmd) that is used in our gated build system. e) Samples: Add relevant samples to the [Azure IoT Samples for C# Repo](https://github.com/Azure-Samples/azure-iot-samples-csharp). Make sure to add a supporting readme file demonstrating the steps to run the sample. @@ -100,7 +100,7 @@ sure your plans and ours are in sync :) Just open an issue on github and tag it } } ``` -3. Post completion of all of the above steps, create a PR against `master`. +3. Post completion of all of the above steps, create a PR against `main`. #### Commit Guidelines We have very precise rules over how our git commit messages can be formatted. This leads to more readable messages that are easy to follow when looking through the project history. @@ -130,7 +130,7 @@ If the commit reverts a previous commit, it should begin with `revert:`, followe **Rebase and Squash** -* Its manadatory to squash all your commits per scope (i.e package). It is also important to rebase your commits on master. +* Its manadatory to squash all your commits per scope (i.e package). It is also important to rebase your commits on `main`. * Optionally you can split your commits on the basis of the package you are providing code to. **Type** diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 767cfeaab6..f597f4b4aa 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -24,5 +24,5 @@ Please use your Azure subscription if you need to share any information from you ## Console log of the issue: - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 981ce363a2..cbc7cce44d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -25,7 +25,7 @@ Please follow the instructions and template below to save us time requesting add 4. Include enough information for us to address the bug: - A detailed description. - A [Minimal Complete Reproducible Example](https://stackoverflow.com/help/mcve). This is code we can cut and paste into a readily available sample and run, or a link to a project you've written that we can compile to reproduce the bug. - - Console logs (https://github.com/Azure/azure-iot-sdk-csharp/tree/master/tools/CaptureLogs). + - Console logs (https://github.com/Azure/azure-iot-sdk-csharp/tree/main/tools/CaptureLogs). 5. Delete these instructions before submitting the bug. @@ -51,5 +51,5 @@ Please be as detailed as possible: which feature has a problem, how often does i Please remove any connection string information! ## Console log of the issue -Consider setting the DEBUG environment variable to '*'. This will produce a much more verbose output that will help debugging +Follow the instructions [here](https://github.com/Azure/azure-iot-sdk-csharp/tree/main/tools/CaptureLogs) to capture SDK logs. Don't forget to remove any connection string information! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 857a0a6add..2dfffb1f67 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,10 +9,10 @@ Need support? --> ## Checklist -- [ ] I have read the [contribution guidelines](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/.github/CONTRIBUTING.md). +- [ ] I have read the [contribution guidelines](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/.github/CONTRIBUTING.md). - [ ] I added or modified the existing tests to cover the change (we do not allow our test coverage to go down). -- [ ] This pull-request is submitted against the `master` branch. - +- [ ] This pull-request is submitted against the `main` branch. + ## Description of the changes diff --git a/azureiot.sln b/azureiot.sln index dd21735d2b..9533df05df 100644 --- a/azureiot.sln +++ b/azureiot.sln @@ -72,11 +72,17 @@ 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 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Shared.Tests", "shared\tests\Microsoft.Azure.Devices.Shared.Tests.csproj", "{CEEE435F-32FC-4DE5-8735-90F6AC950A01}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{2368415A-9C09-4F47-9636-FDCA4B85C88C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -184,6 +190,7 @@ Global {275DEE86-1EEA-47C4-A9C5-797DF20EC8A7} = {3AA089A9-A035-439E-BAF6-C3975A334379} {8E25CDE3-992D-4942-8C38-51A0D8E8EB70} = {9C260BF0-1CCA-45A2-AAB8-6419291B8B88} {CEEE435F-32FC-4DE5-8735-90F6AC950A01} = {3AA089A9-A035-439E-BAF6-C3975A334379} + {2368415A-9C09-4F47-9636-FDCA4B85C88C} = {A48437BA-3C5B-431E-9B2F-96C850E9E1A5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AF61665D-340A-494B-9705-571456BDC752} 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 3bc2867160..5cddb9f422 100644 --- a/common/src/service/HttpClientHelper.cs +++ b/common/src/service/HttpClientHelper.cs @@ -38,14 +38,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 @@ -893,7 +893,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/configure_tls_protocol_version_and_ciphers.md b/configure_tls_protocol_version_and_ciphers.md index e19fee5a76..007f862d20 100644 --- a/configure_tls_protocol_version_and_ciphers.md +++ b/configure_tls_protocol_version_and_ciphers.md @@ -24,9 +24,9 @@ For more information, check out this article about [best practices with .NET and Also follow the instructions on how to [enable and disable ciphers]. [.NET Framework 4.5.1 is no longer supported]: https://devblogs.microsoft.com/dotnet/support-ending-for-the-net-framework-4-4-5-and-4-5-1/ -[TLS registry settings]: https://docs.microsoft.com/en-us/windows-server/security/tls/tls-registry-settings -[best practices with .NEt and TLS]: https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls -[enable and disable ciphers]: https://support.microsoft.com/en-us/help/245030/how-to-restrict-the-use-of-certain-cryptographic-algorithms-and-protoc +[TLS registry settings]: https://docs.microsoft.com/windows-server/security/tls/tls-registry-settings +[best practices with .NEt and TLS]: https://docs.microsoft.com/dotnet/framework/network-programming/tls +[enable and disable ciphers]: https://support.microsoft.com/help/245030/how-to-restrict-the-use-of-certain-cryptographic-algorithms-and-protoc ### Linux Instructions diff --git a/device_connection_and_reliability_readme.md b/device_connection_and_reliability_readme.md new file mode 100644 index 0000000000..ff4694bc6a --- /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/main/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/main/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/main/iot-hub/Samples/device/X509DeviceCertWithChainSample/Program.cs#L43) +- TPM based authentication sample - [ProvisioningDeviceClientSample](https://github.com/Azure-Samples/azure-iot-samples-csharp/blob/main/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/main/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/CryptoKeyGenerator.cs b/e2e/test/helpers/CryptoKeyGenerator.cs new file mode 100644 index 0000000000..8c4f9c82e2 --- /dev/null +++ b/e2e/test/helpers/CryptoKeyGenerator.cs @@ -0,0 +1,62 @@ +// 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.Security.Cryptography; + +#if !NET451 + +using System.Linq; + +#endif + +namespace Microsoft.Azure.Devices.E2ETests +{ + /// + /// Utility methods for generating cryptographically secure keys and passwords. + /// + internal static class CryptoKeyGenerator + { +#if NET451 + private const int DefaultPasswordLength = 16; + private const int GuidLength = 16; +#endif + + /// + /// Size of the SHA 512 key. + /// + internal const int Sha512KeySize = 64; + + /// + /// Generate a key with a specified key size. + /// + /// The size of the key. + /// Byte array representing the key. + internal static byte[] GenerateKeyBytes(int keySize) + { +#if NET451 + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = new RNGCryptoServiceProvider(); + cyptoProvider.GetNonZeroBytes(keyBytes); +#else + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = RandomNumberGenerator.Create(); + while (keyBytes.Contains(byte.MinValue)) + { + cyptoProvider.GetBytes(keyBytes); + } +#endif + return keyBytes; + } + + /// + /// Generates a key of the specified size. + /// + /// Desired key size. + /// A generated key. + internal static string GenerateKey(int keySize) + { + return Convert.ToBase64String(GenerateKeyBytes(keySize)); + } + } +} 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 99% rename from e2e/test/Helpers/TestDeviceCallbackHandler.cs rename to e2e/test/helpers/TestDeviceCallbackHandler.cs index a3e2fccced..02d2fbb24f 100644 --- a/e2e/test/Helpers/TestDeviceCallbackHandler.cs +++ b/e2e/test/helpers/TestDeviceCallbackHandler.cs @@ -180,7 +180,7 @@ public async Task SetClientPropertyUpdateCallbackHandlerAsync(string expected string userContext = "myContext"; await _deviceClient - .SubscribeToWritablePropertiesEventAsync( + .SubscribeToWritablePropertyUpdateRequestsAsync( (patch, context) => { _logger.Trace($"{nameof(SetClientPropertyUpdateCallbackHandlerAsync)}: DeviceClient {_testDevice.Id} callback property: WritableProperty: {patch}, {context}"); 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/iothub/FileUploadE2ETests.cs b/e2e/test/iothub/FileUploadE2ETests.cs index d2ed780c50..d461b0788b 100644 --- a/e2e/test/iothub/FileUploadE2ETests.cs +++ b/e2e/test/iothub/FileUploadE2ETests.cs @@ -129,7 +129,7 @@ private async Task UploadFileGranularAsync(Stream source, string filename, Http1 { cert = s_selfSignedCertificate; x509Auth = new DeviceAuthenticationWithX509Certificate(testDevice.Id, cert); - + deviceClient = DeviceClient.Create(testDevice.IoTHubHostName, x509Auth, Client.TransportType.Http1); } else diff --git a/e2e/test/iothub/command/CommandE2ETests.cs b/e2e/test/iothub/command/CommandE2ETests.cs index e1f00fcc26..8be5c30240 100644 --- a/e2e/test/iothub/command/CommandE2ETests.cs +++ b/e2e/test/iothub/command/CommandE2ETests.cs @@ -39,27 +39,23 @@ public class CommandE2ETests : E2EMsTestBase private static readonly TimeSpan s_defaultCommandTimeoutMinutes = TimeSpan.FromMinutes(1); [LoggedTestMethod] - public async Task Command_DeviceReceivesCommandAndResponse_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Command_DeviceReceivesCommandAndResponse(Client.TransportType transportType) { - await SendCommandAndRespondAsync(Client.TransportType.Mqtt_Tcp_Only, SetDeviceReceiveCommandAsync).ConfigureAwait(false); + await SendCommandAndRespondAsync(transportType, SetDeviceReceiveCommandAsync).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Command_DeviceReceivesCommandAndResponse_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Command_DeviceReceivesCommandAndResponseWithComponent(Client.TransportType transportType) { - await SendCommandAndRespondAsync(Client.TransportType.Mqtt_WebSocket_Only, SetDeviceReceiveCommandAsync).ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Command_DeviceReceivesCommandAndResponseWithComponent_Mqtt() - { - await SendCommandAndRespondAsync(Client.TransportType.Mqtt_Tcp_Only, SetDeviceReceiveCommandAsync, withComponent: true).ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Command_DeviceReceivesCommandAndResponseWithComponent_MqttWs() - { - await SendCommandAndRespondAsync(Client.TransportType.Mqtt_WebSocket_Only, SetDeviceReceiveCommandAsync, withComponent: true).ConfigureAwait(false); + await SendCommandAndRespondAsync(transportType, SetDeviceReceiveCommandAsync, withComponent: true).ConfigureAwait(false); } public static async Task DigitalTwinsSendCommandAndVerifyResponseAsync(string deviceId, string componentName, string commandName, MsTestLogger logger) diff --git a/e2e/test/iothub/properties/PropertiesE2ETests.cs b/e2e/test/iothub/properties/PropertiesE2ETests.cs index 7db552aa57..6d68028541 100644 --- a/e2e/test/iothub/properties/PropertiesE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesE2ETests.cs @@ -33,153 +33,113 @@ public class PropertiesE2ETests : E2EMsTestBase }; [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyAndGetsItBack_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceSetsPropertyAndGetsItBack(Client.TransportType transportType) { - await Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyAndGetsItBack_MqttWs() - { - await Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyMapAndGetsItBack_Mqtt() - { - await Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyMapAndGetsItBack_MqttWs() - { - await Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes_Mqtt() - { - await Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes( - Client.TransportType.Mqtt_Tcp_Only, - Guid.NewGuid().ToString()) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes_MqttWs() - { - await Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes( - Client.TransportType.Mqtt_WebSocket_Only, - Guid.NewGuid().ToString()) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEvent_Mqtt() - { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_Tcp_Only, - Guid.NewGuid().ToString()) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEvent_MqttWs() - { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_WebSocket_Only, - Guid.NewGuid().ToString()) - .ConfigureAwait(false); + await Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceSetsPropertyMapAndGetsItBack(Client.TransportType transportType) { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_Tcp_Only, - s_mapOfPropertyValues) - .ConfigureAwait(false); + await Properties_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes(Client.TransportType transportType) { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_WebSocket_Only, - s_mapOfPropertyValues) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEvent(Client.TransportType transportType) { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent(Client.TransportType transportType) { - await Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyAndServiceReceivesIt_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds(Client.TransportType transportType) { - await Properties_DeviceSetsPropertyAndServiceReceivesItAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceSetsPropertyAndServiceReceivesIt_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds(Client.TransportType transportType) { - await Properties_DeviceSetsPropertyAndServiceReceivesItAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingIt_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet(Client.TransportType transportType) { - await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingIt_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceSetsPropertyAndServiceReceivesIt(Client.TransportType transportType) { - await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await Properties_DeviceSetsPropertyAndServiceReceivesItAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ClientHandlesRejectionInvalidPropertyName_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingIt(Client.TransportType transportType) { - await Properties_ClientHandlesRejectionInvalidPropertyNameAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_ClientHandlesRejectionInvalidPropertyName_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_ClientHandlesRejectionInvalidPropertyName_Mqtt(Client.TransportType transportType) { - await Properties_ClientHandlesRejectionInvalidPropertyNameAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await Properties_ClientHandlesRejectionInvalidPropertyNameAsync(transportType).ConfigureAwait(false); } private async Task Properties_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(Client.TransportType transport) @@ -210,7 +170,7 @@ public static async Task Properties_DeviceSetsPropertyAndGetsItBackAsync(Devi // Validate the updated properties from the device-client ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.TryGetValue(propName, out T propFromCollection); + bool isPropertyPresent = clientProperties.ReportedFromClient.TryGetValue(propName, out T propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().BeEquivalentTo(propValue); @@ -245,7 +205,7 @@ private async Task Properties_ServiceSetsWritablePropertyAndDeviceUnsubscribes(C // Set a callback await deviceClient. - SubscribeToWritablePropertiesEventAsync( + SubscribeToWritablePropertyUpdateRequestsAsync( (patch, context) => { Assert.Fail("After having unsubscribed from receiving client property update notifications " + @@ -258,7 +218,7 @@ await deviceClient. // Unsubscribe await deviceClient - .SubscribeToWritablePropertiesEventAsync(null, null) + .SubscribeToWritablePropertyUpdateRequestsAsync(null, null) .ConfigureAwait(false); await RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, propName, propValue) @@ -289,7 +249,7 @@ await Task // Validate the updated properties from the device-client ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.Writable.TryGetValue(propName, out T propValueFromCollection); + bool isPropertyPresent = clientProperties.WritablePropertyRequests.TryGetValue(propName, out T propValueFromCollection); isPropertyPresent.Should().BeTrue(); propValueFromCollection.Should().BeEquivalentTo(propValue); @@ -301,10 +261,86 @@ await Task string serializedActualPropertyValue = JsonConvert.SerializeObject(actualProp); serializedActualPropertyValue.Should().Be(JsonConvert.SerializeObject(propValue)); - await deviceClient.SubscribeToWritablePropertiesEventAsync(null, null).ConfigureAwait(false); + await deviceClient.SubscribeToWritablePropertyUpdateRequestsAsync(null, null).ConfigureAwait(false); await deviceClient.CloseAsync().ConfigureAwait(false); } + private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(Client.TransportType transport, T propValue) + { + using var cts = new CancellationTokenSource(s_maxWaitTimeForCallback); + string propName = Guid.NewGuid().ToString(); + + Logger.Trace($"{nameof(Properties_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync)}: name={propName}, value={propValue}"); + + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + + using var writablePropertyCallbackSemaphore = new SemaphoreSlim(0, 1); + await deviceClient + .SubscribeToWritablePropertyUpdateRequestsAsync( + async (writableProperties, userContext) => + { + try + { + bool isPropertyPresent = writableProperties.TryGetValue(propName, out T propertyFromCollection); + + isPropertyPresent.Should().BeTrue(); + propertyFromCollection.Should().BeEquivalentTo(propValue); + userContext.Should().BeNull(); + + var writablePropertyAcks = new ClientPropertyCollection(); + foreach (KeyValuePair writableProperty in writableProperties) + { + if (writableProperty.Value is WritableClientProperty writableClientProperty) + { + writablePropertyAcks.Add(writableProperty.Key, writableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK)); + } + } + + await deviceClient.UpdateClientPropertiesAsync(writablePropertyAcks).ConfigureAwait(false); + } + finally + { + writablePropertyCallbackSemaphore.Release(); + } + }, + null, + cts.Token) + .ConfigureAwait(false); + + using var testDeviceCallbackHandler = new TestDeviceCallbackHandler(deviceClient, testDevice, Logger); + + await Task + .WhenAll( + RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, propName, propValue), + writablePropertyCallbackSemaphore.WaitAsync(cts.Token)) + .ConfigureAwait(false); + + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + + // Validate that the writable property update request was received + bool isWritablePropertyRequestPresent = clientProperties.WritablePropertyRequests.TryGetValue(propName, out T writablePropertyRequest); + isWritablePropertyRequestPresent.Should().BeTrue(); + writablePropertyRequest.Should().BeEquivalentTo(propValue); + + // Validate that the writable property update request was acknowledged + + bool isWritablePropertyAckPresent = clientProperties.ReportedFromClient.TryGetValue(propName, out IWritablePropertyResponse writablePropertyAck); + isWritablePropertyAckPresent.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAck.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentSpecific = clientProperties.ReportedFromClient.TryGetValue(propName, out NewtonsoftJsonWritablePropertyResponse writablePropertyAckNewtonSoft); + isWritablePropertyAckPresentSpecific.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAckNewtonSoft.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentAsValue = clientProperties.ReportedFromClient.TryGetValue(propName, out T writablePropertyAckValue); + isWritablePropertyAckPresentAsValue.Should().BeTrue(); + writablePropertyAckValue.Should().BeEquivalentTo(propValue); + } + private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(Client.TransportType transport) { string propName = Guid.NewGuid().ToString(); @@ -319,7 +355,7 @@ private async Task Properties_ServiceSetsWritablePropertyAndDeviceReceivesItOnNe await registryManager.UpdateTwinAsync(testDevice.Id, twinPatch, "*").ConfigureAwait(false); ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.Writable.TryGetValue(propName, out string propFromCollection); + bool isPropertyPresent = clientProperties.WritablePropertyRequests.TryGetValue(propName, out string propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().Be(propValue); diff --git a/e2e/test/iothub/properties/PropertiesFaultInjectionTests.cs b/e2e/test/iothub/properties/PropertiesFaultInjectionTests.cs index 3c71d594f6..720bc228dd 100644 --- a/e2e/test/iothub/properties/PropertiesFaultInjectionTests.cs +++ b/e2e/test/iothub/properties/PropertiesFaultInjectionTests.cs @@ -22,10 +22,14 @@ public class PropertiesFaultInjectionTests : E2EMsTestBase private static readonly string s_devicePrefix = $"E2E_{nameof(PropertiesFaultInjectionTests)}_"; [LoggedTestMethod] - public async Task Properties_DeviceUpdateClientPropertiesTcpConnRecovery_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceUpdateClientPropertiesTcpConnRecovery(Client.TransportType transportType) { await Properties_DeviceUpdateClientPropertiesRecoveryAsync( - Client.TransportType.Mqtt_Tcp_Only, + transportType, FaultInjection.FaultType_Tcp, FaultInjection.FaultCloseReason_Boom, FaultInjection.DefaultFaultDelay) @@ -33,21 +37,12 @@ await Properties_DeviceUpdateClientPropertiesRecoveryAsync( } [LoggedTestMethod] - public async Task Properties_DeviceUpdateClientPropertiesTcpConnRecovery_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + public async Task Properties_DeviceUpdateClientPropertiesGracefulShutdownRecovery_Mqtt(Client.TransportType transportType) { await Properties_DeviceUpdateClientPropertiesRecoveryAsync( - Client.TransportType.Mqtt_WebSocket_Only, - FaultInjection.FaultType_Tcp, - FaultInjection.FaultCloseReason_Boom, - FaultInjection.DefaultFaultDelay) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceUpdateClientPropertiesGracefulShutdownRecovery_Mqtt() - { - await Properties_DeviceUpdateClientPropertiesRecoveryAsync( - Client.TransportType.Mqtt_Tcp_Only, + transportType, FaultInjection.FaultType_GracefulShutdownMqtt, FaultInjection.FaultCloseReason_Bye, FaultInjection.DefaultFaultDelay) @@ -55,21 +50,27 @@ await Properties_DeviceUpdateClientPropertiesRecoveryAsync( } [LoggedTestMethod] - public async Task Properties_DeviceUpdateClientPropertiesGracefulShutdownRecovery_MqttWs() + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceUpdateClientPropertiesGracefulShutdownRecovery_Amqp(Client.TransportType transportType) { await Properties_DeviceUpdateClientPropertiesRecoveryAsync( - Client.TransportType.Mqtt_WebSocket_Only, - FaultInjection.FaultType_GracefulShutdownMqtt, + transportType, + FaultInjection.FaultType_GracefulShutdownAmqp, FaultInjection.FaultCloseReason_Bye, FaultInjection.DefaultFaultDelay) .ConfigureAwait(false); } [LoggedTestMethod] - public async Task Properties_DeviceReceivePropertyUpdateTcpConnRecovery_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceReceivePropertyUpdateTcpConnRecovery(Client.TransportType transportType) { await Properties_DeviceReceivePropertyUpdateRecoveryAsync( - Client.TransportType.Mqtt_Tcp_Only, + transportType, FaultInjection.FaultType_Tcp, FaultInjection.FaultCloseReason_Boom, FaultInjection.DefaultFaultDelay) @@ -77,21 +78,12 @@ await Properties_DeviceReceivePropertyUpdateRecoveryAsync( } [LoggedTestMethod] - public async Task Properties_DeviceReceivePropertyUpdateTcpConnRecovery_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + public async Task Properties_DeviceReceivePropertyUpdateGracefulShutdownRecovery_Mqtt(Client.TransportType transportType) { await Properties_DeviceReceivePropertyUpdateRecoveryAsync( - Client.TransportType.Mqtt_WebSocket_Only, - FaultInjection.FaultType_Tcp, - FaultInjection.FaultCloseReason_Boom, - FaultInjection.DefaultFaultDelay) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceReceivePropertyUpdateGracefulShutdownRecovery_Mqtt() - { - await Properties_DeviceReceivePropertyUpdateRecoveryAsync( - Client.TransportType.Mqtt_Tcp_Only, + transportType, FaultInjection.FaultType_GracefulShutdownMqtt, FaultInjection.FaultCloseReason_Bye, FaultInjection.DefaultFaultDelay) @@ -99,11 +91,13 @@ await Properties_DeviceReceivePropertyUpdateRecoveryAsync( } [LoggedTestMethod] - public async Task Properties_DeviceReceivePropertyUpdateGracefulShutdownRecovery_MqttWs() + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceReceivePropertyUpdateGracefulShutdownRecovery_Amqp(Client.TransportType transportType) { await Properties_DeviceReceivePropertyUpdateRecoveryAsync( - Client.TransportType.Mqtt_WebSocket_Only, - FaultInjection.FaultType_GracefulShutdownMqtt, + transportType, + FaultInjection.FaultType_GracefulShutdownAmqp, FaultInjection.FaultCloseReason_Bye, FaultInjection.DefaultFaultDelay) .ConfigureAwait(false); @@ -129,7 +123,7 @@ static async Task TestOperationAsync(DeviceClient deviceClient, TestDevice testD ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); clientProperties.Should().NotBeNull(); - bool isPropertyPresent = clientProperties.TryGetValue(propName, out string propFromCollection); + bool isPropertyPresent = clientProperties.ReportedFromClient.TryGetValue(propName, out string propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().Be(propValue); } diff --git a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs index 96040512d5..31db30de03 100644 --- a/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs +++ b/e2e/test/iothub/properties/PropertiesWithComponentsE2ETests.cs @@ -35,169 +35,131 @@ public class PropertiesWithComponentsE2ETests : E2EMsTestBase }; [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBack_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBack(Client.TransportType transportType) { - await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBack_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBack(Client.TransportType transportType) { - await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBack_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes(Client.TransportType transportType) { - await PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBack_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceUnsubscribes(Client.TransportType transportType) { - await PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEvent(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes( - Client.TransportType.Mqtt_Tcp_Only, - Guid.NewGuid()) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceUnsubscribes( - Client.TransportType.Mqtt_WebSocket_Only, - Guid.NewGuid()) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEvent_Mqtt() - { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_Tcp_Only, - Guid.NewGuid()) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEvent_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_WebSocket_Only, - Guid.NewGuid()) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesIt(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_Tcp_Only, - s_mapOfPropertyValues) - .ConfigureAwait(false); + await PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesItAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEvent_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingIt(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAsync( - Client.TransportType.Mqtt_WebSocket_Only, - s_mapOfPropertyValues) - .ConfigureAwait(false); + await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyName(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyNameAsync(transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGet_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndResponds(Client.TransportType transportType) { - await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(transportType, Guid.NewGuid().ToString()).ConfigureAwait(false); } [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesIt_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task PropertiesWithComponents_ServiceSetsWritablePropertyMapAndDeviceReceivesEventAndResponds(Client.TransportType transportType) { - await PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesItAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); + await PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(transportType, s_mapOfPropertyValues).ConfigureAwait(false); } - [LoggedTestMethod] - public async Task PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesIt_MqttWs() - { - await PropertiesWithComponents_DeviceSetsPropertyAndServiceReceivesItAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync_Mqtt() - { - await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync_MqttWs() - { - await Properties_DeviceSendsNullValueForPropertyResultsServiceRemovingItAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyName_Mqtt() - { - await PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyNameAsync( - Client.TransportType.Mqtt_Tcp_Only) - .ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyName_MqttWs() - { - await PropertiesWithComponents_ClientHandlesRejectionInvalidPropertyNameAsync( - Client.TransportType.Mqtt_WebSocket_Only) - .ConfigureAwait(false); - } - - private async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(Client.TransportType transport) + private async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackSingleDeviceAsync(Client.TransportType transport, T propValue) { TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); - await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, Guid.NewGuid().ToString(), Logger).ConfigureAwait(false); - } - - private async Task PropertiesWithComponents_DeviceSetsPropertyMapAndGetsItBackSingleDeviceAsync(Client.TransportType transport) - { - TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); - using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); - - await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, s_mapOfPropertyValues, Logger).ConfigureAwait(false); + await PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(deviceClient, testDevice.Id, propValue, Logger).ConfigureAwait(false); } public static async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBackAsync(DeviceClient deviceClient, string deviceId, T propValue, MsTestLogger logger) @@ -212,7 +174,7 @@ public static async Task PropertiesWithComponents_DeviceSetsPropertyAndGetsItBac // Validate the updated properties from the device-client ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.TryGetValue(ComponentName, propName, out T propFromCollection); + bool isPropertyPresent = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out T propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().BeEquivalentTo(propValue); @@ -252,7 +214,7 @@ private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDevice // Set a callback await deviceClient. - SubscribeToWritablePropertiesEventAsync( + SubscribeToWritablePropertyUpdateRequestsAsync( (patch, context) => { Assert.Fail("After having unsubscribed from receiving client property update notifications " + @@ -265,7 +227,7 @@ await deviceClient. // Unsubscribe await deviceClient - .SubscribeToWritablePropertiesEventAsync(null, null) + .SubscribeToWritablePropertyUpdateRequestsAsync(null, null) .ConfigureAwait(false); await RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, ComponentName, propName, propValue) @@ -296,7 +258,7 @@ await Task // Validate the updated properties from the device-client ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.Writable.TryGetValue(ComponentName, propName, out T propFromCollection); + bool isPropertyPresent = clientProperties.WritablePropertyRequests.TryGetValue(ComponentName, propName, out T propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().BeEquivalentTo(propValue); @@ -308,10 +270,92 @@ await Task string serializedActualPropertyValue = JsonConvert.SerializeObject(actualProp); serializedActualPropertyValue.Should().Be(JsonConvert.SerializeObject(propValue)); - await deviceClient.SubscribeToWritablePropertiesEventAsync(null, null).ConfigureAwait(false); + await deviceClient.SubscribeToWritablePropertyUpdateRequestsAsync(null, null).ConfigureAwait(false); await deviceClient.CloseAsync().ConfigureAwait(false); } + private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync(Client.TransportType transport, T propValue) + { + using var cts = new CancellationTokenSource(s_maxWaitTimeForCallback); + string propName = Guid.NewGuid().ToString(); + + Logger.Trace($"{nameof(PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesEventAndRespondsAsync)}: name={propName}, value={propValue}"); + + TestDevice testDevice = await TestDevice.GetTestDeviceAsync(Logger, _devicePrefix).ConfigureAwait(false); + using var deviceClient = DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, transport); + + using var writablePropertyCallbackSemaphore = new SemaphoreSlim(0, 1); + await deviceClient + .SubscribeToWritablePropertyUpdateRequestsAsync( + async (writableProperties, userContext) => + { + try + { + bool isPropertyPresent = writableProperties.TryGetValue(ComponentName, propName, out T propertyFromCollection); + + isPropertyPresent.Should().BeTrue(); + propertyFromCollection.Should().BeEquivalentTo(propValue); + userContext.Should().BeNull(); + + var writablePropertyAcks = new ClientPropertyCollection(); + foreach (KeyValuePair writableProperty in writableProperties) + { + if (writableProperty.Value is IDictionary componentProperties) + { + foreach (KeyValuePair componentProperty in componentProperties) + { + if (componentProperty.Value is WritableClientProperty writableClientProperty) + { + writablePropertyAcks.AddComponentProperty(writableProperty.Key, componentProperty.Key, writableClientProperty.AcknowledgeWith(CommonClientResponseCodes.OK)); + } + } + } + } + + await deviceClient.UpdateClientPropertiesAsync(writablePropertyAcks).ConfigureAwait(false); + } + finally + { + writablePropertyCallbackSemaphore.Release(); + } + }, + null, + cts.Token) + .ConfigureAwait(false); + + using var testDeviceCallbackHandler = new TestDeviceCallbackHandler(deviceClient, testDevice, Logger); + + await Task + .WhenAll( + RegistryManagerUpdateWritablePropertyAsync(testDevice.Id, ComponentName, propName, propValue), + writablePropertyCallbackSemaphore.WaitAsync(cts.Token)) + .ConfigureAwait(false); + + // Validate the updated properties from the device-client + ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); + + // Validate that the writable property update request was received + bool isWritablePropertyRequestPresent = clientProperties.WritablePropertyRequests.TryGetValue(ComponentName, propName, out T writablePropertyRequest); + isWritablePropertyRequestPresent.Should().BeTrue(); + writablePropertyRequest.Should().BeEquivalentTo(propValue); + + // Validate that the writable property update request was acknowledged + + bool isWritablePropertyAckPresent = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out IWritablePropertyResponse writablePropertyAck); + isWritablePropertyAckPresent.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAck.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentSpecific = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out NewtonsoftJsonWritablePropertyResponse writablePropertyAckNewtonSoft); + isWritablePropertyAckPresentSpecific.Should().BeTrue(); + // TryGetValue doesn't have nested deserialization, so we'll have to deserialize the retrieved value + deviceClient.PayloadConvention.PayloadSerializer.ConvertFromObject(writablePropertyAckNewtonSoft.Value).Should().BeEquivalentTo(propValue); + + bool isWritablePropertyAckPresentAsValue = clientProperties.ReportedFromClient.TryGetValue(ComponentName, propName, out T writablePropertyAckValue); + isWritablePropertyAckPresentAsValue.Should().BeTrue(); + writablePropertyAckValue.Should().BeEquivalentTo(propValue); + } + private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDeviceReceivesItOnNextGetAsync(Client.TransportType transport) { string propName = Guid.NewGuid().ToString(); @@ -331,7 +375,7 @@ private async Task PropertiesWithComponents_ServiceSetsWritablePropertyAndDevice await registryManager.UpdateTwinAsync(testDevice.Id, twinPatch, "*").ConfigureAwait(false); ClientProperties clientProperties = await deviceClient.GetClientPropertiesAsync().ConfigureAwait(false); - bool isPropertyPresent = clientProperties.Writable.TryGetValue(ComponentName, propName, out string propFromCollection); + bool isPropertyPresent = clientProperties.WritablePropertyRequests.TryGetValue(ComponentName, propName, out string propFromCollection); isPropertyPresent.Should().BeTrue(); propFromCollection.Should().Be(propValue); diff --git a/e2e/test/iothub/service/RegistryManagerE2ETests.cs b/e2e/test/iothub/service/RegistryManagerE2ETests.cs index 0e4c681fcb..c8a1550281 100644 --- a/e2e/test/iothub/service/RegistryManagerE2ETests.cs +++ b/e2e/test/iothub/service/RegistryManagerE2ETests.cs @@ -121,7 +121,10 @@ public async Task RegistryManager_BulkLifecycle() var devices = new List(); for (int i = 0; i < bulkCount; i++) { - devices.Add(new Device(_devicePrefix + Guid.NewGuid())); + var device = new Device(_devicePrefix + Guid.NewGuid()); + device.Scope = "someScope" + Guid.NewGuid(); + device.ParentScopes.Add("someParentScope" + Guid.NewGuid()); + devices.Add(device); } using RegistryManager registryManager = RegistryManager.CreateFromConnectionString(TestConfiguration.IoTHub.ConnectionString); @@ -133,7 +136,11 @@ public async Task RegistryManager_BulkLifecycle() foreach (Device device in devices) { // After a bulk add, every device should be able to be retrieved - Assert.IsNotNull(await registryManager.GetDeviceAsync(device.Id).ConfigureAwait(false)); + Device retrievedDevice = await registryManager.GetDeviceAsync(device.Id).ConfigureAwait(false); + Assert.IsNotNull(retrievedDevice.Id); + Assert.AreEqual(device.Scope, retrievedDevice.Scope); + Assert.AreEqual(1, retrievedDevice.ParentScopes.Count); + Assert.AreEqual(device.ParentScopes.ElementAt(0), retrievedDevice.ParentScopes.ElementAt(0)); } var twins = new List(); diff --git a/e2e/test/iothub/telemetry/TelemetrySendE2ETests.cs b/e2e/test/iothub/telemetry/TelemetrySendE2ETests.cs index 5eed7aab2c..fac19320d7 100644 --- a/e2e/test/iothub/telemetry/TelemetrySendE2ETests.cs +++ b/e2e/test/iothub/telemetry/TelemetrySendE2ETests.cs @@ -19,27 +19,23 @@ public partial class TelemetrySendE2ETests : E2EMsTestBase private readonly string ModulePrefix = $"{nameof(TelemetrySendE2ETests)}_"; [LoggedTestMethod] - public async Task Telemetry_DeviceSendSingleTelemetry_Mqtt() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Telemetry_DeviceSendSingleTelemetry(Client.TransportType transportType) { - await SendTelemetryAsync(TestDeviceType.Sasl, Client.TransportType.Mqtt_Tcp_Only).ConfigureAwait(false); + await SendTelemetryAsync(TestDeviceType.Sasl, transportType).ConfigureAwait(false); } [LoggedTestMethod] - public async Task Telemetry_DeviceSendSingleTelemetry_MqttWs() + [DataRow(Client.TransportType.Mqtt_Tcp_Only)] + [DataRow(Client.TransportType.Mqtt_WebSocket_Only)] + [DataRow(Client.TransportType.Amqp_Tcp_Only)] + [DataRow(Client.TransportType.Amqp_WebSocket_Only)] + public async Task Telemetry_DeviceSendSingleTelemetryWithComponent(Client.TransportType transportType) { - await SendTelemetryAsync(TestDeviceType.Sasl, Client.TransportType.Mqtt_WebSocket_Only).ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Telemetry_DeviceSendSingleTelemetryWithComponent_Mqtt() - { - await SendTelemetryWithComponentAsync(TestDeviceType.Sasl, Client.TransportType.Mqtt_Tcp_Only).ConfigureAwait(false); - } - - [LoggedTestMethod] - public async Task Telemetry_DeviceSendSingleTelemetryWithComponent_MqttWs() - { - await SendTelemetryWithComponentAsync(TestDeviceType.Sasl, Client.TransportType.Mqtt_WebSocket_Only).ConfigureAwait(false); + await SendTelemetryWithComponentAsync(TestDeviceType.Sasl, transportType).ConfigureAwait(false); } private async Task SendTelemetryAsync(TestDeviceType type, Client.TransportType transport) @@ -84,7 +80,7 @@ public static async Task SendSingleMessageWithComponentAsync(DeviceClient device } } - public static (Client.TelemetryMessage message, string p1Value) ComposeTelemetryMessageWithComponent(MsTestLogger logger) + public static (TelemetryMessage message, string p1Value) ComposeTelemetryMessageWithComponent(MsTestLogger logger) { string messageId = Guid.NewGuid().ToString(); string p1Value = Guid.NewGuid().ToString(); @@ -106,7 +102,7 @@ public static (Client.TelemetryMessage message, string p1Value) ComposeTelemetry return (message, p1Value); } - public static (Client.TelemetryMessage message, string p1Value) ComposeTelemetryMessage(MsTestLogger logger) + public static (TelemetryMessage message, string p1Value) ComposeTelemetryMessage(MsTestLogger logger) { string messageId = Guid.NewGuid().ToString(); string p1Value = Guid.NewGuid().ToString(); @@ -125,6 +121,5 @@ public static (Client.TelemetryMessage message, string p1Value) ComposeTelemetry return (message, p1Value); } - } } diff --git a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 index 65e1dd9f15..f9eed88cd5 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,14 +652,21 @@ 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 az keyvault secret set --vault-name $keyVaultName --name "DPS-IDSCOPE" --value $dpsIdScope --output none az keyvault secret set --vault-name $keyVaultName --name "PROVISIONING-CONNECTION-STRING" --value $dpsConnectionString --output none az keyvault secret set --vault-name $keyVaultName --name "CUSTOM-ALLOCATION-POLICY-WEBHOOK" --value $customAllocationPolicyWebhook --output none -az keyvault secret set --vault-name $keyVaultName --name "DPS-GLOBALDEVICEENDPOINT" --value "global.azure-devices-provisioning.net" --output none + +$dpsEndpoint = "global.azure-devices-provisioning.net" +if ($Region.EndsWith('euap', 'CurrentCultureIgnoreCase')) +{ + $dpsEndpoint = "global-canary.azure-devices-provisioning.net" +} +az keyvault secret set --vault-name $keyVaultName --name "DPS-GLOBALDEVICEENDPOINT" --value $dpsEndpoint --output none + az keyvault secret set --vault-name $keyVaultName --name "DPS-X509-PFX-CERTIFICATE-PASSWORD" --value $dpsX509PfxCertificatePassword --output none az keyvault secret set --vault-name $keyVaultName --name "IOTHUB-X509-PFX-CERTIFICATE" --value $iothubX509PfxCertificate --output none az keyvault secret set --vault-name $keyVaultName --name "DPS-INDIVIDUALX509-PFX-CERTIFICATE" --value $dpsIndividualX509PfxCertificate --output none @@ -616,7 +688,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 +703,18 @@ 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 "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 +742,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..680d898a5e 100644 --- a/e2e/test/prerequisites/readme.md +++ b/e2e/test/prerequisites/readme.md @@ -1,11 +1,11 @@ # Azure IoT C# End-to-end test prerequisites -The E2E tests require some Azure resources to be set up and configured. Running the [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) powershell script is a convenient way of getting all the resources setup with the required configuration. +The E2E tests require some Azure resources to be set up and configured. Running the [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) powershell script is a convenient way of getting all the resources setup with the required configuration. -Note: The [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) script will setup all the resources necessary to run the full test suite. Ensure to delete these resources when not required as they will cost money. If you want to specifically create some resources, you can take a look at the script for help. +Note: The [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) script will setup all the resources necessary to run the full test suite. Ensure to delete these resources when not required as they will cost money. If you want to specifically create some resources, you can take a look at the script for help. -- Navigate to [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) +- Navigate to [e2eTestsSetup.ps1](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1) - Open powershell in Administrator mode and run the following command by replacing the variables in brackets with your own preferred values. @@ -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 c0ac148b0b..5f56f48c22 100644 --- a/e2e/test/provisioning/ProvisioningE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningE2ETests.cs @@ -473,9 +473,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/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs index 72ee57237b..3757ae6825 100644 --- a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Tracing; using System.Net; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Common; using Microsoft.Azure.Devices.Provisioning.Security.Samples; using Microsoft.Azure.Devices.Provisioning.Service; using Microsoft.Azure.Devices.Shared; diff --git a/iothub/device/devdoc/Convention-based operations.md b/iothub/device/devdoc/Convention-based operations.md index 1ffe4c3b6d..49216ecc89 100644 --- a/iothub/device/devdoc/Convention-based operations.md +++ b/iothub/device/devdoc/Convention-based operations.md @@ -79,7 +79,6 @@ public abstract class PayloadCollection : IEnumerable, IEnumerable> GetEnumerator(); public virtual byte[] GetPayloadObjectBytes(); public virtual string GetSerializedString(); - protected void SetCollection(PayloadCollection payloadCollection); IEnumerator System.Collections.IEnumerable.GetEnumerator(); public bool TryGetValue(string key, out T value); } @@ -127,32 +126,39 @@ public Task UpdateClientPropertiesAsync(ClientPr /// The global call back to handle all writable property updates. /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. -public Task SubscribeToWritablePropertiesEventAsync(Func callback, object userContext, CancellationToken cancellationToken = default); +public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func callback, object userContext, CancellationToken cancellationToken = default); ``` #### All related types ```csharp -public class ClientProperties : ClientPropertyCollection { +public class ClientProperties { public ClientProperties(); - public ClientPropertyCollection Writable { get; private set; } + public ClientPropertyCollection ReportedFromClient { get; private set; } + public ClientPropertyCollection WritablePropertyRequests { get; private set; } } public class ClientPropertyCollection : PayloadCollection { public ClientPropertyCollection(); public long Version { get; protected set; } - public void Add(IDictionary properties); public void AddComponentProperties(string componentName, IDictionary properties); public void AddComponentProperty(string componentName, string propertyName, object propertyValue); - public void AddOrUpdate(IDictionary properties); public void AddOrUpdateComponentProperties(string componentName, IDictionary properties); public void AddOrUpdateComponentProperty(string componentName, string propertyName, object propertyValue); + public void AddOrUpdateRootProperties(IDictionary properties); public void AddOrUpdateRootProperty(string propertyName, object propertyValue); + public void AddRootProperties(IDictionary properties); public void AddRootProperty(string propertyName, object propertyValue); public bool Contains(string componentName, string propertyName); public virtual bool TryGetValue(string componentName, string propertyName, out T propertyValue); } +public class WritableClientProperty { + public object Value { get; internal set; } + public long Version { get; internal set; } + public IWritablePropertyResponse AcknowledgeWith(int statusCode, string description = null); +} + public interface IWritablePropertyResponse { int AckCode { get; set; } string AckDescription { get; set; } @@ -169,7 +175,6 @@ public sealed class NewtonsoftJsonWritablePropertyResponse : IWritablePropertyRe } public class ClientPropertiesUpdateResponse { - public ClientPropertiesUpdateResponse(); public string RequestId { get; internal set; } public long Version { get; internal set; } } diff --git a/iothub/device/devdoc/architecture.md b/iothub/device/devdoc/architecture.md index 4c903187e9..07d988d7e0 100644 --- a/iothub/device/devdoc/architecture.md +++ b/iothub/device/devdoc/architecture.md @@ -138,7 +138,7 @@ Immediate cancellation is achieved by using `Dispose()` which closes and dispose | `(ConnectionStatus.Disabled, ConnectionStatusChangeReason.Client_Close)` | Application disposed the client. | | `(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Communication_Error)` | If no callback subscriptions exist, the client will not automatically connect. A future operation will attempt to reconnect the client. | | `(ConnectionStatus.Disconnected_Retrying, ConnectionStatusChangeReason.Communication_Error)` | If any callback subscriptions exist (methods, twin, events) and connectivity is lost, the client will try to reconnect. | -| `(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Retry_Expired)` | Retry timeout. The `RetryDelegatingHandler` will attempt to recover links for a duration of `OperationTimeoutInMilliseconds` (default 4 minutes) according to [this retry policy](https://github.com/Azure/azure-iot-sdk-csharp/blob/master/iothub/device/devdoc/requirements/retrypolicy.md). | +| `(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Retry_Expired)` | Retry timeout. The `RetryDelegatingHandler` will attempt to recover links for a duration of `OperationTimeoutInMilliseconds` (default 4 minutes) according to [this retry policy](https://github.com/Azure/azure-iot-sdk-csharp/blob/main/iothub/device/devdoc/requirements/retrypolicy.md). | | `(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Bad_Credential)` | UnauthorizedException during Retry. | | `(ConnectionStatus.Disconnected, ConnectionStatusChangeReason.Device_Disabled)` | DeviceDisabledException during Retry. | 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/AuthenticationWithTokenRefresh.cs b/iothub/device/src/AuthenticationWithTokenRefresh.cs index 8c221d23cc..a682868796 100644 --- a/iothub/device/src/AuthenticationWithTokenRefresh.cs +++ b/iothub/device/src/AuthenticationWithTokenRefresh.cs @@ -100,7 +100,7 @@ public async Task GetTokenAsync(string iotHub) { if (_isDisposed) { - throw new ObjectDisposedException("The authentication method instance has already been disposed, so this client is no longer usable. " + + throw new ObjectDisposedException(GetType().Name, "The authentication method instance has already been disposed, so this client is no longer usable. " + "Please close and dispose your current client instance. To continue carrying out operations from your device/ module, " + "create a new authentication method instance and use it for reinitializing your client."); } diff --git a/iothub/device/src/ClientProperties.cs b/iothub/device/src/ClientProperties.cs index 08520d5792..31c8fe4966 100644 --- a/iothub/device/src/ClientProperties.cs +++ b/iothub/device/src/ClientProperties.cs @@ -8,9 +8,9 @@ namespace Microsoft.Azure.Devices.Client /// /// /// The class is not meant to be constructed by customer code. - /// It is intended to be returned fully populated from the client method . + /// It is intended to be returned fully populated from the internal client method . /// - public class ClientProperties : ClientPropertyCollection + public class ClientProperties { /// /// Initializes a new instance of . @@ -19,27 +19,36 @@ public class ClientProperties : ClientPropertyCollection /// public ClientProperties() { - Writable = new ClientPropertyCollection(); + WritablePropertyRequests = new ClientPropertyCollection(); + ReportedFromClient = new ClientPropertyCollection(); } /// /// Initializes a new instance of with the specified collections. /// - /// A collection of writable properties returned from IoT Hub. - /// A collection of read-only properties returned from IoT Hub. - internal ClientProperties(ClientPropertyCollection requestedPropertyCollection, ClientPropertyCollection readOnlyPropertyCollection) + /// A collection of writable property requests returned from IoT Hub. + /// A collection of client reported properties returned from IoT Hub. + internal ClientProperties(ClientPropertyCollection writablePropertyRequestCollection, ClientPropertyCollection clientReportedPropertyCollection) { - SetCollection(readOnlyPropertyCollection); - Version = readOnlyPropertyCollection.Version; - Writable = requestedPropertyCollection; + WritablePropertyRequests = writablePropertyRequestCollection; + ReportedFromClient = clientReportedPropertyCollection; } /// - /// The collection of writable properties. + /// The collection of writable property requests received from service. /// /// - /// See the Writable properties documentation for more information. + /// See the Writable properties documentation for more information. /// - public ClientPropertyCollection Writable { get; private set; } + public ClientPropertyCollection WritablePropertyRequests { get; private set; } + + /// + /// The collection of properties reported by the client. + /// + /// + /// Client reported properties can either be Read-only properties + /// or they can be Writable properties. + /// + public ClientPropertyCollection ReportedFromClient { get; private set; } } } diff --git a/iothub/device/src/ClientPropertiesAsDictionary.cs b/iothub/device/src/ClientPropertiesAsDictionary.cs new file mode 100644 index 0000000000..e272b557ab --- /dev/null +++ b/iothub/device/src/ClientPropertiesAsDictionary.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Client +{ + internal class ClientPropertiesAsDictionary + { + [JsonProperty(PropertyName = "desired", DefaultValueHandling = DefaultValueHandling.Ignore)] + internal IDictionary Desired { get; set; } + + [JsonProperty(PropertyName = "reported", DefaultValueHandling = DefaultValueHandling.Ignore)] + internal IDictionary Reported { get; set; } + + internal ClientProperties ToClientProperties(PayloadConvention payloadConvention) + { + ClientPropertyCollection writablePropertyRequestCollection = ClientPropertyCollection.FromClientPropertiesAsDictionary(Desired, payloadConvention); + ClientPropertyCollection clientReportedPropertyCollection = ClientPropertyCollection.FromClientPropertiesAsDictionary(Reported, payloadConvention); + + return new ClientProperties(writablePropertyRequestCollection, clientReportedPropertyCollection); + } + } +} diff --git a/iothub/device/src/ClientPropertiesUpdateResponse.cs b/iothub/device/src/ClientPropertiesUpdateResponse.cs index a23deb32f3..b204260794 100644 --- a/iothub/device/src/ClientPropertiesUpdateResponse.cs +++ b/iothub/device/src/ClientPropertiesUpdateResponse.cs @@ -8,6 +8,10 @@ namespace Microsoft.Azure.Devices.Client /// public class ClientPropertiesUpdateResponse { + internal ClientPropertiesUpdateResponse() + { + } + /// /// The request Id that is associated with the operation. /// @@ -20,6 +24,11 @@ public class ClientPropertiesUpdateResponse /// /// The updated version after the property patch has been applied. /// + /// + /// For clients communicating with IoT hub via IoT Edge, since the patch isn't applied immediately an updated version number is not returned. + /// You can call + /// and verify from to check if your patch is successfully applied. + /// public long Version { get; internal set; } } } diff --git a/iothub/device/src/ClientPropertyCollection.cs b/iothub/device/src/ClientPropertyCollection.cs index 00e2999b9b..f624c38c30 100644 --- a/iothub/device/src/ClientPropertyCollection.cs +++ b/iothub/device/src/ClientPropertyCollection.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.Client { @@ -35,32 +37,47 @@ public void AddRootProperty(string propertyName, object propertyValue) /// /// - /// - /// + /// /// /// Adds the value to the collection. /// /// The component with the property to add. /// The name of the property to add. /// The value of the property to add. + /// or is null. public void AddComponentProperty(string componentName, string propertyName, object propertyValue) - => AddInternal(new Dictionary { { propertyName, propertyValue } }, componentName, false); + { + if (componentName == null) + { + throw new ArgumentNullException(nameof(componentName)); + } + + AddInternal(new Dictionary { { propertyName, propertyValue } }, componentName, false); + } /// /// - /// /// /// Adds the value to the collection. /// /// The component with the properties to add. /// A collection of properties to add. + /// A property name in already exists in the collection. + /// or a property name in is null. public void AddComponentProperties(string componentName, IDictionary properties) - => AddInternal(properties, componentName, true); + { + if (componentName == null) + { + throw new ArgumentNullException(nameof(componentName)); + } + + AddInternal(properties, componentName, false); + } /// - /// + /// /// - /// Adds the values to the collection. + /// Adds the collection of root-level property values to the collection. /// /// /// If the collection already has a key matching a property name supplied this method will throw an . @@ -69,16 +86,10 @@ public void AddComponentProperties(string componentName, IDictionary /// to ensure the correct formatting is applied when the object is serialized. /// - /// - /// This method directly adds the supplied to the collection. - /// For component-level properties, either ensure that you include the component identifier markers {"__t": "c"} as a part of the supplied , - /// or use the convenience method instead. - /// For more information see . - /// /// /// A collection of properties to add. /// is null. - public void Add(IDictionary properties) + public void AddRootProperties(IDictionary properties) { if (properties == null) { @@ -93,21 +104,28 @@ public void Add(IDictionary properties) /// /// /// - /// /// The name of the property to add or update. /// The value of the property to add or update. + /// is null. public void AddOrUpdateRootProperty(string propertyName, object propertyValue) => AddInternal(new Dictionary { { propertyName, propertyValue } }, null, true); /// /// /// - /// /// The component with the property to add or update. /// The name of the property to add or update. /// The value of the property to add or update. + /// or is null. public void AddOrUpdateComponentProperty(string componentName, string propertyName, object propertyValue) - => AddInternal(new Dictionary { { propertyName, propertyValue } }, componentName, true); + { + if (componentName == null) + { + throw new ArgumentNullException(nameof(componentName)); + } + + AddInternal(new Dictionary { { propertyName, propertyValue } }, componentName, true); + } /// /// @@ -121,13 +139,20 @@ public void AddOrUpdateComponentProperty(string componentName, string propertyNa /// /// The component with the properties to add or update. /// A collection of properties to add or update. + /// or a property name in is null. public void AddOrUpdateComponentProperties(string componentName, IDictionary properties) - => AddInternal(properties, componentName, true); + { + if (componentName == null) + { + throw new ArgumentNullException(nameof(componentName)); + } + + AddInternal(properties, componentName, true); + } /// /// - /// - /// + /// /// /// If the collection has a key that matches this will overwrite the current value. Otherwise it will attempt to add this to the collection. /// @@ -135,18 +160,20 @@ public void AddOrUpdateComponentProperties(string componentName, IDictionary /// to ensure the correct formatting is applied when the object is serialized. /// - /// - /// This method directly adds or updates the supplied to the collection. - /// For component-level properties, either ensure that you include the component identifier markers {"__t": "c"} as a part of the supplied , - /// or use the convenience method instead. - /// For more information see . - /// /// /// A collection of properties to add or update. - public void AddOrUpdate(IDictionary properties) - => properties + /// is null. + public void AddOrUpdateRootProperties(IDictionary properties) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } + + properties .ToList() .ForEach(entry => Collection[entry.Key] = entry.Value); + } /// /// Determines whether the specified property is present. @@ -182,8 +209,9 @@ public bool Contains(string componentName, string propertyName) /// The type to cast the object to. /// The component which holds the required property. /// The property to get. - /// The value of the component-level property. - /// true if the property collection contains a component level property with the specified key; otherwise, false. + /// When this method returns true, this contains the value of the component-level property. + /// When this method returns false, this contains the default value of the type T passed in. + /// True if a component-level property of type T with the specified key was found; otherwise, it returns false. public virtual bool TryGetValue(string componentName, string propertyName, out T propertyValue) { if (Logging.IsEnabled && Convention == null) @@ -192,35 +220,130 @@ public virtual bool TryGetValue(string componentName, string propertyName, ou $"TryGetValue will attempt to get the property value but may not behave as expected.", nameof(TryGetValue)); } + // If either the component name or the property name is null, empty or whitespace, + // then return false with the default value of the type passed in. + if (string.IsNullOrWhiteSpace(componentName) || string.IsNullOrWhiteSpace(propertyName)) + { + propertyValue = default; + return false; + } + + // While retrieving the property value from the collection: + // 1. A property collection constructed by the client application - can be retrieved using dictionary indexer. + // 2. Client property received through writable property update callbacks - stored internally as a WritableClientProperty. + // 3. Client property returned through GetClientProperties: + // a. Client reported properties sent by the client application in response to writable property update requests - stored as a JSON object + // and needs to be converted to an IWritablePropertyResponse implementation using the payload serializer. + // b. Client reported properties sent by the client application - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + // c. Writable property update request received - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + if (Contains(componentName, propertyName)) { object componentProperties = Collection[componentName]; + // If the ClientPropertyCollection was constructed by the user application (eg. for updating the client properties) + // or was returned by the application as a writable property update request then the componentProperties are retrieved as a dictionary. + // The required property value can be fetched from the dictionary directly. if (componentProperties is IDictionary nestedDictionary) { - if (nestedDictionary.TryGetValue(propertyName, out object dictionaryElement)) + // First verify that the retrieved dictionary contains the component identifier { "__t": "c" }. + // If not, then the retrieved nested dictionary is actually a root-level property of type map. + if (nestedDictionary.TryGetValue(ConventionBasedConstants.ComponentIdentifierKey, out object componentIdentifierValue) + && componentIdentifierValue.ToString() == ConventionBasedConstants.ComponentIdentifierValue) { - // If the value is null, go ahead and return it. - if (dictionaryElement == null) + if (nestedDictionary.TryGetValue(propertyName, out object dictionaryElement)) { - propertyValue = default; - return true; - } - - // If the object is of type T or can be cast to type T, go ahead and return it. - if (dictionaryElement is T valueRef - || NumericHelpers.TryCastNumericTo(dictionaryElement, out valueRef)) - { - propertyValue = valueRef; - return true; + // If the value associated with the key is null, then return true with the default value of the type passed in. + if (dictionaryElement == null) + { + propertyValue = default; + return true; + } + + // Case 1: + // If the object is of type T or can be cast to type T, go ahead and return it. + if (dictionaryElement is T valueRef + || ObjectConversionHelpers.TryCastNumericTo(dictionaryElement, out valueRef)) + { + propertyValue = valueRef; + return true; + } + + // Case 2: + // Check if the retrieved value is a writable property update request + if (dictionaryElement is WritableClientProperty writableClientProperty) + { + object writableClientPropertyValue = writableClientProperty.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writableClientPropertyValue, Convention, out propertyValue)) + { + return true; + } + } } } } else { - // If it's not, we need to try to convert it using the serializer. - Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out propertyValue); - return true; + try + { + // First verify that the retrieved dictionary contains the component identifier { "__t": "c" }. + // If not, then the retrieved nested dictionary is actually a root-level property of type map. + if (Convention + .PayloadSerializer + .TryGetNestedObjectValue(componentProperties, ConventionBasedConstants.ComponentIdentifierKey, out string componentIdentifierValue) + && componentIdentifierValue == ConventionBasedConstants.ComponentIdentifierValue) + { + Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out object retrievedPropertyValue); + + try + { + // Case 3a: + // Check if the retrieved value is a writable property update acknowledgment + var newtonsoftWritablePropertyResponse = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); + + if (typeof(IWritablePropertyResponse).IsAssignableFrom(typeof(T))) + { + // If T is IWritablePropertyResponse the property value should be of type IWritablePropertyResponse as defined in the PayloadSerializer. + // We'll convert the json object to NewtonsoftJsonWritablePropertyResponse and then convert it to the appropriate IWritablePropertyResponse object. + propertyValue = (T)Convention.PayloadSerializer.CreateWritablePropertyResponse( + newtonsoftWritablePropertyResponse.Value, + newtonsoftWritablePropertyResponse.AckCode, + newtonsoftWritablePropertyResponse.AckVersion, + newtonsoftWritablePropertyResponse.AckDescription); + return true; + } + + object writablePropertyValue = newtonsoftWritablePropertyResponse.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writablePropertyValue, Convention, out propertyValue)) + { + return true; + } + } + catch + { + // In case of an exception ignore it and continue. + } + + // Case 3b, 3c: + // Since the value cannot be cast to directly, we need to try to convert it using the serializer. + // If it can be successfully converted, go ahead and return it. + if (Convention.PayloadSerializer.TryGetNestedObjectValue(componentProperties, propertyName, out propertyValue)) + { + return true; + } + } + } + catch + { + // In case the value cannot be converted using the serializer, + // then return false with the default value of the type passed in. + } } } @@ -231,11 +354,12 @@ public virtual bool TryGetValue(string componentName, string propertyName, ou /// /// Converts a collection to a properties collection. /// - /// This internal class is aware of the implementation of the TwinCollection. + /// This method is used to translate the twin desired properties into writable property update requests. + /// This internal class is aware of the implementation of the TwinCollection. /// The TwinCollection object to convert. /// A convention handler that defines the content encoding and serializer to use for the payload. /// A new instance of the class from an existing using an optional . - internal static ClientPropertyCollection FromTwinCollection(TwinCollection twinCollection, PayloadConvention payloadConvention) + internal static ClientPropertyCollection WritablePropertyUpdateRequestsFromTwinCollection(TwinCollection twinCollection, PayloadConvention payloadConvention) { if (twinCollection == null) { @@ -249,7 +373,50 @@ internal static ClientPropertyCollection FromTwinCollection(TwinCollection twinC foreach (KeyValuePair property in twinCollection) { - propertyCollectionToReturn.Add(property.Key, payloadConvention.PayloadSerializer.DeserializeToType(Newtonsoft.Json.JsonConvert.SerializeObject(property.Value))); + object propertyValueAsObject = property.Value; + string propertyValueAsString = DefaultPayloadConvention.Instance.PayloadSerializer.SerializeToString(propertyValueAsObject); + + // Check if the property value is for a root property or a component property. + // A component property be a JObject and will have the "__t": "c" identifiers. + bool isComponentProperty = propertyValueAsObject is JObject + && payloadConvention.PayloadSerializer.TryGetNestedObjectValue(propertyValueAsString, ConventionBasedConstants.ComponentIdentifierKey, out string _); + + if (isComponentProperty) + { + // If this is a component property then the collection is a JObject with each individual property as a writable property update request. + var propertyValueAsJObject = (JObject)propertyValueAsObject; + var collectionDictionary = new Dictionary(propertyValueAsJObject.Count); + + foreach (KeyValuePair componentProperty in propertyValueAsJObject) + { + object individualPropertyValue; + if (componentProperty.Key == ConventionBasedConstants.ComponentIdentifierKey) + { + individualPropertyValue = componentProperty.Value; + } + else + { + individualPropertyValue = new WritableClientProperty + { + Convention = payloadConvention, + Value = payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(componentProperty.Value)), + Version = twinCollection.Version, + }; + } + collectionDictionary.Add(componentProperty.Key, individualPropertyValue); + } + propertyCollectionToReturn.Add(property.Key, collectionDictionary); + } + else + { + var writableProperty = new WritableClientProperty + { + Convention = payloadConvention, + Value = payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(propertyValueAsObject)), + Version = twinCollection.Version, + }; + propertyCollectionToReturn.Add(property.Key, writableProperty); + } } // The version information is not accessible via the enumerator, so assign it separately. propertyCollectionToReturn.Version = twinCollection.Version; @@ -257,11 +424,12 @@ internal static ClientPropertyCollection FromTwinCollection(TwinCollection twinC return propertyCollectionToReturn; } - internal static ClientPropertyCollection FromClientTwinDictionary(IDictionary clientTwinPropertyDictionary, PayloadConvention payloadConvention) + // This method is used to convert the received twin into client properties (reported + desired). + internal static ClientPropertyCollection FromClientPropertiesAsDictionary(IDictionary clientProperties, PayloadConvention payloadConvention) { - if (clientTwinPropertyDictionary == null) + if (clientProperties == null) { - throw new ArgumentNullException(nameof(clientTwinPropertyDictionary)); + throw new ArgumentNullException(nameof(clientProperties)); } var propertyCollectionToReturn = new ClientPropertyCollection @@ -269,7 +437,7 @@ internal static ClientPropertyCollection FromClientTwinDictionary(IDictionary property in clientTwinPropertyDictionary) + foreach (KeyValuePair property in clientProperties) { // The version information should not be a part of the enumerable ProperyCollection, but rather should be // accessible through its dedicated accessor. @@ -279,7 +447,7 @@ internal static ClientPropertyCollection FromClientTwinDictionary(IDictionary(Newtonsoft.Json.JsonConvert.SerializeObject(property.Value))); + propertyCollectionToReturn.Add(property.Key, payloadConvention.PayloadSerializer.DeserializeToType(JsonConvert.SerializeObject(property.Value))); } } @@ -296,14 +464,14 @@ internal static ClientPropertyCollection FromClientTwinDictionary(IDictionaryThe component with the properties to add or update. /// Forces the collection to use the Add or Update behavior. /// Setting to true will simply overwrite the value. Setting to false will use - /// is null for a top-level property operation. + /// is null. private void AddInternal(IDictionary properties, string componentName = default, bool forceUpdate = false) { // If the componentName is null then simply add the key-value pair to Collection dictionary. // This will either insert a property or overwrite it if it already exists. if (componentName == null) { - // If both the component name and properties collection are null then throw a ArgumentNullException. + // If both the component name and properties collection are null then throw an ArgumentNullException. // This is not a valid use-case. if (properties == null) { @@ -312,6 +480,12 @@ private void AddInternal(IDictionary properties, string componen foreach (KeyValuePair entry in properties) { + // A null property key is not allowed. Throw an ArgumentNullException. + if (entry.Key == null) + { + throw new ArgumentNullException(nameof(entry.Key)); + } + if (forceUpdate) { Collection[entry.Key] = entry.Value; @@ -341,6 +515,12 @@ private void AddInternal(IDictionary properties, string componen foreach (KeyValuePair entry in properties) { + // A null property key is not allowed. Throw an ArgumentNullException. + if (entry.Key == null) + { + throw new ArgumentNullException(nameof(entry.Key)); + } + if (forceUpdate) { componentProperties[entry.Key] = entry.Value; diff --git a/iothub/device/src/ClientTwinProperties.cs b/iothub/device/src/ClientTwinProperties.cs index 63b13195f5..53f1e1ea3c 100644 --- a/iothub/device/src/ClientTwinProperties.cs +++ b/iothub/device/src/ClientTwinProperties.cs @@ -23,8 +23,8 @@ internal ClientTwinProperties() internal ClientProperties ToClientProperties(PayloadConvention payloadConvention) { - ClientPropertyCollection writablePropertyCollection = ClientPropertyCollection.FromClientTwinDictionary(Desired, payloadConvention); - ClientPropertyCollection propertyCollection = ClientPropertyCollection.FromClientTwinDictionary(Reported, payloadConvention); + ClientPropertyCollection writablePropertyCollection = ClientPropertyCollection.FromClientPropertiesAsDictionary(Desired, payloadConvention); + ClientPropertyCollection propertyCollection = ClientPropertyCollection.FromClientPropertiesAsDictionary(Reported, payloadConvention); return new ClientProperties(writablePropertyCollection, propertyCollection); } 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..5332b29ce2 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/Common/UrlEncodedDictionarySerializer.cs b/iothub/device/src/Common/UrlEncodedDictionarySerializer.cs index b3e87099a3..c093feca0a 100644 --- a/iothub/device/src/Common/UrlEncodedDictionarySerializer.cs +++ b/iothub/device/src/Common/UrlEncodedDictionarySerializer.cs @@ -345,7 +345,7 @@ private Token CreateToken(TokenType tokenType, int readCount) // '?' is not a valid character for message property names or values, but instead signifies the start of a query string // in the case of an MQTT topic. For this reason, we'll replace the '?' from the property key before adding it into // application properties collection. - // https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-construct + // https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-construct string tokenValue = readCount == 0 ? null : value.Substring(position - readCount, readCount).Replace(QueryStringIdentifier, string.Empty); return new Token(tokenType, tokenValue); diff --git a/iothub/device/src/DeviceClient.ConventionBasedOperations.cs b/iothub/device/src/DeviceClient.ConventionBasedOperations.cs index 4de460fb69..4b22036d85 100644 --- a/iothub/device/src/DeviceClient.ConventionBasedOperations.cs +++ b/iothub/device/src/DeviceClient.ConventionBasedOperations.cs @@ -52,7 +52,7 @@ public Task SubscribeToCommandsAsync( /// A cancellation token to cancel the operation. /// The device properties. public Task GetClientPropertiesAsync(CancellationToken cancellationToken = default) - => InternalClient.GetClientPropertiesAsync(cancellationToken); + => InternalClient.GetClientTwinPropertiesAsync(cancellationToken); /// /// Update the client properties. @@ -70,7 +70,7 @@ public Task UpdateClientPropertiesAsync(ClientPr /// The global call back to handle all writable property updates. /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. - public Task SubscribeToWritablePropertiesEventAsync(Func callback, object userContext, CancellationToken cancellationToken = default) - => InternalClient.SubscribeToWritablePropertiesEventAsync(callback, userContext, cancellationToken); + public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func callback, object userContext, CancellationToken cancellationToken = default) + => InternalClient.SubscribeToWritablePropertyUpdateRequestsAsync(callback, userContext, cancellationToken); } } diff --git a/iothub/device/src/DeviceClient.cs b/iothub/device/src/DeviceClient.cs index a21c2b261e..cdb44bcbec 100644 --- a/iothub/device/src/DeviceClient.cs +++ b/iothub/device/src/DeviceClient.cs @@ -5,8 +5,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Net.Sockets; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using DotNetty.Transport.Channels; +using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport; using Microsoft.Azure.Devices.Shared; @@ -304,7 +308,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The receive message or null if there was no message until the default timeout public Task ReceiveAsync() => InternalClient.ReceiveAsync(); @@ -316,7 +320,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// A cancellation token to cancel the operation. /// Thrown when the operation has been canceled. @@ -330,7 +334,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// /// You cannot Reject or Abandon messages over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The receive message or null if there was no message until the specified time has elapsed public Task ReceiveAsync(TimeSpan timeout) => InternalClient.ReceiveAsync(timeout); @@ -384,7 +388,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// The previously received message @@ -395,7 +399,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// A cancellation token to cancel the operation. @@ -408,7 +412,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// The lock identifier for the previously received message @@ -419,7 +423,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Abandon a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// A cancellation token to cancel the operation. @@ -432,7 +436,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message lockToken. /// The previously received message @@ -443,7 +447,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// A cancellation token to cancel the operation. /// The message lockToken. @@ -456,7 +460,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// The lock identifier for the previously received message @@ -467,7 +471,7 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// /// You cannot Reject a message over MQTT protocol. - /// For more details, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. + /// For more details, see https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-messages-c2d#the-cloud-to-device-message-life-cycle. /// /// The message. /// A cancellation token to cancel the operation. @@ -479,6 +483,22 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// Sends an event to a hub /// /// The message to send. Should be disposed after sending. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request within the timeout specified for the operation. + /// The timeout values are largely transport protocol specific. Check the corresponding transport settings to see if they can be configured. + /// The operation timeout for the client can be set using . + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The task to await public Task SendEventAsync(Message message) => InternalClient.SendEventAsync(message); @@ -487,7 +507,22 @@ public Task SetReceiveMessageHandlerAsync(ReceiveMessageCallback messageHandler, /// /// The message to send. Should be disposed after sending. /// A cancellation token to cancel the operation. - /// Thrown when the operation has been canceled. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request before the expiration of the passed . + /// If a cancellation token is not supplied to the operation call, a cancellation token with an expiration time of 4 minutes is used. + /// + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The task to await public Task SendEventAsync(Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(message, cancellationToken); @@ -531,7 +566,7 @@ public Task UploadToBlobAsync(string blobName, Stream source, CancellationToken InternalClient.UploadToBlobAsync(blobName, source, cancellationToken); /// - /// Get a file upload SAS URI which the Azure Storage SDK can use to upload a file to blob for this device. See this documentation for more details + /// Get a file upload SAS URI which the Azure Storage SDK can use to upload a file to blob for this device. See this documentation for more details /// /// The request details for getting the SAS URI, including the destination blob name. /// The cancellation token. @@ -540,7 +575,7 @@ public Task GetFileUploadSasUriAsync(FileUploadSasUriR InternalClient.GetFileUploadSasUriAsync(request, cancellationToken); /// - /// Notify IoT Hub that a device's file upload has finished. See this documentation for more details + /// Notify IoT Hub that a device's file upload has finished. See this documentation for more details /// /// The notification details, including if the file upload succeeded. /// The cancellation token. 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/IDelegatingHandler.cs b/iothub/device/src/IDelegatingHandler.cs index 3729e0ed47..21ce566ae4 100644 --- a/iothub/device/src/IDelegatingHandler.cs +++ b/iothub/device/src/IDelegatingHandler.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; @@ -59,9 +60,9 @@ internal interface IDelegatingHandler : IContinuationProvider SendTwinGetAsync(CancellationToken cancellationToken); + Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken); - Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken); + Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken); Task EnableTwinPatchAsync(CancellationToken cancellationToken); 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.ConventionBasedOperations.cs b/iothub/device/src/InternalClient.ConventionBasedOperations.cs index 4e785e96d0..a8e19cb9cc 100644 --- a/iothub/device/src/InternalClient.ConventionBasedOperations.cs +++ b/iothub/device/src/InternalClient.ConventionBasedOperations.cs @@ -2,11 +2,13 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; namespace Microsoft.Azure.Devices.Client { @@ -64,11 +66,15 @@ internal Task SubscribeToCommandsAsync(Func GetClientPropertiesAsync(CancellationToken cancellationToken) + internal async Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { try { - return await InnerHandler.GetPropertiesAsync(PayloadConvention, cancellationToken).ConfigureAwait(false); + ClientPropertiesAsDictionary clientPropertiesDictionary = await InnerHandler + .GetClientTwinPropertiesAsync(cancellationToken) + .ConfigureAwait(false); + + return clientPropertiesDictionary.ToClientProperties(PayloadConvention); } catch (IotHubCommunicationException ex) when (ex.InnerException is OperationCanceledException) { @@ -87,7 +93,10 @@ internal async Task UpdateClientPropertiesAsync( try { clientProperties.Convention = PayloadConvention; - return await InnerHandler.SendPropertyPatchAsync(clientProperties, cancellationToken).ConfigureAwait(false); + byte[] body = clientProperties.GetPayloadObjectBytes(); + using Stream bodyStream = new MemoryStream(body); + + return await InnerHandler.SendClientTwinPropertyPatchAsync(bodyStream, cancellationToken).ConfigureAwait(false); } catch (IotHubCommunicationException ex) when (ex.InnerException is OperationCanceledException) { @@ -96,13 +105,13 @@ internal async Task UpdateClientPropertiesAsync( } } - internal Task SubscribeToWritablePropertiesEventAsync(Func callback, object userContext, CancellationToken cancellationToken) + internal Task SubscribeToWritablePropertyUpdateRequestsAsync(Func callback, object userContext, CancellationToken cancellationToken) { // Subscribe to DesiredPropertyUpdateCallback internally and use the callback received internally to invoke the user supplied Property callback. var desiredPropertyUpdateCallback = new DesiredPropertyUpdateCallback((twinCollection, userContext) => { // convert a TwinCollection to PropertyCollection - var propertyCollection = ClientPropertyCollection.FromTwinCollection(twinCollection, PayloadConvention); + var propertyCollection = ClientPropertyCollection.WritablePropertyUpdateRequestsFromTwinCollection(twinCollection, PayloadConvention); callback.Invoke(propertyCollection, userContext); return TaskHelpers.CompletedTask; diff --git a/iothub/device/src/InternalClient.cs b/iothub/device/src/InternalClient.cs index 4a3468af90..d729b949f8 100644 --- a/iothub/device/src/InternalClient.cs +++ b/iothub/device/src/InternalClient.cs @@ -13,6 +13,8 @@ using System.IO; using Microsoft.Azure.Devices.Client.Exceptions; using System.ComponentModel; +using System.Text; +using Newtonsoft.Json; #if NET451 @@ -1167,18 +1169,14 @@ public async Task GetTwinAsync() /// For the complete device twin object, use Microsoft.Azure.Devices.RegistryManager.GetTwinAsync(string deviceId). /// /// The device twin object for the current device - public Task GetTwinAsync(CancellationToken cancellationToken) + public async Task GetTwinAsync(CancellationToken cancellationToken) { - // `GetTwinAsync` shall call `SendTwinGetAsync` on the transport to get the twin state. - try - { - return InnerHandler.SendTwinGetAsync(cancellationToken); - } - catch (IotHubCommunicationException ex) when (ex.InnerException is OperationCanceledException) + TwinProperties twinProperties = await InnerHandler + .GetClientTwinPropertiesAsync(cancellationToken).ConfigureAwait(false); + return new Twin { - cancellationToken.ThrowIfCancellationRequested(); - throw; - } + Properties = twinProperties, + }; } /// @@ -1205,7 +1203,7 @@ public async Task UpdateReportedPropertiesAsync(TwinCollection reportedPropertie /// /// Reported properties to push /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) + public async Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) { // `UpdateReportedPropertiesAsync` shall throw an `ArgumentNull` exception if `reportedProperties` is null. if (reportedProperties == null) @@ -1213,16 +1211,10 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, Can throw new ArgumentNullException(nameof(reportedProperties)); } - // `UpdateReportedPropertiesAsync` shall call `SendTwinPatchAsync` on the transport to update the reported properties. - try - { - return InnerHandler.SendTwinPatchAsync(reportedProperties, cancellationToken); - } - catch (IotHubCommunicationException ex) when (ex.InnerException is OperationCanceledException) - { - cancellationToken.ThrowIfCancellationRequested(); - throw; - } + string body = JsonConvert.SerializeObject(reportedProperties); + using Stream bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(body)); + + await InnerHandler.SendClientTwinPropertyPatchAsync(bodyStream, cancellationToken).ConfigureAwait(false); } // Codes_SRS_DEVICECLIENT_18_005: When a patch is received from the service, the `callback` shall be called. @@ -1409,13 +1401,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 c0ce51b752..d8ced78b94 100644 --- a/iothub/device/src/Microsoft.Azure.Devices.Client.csproj +++ b/iothub/device/src/Microsoft.Azure.Devices.Client.csproj @@ -77,6 +77,8 @@ + + @@ -84,6 +86,8 @@ + + @@ -91,8 +95,6 @@ - - diff --git a/iothub/device/src/ModuleClient.ConventionBasedOperations.cs b/iothub/device/src/ModuleClient.ConventionBasedOperations.cs index 71ba4a4669..59cbf76061 100644 --- a/iothub/device/src/ModuleClient.ConventionBasedOperations.cs +++ b/iothub/device/src/ModuleClient.ConventionBasedOperations.cs @@ -52,7 +52,7 @@ public Task SubscribeToCommandsAsync( /// A cancellation token to cancel the operation. /// The device properties. public Task GetClientPropertiesAsync(CancellationToken cancellationToken = default) - => InternalClient.GetClientPropertiesAsync(cancellationToken); + => InternalClient.GetClientTwinPropertiesAsync(cancellationToken); /// /// Update the client properties. @@ -70,7 +70,7 @@ public Task UpdateClientPropertiesAsync(ClientPr /// The global call back to handle all writable property updates. /// Generic parameter to be interpreted by the client code. /// A cancellation token to cancel the operation. - public Task SubscribeToWritablePropertiesEventAsync(Func callback, object userContext, CancellationToken cancellationToken = default) - => InternalClient.SubscribeToWritablePropertiesEventAsync(callback, userContext, cancellationToken); + public Task SubscribeToWritablePropertyUpdateRequestsAsync(Func callback, object userContext, CancellationToken cancellationToken = default) + => InternalClient.SubscribeToWritablePropertyUpdateRequestsAsync(callback, userContext, cancellationToken); } } diff --git a/iothub/device/src/ModuleClient.cs b/iothub/device/src/ModuleClient.cs index d8925c3d94..7edce72115 100644 --- a/iothub/device/src/ModuleClient.cs +++ b/iothub/device/src/ModuleClient.cs @@ -5,14 +5,19 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Net; using System.Net.Http; using System.Net.Security; +using System.Net.Sockets; +using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; +using DotNetty.Transport.Channels; using Microsoft.Azure.Devices.Client.Edge; +using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Extensions; using Microsoft.Azure.Devices.Client.Transport; using Microsoft.Azure.Devices.Shared; @@ -350,6 +355,22 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// Sends an event to IoT hub /// /// The message. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request within the timeout specified for the operation. + /// The timeout values are largely transport protocol specific. Check the corresponding transport settings to see if they can be configured. + /// The operation timeout for the client can be set using . + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The message containing the event public Task SendEventAsync(Message message) => InternalClient.SendEventAsync(message); @@ -358,13 +379,28 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// The message. /// A cancellation token to cancel the operation. - /// Thrown when the operation has been canceled. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request before the expiration of the passed . + /// If a cancellation token is not supplied to the operation call, a cancellation token with an expiration time of 4 minutes is used. + /// + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the list of exceptions is not exhaustive. + /// /// The message containing the event public Task SendEventAsync(Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(message, cancellationToken); /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The messages. /// The task containing the event @@ -372,7 +408,7 @@ public void SetRetryPolicy(IRetryPolicy retryPolicy) /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing /// Sends a batch of events to IoT hub. Requires AMQP or AMQP over WebSockets. + /// For more information on IoT Edge module routing /// Sends a batch of events to IoT hub. Requires AMQP or AMQP over WebSockets. /// /// An IEnumerable set of Message objects. /// A cancellation token to cancel the operation. @@ -560,7 +596,22 @@ public Task UpdateReportedPropertiesAsync(TwinCollection reportedProperties, Can /// /// The output target for sending the given message /// The message to send - /// Thrown when the operation has been canceled. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request within the timeout specified for the operation. + /// The timeout values are largely transport protocol specific. Check the corresponding transport settings to see if they can be configured. + /// The operation timeout for the client can be set using . + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the above list is not exhaustive. + /// /// The message containing the event public Task SendEventAsync(string outputName, Message message) => InternalClient.SendEventAsync(outputName, message); @@ -571,14 +622,29 @@ public Task SendEventAsync(string outputName, Message message) => /// The output target for sending the given message /// The message to send /// A cancellation token to cancel the operation. - /// Thrown when the operation has been canceled. + /// Thrown when a required parameter is null. + /// Thrown if the service does not respond to the request before the expiration of the passed . + /// If a cancellation token is not supplied to the operation call, a cancellation token with an expiration time of 4 minutes is used. + /// + /// Thrown if the client encounters a transient retryable exception. + /// Thrown if a socket error occurs. + /// Thrown if an error occurs when performing an operation on a WebSocket connection. + /// Thrown if an I/O error occurs. + /// Thrown if the MQTT transport layer closes unexpectedly. + /// Thrown if an error occurs when communicating with IoT Hub service. + /// If is set to true then it is a transient exception. + /// If is set to false then it is a non-transient exception. + /// + /// In case of a transient issue, retrying the operation should work. In case of a non-transient issue, inspect the error details and take steps accordingly. + /// Please note that the above list is not exhaustive. + /// /// The message containing the event public Task SendEventAsync(string outputName, Message message, CancellationToken cancellationToken) => InternalClient.SendEventAsync(outputName, message, cancellationToken); /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The output target for sending the given message /// A list of one or more messages to send @@ -589,7 +655,7 @@ public Task SendEventBatchAsync(string outputName, IEnumerable messages /// /// Sends a batch of events to IoT hub. Use AMQP or HTTPs for a true batch operation. MQTT will just send the messages one after the other. - /// For more information on IoT Edge module routing + /// For more information on IoT Edge module routing /// /// The output target for sending the given message /// A list of one or more messages to send diff --git a/iothub/device/src/ObjectConversionHelpers.cs b/iothub/device/src/ObjectConversionHelpers.cs new file mode 100644 index 0000000000..eb327e43ad --- /dev/null +++ b/iothub/device/src/ObjectConversionHelpers.cs @@ -0,0 +1,82 @@ +// 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.Globalization; +using Microsoft.Azure.Devices.Shared; + +namespace Microsoft.Azure.Devices.Client +{ + internal class ObjectConversionHelpers + { + internal static bool TryCastOrConvert(object objectToCastOrConvert, PayloadConvention payloadConvention, out T value) + { + // If the object is of type T or can be cast to type T, go ahead and return it. + if (TryCast(objectToCastOrConvert, out value)) + { + return true; + } + + try + { + // If the cannot be cast to directly we need to try to convert it using the serializer. + // If it can be successfully converted, go ahead and return it. + value = payloadConvention.PayloadSerializer.ConvertFromObject(objectToCastOrConvert); + return true; + } + catch + { + } + + value = default; + return false; + } + + internal static bool TryCast(object objectToCast, out T value) + { + if (objectToCast is T valueRef + || TryCastNumericTo(objectToCast, out valueRef)) + { + value = valueRef; + return true; + } + + value = default; + return false; + } + + internal static bool TryCastNumericTo(object input, out T result) + { + if (TryGetNumeric(input)) + { + try + { + result = (T)Convert.ChangeType(input, typeof(T), CultureInfo.InvariantCulture); + return true; + } + catch + { + } + } + + result = default; + return false; + } + + private static bool TryGetNumeric(object expression) + { + if (expression == null) + { + return false; + } + + return double.TryParse( + Convert.ToString( + expression, + CultureInfo.InvariantCulture), + NumberStyles.Any, + NumberFormatInfo.InvariantInfo, + out _); + } + } +} diff --git a/iothub/device/src/PayloadCollection.cs b/iothub/device/src/PayloadCollection.cs index 1943264358..d4dcbc2295 100644 --- a/iothub/device/src/PayloadCollection.cs +++ b/iothub/device/src/PayloadCollection.cs @@ -107,13 +107,11 @@ public bool Contains(string key) /// /// Gets the value of the object from the collection. /// - /// - /// This class is used for both sending and receiving properties for the device. - /// /// The type to cast the object to. /// The key of the property to get. - /// The value of the object from the collection. - /// True if the collection contains an element with the specified key; otherwise, it returns false. + /// When this method returns true, this contains the value of the object from the collection. + /// When this method returns false, this contains the default value of the type T passed in. + /// True if a value of type T with the specified key was found; otherwise, it returns false. public bool TryGetValue(string key, out T value) { if (Logging.IsEnabled && Convention == null) @@ -122,26 +120,99 @@ public bool TryGetValue(string key, out T value) $"TryGetValue will attempt to get the property value but may not behave as expected.", nameof(TryGetValue)); } + // If the key is null, empty or whitespace, then return false with the default value of the type passed in. + if (string.IsNullOrWhiteSpace(key)) + { + value = default; + return false; + } + + // While retrieving the telemetry value from the collection, a simple dictionary indexer should work. + // While retrieving the property value from the collection: + // 1. A property collection constructed by the client application - can be retrieved using dictionary indexer. + // 2. Client property received through writable property update callbacks - stored internally as a WritableClientProperty. + // 3. Client property returned through GetClientProperties: + // a. Client reported properties sent by the client application in response to writable property update requests - stored as a JSON object + // and needs to be converted to an IWritablePropertyResponse implementation using the payload serializer. + // b. Client reported properties sent by the client application - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. + // c. Writable property update request received - stored as a JSON object + // and needs to be converted to the expected type using the payload serializer. if (Collection.ContainsKey(key)) { - // If the value is null, go ahead and return it. - if (Collection[key] == null) + object retrievedPropertyValue = Collection[key]; + + // If the value associated with the key is null, then return true with the default value of the type passed in. + if (retrievedPropertyValue == null) { value = default; return true; } + // Case 1: // If the object is of type T or can be cast to type T, go ahead and return it. - if (Collection[key] is T valueRef - || NumericHelpers.TryCastNumericTo(Collection[key], out valueRef)) + if (ObjectConversionHelpers.TryCast(retrievedPropertyValue, out value)) { - value = valueRef; return true; } - // If it's not, we need to try to convert it using the serializer. - value = Convention.PayloadSerializer.ConvertFromObject(Collection[key]); - return true; + // Case 2: + // Check if the retrieved value is a writable property update request + if (retrievedPropertyValue is WritableClientProperty writableClientProperty) + { + object writableClientPropertyValue = writableClientProperty.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writableClientPropertyValue, Convention, out value)) + { + return true; + } + } + + try + { + try + { + // Case 3a: + // Check if the retrieved value is a writable property update acknowledgment + var newtonsoftWritablePropertyResponse = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); + + if (typeof(IWritablePropertyResponse).IsAssignableFrom(typeof(T))) + { + // If T is IWritablePropertyResponse the property value should be of type IWritablePropertyResponse as defined in the PayloadSerializer. + // We'll convert the json object to NewtonsoftJsonWritablePropertyResponse and then convert it to the appropriate IWritablePropertyResponse object. + value = (T)Convention.PayloadSerializer.CreateWritablePropertyResponse( + newtonsoftWritablePropertyResponse.Value, + newtonsoftWritablePropertyResponse.AckCode, + newtonsoftWritablePropertyResponse.AckVersion, + newtonsoftWritablePropertyResponse.AckDescription); + return true; + } + + var writablePropertyValue = newtonsoftWritablePropertyResponse.Value; + + // If the object is of type T or can be cast or converted to type T, go ahead and return it. + if (ObjectConversionHelpers.TryCastOrConvert(writablePropertyValue, Convention, out value)) + { + return true; + } + } + catch + { + // In case of an exception ignore it and continue. + } + + // Case 3b, 3c: + // If the value is neither a writable property nor can be cast to directly, we need to try to convert it using the serializer. + // If it can be successfully converted, go ahead and return it. + value = Convention.PayloadSerializer.ConvertFromObject(retrievedPropertyValue); + return true; + } + catch + { + // In case the value cannot be converted using the serializer, + // then return false with the default value of the type passed in. + } } value = default; @@ -179,20 +250,5 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } - - /// - /// Will set the underlying of the payload collection. - /// - /// The collection to get the underlying dictionary from. - protected void SetCollection(PayloadCollection payloadCollection) - { - if (payloadCollection == null) - { - throw new ArgumentNullException(); - } - - Collection = payloadCollection.Collection; - Convention = payloadCollection.Convention; - } } } 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/TelemetryCollection.cs b/iothub/device/src/TelemetryCollection.cs index 99d0164aff..321bdbec7a 100644 --- a/iothub/device/src/TelemetryCollection.cs +++ b/iothub/device/src/TelemetryCollection.cs @@ -2,6 +2,8 @@ // 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; namespace Microsoft.Azure.Devices.Client { @@ -32,5 +34,42 @@ public override void AddOrUpdate(string telemetryName, object telemetryValue) { base.AddOrUpdate(telemetryName, telemetryValue); } + + /// + /// Adds the telemetry values to the telemetry collection. + /// + /// + /// + /// An element with the same key already exists in the collection. + /// is null. + public void Add(IDictionary telemetryValues) + { + if (telemetryValues == null) + { + throw new ArgumentNullException(nameof(telemetryValues)); + } + + telemetryValues + .ToList() + .ForEach(entry => base.Add(entry.Key, entry.Value)); + } + + /// + /// Adds or updates the telemetry values in the telemetry collection. + /// + /// + /// + /// is null. + public void AddOrUpdate(IDictionary telemetryValues) + { + if (telemetryValues == null) + { + throw new ArgumentNullException(nameof(telemetryValues)); + } + + telemetryValues + .ToList() + .ForEach(entry => base.AddOrUpdate(entry.Key, entry.Value)); + } } } 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/Amqp/AmqpConnectionHolder.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs index c82660aed5..6aefe5494c 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionHolder.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Shared; +using Microsoft.Azure.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Extensions; using Microsoft.Azure.Devices.Client.Transport.AmqpIot; -using System.Collections.Generic; +using Microsoft.Azure.Devices.Shared; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -36,7 +37,7 @@ public AmqpConnectionHolder(DeviceIdentity deviceIdentity) public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) @@ -275,4 +276,4 @@ internal DeviceIdentity GetDeviceIdentityOfAuthenticationProvider() return _deviceIdentity; } } -} \ No newline at end of file +} diff --git a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs index caffb063b4..d682ffc51a 100644 --- a/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs +++ b/iothub/device/src/Transport/Amqp/AmqpConnectionPool.cs @@ -4,9 +4,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Shared; -using Microsoft.Azure.Devices.Client.Transport.AmqpIot; +using Microsoft.Azure.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; +using Microsoft.Azure.Devices.Client.Transport.AmqpIot; +using Microsoft.Azure.Devices.Shared; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -19,7 +20,7 @@ internal class AmqpConnectionPool : IAmqpUnitManager public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs index 91c32fbace..e9225b5d55 100644 --- a/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs +++ b/iothub/device/src/Transport/Amqp/AmqpTransportHandler.cs @@ -4,11 +4,15 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIot; using Microsoft.Azure.Devices.Shared; +using Newtonsoft.Json; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -21,7 +25,7 @@ internal class AmqpTransportHandler : TransportHandler private readonly AmqpUnit _amqpUnit; private readonly Action _onDesiredStatePatchListener; private readonly object _lock = new object(); - private ConcurrentDictionary> _twinResponseCompletions = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _twinResponseCompletions = new ConcurrentDictionary>(); private bool _closed; static AmqpTransportHandler() @@ -347,52 +351,80 @@ public override async Task DisableTwinPatchAsync(CancellationToken cancellationT } } - public override async Task SendTwinGetAsync(CancellationToken cancellationToken) + public override async Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { - Logging.Enter(this, cancellationToken, nameof(SendTwinGetAsync)); + Logging.Enter(this, cancellationToken, nameof(GetClientTwinPropertiesAsync)); try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - Twin twin = await RoundTripTwinMessageAsync(AmqpTwinMessageType.Get, null, cancellationToken) + AmqpMessage responseFromService = await RoundTripTwinMessageAsync(AmqpTwinMessageType.Get, null, cancellationToken) .ConfigureAwait(false); - return twin ?? throw new InvalidOperationException("Service rejected the message"); + + if (responseFromService == null) + { + throw new InvalidOperationException("Service rejected the message"); + } + + // We will use UTF-8 for decoding the service response. This is because UTF-8 is the only currently supported encoding format. + using var reader = new StreamReader(responseFromService.BodyStream, Encoding.UTF8); + string body = reader.ReadToEnd(); + + try + { + // We will use NewtonSoft Json to deserialize the service response to the appropriate type; i.e. Twin for non-convention-based operation + // and ClientProperties for convention-based operations. + return JsonConvert.DeserializeObject(body); + } + catch (JsonReaderException ex) + { + if (Logging.IsEnabled) + Logging.Error(this, $"Failed to parse Twin JSON: {ex}. Message body: '{body}'"); + + throw; + } } finally { - Logging.Exit(this, cancellationToken, nameof(SendTwinGetAsync)); + Logging.Exit(this, cancellationToken, nameof(GetClientTwinPropertiesAsync)); } } - public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) + public override async Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { - Logging.Enter(this, reportedProperties, cancellationToken, nameof(SendTwinPatchAsync)); + Logging.Enter(this, reportedProperties, cancellationToken, nameof(SendClientTwinPropertyPatchAsync)); try { await EnableTwinPatchAsync(cancellationToken).ConfigureAwait(false); - await RoundTripTwinMessageAsync(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); + AmqpMessage responseFromService = await RoundTripTwinMessageAsync(AmqpTwinMessageType.Patch, reportedProperties, cancellationToken).ConfigureAwait(false); + + long updatedVersion = GetVersion(responseFromService); + return new ClientPropertiesUpdateResponse + { + Version = updatedVersion, + }; } finally { - Logging.Exit(this, reportedProperties, cancellationToken, nameof(SendTwinPatchAsync)); + Logging.Exit(this, reportedProperties, cancellationToken, nameof(SendClientTwinPropertyPatchAsync)); } } - private async Task RoundTripTwinMessageAsync( + private async Task RoundTripTwinMessageAsync( AmqpTwinMessageType amqpTwinMessageType, - TwinCollection reportedProperties, + Stream reportedProperties, CancellationToken cancellationToken) { Logging.Enter(this, cancellationToken, nameof(RoundTripTwinMessageAsync)); string correlationId = amqpTwinMessageType + Guid.NewGuid().ToString(); - Twin response = null; + AmqpMessage response = default; try { cancellationToken.ThrowIfCancellationRequested(); - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new TaskCompletionSource(); _twinResponseCompletions[correlationId] = taskCompletionSource; await _amqpUnit.SendTwinMessageAsync(amqpTwinMessageType, correlationId, reportedProperties, _operationTimeout).ConfigureAwait(false); @@ -543,37 +575,62 @@ public override Task SendPropertyPatchAsync(Clie #region Helpers - private void TwinMessageListener(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) + private void TwinMessageListener(AmqpMessage responseFromService, string correlationId, IotHubException ex = default) { if (correlationId == null) { - // This is desired property updates, so call the callback with TwinCollection. - _onDesiredStatePatchListener(twinCollection); + // This is desired property updates, so invoke the callback with TwinCollection. + using var reader = new StreamReader(responseFromService.BodyStream, Encoding.UTF8); + string responseBody = reader.ReadToEnd(); + + _onDesiredStatePatchListener(JsonConvert.DeserializeObject(responseBody)); } - else + else if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) + || correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) { - if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase) - || correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) + Logging.Info(this, $"Received a response for operation with correlation Id {correlationId}.", nameof(TwinMessageListener)); + + // For Get and Patch, complete the task. + if (_twinResponseCompletions.TryRemove(correlationId, out TaskCompletionSource task)) { - // For Get and Patch, complete the task. - if (_twinResponseCompletions.TryRemove(correlationId, out TaskCompletionSource task)) + if (ex == default) { - if (ex == default) - { - task.SetResult(twin); - } - else - { - task.SetException(ex); - } + task.SetResult(responseFromService); } else { - // This can happen if we received a message from service with correlation Id that was not set by SDK or does not exist in dictionary. - Logging.Info("Could not remove correlation id to complete the task awaiter for a twin operation.", nameof(TwinMessageListener)); + task.SetException(ex); } } + else + { + // This can happen if we received a message from service with correlation Id that was not sent by SDK or does not exist in dictionary. + Logging.Error(this, $"Could not remove correlation id {correlationId} to complete the task awaiter for a twin operation.", nameof(TwinMessageListener)); + } + } + else if (correlationId.StartsWith(AmqpTwinMessageType.Put.ToString(), StringComparison.OrdinalIgnoreCase)) + { + // This is an acknowledgment received from service for subscribing to desired property updates. + Logging.Info(this, $"Subscribed for twin successfully with a correlation Id of {correlationId}.", nameof(TwinMessageListener)); } + else + { + // This can happen if we received a message from service with correlation Id that was not sent by SDK or does not exist in dictionary. + Logging.Error(this, $"Received an unexpected response from service with correlation Id {correlationId}.", nameof(TwinMessageListener)); + } + } + + internal static long GetVersion(AmqpMessage response) + { + if (response != null) + { + if (response.MessageAnnotations.Map.TryGetValue(AmqpIotConstants.ResponseVersionName, out long version)) + { + return version; + } + } + + return -1; } #endregion Helpers diff --git a/iothub/device/src/Transport/Amqp/AmqpUnit.cs b/iothub/device/src/Transport/Amqp/AmqpUnit.cs index fe09b63a40..833c6ab556 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnit.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnit.cs @@ -10,6 +10,8 @@ using Microsoft.Azure.Devices.Shared; using Microsoft.Azure.Devices.Client.Transport.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; +using System.IO; +using Microsoft.Azure.Amqp; namespace Microsoft.Azure.Devices.Client.Transport.AmqpIot { @@ -19,7 +21,7 @@ internal class AmqpUnit : IDisposable private readonly DeviceIdentity _deviceIdentity; private readonly Func _onMethodCallback; - private readonly Action _twinMessageListener; + private readonly Action _twinMessageListener; private readonly Func _onModuleMessageReceivedCallback; private readonly Func _onDeviceMessageReceivedCallback; private readonly IAmqpConnectionHolder _amqpConnectionHolder; @@ -54,7 +56,7 @@ public AmqpUnit( DeviceIdentity deviceIdentity, IAmqpConnectionHolder amqpConnectionHolder, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) @@ -747,21 +749,21 @@ private async Task OpenTwinSenderLinkAsync(AmqpIotSession amqpIotSession, string } } - private void OnDesiredPropertyReceived(Twin twin, string correlationId, TwinCollection twinCollection, IotHubException ex = default) + private void OnDesiredPropertyReceived(AmqpMessage responseFromService, string correlationId, IotHubException ex = default) { - Logging.Enter(this, twin, nameof(OnDesiredPropertyReceived)); + Logging.Enter(this, responseFromService, nameof(OnDesiredPropertyReceived)); try { - _twinMessageListener?.Invoke(twin, correlationId, twinCollection, ex); + _twinMessageListener?.Invoke(responseFromService, correlationId, ex); } finally { - Logging.Exit(this, twin, nameof(OnDesiredPropertyReceived)); + Logging.Exit(this, responseFromService, nameof(OnDesiredPropertyReceived)); } } - public async Task SendTwinMessageAsync(AmqpTwinMessageType amqpTwinMessageType, string correlationId, TwinCollection reportedProperties, TimeSpan timeout) + public async Task SendTwinMessageAsync(AmqpTwinMessageType amqpTwinMessageType, string correlationId, Stream reportedProperties, TimeSpan timeout) { Logging.Enter(this, timeout, nameof(SendTwinMessageAsync)); diff --git a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs index f9d4723f84..a94dfe6829 100644 --- a/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/AmqpUnitManager.cs @@ -4,9 +4,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Shared; -using Microsoft.Azure.Devices.Client.Transport.AmqpIot; +using Microsoft.Azure.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; +using Microsoft.Azure.Devices.Client.Transport.AmqpIot; +using Microsoft.Azure.Devices.Shared; namespace Microsoft.Azure.Devices.Client.Transport.Amqp { @@ -30,7 +31,7 @@ internal static AmqpUnitManager GetInstance() public AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected) diff --git a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs index 509b5da002..09ffe1abc3 100644 --- a/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs +++ b/iothub/device/src/Transport/Amqp/IAmqpUnitManager.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.Azure.Amqp; using Microsoft.Azure.Devices.Client.Exceptions; using Microsoft.Azure.Devices.Client.Transport.AmqpIot; using Microsoft.Azure.Devices.Shared; @@ -14,7 +15,7 @@ internal interface IAmqpUnitManager AmqpUnit CreateAmqpUnit( DeviceIdentity deviceIdentity, Func onMethodCallback, - Action twinMessageListener, + Action twinMessageListener, Func onModuleMessageReceivedCallback, Func onDeviceMessageReceivedCallback, Action onUnitDisconnected); diff --git a/iothub/device/src/Transport/AmqpIot/AmqpIotConstants.cs b/iothub/device/src/Transport/AmqpIot/AmqpIotConstants.cs index aef7b0f559..d42f6dcc7d 100644 --- a/iothub/device/src/Transport/AmqpIot/AmqpIotConstants.cs +++ b/iothub/device/src/Transport/AmqpIot/AmqpIotConstants.cs @@ -27,6 +27,7 @@ internal static class AmqpIotConstants internal static readonly Accepted AcceptedOutcome = AmqpConstants.AcceptedOutcome; internal const string ResponseStatusName = "status"; + internal const string ResponseVersionName = "version"; internal const string TelemetrySenderLinkSuffix = "TelemetrySenderLink"; internal const string TelemetryReceiveLinkSuffix = "TelemetryReceiverLink"; internal const string EventsReceiverLinkSuffix = "EventsReceiverLink"; 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/AmqpIot/AmqpIotReceivingLink.cs b/iothub/device/src/Transport/AmqpIot/AmqpIotReceivingLink.cs index 6efe36b415..b16f846219 100644 --- a/iothub/device/src/Transport/AmqpIot/AmqpIotReceivingLink.cs +++ b/iothub/device/src/Transport/AmqpIot/AmqpIotReceivingLink.cs @@ -24,7 +24,7 @@ internal class AmqpIotReceivingLink private Action _onEventsReceived; private Action _onDeviceMessageReceived; private Action _onMethodReceived; - private Action _onTwinMessageReceived; + private Action _onTwinMessageReceived; public AmqpIotReceivingLink(ReceivingAmqpLink receivingAmqpLink) { @@ -259,7 +259,7 @@ private void DisposeDelivery(AmqpMessage amqpMessage, bool settled, Accepted acc #region Twin handling - internal void RegisterTwinListener(Action onDesiredPropertyReceived) + internal void RegisterTwinListener(Action onDesiredPropertyReceived) { _onTwinMessageReceived = onDesiredPropertyReceived; _receivingAmqpLink.RegisterMessageListener(OnTwinChangesReceived); @@ -278,9 +278,6 @@ private void OnTwinChangesReceived(AmqpMessage amqpMessage) string correlationId = amqpMessage.Properties?.CorrelationId?.ToString(); int status = GetStatus(amqpMessage); - Twin twin = null; - TwinCollection twinProperties = null; - if (status >= 400) { // Handle failures @@ -295,43 +292,12 @@ private void OnTwinChangesReceived(AmqpMessage amqpMessage) // Retry for Http status code request timeout, Too many requests and server errors var exception = new IotHubException(error, status >= 500 || status == 429 || status == 408); - _onTwinMessageReceived.Invoke(null, correlationId, null, exception); + _onTwinMessageReceived.Invoke(null, correlationId, exception); } } else { - if (correlationId == null) - { - // Here we are getting desired property update notifications and want to handle it first - using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); - string patch = reader.ReadToEnd(); - twinProperties = JsonConvert.DeserializeObject(patch); - } - else if (correlationId.StartsWith(AmqpTwinMessageType.Get.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This a response of a GET TWIN so return (set) the full twin - using var reader = new StreamReader(amqpMessage.BodyStream, System.Text.Encoding.UTF8); - string body = reader.ReadToEnd(); - TwinProperties properties = JsonConvert.DeserializeObject(body); - twin = new Twin(properties); - } - else if (correlationId.StartsWith(AmqpTwinMessageType.Patch.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This can be used to coorelate success response with updating reported properties - // However currently we do not have it as request response style implementation - Logging.Info("Updated twin reported properties successfully", nameof(OnTwinChangesReceived)); - } - else if (correlationId.StartsWith(AmqpTwinMessageType.Put.ToString(), StringComparison.OrdinalIgnoreCase)) - { - // This is an acknowledgement received from service for subscribing to desired property updates - Logging.Info("Subscribed for twin successfully", nameof(OnTwinChangesReceived)); - } - else - { - // This shouldn't happen - Logging.Info("Received a correlation Id for Twin operation that does not match Get, Patch or Put request", nameof(OnTwinChangesReceived)); - } - _onTwinMessageReceived.Invoke(twin, correlationId, twinProperties, null); + _onTwinMessageReceived.Invoke(amqpMessage, correlationId, null); } } finally diff --git a/iothub/device/src/Transport/AmqpIot/AmqpIotSendingLink.cs b/iothub/device/src/Transport/AmqpIot/AmqpIotSendingLink.cs index ae932f68d5..d94e7e5e84 100644 --- a/iothub/device/src/Transport/AmqpIot/AmqpIotSendingLink.cs +++ b/iothub/device/src/Transport/AmqpIot/AmqpIotSendingLink.cs @@ -223,17 +223,14 @@ internal async Task SendTwinGetMessageAsync(string correlationId return new AmqpIotOutcome(outcome); } - internal async Task SendTwinPatchMessageAsync(string correlationId, TwinCollection reportedProperties, TimeSpan timeout) + internal async Task SendTwinPatchMessageAsync(string correlationId, Stream reportedProperties, TimeSpan timeout) { if (Logging.IsEnabled) { Logging.Enter(this, nameof(SendTwinPatchMessageAsync)); } - string body = JsonConvert.SerializeObject(reportedProperties); - var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(body)); - - using var amqpMessage = AmqpMessage.Create(bodyStream, true); + using var amqpMessage = AmqpMessage.Create(reportedProperties, true); amqpMessage.Properties.CorrelationId = correlationId; amqpMessage.MessageAnnotations.Map["operation"] = "PATCH"; amqpMessage.MessageAnnotations.Map["resource"] = "/properties/reported"; diff --git a/iothub/device/src/Transport/DefaultDelegatingHandler.cs b/iothub/device/src/Transport/DefaultDelegatingHandler.cs index 19f9e4a4f5..d115e66116 100644 --- a/iothub/device/src/Transport/DefaultDelegatingHandler.cs +++ b/iothub/device/src/Transport/DefaultDelegatingHandler.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Shared; @@ -178,28 +179,28 @@ public virtual Task DisableTwinPatchAsync(CancellationToken cancellationToken) return InnerHandler?.DisableTwinPatchAsync(cancellationToken) ?? TaskHelpers.CompletedTask; } - public virtual Task SendTwinGetAsync(CancellationToken cancellationToken) + public virtual Task EnableEventReceiveAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); - return InnerHandler?.SendTwinGetAsync(cancellationToken) ?? Task.FromResult((Twin)null); + return InnerHandler?.EnableEventReceiveAsync(cancellationToken) ?? TaskHelpers.CompletedTask; } - public virtual Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) + public virtual Task DisableEventReceiveAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); - return InnerHandler?.SendTwinPatchAsync(reportedProperties, cancellationToken) ?? TaskHelpers.CompletedTask; + return InnerHandler?.DisableEventReceiveAsync(cancellationToken) ?? TaskHelpers.CompletedTask; } - public virtual Task EnableEventReceiveAsync(CancellationToken cancellationToken) + public virtual Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); - return InnerHandler?.EnableEventReceiveAsync(cancellationToken) ?? TaskHelpers.CompletedTask; + return InnerHandler?.GetClientTwinPropertiesAsync(cancellationToken) ?? Task.FromResult(default); } - public virtual Task DisableEventReceiveAsync(CancellationToken cancellationToken) + public virtual Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { ThrowIfDisposed(); - return InnerHandler?.DisableEventReceiveAsync(cancellationToken) ?? TaskHelpers.CompletedTask; + return InnerHandler?.SendClientTwinPropertyPatchAsync(reportedProperties, cancellationToken) ?? Task.FromResult(null); } public virtual Task GetPropertiesAsync(PayloadConvention payloadConvention, CancellationToken cancellationToken) diff --git a/iothub/device/src/Transport/ErrorDelegatingHandler.cs b/iothub/device/src/Transport/ErrorDelegatingHandler.cs index 058f6ce00e..2b231e6209 100644 --- a/iothub/device/src/Transport/ErrorDelegatingHandler.cs +++ b/iothub/device/src/Transport/ErrorDelegatingHandler.cs @@ -105,16 +105,6 @@ public override Task DisableTwinPatchAsync(CancellationToken cancellationToken) return ExecuteWithErrorHandlingAsync(() => base.DisableTwinPatchAsync(cancellationToken)); } - public override Task SendTwinGetAsync(CancellationToken cancellationToken) - { - return ExecuteWithErrorHandlingAsync(() => base.SendTwinGetAsync(cancellationToken)); - } - - public override Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) - { - return ExecuteWithErrorHandlingAsync(() => base.SendTwinPatchAsync(reportedProperties, cancellationToken)); - } - public override Task AbandonAsync(string lockToken, CancellationToken cancellationToken) { return ExecuteWithErrorHandlingAsync(() => base.AbandonAsync(lockToken, cancellationToken)); @@ -145,14 +135,14 @@ public override Task SendMethodResponseAsync(MethodResponseInternal methodRespon return ExecuteWithErrorHandlingAsync(() => base.SendMethodResponseAsync(methodResponse, cancellationToken)); } - public override Task GetPropertiesAsync(PayloadConvention payloadConvention, CancellationToken cancellationToken) + public override Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { - return ExecuteWithErrorHandlingAsync(() => base.GetPropertiesAsync(payloadConvention, cancellationToken)); + return ExecuteWithErrorHandlingAsync(() => base.GetClientTwinPropertiesAsync(cancellationToken)); } - public override Task SendPropertyPatchAsync(ClientPropertyCollection reportedProperties, CancellationToken cancellationToken) + public override Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { - return ExecuteWithErrorHandlingAsync(() => base.SendPropertyPatchAsync(reportedProperties, cancellationToken)); + return ExecuteWithErrorHandlingAsync(() => base.SendClientTwinPropertyPatchAsync(reportedProperties, cancellationToken)); } private static bool IsNetworkExceptionChain(Exception exceptionChain) diff --git a/iothub/device/src/Transport/HttpTransportHandler.cs b/iothub/device/src/Transport/HttpTransportHandler.cs index 354a496e3d..0ebe9f6243 100644 --- a/iothub/device/src/Transport/HttpTransportHandler.cs +++ b/iothub/device/src/Transport/HttpTransportHandler.cs @@ -255,11 +255,6 @@ await _httpClientHelper.PostAsync( cancellationToken).ConfigureAwait(false); } - public override Task SendTwinGetAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException("Device twins are only supported with Mqtt protocol."); - } - public override async Task ReceiveAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -403,14 +398,14 @@ public override Task RejectAsync(string lockToken, CancellationToken cancellatio cancellationToken); } - public override Task GetPropertiesAsync(PayloadConvention payloadConvention, CancellationToken cancellationToken) + public override Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { - throw new NotImplementedException("Property operations are not supported over HTTP. Please use MQTT protocol instead."); + throw new NotImplementedException("This operation is not supported over HTTP. Please use MQTT protocol instead."); } - public override Task SendPropertyPatchAsync(ClientPropertyCollection reportedProperties, CancellationToken cancellationToken) + public override Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { - throw new NotImplementedException("Property operations are not supported over HTTP. Please use MQTT protocol instead."); + throw new NotImplementedException("This operation is not supported over HTTP. Please use MQTT protocol instead."); } // This is for invoking methods from an edge module to another edge device or edge module. diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs index e464351276..249f571ae8 100644 --- a/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs +++ b/iothub/device/src/Transport/Mqtt/MqttTransportHandler.cs @@ -917,36 +917,26 @@ public override async Task DisableTwinPatchAsync(CancellationToken cancellationT Logging.Exit(this, cancellationToken, nameof(DisableTwinPatchAsync)); } - public override async Task SendTwinGetAsync(CancellationToken cancellationToken) + public override async Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - EnsureValidState(); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_014: `SendTwinGetAsync` shall allocate a `Message` object to hold the `GET` request using var request = new Message(); - - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_015: `SendTwinGetAsync` shall generate a GUID to use as the $rid property on the request - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_016: `SendTwinGetAsync` shall set the `Message` topic to '$iothub/twin/GET/?$rid=' where REQUEST_ID is the GUID that was generated string rid = Guid.NewGuid().ToString(); request.MqttTopicName = TwinGetTopic.FormatInvariant(rid); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_017: `SendTwinGetAsync` shall wait for a response from the service with a matching $rid value - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_019: If the response is failed, `SendTwinGetAsync` shall return that failure to the caller. - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_020: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinGetAsync` shall fail with a timeout error using Message response = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_021: If the response contains a success code, `SendTwinGetAsync` shall return success to the caller - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_018: When a response is received, `SendTwinGetAsync` shall return the Twin object to the caller - using var reader = new StreamReader(response.GetBodyStream(), System.Text.Encoding.UTF8); + // We will use UTF-8 for decoding the service response. This is because UTF-8 is the only currently supported encoding format. + using var reader = new StreamReader(response.GetBodyStream(), DefaultPayloadConvention.Instance.PayloadEncoder.ContentEncoding); string body = reader.ReadToEnd(); try { - return new Twin - { - Properties = JsonConvert.DeserializeObject(body), - }; + // We will use NewtonSoft Json to deserialize the service response to the appropriate type; i.e. Twin for non-convention-based operation + // and ClientProperties for convention-based operations. + return JsonConvert.DeserializeObject(body); } catch (JsonReaderException ex) { @@ -957,29 +947,29 @@ public override async Task SendTwinGetAsync(CancellationToken cancellation } } - public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) + public override async Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); EnsureValidState(); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_025: `SendTwinPatchAsync` shall serialize the `reported` object into a JSON string - string body = JsonConvert.SerializeObject(reportedProperties); - using var bodyStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body)); + using var request = new Message(reportedProperties); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_022: `SendTwinPatchAsync` shall allocate a `Message` object to hold the update request - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_026: `SendTwinPatchAsync` shall set the body of the message to the JSON string - using var request = new Message(bodyStream); - - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_023: `SendTwinPatchAsync` shall generate a GUID to use as the $rid property on the request - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_024: `SendTwinPatchAsync` shall set the `Message` topic to '$iothub/twin/PATCH/properties/reported/?$rid=' where REQUEST_ID is the GUID that was generated string rid = Guid.NewGuid().ToString(); request.MqttTopicName = TwinPatchTopic.FormatInvariant(rid); - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_027: `SendTwinPatchAsync` shall wait for a response from the service with a matching $rid value - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_028: If the response is failed, `SendTwinPatchAsync` shall return that failure to the caller. - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_029: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinPatchAsync` shall fail with a timeout error. - // Codes_SRS_CSHARP_MQTT_TRANSPORT_18_030: If the response contains a success code, `SendTwinPatchAsync` shall return success to the caller. - await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); + using Message message = await SendTwinRequestAsync(request, rid, cancellationToken).ConfigureAwait(false); + + var response = new ClientPropertiesUpdateResponse(); + if (message.Properties.TryGetValue(RequestIdKey, out string requestIdRetrieved)) + { + response.RequestId = requestIdRetrieved; + } + if (message.Properties.TryGetValue(VersionKey, out string versionRetrievedAsString)) + { + response.Version = long.Parse(versionRetrievedAsString, CultureInfo.InvariantCulture); + } + + return response; } public override async Task GetPropertiesAsync(PayloadConvention payloadConvention, CancellationToken cancellationToken) diff --git a/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs b/iothub/device/src/Transport/Mqtt/MqttTransportSettings.cs index 99fe30cbd9..e899c55994 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; } @@ -198,7 +200,7 @@ public bool CertificateRevocationCheck /// Setting a will message is a way for clients to notify other subscribed clients about ungraceful disconnects in an appropriate way. /// In response to the ungraceful disconnect, the service will send the last-will message to the configured telemetry channel. /// The telemetry channel can be either the default Events endpoint or a custom endpoint defined by IoT Hub routing. - /// For more details, refer to https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. + /// For more details, refer to https://docs.microsoft.com/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. /// public bool HasWill { get; set; } @@ -207,7 +209,7 @@ public bool CertificateRevocationCheck /// /// /// The telemetry channel can be either the default Events endpoint or a custom endpoint defined by IoT Hub routing. - /// For more details, refer to https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. + /// For more details, refer to https://docs.microsoft.com/azure/iot-hub/iot-hub-mqtt-support#using-the-mqtt-protocol-directly-as-a-device. /// public IWillMessage WillMessage { get; set; } diff --git a/iothub/device/src/Transport/RetryDelegatingHandler.cs b/iothub/device/src/Transport/RetryDelegatingHandler.cs index 35ead90d7b..b328d89685 100644 --- a/iothub/device/src/Transport/RetryDelegatingHandler.cs +++ b/iothub/device/src/Transport/RetryDelegatingHandler.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -496,113 +497,113 @@ await _internalRetryPolicy } } - public override async Task SendTwinGetAsync(CancellationToken cancellationToken) + public override async Task CompleteAsync(string lockToken, CancellationToken cancellationToken) { try { - Logging.Enter(this, cancellationToken, nameof(SendTwinGetAsync)); + Logging.Enter(this, lockToken, cancellationToken, nameof(CompleteAsync)); - return await _internalRetryPolicy + await _internalRetryPolicy .ExecuteAsync( async () => { await EnsureOpenedAsync(cancellationToken).ConfigureAwait(false); - return await base.SendTwinGetAsync(cancellationToken).ConfigureAwait(false); + await base.CompleteAsync(lockToken, cancellationToken).ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); } finally { - Logging.Exit(this, cancellationToken, nameof(SendTwinGetAsync)); + Logging.Exit(this, lockToken, cancellationToken, nameof(CompleteAsync)); } } - public override async Task SendTwinPatchAsync(TwinCollection reportedProperties, CancellationToken cancellationToken) + public override async Task AbandonAsync(string lockToken, CancellationToken cancellationToken) { try { - Logging.Enter(this, reportedProperties, cancellationToken, nameof(SendTwinPatchAsync)); + Logging.Enter(this, lockToken, cancellationToken, nameof(AbandonAsync)); await _internalRetryPolicy .ExecuteAsync( async () => { await EnsureOpenedAsync(cancellationToken).ConfigureAwait(false); - await base.SendTwinPatchAsync(reportedProperties, cancellationToken).ConfigureAwait(false); + await base.AbandonAsync(lockToken, cancellationToken).ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); } finally { - Logging.Exit(this, reportedProperties, cancellationToken, nameof(SendTwinPatchAsync)); + Logging.Exit(this, lockToken, cancellationToken, nameof(AbandonAsync)); } } - public override async Task CompleteAsync(string lockToken, CancellationToken cancellationToken) + public override async Task RejectAsync(string lockToken, CancellationToken cancellationToken) { try { - Logging.Enter(this, lockToken, cancellationToken, nameof(CompleteAsync)); + Logging.Enter(this, lockToken, cancellationToken, nameof(RejectAsync)); await _internalRetryPolicy .ExecuteAsync( async () => { await EnsureOpenedAsync(cancellationToken).ConfigureAwait(false); - await base.CompleteAsync(lockToken, cancellationToken).ConfigureAwait(false); + await base.RejectAsync(lockToken, cancellationToken).ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); } finally { - Logging.Exit(this, lockToken, cancellationToken, nameof(CompleteAsync)); + Logging.Exit(this, lockToken, cancellationToken, nameof(RejectAsync)); } } - public override async Task AbandonAsync(string lockToken, CancellationToken cancellationToken) + public override async Task GetClientTwinPropertiesAsync(CancellationToken cancellationToken) { try { - Logging.Enter(this, lockToken, cancellationToken, nameof(AbandonAsync)); + Logging.Enter(this, cancellationToken, nameof(GetClientTwinPropertiesAsync)); - await _internalRetryPolicy + return await _internalRetryPolicy .ExecuteAsync( async () => { await EnsureOpenedAsync(cancellationToken).ConfigureAwait(false); - await base.AbandonAsync(lockToken, cancellationToken).ConfigureAwait(false); + return await base.GetClientTwinPropertiesAsync(cancellationToken).ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); } finally { - Logging.Exit(this, lockToken, cancellationToken, nameof(AbandonAsync)); + Logging.Exit(this, cancellationToken, nameof(GetClientTwinPropertiesAsync)); } } - public override async Task RejectAsync(string lockToken, CancellationToken cancellationToken) + public override async Task SendClientTwinPropertyPatchAsync(Stream reportedProperties, CancellationToken cancellationToken) { try { - Logging.Enter(this, lockToken, cancellationToken, nameof(RejectAsync)); + Logging.Enter(this, reportedProperties, cancellationToken, nameof(SendClientTwinPropertyPatchAsync)); - await _internalRetryPolicy + return await _internalRetryPolicy .ExecuteAsync( async () => { await EnsureOpenedAsync(cancellationToken).ConfigureAwait(false); - await base.RejectAsync(lockToken, cancellationToken).ConfigureAwait(false); + return await base.SendClientTwinPropertyPatchAsync(reportedProperties, cancellationToken).ConfigureAwait(false); }, cancellationToken) .ConfigureAwait(false); } finally { - Logging.Exit(this, lockToken, cancellationToken, nameof(RejectAsync)); + Logging.Exit(this, reportedProperties, cancellationToken, nameof(SendClientTwinPropertyPatchAsync)); } } diff --git a/iothub/device/src/WritableClientProperty.cs b/iothub/device/src/WritableClientProperty.cs new file mode 100644 index 0000000000..409c876048 --- /dev/null +++ b/iothub/device/src/WritableClientProperty.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Azure.Devices.Shared; + +namespace Microsoft.Azure.Devices.Client +{ + /// + /// The writable property update request received from service. + /// + /// + /// A writable property update request should be acknowledged by the device or module by sending a reported property. + /// This type contains a convenience method to format the reported property as per IoT Plug and Play convention. + /// For more details see . + /// + public class WritableClientProperty + { + internal WritableClientProperty() + { + } + + /// + /// The value of the writable property update request. + /// + public object Value { get; internal set; } + + /// + /// The version number associated with the writable property update request. + /// + public long Version { get; internal set; } + + internal PayloadConvention Convention { get; set; } + + /// + /// Creates a writable property update response that can be reported back to the service. + /// + /// + /// This writable property update response will contain the property value and version supplied in the writable property update request. + /// If you would like to construct your own writable property update response with custom value and version number, you can + /// create an instance of . + /// See for more details. + /// + /// An acknowledgment code that uses an HTTP status code. + /// An optional acknowledgment description. + /// A writable property update response that can be reported back to the service. + public IWritablePropertyResponse AcknowledgeWith(int statusCode, string description = default) + { + return Convention.PayloadSerializer.CreateWritablePropertyResponse(Value, statusCode, Version, description); + } + } +} diff --git a/iothub/device/tests/ClientPropertiesTests.cs b/iothub/device/tests/ClientPropertiesTests.cs index 4dff48991e..243c01a5d3 100644 --- a/iothub/device/tests/ClientPropertiesTests.cs +++ b/iothub/device/tests/ClientPropertiesTests.cs @@ -7,7 +7,7 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Microsoft.Azure.Devices.Client.Tests +namespace Microsoft.Azure.Devices.Client.Test { [TestClass] [TestCategory("Unit")] @@ -57,7 +57,7 @@ public void ClientPropertyCollection_CanEnumerateClientProperties() // assert // These are the device reported property values. - foreach (var deviceReportedKeyValuePairs in clientProperties) + foreach (var deviceReportedKeyValuePairs in clientProperties.ReportedFromClient) { if (deviceReportedKeyValuePairs.Key.Equals(StringPropertyName)) { @@ -78,7 +78,7 @@ public void ClientPropertyCollection_CanEnumerateClientProperties() } // These are the property values for which service has requested an update. - foreach (var updateRequestedKeyValuePairs in clientProperties.Writable) + foreach (var updateRequestedKeyValuePairs in clientProperties.WritablePropertyRequests) { if (updateRequestedKeyValuePairs.Key.Equals(DoublePropertyName)) { diff --git a/iothub/device/tests/ClientPropertyCollectionTests.cs b/iothub/device/tests/ClientPropertyCollectionTests.cs index f9ea6caa2e..4e953ac53b 100644 --- a/iothub/device/tests/ClientPropertyCollectionTests.cs +++ b/iothub/device/tests/ClientPropertyCollectionTests.cs @@ -6,8 +6,9 @@ using FluentAssertions; using Microsoft.Azure.Devices.Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; -namespace Microsoft.Azure.Devices.Client.Tests +namespace Microsoft.Azure.Devices.Client.Test { [TestClass] [TestCategory("Unit")] @@ -56,6 +57,7 @@ public class ClientPropertyCollectionTests [TestMethod] public void ClientPropertyCollection_CanAddSimpleObjectsAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection { { StringPropertyName, StringPropertyValue }, @@ -70,6 +72,8 @@ public void ClientPropertyCollection_CanAddSimpleObjectsAndGetBackWithoutDeviceC { DateTimePropertyName, s_dateTimePropertyValue } }; + // act, assert + clientProperties.TryGetValue(StringPropertyName, out string stringOutValue); stringOutValue.Should().Be(StringPropertyValue); @@ -107,22 +111,30 @@ public void ClientPropertyCollection_CanAddSimpleObjectsAndGetBackWithoutDeviceC [TestMethod] public void ClientPropertyCollection_AddSimpleObjectAgainThrowsException() { + // arrange var clientProperties = new ClientPropertyCollection { { StringPropertyName, StringPropertyValue } }; + // act Action act = () => clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + + // assert act.Should().Throw("\"Add\" method does not support adding a key that already exists in the collection."); } [TestMethod] public void ClientPropertyCollection_CanUpdateSimpleObjectAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection { { StringPropertyName, StringPropertyValue } }; + + // act, assert + clientProperties.TryGetValue(StringPropertyName, out string outValue); outValue.Should().Be(StringPropertyValue); @@ -134,10 +146,12 @@ public void ClientPropertyCollection_CanUpdateSimpleObjectAndGetBackWithoutDevic [TestMethod] public void ClientPropertyCollection_CanAddNullPropertyAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); clientProperties.AddRootProperty(IntPropertyName, null); + // act, assert clientProperties.TryGetValue(StringPropertyName, out string outStringValue); outStringValue.Should().Be(StringPropertyValue); @@ -149,10 +163,13 @@ public void ClientPropertyCollection_CanAddNullPropertyAndGetBackWithoutDeviceCl [TestMethod] public void ClientPropertyCollection_CanAddMultiplePropertyAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); clientProperties.AddRootProperty(IntPropertyName, IntPropertyValue); + // act, assert + clientProperties.TryGetValue(StringPropertyName, out string outStringValue); outStringValue.Should().Be(StringPropertyValue); @@ -160,24 +177,57 @@ public void ClientPropertyCollection_CanAddMultiplePropertyAndGetBackWithoutDevi outIntValue.Should().Be(IntPropertyValue); } + [TestMethod] + public void ClientPropertyCollection_TryGetValueShouldReturnFalseIfValueNotFound() + { + // arrange + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(IntPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollection_TryGetValueShouldReturnFalseIfValueCouldNotBeDeserialized() + { + // arrange + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddRootProperty(StringPropertyName, StringPropertyValue); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(StringPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + [TestMethod] public void ClientPropertyCollection_CanAddSimpleObjectWithComponentAndGetBackWithoutDeviceClient() { - var clientProperties = new ClientPropertyCollection + // arrange + var componentLevelProperties = new Dictionary { - { ComponentName, new Dictionary { - { StringPropertyName, StringPropertyValue }, - { BoolPropertyName, BoolPropertyValue }, - { DoublePropertyName, DoublePropertyValue }, - { FloatPropertyName, FloatPropertyValue }, - { IntPropertyName, IntPropertyValue }, - { ShortPropertyName, ShortPropertyValue }, - { ObjectPropertyName, s_objectPropertyValue }, - { ArrayPropertyName, s_arrayPropertyValue }, - { MapPropertyName, s_mapPropertyValue }, - { DateTimePropertyName, s_dateTimePropertyValue } } - } + { StringPropertyName, StringPropertyValue }, + { BoolPropertyName, BoolPropertyValue }, + { DoublePropertyName, DoublePropertyValue }, + { FloatPropertyName, FloatPropertyValue }, + { IntPropertyName, IntPropertyValue }, + { ShortPropertyName, ShortPropertyValue }, + { ObjectPropertyName, s_objectPropertyValue }, + { ArrayPropertyName, s_arrayPropertyValue }, + { MapPropertyName, s_mapPropertyValue }, + { DateTimePropertyName, s_dateTimePropertyValue } }; + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperties(ComponentName, componentLevelProperties); + + // act, assert clientProperties.TryGetValue(ComponentName, StringPropertyName, out string stringOutValue); stringOutValue.Should().Be(StringPropertyValue); @@ -216,19 +266,26 @@ public void ClientPropertyCollection_CanAddSimpleObjectWithComponentAndGetBackWi [TestMethod] public void ClientPropertyCollection_AddSimpleObjectWithComponentAgainThrowsException() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + // act Action act = () => clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + // assert act.Should().Throw("\"Add\" method does not support adding a key that already exists in the collection."); } [TestMethod] public void ClientPropertyCollection_CanUpdateSimpleObjectWithComponentAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + // act, assert + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); outValue.Should().Be(StringPropertyValue); @@ -240,10 +297,13 @@ public void ClientPropertyCollection_CanUpdateSimpleObjectWithComponentAndGetBac [TestMethod] public void ClientPropertyCollection_CanAddNullPropertyWithComponentAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); clientProperties.AddComponentProperty(ComponentName, IntPropertyName, null); + // act, assert + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outStringValue); outStringValue.Should().Be(StringPropertyValue); @@ -255,10 +315,13 @@ public void ClientPropertyCollection_CanAddNullPropertyWithComponentAndGetBackWi [TestMethod] public void ClientPropertyCollection_CanAddMultiplePropertyWithComponentAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); clientProperties.AddComponentProperty(ComponentName, IntPropertyName, IntPropertyValue); + // act, assert + clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outStringValue); outStringValue.Should().Be(StringPropertyValue); @@ -269,12 +332,15 @@ public void ClientPropertyCollection_CanAddMultiplePropertyWithComponentAndGetBa [TestMethod] public void ClientPropertyCollection_CanAddSimpleWritablePropertyAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); - var writableResponse = new NewtonsoftJsonWritablePropertyResponse(StringPropertyValue, CommonClientResponseCodes.OK, 2, WritablePropertyDescription); clientProperties.AddRootProperty(StringPropertyName, writableResponse); + // act clientProperties.TryGetValue(StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + + // assert outValue.Value.Should().Be(writableResponse.Value); outValue.AckCode.Should().Be(writableResponse.AckCode); outValue.AckVersion.Should().Be(writableResponse.AckVersion); @@ -284,12 +350,15 @@ public void ClientPropertyCollection_CanAddSimpleWritablePropertyAndGetBackWitho [TestMethod] public void ClientPropertyCollection_CanAddWritablePropertyWithComponentAndGetBackWithoutDeviceClient() { + // arrange var clientProperties = new ClientPropertyCollection(); - var writableResponse = new NewtonsoftJsonWritablePropertyResponse(StringPropertyValue, CommonClientResponseCodes.OK, 2, WritablePropertyDescription); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, writableResponse); + // act clientProperties.TryGetValue(ComponentName, StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + + // assert outValue.Value.Should().Be(writableResponse.Value); outValue.AckCode.Should().Be(writableResponse.AckCode); outValue.AckVersion.Should().Be(writableResponse.AckVersion); @@ -299,15 +368,464 @@ public void ClientPropertyCollection_CanAddWritablePropertyWithComponentAndGetBa [TestMethod] public void ClientPropertyCollection_AddingComponentAddsComponentIdentifier() { + // arrange var clientProperties = new ClientPropertyCollection(); clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + // act clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); clientProperties.TryGetValue(ComponentName, ConventionBasedConstants.ComponentIdentifierKey, out string componentOut); + // assert outValue.Should().Be(StringPropertyValue); componentOut.Should().Be(ConventionBasedConstants.ComponentIdentifierValue); } + + [TestMethod] + public void ClientPropertyCollection_TryGetValueWithComponentShouldReturnFalseIfValueNotFound() + { + // arrange + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(ComponentName, IntPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollection_TryGetValueWithComponentShouldReturnFalseIfValueCouldNotBeDeserialized() + { + // arrange + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddComponentProperty(ComponentName, StringPropertyName, StringPropertyValue); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(ComponentName, StringPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollection_TryGetValueWithComponentShouldReturnFalseIfNotAComponent() + { + // arrange + var clientProperties = new ClientPropertyCollection(); + clientProperties.AddRootProperty(MapPropertyName, s_mapPropertyValue); + string incorrectlyMappedComponentName = MapPropertyName; + string incorrectlyMappedComponentPropertyName = "key1"; + + // act + bool isValueRetrieved = clientProperties.TryGetValue(incorrectlyMappedComponentName, incorrectlyMappedComponentPropertyName, out object propertyValue); + + // assert + isValueRetrieved.Should().BeFalse(); + propertyValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullPropertyNameThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddRootProperty(null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullPropertyNameThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddOrUpdateRootProperty(null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullPropertyValueSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove property "abc" from its properties. + testPropertyCollection.AddRootProperty("abc", null); + + // assert + bool isValueRetrieved = testPropertyCollection.TryGetValue("abc", out object propertyValue); + isValueRetrieved.Should().BeTrue(); + propertyValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullPropertyValueSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove property "abc" from its properties. + testPropertyCollection.AddOrUpdateRootProperty("abc", null); + + // assert + bool isValueRetrieved = testPropertyCollection.TryGetValue("abc", out object propertyValue); + isValueRetrieved.Should().BeTrue(); + propertyValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddPropertyValueAlreadyExistsThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddRootProperty("abc", 123); + + // act + Action testAction = () => testPropertyCollection.AddRootProperty("abc", 1); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdatePropertyValueAlreadyExistsSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddRootProperty("abc", 123); + + // act + testPropertyCollection.AddOrUpdateRootProperty("abc", 1); + + // assert + bool isValueRetrieved = testPropertyCollection.TryGetValue("abc", out int propertyValue); + isValueRetrieved.Should().BeTrue(); + propertyValue.Should().Be(1); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullClientPropertyCollectionThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddRootProperties(null); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullClientPropertyCollectionThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddOrUpdateRootProperties(null); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddClientPropertyCollectionAlreadyExistsThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddRootProperty("abc", 123); + var propertyValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + Action testAction = () => testPropertyCollection.AddRootProperties(propertyValues); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateClientPropertyCollectionAlreadyExistsSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddRootProperty("abc", 123); + var propertyValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + testPropertyCollection.AddOrUpdateRootProperties(propertyValues); + + // assert + bool isValue1Retrieved = testPropertyCollection.TryGetValue("qwe", out int value1Retrieved); + isValue1Retrieved.Should().BeTrue(); + value1Retrieved.Should().Be(98); + + bool isValue2Retrieved = testPropertyCollection.TryGetValue("abc", out int value2Retrieved); + isValue2Retrieved.Should().BeTrue(); + value2Retrieved.Should().Be(2); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullPropertyNameWithComponentThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddComponentProperty("testComponent", null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullPropertyNameWithComponentThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddOrUpdateComponentProperty("testComponent", null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullComponentNameThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddComponentProperty(null, "abc", 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullComponentNameThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + + // act + Action testAction = () => testPropertyCollection.AddOrUpdateComponentProperty(null, "abc", 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullPropertyValueWithComponentSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "qwe", 123); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove property "abc" from its properties. + testPropertyCollection.AddComponentProperty("testComponent", "abc", null); + + // assert + bool isValue1Retrieved = testPropertyCollection.TryGetValue("testComponent", "qwe", out int property1Value); + isValue1Retrieved.Should().BeTrue(); + property1Value.Should().Be(123); + + bool isValue2Retrieved = testPropertyCollection.TryGetValue("testComponent", "abc", out object property2Value); + isValue2Retrieved.Should().BeTrue(); + property2Value.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullPropertyValueWithComponentSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "qwe", 123); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove property "abc" from its properties. + testPropertyCollection.AddOrUpdateComponentProperty("testComponent", "abc", null); + + // assert + bool isValue1Retrieved = testPropertyCollection.TryGetValue("testComponent", "qwe", out int property1Value); + isValue1Retrieved.Should().BeTrue(); + property1Value.Should().Be(123); + + bool isValue2Retrieved = testPropertyCollection.TryGetValue("testComponent", "abc", out object property2Value); + isValue2Retrieved.Should().BeTrue(); + property2Value.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddNullClientPropertyCollectionWithComponentSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.Convention = DefaultPayloadConvention.Instance; + testPropertyCollection.AddComponentProperty("testComponent", "qwe", 98); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove component "testComponent" from its properties. + testPropertyCollection.AddComponentProperties("testComponent", null); + + // assert + bool iscomponentValueRetrieved = testPropertyCollection.TryGetValue("testComponent", "qwe", out int property2Value); + iscomponentValueRetrieved.Should().BeFalse(); + + bool iscomponentRetrieved = testPropertyCollection.TryGetValue("testComponent", out object componentValue); + iscomponentRetrieved.Should().BeTrue(); + componentValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateNullClientPropertyCollectionWithComponentThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.Convention = DefaultPayloadConvention.Instance; + testPropertyCollection.AddComponentProperty("testComponent", "qwe", 98); + + // act + // This should add an entry in the dictionary with a null value. + // This patch would be interpreted by the service as the client wanting to remove component "testComponent" from its properties. + testPropertyCollection.AddOrUpdateComponentProperties("testComponent", null); + + // assert + bool iscomponentValueRetrieved = testPropertyCollection.TryGetValue("testComponent", "qwe", out int property2Value); + iscomponentValueRetrieved.Should().BeFalse(); + + bool iscomponentRetrieved = testPropertyCollection.TryGetValue("testComponent", out object componentValue); + iscomponentRetrieved.Should().BeTrue(); + componentValue.Should().BeNull(); + } + + [TestMethod] + public void ClientPropertyCollection_AddPropertyValueAlreadyExistsWithComponentThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "abc", 123); + + // act + Action testAction = () => testPropertyCollection.AddComponentProperty("testComponent", "abc", 1); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdatePropertyValueAlreadyExistsWithComponentSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "abc", 123); + + // act + testPropertyCollection.AddOrUpdateComponentProperty("testComponent", "abc", 1); + + // assert + bool isValueRetrieved = testPropertyCollection.TryGetValue("testComponent", "abc", out int propertyValue); + isValueRetrieved.Should().BeTrue(); + propertyValue.Should().Be(1); + } + + [TestMethod] + public void ClientPropertyCollection_AddClientPropertyCollectionAlreadyExistsWithComponentThrows() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "abc", 123); + var propertyValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + Action testAction = () => testPropertyCollection.AddComponentProperties("testComponent", propertyValues); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void ClientPropertyCollection_AddOrUpdateClientPropertyCollectionAlreadyExistsWithComponentSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + testPropertyCollection.AddComponentProperty("testComponent", "abc", 123); + var propertyValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + testPropertyCollection.AddOrUpdateComponentProperties("testComponent", propertyValues); + + // assert + bool isValue1Retrieved = testPropertyCollection.TryGetValue("testComponent", "qwe", out int value1Retrieved); + isValue1Retrieved.Should().BeTrue(); + value1Retrieved.Should().Be(98); + + bool isValue2Retrieved = testPropertyCollection.TryGetValue("testComponent", "abc", out int value2Retrieved); + isValue2Retrieved.Should().BeTrue(); + value2Retrieved.Should().Be(2); + } + + [TestMethod] + public void ClientPropertyCollect_AddRawClassSuccess() + { + // arrange + var testPropertyCollection = new ClientPropertyCollection(); + var propertyValues = new CustomClientProperty + { + Id = 12, + Name = "testProperty" + }; + var propertyValuesAsDictionary = JsonConvert.DeserializeObject>(JsonConvert.SerializeObject(propertyValues)); + + // act + testPropertyCollection.AddRootProperties(propertyValuesAsDictionary); + + // assert + bool isIdPresent = testPropertyCollection.TryGetValue("Id", out int id); + isIdPresent.Should().BeTrue(); + id.Should().Be(12); + + bool isNamePresent = testPropertyCollection.TryGetValue("Name", out string name); + isNamePresent.Should().BeTrue(); + name.Should().Be("testProperty"); + } } internal class CustomClientProperty diff --git a/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs b/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs index 07a820febe..3b171a2b43 100644 --- a/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs +++ b/iothub/device/tests/ClientPropertyCollectionTestsNewtonsoft.cs @@ -10,26 +10,38 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Microsoft.Azure.Devices.Client.Tests +namespace Microsoft.Azure.Devices.Client.Test { [TestClass] [TestCategory("Unit")] // These tests test the deserialization of the service response to a ClientPropertyCollection. // This flow is convention aware and uses NewtonSoft.Json for deserialization. - // For the purpose of these tests we will create an instance of a Twin class to simulate the service response. public class ClientPropertyCollectionTestsNewtonsoft { - internal const string BoolPropertyName = "boolPropertyName"; - internal const string DoublePropertyName = "doublePropertyName"; - internal const string FloatPropertyName = "floatPropertyName"; - internal const string IntPropertyName = "intPropertyName"; - internal const string ShortPropertyName = "shortPropertyName"; - internal const string StringPropertyName = "stringPropertyName"; - internal const string ObjectPropertyName = "objectPropertyName"; - internal const string ArrayPropertyName = "arrayPropertyName"; - internal const string MapPropertyName = "mapPropertyName"; - internal const string DateTimePropertyName = "dateTimePropertyName"; + internal const string RootBoolPropertyName = "rootBoolPropertyName"; + internal const string RootDoublePropertyName = "rootDoublePropertyName"; + internal const string RootFloatPropertyName = "rootFloatPropertyName"; + internal const string RootIntPropertyName = "rootIntPropertyName"; + internal const string RootShortPropertyName = "rootShortPropertyName"; + internal const string RootStringPropertyName = "rootStringPropertyName"; + internal const string RootObjectPropertyName = "rootObjectPropertyName"; + internal const string RootArrayPropertyName = "rootArrayPropertyName"; + internal const string RootMapPropertyName = "rootMapPropertyName"; + internal const string RootDateTimePropertyName = "rootDateTimePropertyName"; + internal const string RootWritablePropertyName = "rootWritablePropertyName"; + internal const string ComponentName = "testableComponent"; + internal const string ComponentBoolPropertyName = "componentBoolPropertyName"; + internal const string ComponentDoublePropertyName = "componentDoublePropertyName"; + internal const string ComponentFloatPropertyName = "componentFloatPropertyName"; + internal const string ComponentIntPropertyName = "componentIntPropertyName"; + internal const string ComponentShortPropertyName = "componentShortPropertyName"; + internal const string ComponentStringPropertyName = "componentStringPropertyName"; + internal const string ComponentObjectPropertyName = "componentObjectPropertyName"; + internal const string ComponentArrayPropertyName = "componentArrayPropertyName"; + internal const string ComponentMapPropertyName = "componentMapPropertyName"; + internal const string ComponentDateTimePropertyName = "componentDateTimePropertyName"; + internal const string ComponentWritablePropertyName = "componentWritablePropertyName"; private const bool BoolPropertyValue = false; private const double DoublePropertyValue = 1.001; @@ -58,8 +70,15 @@ public class ClientPropertyCollectionTestsNewtonsoft { "key3", s_objectPropertyValue } }; - // Create an object that represents all of the properties as top-level properties. - private static readonly RootLevelProperties s_rootLevelProperties = new RootLevelProperties + // Create a writable property response with the expected values. + private static readonly IWritablePropertyResponse s_writablePropertyResponse = new NewtonsoftJsonWritablePropertyResponse( + propertyValue: StringPropertyValue, + ackCode: CommonClientResponseCodes.OK, + ackVersion: 2, + ackDescription: "testableWritablePropertyDescription"); + + // Create an object that represents a client instance having top-level and component-level properties. + private static readonly TestProperties s_testClientProperties = new TestProperties { BooleanProperty = BoolPropertyValue, DoubleProperty = DoublePropertyValue, @@ -70,14 +89,9 @@ public class ClientPropertyCollectionTestsNewtonsoft ObjectProperty = s_objectPropertyValue, ArrayProperty = s_arrayPropertyValue, MapProperty = s_mapPropertyValue, - DateTimeProperty = s_dateTimePropertyValue - }; - - // Create an object that represents all of the properties as component-level properties. - // This adds the "__t": "c" component identifier as a part of "ComponentProperties" class declaration. - private static readonly ComponentLevelProperties s_componentLevelProperties = new ComponentLevelProperties - { - Properties = new ComponentProperties + DateTimeProperty = s_dateTimePropertyValue, + WritablePropertyResponse = s_writablePropertyResponse, + ComponentProperties = new ComponentProperties { BooleanProperty = BoolPropertyValue, DoubleProperty = DoublePropertyValue, @@ -88,124 +102,113 @@ public class ClientPropertyCollectionTestsNewtonsoft ObjectProperty = s_objectPropertyValue, ArrayProperty = s_arrayPropertyValue, MapProperty = s_mapPropertyValue, - DateTimeProperty = s_dateTimePropertyValue - }, + DateTimeProperty = s_dateTimePropertyValue, + WritablePropertyResponse = s_writablePropertyResponse, + } }; - // Create a writable property response with the expected values. - private static readonly IWritablePropertyResponse s_writablePropertyResponse = new NewtonsoftJsonWritablePropertyResponse( - propertyValue: StringPropertyValue, - ackCode: CommonClientResponseCodes.OK, - ackVersion: 2, - ackDescription: "testableWritablePropertyDescription"); - - // Create a JObject instance that represents a writable property response sent for a top-level property. - private static readonly JObject s_writablePropertyResponseJObject = new JObject( - new JProperty(StringPropertyName, JObject.FromObject(s_writablePropertyResponse))); - - // Create a JObject instance that represents a writable property response sent for a component-level property. - // This adds the "__t": "c" component identifier to the constructed JObject. - private static readonly JObject s_writablePropertyResponseWithComponentJObject = new JObject( - new JProperty(ComponentName, new JObject( - new JProperty(ConventionBasedConstants.ComponentIdentifierKey, ConventionBasedConstants.ComponentIdentifierValue), - new JProperty(StringPropertyName, JObject.FromObject(s_writablePropertyResponse))))); - - // The above constructed json objects are used for initializing a twin response. - // This is because we are using a Twin instance to simulate the service response. + private static string getClientPropertiesStringResponse = JsonConvert.SerializeObject(new Dictionary { { "reported", s_testClientProperties } }); - private static TwinCollection collectionToRoundTrip = new TwinCollection(JsonConvert.SerializeObject(s_rootLevelProperties)); - private static TwinCollection collectionWithComponentToRoundTrip = new TwinCollection(JsonConvert.SerializeObject(s_componentLevelProperties)); - private static TwinCollection collectionWritablePropertyToRoundTrip = new TwinCollection(s_writablePropertyResponseJObject, null); - private static TwinCollection collectionWritablePropertyWithComponentToRoundTrip = new TwinCollection(s_writablePropertyResponseWithComponentJObject, null); + private static ClientPropertiesAsDictionary clientPropertiesAsDictionary = JsonConvert.DeserializeObject(getClientPropertiesStringResponse); [TestMethod] public void ClientPropertyCollectionNewtonsoft_CanGetValue() { - var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionToRoundTrip, DefaultPayloadConvention.Instance); + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); - clientProperties.TryGetValue(StringPropertyName, out string stringOutValue); + // act, assert + + clientProperties.TryGetValue(RootStringPropertyName, out string stringOutValue); stringOutValue.Should().Be(StringPropertyValue); - clientProperties.TryGetValue(BoolPropertyName, out bool boolOutValue); + clientProperties.TryGetValue(RootBoolPropertyName, out bool boolOutValue); boolOutValue.Should().Be(BoolPropertyValue); - clientProperties.TryGetValue(DoublePropertyName, out double doubleOutValue); + clientProperties.TryGetValue(RootDoublePropertyName, out double doubleOutValue); doubleOutValue.Should().Be(DoublePropertyValue); - clientProperties.TryGetValue(FloatPropertyName, out float floatOutValue); + clientProperties.TryGetValue(RootFloatPropertyName, out float floatOutValue); floatOutValue.Should().Be(FloatPropertyValue); - clientProperties.TryGetValue(IntPropertyName, out int intOutValue); + clientProperties.TryGetValue(RootIntPropertyName, out int intOutValue); intOutValue.Should().Be(IntPropertyValue); - clientProperties.TryGetValue(ShortPropertyName, out short shortOutValue); + clientProperties.TryGetValue(RootShortPropertyName, out short shortOutValue); shortOutValue.Should().Be(ShortPropertyValue); - clientProperties.TryGetValue(ObjectPropertyName, out CustomClientProperty objectOutValue); + clientProperties.TryGetValue(RootObjectPropertyName, out CustomClientProperty objectOutValue); objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); // The two lists won't be exactly equal since TryGetValue doesn't implement nested deserialization // => the complex object inside the list is deserialized to a JObject. - clientProperties.TryGetValue(ArrayPropertyName, out List arrayOutValue); + clientProperties.TryGetValue(RootArrayPropertyName, out List arrayOutValue); arrayOutValue.Should().HaveSameCount(s_arrayPropertyValue); // The two dictionaries won't be exactly equal since TryGetValue doesn't implement nested deserialization // => the complex object inside the dictionary is deserialized to a JObject. - clientProperties.TryGetValue(MapPropertyName, out Dictionary mapOutValue); + clientProperties.TryGetValue(RootMapPropertyName, out Dictionary mapOutValue); mapOutValue.Should().HaveSameCount(s_mapPropertyValue); - clientProperties.TryGetValue(DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + clientProperties.TryGetValue(RootDateTimePropertyName, out DateTimeOffset dateTimeOutValue); dateTimeOutValue.Should().Be(s_dateTimePropertyValue); } [TestMethod] public void ClientPropertyCollectionNewtonsoft_CanGetValueWithComponent() { - var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act, assert - clientProperties.TryGetValue(ComponentName, StringPropertyName, out string stringOutValue); + clientProperties.TryGetValue(ComponentName, ComponentStringPropertyName, out string stringOutValue); stringOutValue.Should().Be(StringPropertyValue); - clientProperties.TryGetValue(ComponentName, BoolPropertyName, out bool boolOutValue); + clientProperties.TryGetValue(ComponentName, ComponentBoolPropertyName, out bool boolOutValue); boolOutValue.Should().Be(BoolPropertyValue); - clientProperties.TryGetValue(ComponentName, DoublePropertyName, out double doubleOutValue); + clientProperties.TryGetValue(ComponentName, ComponentDoublePropertyName, out double doubleOutValue); doubleOutValue.Should().Be(DoublePropertyValue); - clientProperties.TryGetValue(ComponentName, FloatPropertyName, out float floatOutValue); + clientProperties.TryGetValue(ComponentName, ComponentFloatPropertyName, out float floatOutValue); floatOutValue.Should().Be(FloatPropertyValue); - clientProperties.TryGetValue(ComponentName, IntPropertyName, out int intOutValue); + clientProperties.TryGetValue(ComponentName, ComponentIntPropertyName, out int intOutValue); intOutValue.Should().Be(IntPropertyValue); - clientProperties.TryGetValue(ComponentName, ShortPropertyName, out short shortOutValue); + clientProperties.TryGetValue(ComponentName, ComponentShortPropertyName, out short shortOutValue); shortOutValue.Should().Be(ShortPropertyValue); - clientProperties.TryGetValue(ComponentName, ObjectPropertyName, out CustomClientProperty objectOutValue); + clientProperties.TryGetValue(ComponentName, ComponentObjectPropertyName, out CustomClientProperty objectOutValue); objectOutValue.Id.Should().Be(s_objectPropertyValue.Id); objectOutValue.Name.Should().Be(s_objectPropertyValue.Name); // The two lists won't be exactly equal since TryGetValue doesn't implement nested deserialization // => the complex object inside the list is deserialized to a JObject. - clientProperties.TryGetValue(ComponentName, ArrayPropertyName, out List arrayOutValue); + clientProperties.TryGetValue(ComponentName, ComponentArrayPropertyName, out List arrayOutValue); arrayOutValue.Should().HaveSameCount(s_arrayPropertyValue); // The two dictionaries won't be exactly equal since TryGetValue doesn't implement nested deserialization // => the complex object inside the dictionary is deserialized to a JObject. - clientProperties.TryGetValue(ComponentName, MapPropertyName, out Dictionary mapOutValue); + clientProperties.TryGetValue(ComponentName, ComponentMapPropertyName, out Dictionary mapOutValue); mapOutValue.Should().HaveSameCount(s_mapPropertyValue); - clientProperties.TryGetValue(ComponentName, DateTimePropertyName, out DateTimeOffset dateTimeOutValue); + clientProperties.TryGetValue(ComponentName, ComponentDateTimePropertyName, out DateTimeOffset dateTimeOutValue); dateTimeOutValue.Should().Be(s_dateTimePropertyValue); } [TestMethod] public void ClientPropertyCollectionNewtonsoft_CanAddSimpleWritablePropertyAndGetBack() { - var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWritablePropertyToRoundTrip, DefaultPayloadConvention.Instance); + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act + clientProperties.TryGetValue(RootWritablePropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); - clientProperties.TryGetValue(StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + // assert outValue.Value.Should().Be(StringPropertyValue); outValue.AckCode.Should().Be(s_writablePropertyResponse.AckCode); outValue.AckVersion.Should().Be(s_writablePropertyResponse.AckVersion); @@ -215,9 +218,13 @@ public void ClientPropertyCollectionNewtonsoft_CanAddSimpleWritablePropertyAndGe [TestMethod] public void ClientPropertyCollectionNewtonsoft_CanAddWritablePropertyWithComponentAndGetBack() { - var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWritablePropertyWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); - clientProperties.TryGetValue(ComponentName, StringPropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + // act + clientProperties.TryGetValue(ComponentName, ComponentWritablePropertyName, out NewtonsoftJsonWritablePropertyResponse outValue); + + // assert outValue.Value.Should().Be(StringPropertyValue); outValue.AckCode.Should().Be(s_writablePropertyResponse.AckCode); outValue.AckVersion.Should().Be(s_writablePropertyResponse.AckVersion); @@ -227,58 +234,166 @@ public void ClientPropertyCollectionNewtonsoft_CanAddWritablePropertyWithCompone [TestMethod] public void ClientPropertyCollectionNewtonsoft_CanGetComponentIdentifier() { - var clientProperties = ClientPropertyCollection.FromTwinCollection(collectionWithComponentToRoundTrip, DefaultPayloadConvention.Instance); + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); - clientProperties.TryGetValue(ComponentName, StringPropertyName, out string outValue); + // act + clientProperties.TryGetValue(ComponentName, ComponentStringPropertyName, out string outValue); clientProperties.TryGetValue(ComponentName, ConventionBasedConstants.ComponentIdentifierKey, out string componentOut); + // assert outValue.Should().Be(StringPropertyValue); componentOut.Should().Be(ConventionBasedConstants.ComponentIdentifierValue); } + + [TestMethod] + public void ClientPropertyCollectionNewtonSoft_TryGetValueShouldReturnFalseIfValueNotFound() + { + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act + bool isValueRetrieved = clientProperties.TryGetValue("thisPropertyDoesNotExist", out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonSoft_TryGetValueWithComponentShouldReturnFalseIfValueNotFound() + { + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(ComponentName, "thisPropertyDoesNotExist", out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonSoft_TryGetValueShouldReturnFalseIfValueCouldNotBeDeserialized() + { + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(RootStringPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonSoft_TryGetValueWithComponentShouldReturnFalseIfValueCouldNotBeDeserialized() + { + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + + // act + bool isValueRetrieved = clientProperties.TryGetValue(ComponentName, ComponentStringPropertyName, out int outIntValue); + + // assert + isValueRetrieved.Should().BeFalse(); + outIntValue.Should().Be(default); + } + + [TestMethod] + public void ClientPropertyCollectionNewtonSoft_TryGetValueWithComponentShouldReturnFalseIfNotAComponent() + { + // arrange + var clientProperties = ClientPropertyCollection.FromClientPropertiesAsDictionary(clientPropertiesAsDictionary.Reported, DefaultPayloadConvention.Instance); + string incorrectlyMappedComponentName = ComponentMapPropertyName; + string incorrectlyMappedComponentPropertyName = "key1"; + + // act + bool isValueRetrieved = clientProperties.TryGetValue(incorrectlyMappedComponentName, incorrectlyMappedComponentPropertyName, out object propertyValue); + + // assert + isValueRetrieved.Should().BeFalse(); + propertyValue.Should().Be(default); + } } - internal class RootLevelProperties + internal class TestProperties { - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.BoolPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootBoolPropertyName)] public bool BooleanProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.DoublePropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootDoublePropertyName)] public double DoubleProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.FloatPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootFloatPropertyName)] public float FloatProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.IntPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootIntPropertyName)] public int IntProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ShortPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootShortPropertyName)] public short ShortProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.StringPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootStringPropertyName)] public string StringProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ObjectPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootObjectPropertyName)] public object ObjectProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ArrayPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootArrayPropertyName)] public IList ArrayProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.MapPropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootMapPropertyName)] public IDictionary MapProperty { get; set; } - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.DateTimePropertyName)] + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootDateTimePropertyName)] public DateTimeOffset DateTimeProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.RootWritablePropertyName)] + public IWritablePropertyResponse WritablePropertyResponse { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentName)] + public ComponentProperties ComponentProperties { get; set; } } - internal class ComponentProperties : RootLevelProperties + internal class ComponentProperties { [JsonProperty(ConventionBasedConstants.ComponentIdentifierKey)] public string ComponentIdentifier { get; } = ConventionBasedConstants.ComponentIdentifierValue; - } - internal class ComponentLevelProperties - { - [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentName)] - public ComponentProperties Properties { get; set; } + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentBoolPropertyName)] + public bool BooleanProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentDoublePropertyName)] + public double DoubleProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentFloatPropertyName)] + public float FloatProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentIntPropertyName)] + public int IntProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentShortPropertyName)] + public short ShortProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentStringPropertyName)] + public string StringProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentObjectPropertyName)] + public object ObjectProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentArrayPropertyName)] + public IList ArrayProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentMapPropertyName)] + public IDictionary MapProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentDateTimePropertyName)] + public DateTimeOffset DateTimeProperty { get; set; } + + [JsonProperty(ClientPropertyCollectionTestsNewtonsoft.ComponentWritablePropertyName)] + public IWritablePropertyResponse WritablePropertyResponse { get; set; } } } diff --git a/iothub/device/tests/DeviceClientTests.cs b/iothub/device/tests/DeviceClientTests.cs index 5cb0672808..715b0ead00 100644 --- a/iothub/device/tests/DeviceClientTests.cs +++ b/iothub/device/tests/DeviceClientTests.cs @@ -107,7 +107,7 @@ public void DeviceClient_ParsmHostNameGatewayAuthMethodTransportArray_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_Params_GatewayAuthMethod_Works() { @@ -118,7 +118,7 @@ public void DeviceClient_Params_GatewayAuthMethod_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_ParamsGatewayAuthMethodTransport_Works() { @@ -129,7 +129,7 @@ public void DeviceClient_ParamsGatewayAuthMethodTransport_Works() } // This is for the scenario where an IoT Edge device is defined as the downstream device's transparent gateway. - // For more details, see https://docs.microsoft.com/en-us/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string + // For more details, see https://docs.microsoft.com/azure/iot-edge/how-to-authenticate-downstream-device#retrieve-and-modify-connection-string [TestMethod] public void DeviceClient_ParamsGatewayAuthMethodTransportArray_Works() { diff --git a/iothub/device/tests/DeviceClientTwinApiTests.cs b/iothub/device/tests/DeviceClientTwinApiTests.cs index f0e14363b6..3d8464ff01 100644 --- a/iothub/device/tests/DeviceClientTwinApiTests.cs +++ b/iothub/device/tests/DeviceClientTwinApiTests.cs @@ -1,11 +1,16 @@ // 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 Microsoft.VisualStudio.TestTools.UnitTesting; -using NSubstitute; -using System.Threading.Tasks; +using System.IO; +using System.Text; using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; using Microsoft.Azure.Devices.Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using NSubstitute; namespace Microsoft.Azure.Devices.Client.Test { @@ -139,7 +144,7 @@ public async Task DeviceClientGetTwinAsyncCallsSendTwinGetAsync() // assert await innerHandler. Received(1). - SendTwinGetAsync(Arg.Any()).ConfigureAwait(false); + GetClientTwinPropertiesAsync(Arg.Any()).ConfigureAwait(false); } // Tests_SRS_DEVICECLIENT_18_002: `UpdateReportedPropertiesAsync` shall call `SendTwinPatchAsync` on the transport to update the reported properties @@ -151,14 +156,24 @@ public async Task DeviceClientUpdateReportedPropertiesAsyncCallsSendTwinPatchAsy var client = DeviceClient.CreateFromConnectionString(fakeConnectionString); client.InnerHandler = innerHandler; var props = new TwinCollection(); + string body = JsonConvert.SerializeObject(props); + + string receivedBody = null; + await innerHandler + .SendClientTwinPropertyPatchAsync( + Arg.Do(stream => + { + using var streamReader = new StreamReader(stream, Encoding.UTF8); + receivedBody = streamReader.ReadToEnd(); + }), + Arg.Any()) + .ConfigureAwait(false); // act await client.UpdateReportedPropertiesAsync(props).ConfigureAwait(false); // assert - await innerHandler. - Received(1). - SendTwinPatchAsync(Arg.Is(props), Arg.Any()).ConfigureAwait(false); + receivedBody.Should().Be(body); } // Tests_SRS_DEVICECLIENT_18_006: `UpdateReportedPropertiesAsync` shall throw an `ArgumentNull` exception if `reportedProperties` is null 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 7006e6b8e0..481fab720b 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/device/tests/Mqtt/MqttTransportHandlerTests.cs b/iothub/device/tests/Mqtt/MqttTransportHandlerTests.cs index 87275156ea..2fa015bf4e 100644 --- a/iothub/device/tests/Mqtt/MqttTransportHandlerTests.cs +++ b/iothub/device/tests/Mqtt/MqttTransportHandlerTests.cs @@ -91,13 +91,14 @@ public async Task MqttTransportHandlerEnableTwinPatchAsyncTokenCancellationReque [TestMethod] public async Task MqttTransportHandlerSendTwinGetAsyncTokenCancellationRequested() { - await TestOperationCanceledByToken(token => CreateFromConnectionString().SendTwinGetAsync(token)).ConfigureAwait(false); + await TestOperationCanceledByToken(token => CreateFromConnectionString().GetClientTwinPropertiesAsync(token)).ConfigureAwait(false); } [TestMethod] public async Task MqttTransportHandlerSendTwinPatchAsyncTokenCancellationRequested() { - await TestOperationCanceledByToken(token => CreateFromConnectionString().SendTwinPatchAsync(new TwinCollection(), token)).ConfigureAwait(false); + using var bodyStream = new MemoryStream(); + await TestOperationCanceledByToken(token => CreateFromConnectionString().SendClientTwinPropertyPatchAsync(bodyStream, token)).ConfigureAwait(false); } [TestMethod] @@ -383,7 +384,11 @@ public async Task MqttTransportHandlerSendTwinGetAsyncHappyPath() // act await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - var twinReturned = await transport.SendTwinGetAsync(CancellationToken.None).ConfigureAwait(false); + var twinPropertiesReturned = await transport.GetClientTwinPropertiesAsync(CancellationToken.None).ConfigureAwait(false); + var twinReturned = new Twin + { + Properties = twinPropertiesReturned, + }; // assert Assert.AreEqual(twin.Properties.Desired["foo"].ToString(), twinReturned.Properties.Desired["foo"].ToString()); @@ -413,7 +418,7 @@ public async Task MqttTransportHandlerSendTwinGetAsyncReturnsFailure() // act & assert await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - await transport.SendTwinGetAsync(CancellationToken.None).ExpectedAsync().ConfigureAwait(false); + await transport.GetClientTwinPropertiesAsync(CancellationToken.None).ExpectedAsync().ConfigureAwait(false); } // Tests_SRS_CSHARP_MQTT_TRANSPORT_18_020: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinGetAsync` shall fail with a timeout error @@ -427,7 +432,7 @@ public async Task MqttTransportHandlerSendTwinGetAsyncTimesOut() // act & assert await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - var twinReturned = await transport.SendTwinGetAsync(CancellationToken.None).ConfigureAwait(false); + var twinReturned = await transport.GetClientTwinPropertiesAsync(CancellationToken.None).ConfigureAwait(false); } // Tests_SRS_CSHARP_MQTT_TRANSPORT_18_022: `SendTwinPatchAsync` shall allocate a `Message` object to hold the update request @@ -451,7 +456,7 @@ public async Task MqttTransportHandlerSendTwinPatchAsyncHappyPath() .Returns(msg => { var request = msg.Arg(); - StreamReader reader = new StreamReader(request.GetBodyStream(), System.Text.Encoding.UTF8); + using StreamReader reader = new StreamReader(request.GetBodyStream(), Encoding.UTF8); receivedBody = reader.ReadToEnd(); var response = new Message(); @@ -466,13 +471,14 @@ public async Task MqttTransportHandlerSendTwinPatchAsyncHappyPath() return TaskHelpers.CompletedTask; }); + string expectedBody = JsonConvert.SerializeObject(props); + using var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedBody)); // act await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - await transport.SendTwinPatchAsync(props, CancellationToken.None).ConfigureAwait(false); + await transport.SendClientTwinPropertyPatchAsync(bodyStream, CancellationToken.None).ConfigureAwait(false); // assert - string expectedBody = JsonConvert.SerializeObject(props); Assert.AreEqual(expectedBody, receivedBody); } @@ -499,10 +505,12 @@ public async Task MqttTransportHandlerSendTwinPatchAsyncReturnsFailure() transport.OnMessageReceived(response); return TaskHelpers.CompletedTask; }); + string expectedBody = JsonConvert.SerializeObject(props); + using var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedBody)); // act & assert await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - await transport.SendTwinPatchAsync(props, CancellationToken.None).ExpectedAsync().ConfigureAwait(false); + await transport.SendClientTwinPropertyPatchAsync(bodyStream, CancellationToken.None).ExpectedAsync().ConfigureAwait(false); } // Tests_SRS_CSHARP_MQTT_TRANSPORT_18_029: If the response doesn't arrive within `MqttTransportHandler.TwinTimeout`, `SendTwinPatchAsync` shall fail with a timeout error. @@ -514,10 +522,12 @@ public async Task MqttTransportHandlerSendTwinPatchAsyncTimesOut() var transport = this.CreateTransportHandlerWithMockChannel(out IChannel channel); transport.TwinTimeout = TimeSpan.FromMilliseconds(20); var props = new TwinCollection(); + string expectedBody = JsonConvert.SerializeObject(props); + using var bodyStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedBody)); // act & assert await transport.OpenAsync(CancellationToken.None).ConfigureAwait(false); - await transport.SendTwinPatchAsync(props, CancellationToken.None).ConfigureAwait(false); + await transport.SendClientTwinPropertyPatchAsync(bodyStream, CancellationToken.None).ConfigureAwait(false); } // Tests_SRS_CSHARP_MQTT_TRANSPORT_28_04: If OnError is triggered after OpenAsync is called, WaitForTransportClosedAsync shall be invoked. diff --git a/iothub/device/tests/ObjectCastHelpersTests.cs b/iothub/device/tests/ObjectCastHelpersTests.cs new file mode 100644 index 0000000000..52cb0905cb --- /dev/null +++ b/iothub/device/tests/ObjectCastHelpersTests.cs @@ -0,0 +1,36 @@ +// 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.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Client.Test +{ + [TestClass] + [TestCategory("Unit")] + public class ObjectCastHelpersTests + { + [TestMethod] + public void CanConvertNumericTypes() + { + TestNumericConversion(1.001d, true, 1.001f); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion(123, true, 123); + TestNumericConversion("someString", false, 0); + TestNumericConversion(true, false, 0); + } + + private void TestNumericConversion(object input, bool canConvertExpected, T resultExpected) + { + bool canConvertActual = ObjectConversionHelpers.TryCastNumericTo(input, out T result); + + canConvertActual.Should().Be(canConvertExpected); + result.Should().Be(resultExpected); + } + } +} diff --git a/iothub/device/tests/TelemetryCollectionTests.cs b/iothub/device/tests/TelemetryCollectionTests.cs new file mode 100644 index 0000000000..c27f864fcb --- /dev/null +++ b/iothub/device/tests/TelemetryCollectionTests.cs @@ -0,0 +1,161 @@ +// 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.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Devices.Client.Test +{ + [TestClass] + [TestCategory("Unit")] + public class TelemetryCollectionTests + { + [TestMethod] + public void AddNullTelemetryNameThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + Action testAction = () => testTelemetryCollection.Add(null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddOrUpdateNullTelemetryNameThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + Action testAction = () => testTelemetryCollection.AddOrUpdate(null, 123); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddNullTelemetryValueSuccess() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + testTelemetryCollection.Add("abc", null); + + // assert + testTelemetryCollection["abc"].Should().BeNull(); + } + + [TestMethod] + public void AddOrUpdateNullTelemetryValueSuccess() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + testTelemetryCollection.AddOrUpdate("abc", null); + + // assert + testTelemetryCollection["abc"].Should().BeNull(); + } + + [TestMethod] + public void AddTelemetryValueAlreadyExistsThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + testTelemetryCollection.Add("abc", 123); + + // act + Action testAction = () => testTelemetryCollection.Add("abc", 1); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddOrUpdateTelemetryValueAlreadyExistsSuccess() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + testTelemetryCollection.Add("abc", 123); + + // act + testTelemetryCollection.AddOrUpdate("abc", 1); + + // assert + testTelemetryCollection["abc"].Should().Be(1); + } + + [TestMethod] + public void AddNullTelemetryCollectionThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + Action testAction = () => testTelemetryCollection.Add(null); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddOrUpdateNullTelemetryCollectionThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + + // act + Action testAction = () => testTelemetryCollection.AddOrUpdate(null); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddTelemetryCollectionAlreadyExistsThrows() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + testTelemetryCollection.AddOrUpdate("abc", 123); + var telemetryValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + Action testAction = () => testTelemetryCollection.Add(telemetryValues); + + // assert + testAction.Should().Throw(); + } + + [TestMethod] + public void AddOrUpdateTelemetryCollectionAlreadyExistsSuccess() + { + // arrange + var testTelemetryCollection = new TelemetryCollection(); + testTelemetryCollection.AddOrUpdate("abc", 123); + var telemetryValues = new Dictionary + { + { "qwe", 98 }, + { "abc", 2 }, + }; + + // act + testTelemetryCollection.AddOrUpdate(telemetryValues); + + // assert + testTelemetryCollection["qwe"].Should().Be(98); + testTelemetryCollection["abc"].Should().Be(2); + } + } +} diff --git a/iothub/device/tests/TimeoutHelperTests.cs b/iothub/device/tests/TimeoutHelperTests.cs index 8a36ddec40..4e95f4898f 100644 --- a/iothub/device/tests/TimeoutHelperTests.cs +++ b/iothub/device/tests/TimeoutHelperTests.cs @@ -8,7 +8,7 @@ using FluentAssertions; using System.Threading.Tasks; -namespace Microsoft.Azure.Devices.Client.Tests +namespace Microsoft.Azure.Devices.Client.Test { /// /// The timeout helper is a way of keeping track of how much time remains against a specified deadline. diff --git a/iothub/service/samples/readme.md b/iothub/service/samples/readme.md index 4003c89e3c..196360f4a1 100644 --- a/iothub/service/samples/readme.md +++ b/iothub/service/samples/readme.md @@ -15,13 +15,13 @@ Service samples were moved to [Azure-Samples/azure-iot-samples-csharp][samples-r [samples-repo]: https://github.com/Azure-Samples/azure-iot-samples-csharp -[service-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service -[adm-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/AutomaticDeviceManagementSample -[device-streaming-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/DeviceStreamingSample -[edge-deployment-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/EdgeDeploymentSample -[import-export-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/ImportExportDevicesSample -[jobs-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/JobsSample -[reg-man-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/RegistryManagerSample -[service-client-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/ServiceClientSample -[pnp-service-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/PnpServiceSamples -[digital-twin-client-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/DigitalTwinClientSamples \ No newline at end of file +[service-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service +[adm-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/AutomaticDeviceManagementSample +[device-streaming-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/preview/iot-hub/Samples/service/DeviceStreamingSample +[edge-deployment-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/EdgeDeploymentSample +[import-export-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/ImportExportDevicesSample +[jobs-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/JobsSample +[reg-man-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/RegistryManagerSample +[service-client-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/ServiceClientSample +[pnp-service-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/PnpServiceSamples +[digital-twin-client-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/DigitalTwinClientSamples 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/AccessRights.cs b/iothub/service/src/Common/Data/AccessRights.cs index 9887752de9..93f019e0ad 100644 --- a/iothub/service/src/Common/Data/AccessRights.cs +++ b/iothub/service/src/Common/Data/AccessRights.cs @@ -10,7 +10,7 @@ namespace Microsoft.Azure.Devices.Common.Data { /// /// Shared access policy permissions of IoT hub. - /// For more information, see . + /// For more information, see . /// [Flags] [JsonConverter(typeof(StringEnumConverter))] @@ -19,14 +19,14 @@ public enum AccessRights /// /// Grants read access to the identity registry. /// Identity registry stores information about the devices and modules permitted to connect to the IoT hub. - /// For more information, see . + /// For more information, see . /// RegistryRead = 1, /// /// Grants read and write access to the identity registry. /// Identity registry stores information about the devices and modules permitted to connect to the IoT hub. - /// For more information, see . + /// For more information, see . /// RegistryWrite = RegistryRead | 2, 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/Security/CryptoKeyGenerator.cs b/iothub/service/src/Common/Security/CryptoKeyGenerator.cs index a5323235e2..03b082ba10 100644 --- a/iothub/service/src/Common/Security/CryptoKeyGenerator.cs +++ b/iothub/service/src/Common/Security/CryptoKeyGenerator.cs @@ -1,17 +1,20 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Linq; using System; using System.Text; +using System.Security.Cryptography; + +#if NET451 + +using System.Web.Security; + +#endif #if !NET451 -using System.Security.Cryptography; +using System.Linq; -#else - using System.Web.Security; - using System.Security.Cryptography; #endif namespace Microsoft.Azure.Devices.Common @@ -19,11 +22,11 @@ namespace Microsoft.Azure.Devices.Common /// /// Utility methods for generating cryptographically secure keys and passwords. /// - static public class CryptoKeyGenerator + public static class CryptoKeyGenerator { #if NET451 - const int DefaultPasswordLength = 16; - const int GuidLength = 16; + private const int DefaultPasswordLength = 16; + private const int GuidLength = 16; #endif /// @@ -36,22 +39,19 @@ static public class CryptoKeyGenerator /// /// The size of the key. /// Byte array representing the key. + [Obsolete("This method will be deprecated in a future version.")] public static byte[] GenerateKeyBytes(int keySize) { -#if !NET451 - var keyBytes = new byte[keySize]; - using (var cyptoProvider = RandomNumberGenerator.Create()) - { - while (keyBytes.Contains(byte.MinValue)) - { - cyptoProvider.GetBytes(keyBytes); - } - } +#if NET451 + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = new RNGCryptoServiceProvider(); + cyptoProvider.GetNonZeroBytes(keyBytes); #else - var keyBytes = new byte[keySize]; - using (var cyptoProvider = new RNGCryptoServiceProvider()) + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = RandomNumberGenerator.Create(); + while (keyBytes.Contains(byte.MinValue)) { - cyptoProvider.GetNonZeroBytes(keyBytes); + cyptoProvider.GetBytes(keyBytes); } #endif return keyBytes; @@ -62,6 +62,7 @@ public static byte[] GenerateKeyBytes(int keySize) /// /// Desired key size. /// A generated key. + [Obsolete("This method will be deprecated in a future version.")] public static string GenerateKey(int keySize) { return Convert.ToBase64String(GenerateKeyBytes(keySize)); @@ -72,14 +73,14 @@ public static string GenerateKey(int keySize) /// Generate a hexadecimal key of the specified size. /// /// Desired key size. - /// A generated hexadecimal key. + /// A generated hexadecimal key. + [Obsolete("This method will not be carried forward to newer .NET targets.")] public static string GenerateKeyInHex(int keySize) { - var keyBytes = new byte[keySize]; - using (var cyptoProvider = new RNGCryptoServiceProvider()) - { - cyptoProvider.GetNonZeroBytes(keyBytes); - } + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = new RNGCryptoServiceProvider(); + cyptoProvider.GetNonZeroBytes(keyBytes); + return BitConverter.ToString(keyBytes).Replace("-", ""); } @@ -87,29 +88,39 @@ public static string GenerateKeyInHex(int keySize) /// Generate a GUID using random bytes from the framework's cryptograpically strong RNG (Random Number Generator). /// /// A cryptographically secure GUID. + [Obsolete("This method will not be carried forward to newer .NET targets.")] public static Guid GenerateGuid() { byte[] bytes = new byte[GuidLength]; - using (var rng = new RNGCryptoServiceProvider()) - { - rng.GetBytes(bytes); - } + using var rng = new RNGCryptoServiceProvider(); + rng.GetBytes(bytes); - var time = BitConverter.ToUInt32(bytes, 0); - var time_mid = BitConverter.ToUInt16(bytes, 4); - var time_hi_and_ver = BitConverter.ToUInt16(bytes, 6); - time_hi_and_ver = (ushort)((time_hi_and_ver | 0x4000) & 0x4FFF); + uint time = BitConverter.ToUInt32(bytes, 0); + ushort timeMid = BitConverter.ToUInt16(bytes, 4); + ushort timeHiAndVer = BitConverter.ToUInt16(bytes, 6); + timeHiAndVer = (ushort)((timeHiAndVer | 0x4000) & 0x4FFF); bytes[8] = (byte)((bytes[8] | 0x80) & 0xBF); - return new Guid(time, time_mid, time_hi_and_ver, bytes[8], bytes[9], - bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]); + return new Guid( + time, + timeMid, + timeHiAndVer, + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15]); } /// /// Generate a unique password with a default length and without converting it to Base64String. /// /// A unique password. + [Obsolete("This method will not be carried forward to newer .NET targets.")] public static string GeneratePassword() { return GeneratePassword(DefaultPasswordLength, false); @@ -121,9 +132,10 @@ public static string GeneratePassword() /// Desired length of the password. /// Encode the password if set to True. False otherwise. /// A generated password. + [Obsolete("This method will not be carried forward to newer .NET targets.")] public static string GeneratePassword(int length, bool base64Encoding) { - var password = Membership.GeneratePassword(length, length / 2); + string password = Membership.GeneratePassword(length, length / 2); if (base64Encoding) { password = Convert.ToBase64String(Encoding.UTF8.GetBytes(password)); 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 9ca5e2ce2f..ab2d7548f4 100644 --- a/iothub/service/src/DigitalTwin/DigitalTwinClient.cs +++ b/iothub/service/src/DigitalTwin/DigitalTwinClient.cs @@ -29,7 +29,7 @@ public class DigitalTwinClient : IDisposable /// /// Creates an instance of , provided for unit testing purposes only. - /// Use the CreateFromConnectionString method to create an instance to use the client. + /// Use the CreateFromConnectionString or Create method to create an instance to use the client. /// public DigitalTwinClient() { @@ -56,7 +56,7 @@ public static DigitalTwinClient CreateFromConnectionString(string connectionStri /// The delegating handlers to add to the http client pipeline. You can add handlers for tracing, implementing a retry strategy, routing requests through a proxy, etc. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static DigitalTwinClient Create( string hostName, @@ -124,7 +124,7 @@ public virtual async Task> GetDi /// /// Updates a digital twin. - /// For further information on how to create the json-patch, see + /// For further information on how to create the json-patch, see /// /// The Id of the digital twin. /// The application/json-patch+json operations to be performed on the specified digital twin. diff --git a/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs b/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs index a1639436f7..8b12d44899 100644 --- a/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs +++ b/iothub/service/src/DigitalTwin/Serialization/UpdateOperationsUtility.cs @@ -23,7 +23,7 @@ public class UpdateOperationsUtility /// /// Include an add property operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -62,7 +62,7 @@ public void AppendAddPropertyOp(string path, object value) /// /// Include a replace property operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -101,7 +101,7 @@ public void AppendReplacePropertyOp(string path, object value) /// /// Include a remove operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// @@ -145,7 +145,7 @@ public void AppendRemoveOp(string path) /// /// Include an add component operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// This utility appends the "$metadata" identifier to the property values, @@ -182,7 +182,7 @@ public void AppendAddComponentOp(string path, Dictionary propert /// /// Include a replace component operation. - /// Learn more about managing digital twins here . + /// Learn more about managing digital twins here . /// /// /// This utility appends the "$metadata" identifier to the property values, diff --git a/iothub/service/src/DigitalTwin/readme.md b/iothub/service/src/DigitalTwin/readme.md index 1c0f96fb6a..a937bc9f05 100644 --- a/iothub/service/src/DigitalTwin/readme.md +++ b/iothub/service/src/DigitalTwin/readme.md @@ -6,7 +6,7 @@ ### Examples -You can familiarize yourself with different APIs using [samples for DigitalTwinClient](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/service/DigitalTwinClientSamples). +You can familiarize yourself with different APIs using [samples for DigitalTwinClient](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/service/DigitalTwinClientSamples). ## Source code folder structure diff --git a/iothub/service/src/ExportImportDevice.cs b/iothub/service/src/ExportImportDevice.cs index eacfc12b6d..79d1a60a9e 100644 --- a/iothub/service/src/ExportImportDevice.cs +++ b/iothub/service/src/ExportImportDevice.cs @@ -5,6 +5,7 @@ // --------------------------------------------------------------- using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Azure.Devices.Shared; using Newtonsoft.Json; @@ -64,6 +65,8 @@ public ExportImportDevice(Device device, ImportMode importmode) StatusReason = device.StatusReason; Authentication = device.Authentication; Capabilities = device.Capabilities; + DeviceScope = device.Scope; + ParentScopes = device.ParentScopes; } /// @@ -152,6 +155,21 @@ public string TwinETag [JsonProperty(PropertyName = "deviceScope", NullValueHandling = NullValueHandling.Include)] public string DeviceScope { get; set; } + /// + /// The scopes of the upper level edge devices if applicable. + /// + /// + /// For edge devices, the value to set a parent edge device can be retrieved from the parent edge device's property. + /// + /// For leaf devices, this could be set to the same value as or left for the service to copy over. + /// + /// For now, this list can only have 1 element in the collection. + /// + /// For more information, see . + /// + [JsonProperty(PropertyName = "parentScopes", NullValueHandling = NullValueHandling.Ignore)] + public IList ParentScopes { get; internal set; } = new List(); + private static string SanitizeETag(string eTag) { if (!string.IsNullOrWhiteSpace(eTag)) diff --git a/iothub/service/src/FeedbackStatusCode.cs b/iothub/service/src/FeedbackStatusCode.cs index 617bd20bf1..74a99ee213 100644 --- a/iothub/service/src/FeedbackStatusCode.cs +++ b/iothub/service/src/FeedbackStatusCode.cs @@ -14,32 +14,32 @@ public enum FeedbackStatusCode { /// /// Indicates that the cloud-to-device message was successfully delivered to the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Success = 0, /// /// Indicates that the cloud-to-device message expired before it could be delivered to the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Expired = 1, /// /// Indicates that the cloud-to-device message has been placed in a dead-lettered state. /// This happens when the message reaches the maximum count for the number of times it can transition between enqueued and invisible states. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// DeliveryCountExceeded = 2, /// /// Indicates that the cloud-to-device message was rejected by the device. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Rejected = 3, /// /// Indicates that the cloud-to-device message was purged from IoT Hub. - /// For information on cloud-to-device message life cycle, see . + /// For information on cloud-to-device message life cycle, see . /// Purged = 4 } diff --git a/iothub/service/src/JobClient/JobClient.cs b/iothub/service/src/JobClient/JobClient.cs index 32421512b4..18b97cea4c 100644 --- a/iothub/service/src/JobClient/JobClient.cs +++ b/iothub/service/src/JobClient/JobClient.cs @@ -100,7 +100,7 @@ public static JobClient CreateFromConnectionString(string connectionString, Http /// The HTTP transport settings. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static JobClient Create( string hostName, diff --git a/iothub/service/src/JobProperties.cs b/iothub/service/src/JobProperties.cs index fbab545b2b..ca5bb057ee 100644 --- a/iothub/service/src/JobProperties.cs +++ b/iothub/service/src/JobProperties.cs @@ -8,7 +8,7 @@ namespace Microsoft.Azure.Devices { /// /// Contains properties of a Job. - /// See online documentation for more infomration. + /// See online documentation for more infomration. /// public class JobProperties { 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/ManagedIdentity.cs b/iothub/service/src/ManagedIdentity.cs index 2fa2467be1..8ef757b1a5 100644 --- a/iothub/service/src/ManagedIdentity.cs +++ b/iothub/service/src/ManagedIdentity.cs @@ -10,8 +10,8 @@ namespace Microsoft.Azure.Devices { /// /// The managed identity used to access the storage account for IoT hub import and export jobs. - /// For more information on managed identity configuration on IoT hub, see . - /// For more information on managed identities, see + /// For more information on managed identity configuration on IoT hub, see . + /// For more information on managed identities, see /// public class ManagedIdentity { diff --git a/iothub/service/src/MessageSystemPropertyNames.cs b/iothub/service/src/MessageSystemPropertyNames.cs index 86c559a4d8..d768fade28 100644 --- a/iothub/service/src/MessageSystemPropertyNames.cs +++ b/iothub/service/src/MessageSystemPropertyNames.cs @@ -39,7 +39,7 @@ public static class MessageSystemPropertyNames /// /// The number of times a message can transition between the Enqueued and Invisible states. /// After the maximum number of transitions, the IoT hub sets the state of the message to dead-lettered. - /// For more information, see + /// For more information, see /// public const string DeliveryCount = "iothub-deliverycount"; diff --git a/iothub/service/src/RegistryManager.cs b/iothub/service/src/RegistryManager.cs index ed6882f3d1..7568d07f2a 100644 --- a/iothub/service/src/RegistryManager.cs +++ b/iothub/service/src/RegistryManager.cs @@ -134,7 +134,7 @@ public static RegistryManager CreateFromConnectionString(string connectionString /// The HTTP transport settings. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static RegistryManager Create( string hostName, diff --git a/iothub/service/src/ServiceClient.cs b/iothub/service/src/ServiceClient.cs index 5ee7c3856a..d5ba708c8a 100644 --- a/iothub/service/src/ServiceClient.cs +++ b/iothub/service/src/ServiceClient.cs @@ -138,7 +138,7 @@ public static ServiceClient CreateFromConnectionString(string connectionString, /// The options that allow configuration of the service client instance during initialization. /// An instance of . /// - /// For more information on configuring IoT hub with Azure Active Directory, see + /// For more information on configuring IoT hub with Azure Active Directory, see /// public static ServiceClient Create( string hostName, @@ -385,7 +385,7 @@ public virtual Task PurgeMessageQueueAsync(string devic /// /// 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 . + /// For more information see . /// /// An instance of . public virtual FeedbackReceiver GetFeedbackReceiver() @@ -396,7 +396,7 @@ public virtual FeedbackReceiver GetFeedbackReceiver() /// /// Get the which can deliver notifications for file upload operations. /// This call is made over AMQP. - /// For more information see . + /// For more information see . /// /// An instance of . public virtual FileNotificationReceiver GetFileNotificationReceiver() diff --git a/iothub/service/tests/CryptoKeyGenerator.cs b/iothub/service/tests/CryptoKeyGenerator.cs new file mode 100644 index 0000000000..30cd86c15d --- /dev/null +++ b/iothub/service/tests/CryptoKeyGenerator.cs @@ -0,0 +1,62 @@ +// 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.Security.Cryptography; + +#if !NET451 + +using System.Linq; + +#endif + +namespace Microsoft.Azure.Devices.Tests +{ + /// + /// Utility methods for generating cryptographically secure keys and passwords. + /// + internal static class CryptoKeyGenerator + { +#if NET451 + private const int DefaultPasswordLength = 16; + private const int GuidLength = 16; +#endif + + /// + /// Size of the SHA 512 key. + /// + internal const int Sha512KeySize = 64; + + /// + /// Generate a key with a specified key size. + /// + /// The size of the key. + /// Byte array representing the key. + internal static byte[] GenerateKeyBytes(int keySize) + { +#if NET451 + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = new RNGCryptoServiceProvider(); + cyptoProvider.GetNonZeroBytes(keyBytes); +#else + byte[] keyBytes = new byte[keySize]; + using var cyptoProvider = RandomNumberGenerator.Create(); + while (keyBytes.Contains(byte.MinValue)) + { + cyptoProvider.GetBytes(keyBytes); + } +#endif + return keyBytes; + } + + /// + /// Generates a key of the specified size. + /// + /// Desired key size. + /// A generated key. + internal static string GenerateKey(int keySize) + { + return Convert.ToBase64String(GenerateKeyBytes(keySize)); + } + } +} diff --git a/iothub/service/tests/DeviceAuthenticationTests.cs b/iothub/service/tests/DeviceAuthenticationTests.cs index ac933dc3d7..4c2bf8531c 100644 --- a/iothub/service/tests/DeviceAuthenticationTests.cs +++ b/iothub/service/tests/DeviceAuthenticationTests.cs @@ -7,7 +7,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.Devices.Common; +using Microsoft.Azure.Devices.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; diff --git a/iothub/service/tests/ServiceClientTests.cs b/iothub/service/tests/ServiceClientTests.cs index 611a0cdacb..a2d3ab0504 100644 --- a/iothub/service/tests/ServiceClientTests.cs +++ b/iothub/service/tests/ServiceClientTests.cs @@ -49,11 +49,9 @@ public async Task PurgeMessageQueueDeviceNotFoundTest() var authMethod = new ServiceAuthenticationWithSharedAccessPolicyKey("test", "dGVzdFN0cmluZzE="); var builder = IotHubConnectionStringBuilder.Create("acme.azure-devices.net", authMethod); - Func> onCreate = _ => Task.FromResult(new AmqpSession(null, new AmqpSessionSettings(), null)); Action onClose = _ => { }; - - // Instantiate ServiceClient with Mock IHttpClientHelper and IotHubConnection + // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); var serviceClient = new ServiceClient(connection, restOpMock.Object); @@ -80,11 +78,9 @@ private Tuple, ServiceClient, PurgeMessageQueueResult> S var authMethod = new ServiceAuthenticationWithSharedAccessPolicyKey("test", "dGVzdFN0cmluZzE="); var builder = IotHubConnectionStringBuilder.Create("acme.azure-devices.net", authMethod); - Func> onCreate = _ => Task.FromResult(new AmqpSession(null, new AmqpSessionSettings(), null)); Action onClose = _ => { }; - - // Instantiate ServiceClient with Mock IHttpClientHelper and IotHubConnection + // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); var serviceClient = new ServiceClient(connection, restOpMock.Object); @@ -103,7 +99,6 @@ public async Task DisposeTest() // Instantiate ServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); 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(); @@ -123,7 +118,6 @@ public async Task CloseAsyncTest() // Instantiate AmqpServiceClient with Mock IHttpClientHelper and IotHubConnection var connection = new IotHubConnection(onCreate, onClose); 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/samples/readme.md b/provisioning/device/samples/readme.md index c59f6f4629..e80c59f380 100644 --- a/provisioning/device/samples/readme.md +++ b/provisioning/device/samples/readme.md @@ -6,6 +6,6 @@ Device provisioning samples were moved to [Azure-Samples/azure-iot-samples-cshar [samples-repo]: https://github.com/Azure-Samples/azure-iot-samples-csharp -[service-device-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/device -[x509-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/device/X509Sample -[tpm-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/device/TpmSample \ No newline at end of file +[service-device-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/device +[x509-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/device/X509Sample +[tpm-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/device/TpmSample \ No newline at end of file diff --git a/provisioning/device/src/CertificateInstaller.cs b/provisioning/device/src/CertificateInstaller.cs index 077ed41770..3bcf93a907 100644 --- a/provisioning/device/src/CertificateInstaller.cs +++ b/provisioning/device/src/CertificateInstaller.cs @@ -17,7 +17,7 @@ internal static class CertificateInstaller /// /// Because Intermediate Authorities may have been issued by the uploaded CA, the application must present the full chain of /// certificates from the one used during authentication to the one uploaded to the service. - /// See + /// See /// for more information. /// /// The certificate chain to ensure is installed. diff --git a/provisioning/device/src/PlugAndPlay/PnpConvention.cs b/provisioning/device/src/PlugAndPlay/PnpConvention.cs index b3b29e0645..4a64f8244b 100644 --- a/provisioning/device/src/PlugAndPlay/PnpConvention.cs +++ b/provisioning/device/src/PlugAndPlay/PnpConvention.cs @@ -15,7 +15,7 @@ public static class PnpConvention /// /// /// For more information on device provisioning service and plug and play compatibility, - /// and PnP device certification, see . + /// and PnP device certification, see . /// The DPS payload should be in the format: /// /// { diff --git a/provisioning/service/samples/readme.md b/provisioning/service/samples/readme.md index 66d67982d9..6dce11524d 100644 --- a/provisioning/service/samples/readme.md +++ b/provisioning/service/samples/readme.md @@ -7,8 +7,8 @@ Service provisioning samples were moved to [Azure-Samples/azure-iot-samples-csha * [EnrollmentGroupSample][enrollment-group-sample] [samples-repo]: https://github.com/Azure-Samples/azure-iot-samples-csharp -[service-prov-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/service -[group-cert-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/service/GroupCertificateVerificationSample -[bulk-op-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/service/BulkOperationSample -[enrollment-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/service/EnrollmentSample -[enrollment-group-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/provisioning/Samples/service/EnrollmentGroupSample \ No newline at end of file +[service-prov-samples]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/service +[group-cert-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/service/GroupCertificateVerificationSample +[bulk-op-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/service/BulkOperationSample +[enrollment-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/service/EnrollmentSample +[enrollment-group-sample]: https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/provisioning/Samples/service/EnrollmentGroupSample \ No newline at end of file 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/Config/X509CAReferences.cs b/provisioning/service/src/Config/X509CAReferences.cs index d445d7d566..efe1b12daf 100644 --- a/provisioning/service/src/Config/X509CAReferences.cs +++ b/provisioning/service/src/Config/X509CAReferences.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Devices.Provisioning.Service /// /// /// This class creates a representation of an X509 CA references. It can receive primary and secondary - /// CA references, but only the primary is mandatory. + /// CA references. /// /// Users must provide the CA reference as a String. This class will encapsulate both in a /// single . @@ -36,18 +36,11 @@ public class X509CAReferences /// The CA reference is a String with the name that you gave for your certificate. /// /// - /// the String with the primary CA reference. It cannot be null or empty. - /// the String with the secondary CA reference. It can be null or empty. - /// if the primary CA reference is null or empty. + /// the String with the primary CA reference. + /// the String with the secondary CA reference. [JsonConstructor] internal X509CAReferences(string primary, string secondary = null) { - /* SRS_X509_CAREFERENCE_21_001: [The constructor shall throw ArgumentException if the primary CA reference is null or empty.] */ - if(string.IsNullOrWhiteSpace(primary)) - { - throw new ProvisioningServiceClientException("Primary CA reference cannot be null or empty"); - } - /* SRS_X509_CAREFERENCE_21_002: [The constructor shall store the primary and secondary CA references.] */ Primary = primary; Secondary = secondary; } diff --git a/provisioning/service/tests/Config/X509CAReferencesTests.cs b/provisioning/service/tests/Config/X509CAReferencesTests.cs index 8f292ef792..750747ad83 100644 --- a/provisioning/service/tests/Config/X509CAReferencesTests.cs +++ b/provisioning/service/tests/Config/X509CAReferencesTests.cs @@ -9,20 +9,6 @@ namespace Microsoft.Azure.Devices.Provisioning.Service.Test [TestCategory("Unit")] public class X509CAReferencesTests { - /* SRS_X509_CAREFERENCE_21_001: [The constructor shall throw ArgumentException if the primary CA reference is null or empty.] */ - - [TestMethod] - public void X509CAReferencesThrowsOnInvalidPrimaryReferences() - { - // act and assert -#pragma warning disable CA1806 // Do not ignore method results - TestAssert.Throws(() => new X509CAReferences(null)); - TestAssert.Throws(() => new X509CAReferences("")); - TestAssert.Throws(() => new X509CAReferences(" ")); - TestAssert.Throws(() => new X509CAReferences(null, "valid-ca-reference")); -#pragma warning restore CA1806 // Do not ignore method results - } - /* SRS_X509_CAREFERENCE_21_002: [The constructor shall store the primary and secondary CA references.] */ [TestMethod] 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/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 bdf2600634..84e680486f 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 @@ -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..fa6df96196 100644 --- a/readme.md +++ b/readme.md @@ -9,30 +9,57 @@ 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. | Service Environment | Status | | --- | --- | -| [Master](https://github.com/Azure/azure-iot-sdk-csharp/tree/master) | [![Build Status](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_apis/build/status/csharp/CSharp%20Prod%20-%20West%20Central%20US?branchName=master)](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_build/latest?definitionId=44&repositoryFilter=9&branchName=master) | +| [Main](https://github.com/Azure/azure-iot-sdk-csharp/tree/main) | [![Build Status](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_apis/build/status/csharp/CSharp%20Prod%20-%20West%20Central%20US?branchName=main)](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_build/latest?definitionId=44&repositoryFilter=9&branchName=main) | | [Preview](https://github.com/Azure/azure-iot-sdk-csharp/tree/preview) | [![Build Status](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_apis/build/status/csharp/CSharp%20Canary%20-%20Central%20US%20EUAP?branchName=preview)](https://azure-iot-sdks.visualstudio.com/azure-iot-sdks/_build/latest?definitionId=402&repositoryFilter=9&branchName=preview) | ### Recommended NuGet packages -| Package Name | Release Version | Pre-release Version | -| --- | --- | --- | -| Microsoft.Azure.Devices.Client | [![NuGet][iothub-device-release]][iothub-device-nuget] | [![NuGet][iothub-device-prerelease]][iothub-device-nuget] | -| Microsoft.Azure.Devices | [![NuGet][iothub-service-release]][iothub-service-nuget] | [![NuGet][iothub-service-prerelease]][iothub-service-nuget] | -| Microsoft.Azure.Devices.Shared | [![NuGet][iothub-shared-release]][iothub-shared-nuget] | [![NuGet][iothub-shared-prerelease]][iothub-shared-nuget] | -| Microsoft.Azure.Devices.Provisioning.Client | [![NuGet][dps-device-release]][dps-device-nuget] | [![NuGet][dps-device-prerelease]][dps-device-nuget] | -| Microsoft.Azure.Devices.Provisioning.Transport.Amqp | [![NuGet][dps-device-amqp-release]][dps-device-amqp-nuget]| [![NuGet][dps-device-amqp-prerelease]][dps-device-amqp-nuget] | -| Microsoft.Azure.Devices.Provisioning.Transport.Http | [![NuGet][dps-device-http-release]][dps-device-http-nuget]| [![NuGet][dps-device-http-prerelease]][dps-device-http-nuget] | -| Microsoft.Azure.Devices.Provisioning.Transport.Mqtt | [![NuGet][dps-device-mqtt-release]][dps-device-mqtt-nuget]| [![NuGet][dps-device-mqtt-prerelease]][dps-device-mqtt-nuget] | -| Microsoft.Azure.Devices.Provisioning.Service | [![NuGet][dps-service-release]][dps-service-nuget] | [![NuGet][dps-service-prerelease]][dps-service-nuget] | -| Microsoft.Azure.Devices.Provisioning.Security.Tpm | [![NuGet][dps-tpm-release]][dps-tpm-nuget] | [![NuGet][dps-tpm-prerelease]][dps-tpm-nuget] | -| Microsoft.Azure.Devices.DigitalTwin.Client | N/A | [![NuGet][pnp-device-prerelease]][pnp-device-nuget] | -| Microsoft.Azure.Devices.DigitalTwin.Service | N/A | [![NuGet][pnp-service-prerelease]][pnp-service-nuget] | +| Package Name | Release Version | +| --- | --- | +| Microsoft.Azure.Devices.Client | [![NuGet][iothub-device-release]][iothub-device-nuget] | +| Microsoft.Azure.Devices | [![NuGet][iothub-service-release]][iothub-service-nuget] | +| Microsoft.Azure.Devices.Shared | [![NuGet][iothub-shared-release]][iothub-shared-nuget] | +| Microsoft.Azure.Devices.Provisioning.Client | [![NuGet][dps-device-release]][dps-device-nuget] | +| Microsoft.Azure.Devices.Provisioning.Transport.Amqp | [![NuGet][dps-device-amqp-release]][dps-device-amqp-nuget]| +| Microsoft.Azure.Devices.Provisioning.Transport.Http | [![NuGet][dps-device-http-release]][dps-device-http-nuget]| +| Microsoft.Azure.Devices.Provisioning.Transport.Mqtt | [![NuGet][dps-device-mqtt-release]][dps-device-mqtt-nuget]| +| Microsoft.Azure.Devices.Provisioning.Service | [![NuGet][dps-service-release]][dps-service-nuget] | +| Microsoft.Azure.Devices.Provisioning.Security.Tpm | [![NuGet][dps-tpm-release]][dps-tpm-nuget] | +| Microsoft.Azure.Devices.DigitalTwin.Client | N/A | +| Microsoft.Azure.Devices.DigitalTwin.Service | N/A | + +> Note: +> 1. In addition to stable builds we also release pre-release builds that contain preview features. You can find details about the preview features released by looking at the [release notes](https://github.com/Azure/azure-iot-sdk-csharp/releases). It is not recommended to take dependency on preview NuGets for production applications as breaking changes can be introduced in preview packages. +> 2. Device streaming feature is not being included in our newer preview releases as there is no active development going on in the service. For more details on the feature, see [here](https://docs.microsoft.com/azure/iot-hub/iot-hub-device-streams-overview). +> +> This feature has not been included in any preview release after [2020-10-14](https://github.com/Azure/azure-iot-sdk-csharp/releases/tag/preview_2020-10-14). However, the feature is still available under [previews/deviceStreaming branch](https://github.com/Azure/azure-iot-sdk-csharp/tree/previews/deviceStreaming). +> +> The latest preview NuGet versions that contain the device streaming feature are: + Microsoft.Azure.Devices.Client - 1.32.0-preview-001 + Microsoft.Azure.Devices - 1.28.0-preview-001 +> 3. Stable and preview NuGet versions are not interdependent; eg. for NuGet packages versioned 1.25.0 (stable release) and 1.25.0-preview-001 (preview release), there is no guarantee that v1.25.0 contains the feature(s) previewed in v1.25.0-preview-001. For a list of updates shipped with each NuGet package, please refer to the [release notes](https://github.com/Azure/azure-iot-sdk-csharp/releases). > Note: > Device streaming feature is not being included in our newer preview releases as there is no active development going on in the service. For more details on the feature, see [here](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-device-streams-overview). It is not recommended to take dependency on preview nugets for production applications as breaking changes can be introduced in preview nugets. @@ -63,7 +90,7 @@ Visit [Azure IoT Dev Center][iot-dev-center] to learn more about developing appl Most of our samples are available at [Azure IoT Samples for C#](https://github.com/Azure-Samples/azure-iot-samples-csharp). -If you are looking for a good device sample to get started with, please see the [device reconnection sample](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Samples/device/DeviceReconnectionSample). +If you are looking for a good device sample to get started with, please see the [device reconnection sample](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/main/iot-hub/Samples/device/DeviceReconnectionSample). It shows how to connect a device, handle disconnect events, cases to handle when making calls, and when to re-initialize the `DeviceClient`. ## Contribute to the Azure IoT C# SDK @@ -147,6 +174,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/main/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 +217,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. @@ -215,34 +245,24 @@ To learn more, review the [privacy statement](https://go.microsoft.com/fwlink/?L [azure-iot-sdks]: https://github.com/azure/azure-iot-sdks [dotnet-api-reference]: https://docs.microsoft.com/dotnet/api/overview/azure/iot/client?view=azure-dotnet [iothub-device-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Client.svg?style=plastic -[iothub-device-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Client.svg?style=plastic [iothub-device-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Client/ [iothub-service-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.svg?style=plastic -[iothub-service-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.svg?style=plastic [iothub-service-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices/ [iothub-shared-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Shared.svg?style=plastic -[iothub-shared-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Shared.svg?style=plastic [iothub-shared-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Shared/ [dps-device-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Client.svg?style=plastic -[dps-device-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Client.svg?style=plastic [dps-device-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Client/ [dps-device-amqp-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.svg?style=plastic -[dps-device-amqp-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.svg?style=plastic [dps-device-amqp-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Transport.Amqp/ [dps-device-http-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Transport.Http.svg?style=plastic -[dps-device-http-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Transport.Http.svg?style=plastic [dps-device-http-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Transport.Http/ [dps-device-mqtt-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.svg?style=plastic -[dps-device-mqtt-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.svg?style=plastic [dps-device-mqtt-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt/ [dps-service-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Service.svg?style=plastic -[dps-service-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Service.svg?style=plastic [dps-service-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Service/ [dps-tpm-release]: https://img.shields.io/nuget/v/Microsoft.Azure.Devices.Provisioning.Security.Tpm.svg?style=plastic -[dps-tpm-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.Provisioning.Security.Tpm.svg?style=plastic [dps-tpm-nuget]: https://www.nuget.org/packages/Microsoft.Azure.Devices.Provisioning.Security.Tpm/ -[pnp-device-prerelease]: https://img.shields.io/nuget/vpre/Microsoft.Azure.Devices.DigitalTwin.Client.svg?style=plastic [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/shared/src/NewtonsoftJsonPayloadSerializer.cs b/shared/src/NewtonsoftJsonPayloadSerializer.cs index b38063178b..349c76676b 100644 --- a/shared/src/NewtonsoftJsonPayloadSerializer.cs +++ b/shared/src/NewtonsoftJsonPayloadSerializer.cs @@ -54,10 +54,23 @@ public override bool TryGetNestedObjectValue(object nestedObject, string prop { return false; } - if (((JObject)nestedObject).TryGetValue(propertyName, out JToken element)) + + try + { + // The supplied nested object is either a JObject or the string representation of a JObject. + JObject nestedObjectAsJObject = nestedObject.GetType() == typeof(string) + ? DeserializeToType((string)nestedObject) + : nestedObject as JObject; + + if (nestedObjectAsJObject != null && nestedObjectAsJObject.TryGetValue(propertyName, out JToken element)) + { + outValue = element.ToObject(); + return true; + } + } + catch { - outValue = element.ToObject(); - return true; + // Catch and ignore any exceptions caught } return false; } diff --git a/shared/src/PayloadConvention.cs b/shared/src/PayloadConvention.cs index 99fd943723..468083e9c1 100644 --- a/shared/src/PayloadConvention.cs +++ b/shared/src/PayloadConvention.cs @@ -7,7 +7,7 @@ namespace Microsoft.Azure.Devices.Shared /// The payload convention class. /// /// The payload convention is used to define a specific serializer as well as a specific content encoding. - /// For example, IoT has a convention that is designed + /// For example, IoT has a convention that is designed /// to make it easier to get started with products that use specific conventions by default. public abstract class PayloadConvention { diff --git a/shared/src/PayloadSerializer.cs b/shared/src/PayloadSerializer.cs index 9fced3233e..15e7ee435f 100644 --- a/shared/src/PayloadSerializer.cs +++ b/shared/src/PayloadSerializer.cs @@ -56,7 +56,8 @@ public abstract class PayloadSerializer /// An example of this would be a property under the component. /// /// The type to convert the retrieved property to. - /// The object that might contain the nested property. + /// The object that might contain the nested property. + /// This needs to be in the json object equivalent format as required by the serializer or the string representation of it. /// The name of the property to be retrieved. /// True if the nested object contains an element with the specified key; otherwise, it returns false. /// diff --git a/supported_platforms.md b/supported_platforms.md index 821b7a1977..e3562535d4 100644 --- a/supported_platforms.md +++ b/supported_platforms.md @@ -35,7 +35,7 @@ OS name: "windows server 2019", version: "10.0", arch: "amd64", family: "windows ## Ubuntu 20.04 -Note that, while we only directly test on Ubuntu 1604, we do generally support other [Linux distributions supported by .NET core](https://docs.microsoft.com/en-us/dotnet/core/install/linux). +Note that, while we only directly test on Ubuntu 20.04, we do generally support other [Linux distributions supported by .NET core](https://docs.microsoft.com/dotnet/core/install/linux). Nightly test platform details: @@ -46,7 +46,7 @@ Nightly test platform details: Default locale: en_US, platform encoding: UTF-8 -OS name: "linux", kernel version: "5.8.0-1036-azure", arch: "amd64", family: "unix" +OS name: "linux", kernel version: "5.8.0-1040-azure", arch: "amd64", family: "unix" ## Miscellaneous support notes 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..2918d1f1dd 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/main/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/main/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/main/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/tools/diffscripts/README.md b/tools/diffscripts/README.md index e222c78f17..d79ef25b18 100644 --- a/tools/diffscripts/README.md +++ b/tools/diffscripts/README.md @@ -108,7 +108,7 @@ VERBOSE: Repository base path: C:\repos VERBOSE: AsmDiff executable: dotnet-asmdiff.exe VERBOSE: Using user supplied iot-sdk-internals repository. VERBOSE: Using C:\adtexplorer\ for the internals sdk repository base directory. -VERBOSE: Directory where the SDK markdown files will be generated: C:\adtexplorer\sdk_design_docs\CSharp\master +VERBOSE: Directory where the SDK markdown files will be generated: C:\adtexplorer\sdk_design_docs\CSharp\main ... ... ``` @@ -193,15 +193,15 @@ Creating markdown for C:\repos\azure-iot-sdk-csharp\provisioning\transport\http\ Creating markdown for C:\repos\azure-iot-sdk-csharp\security\tpm\src\bin\Release\netstandard2.1\Microsoft.Azure.Devices.Provisioning.Security.Tpm.dll Changes have been detected. Verify each file listed below to be sure of the scope of changes. -There have been 3 deletions and 9 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Client.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Client.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Security.Tpm.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Service.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Http.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.md -There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Shared.md -There have been 9 deletions and 7 additions to sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.md +There have been 3 deletions and 9 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Client.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Client.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Security.Tpm.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Service.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Http.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.md +There have been 0 deletions and 2 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Shared.md +There have been 9 deletions and 7 additions to sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.md Finished generating the markdown files for comparison. Review the output above for release notes and to determine if there are version changes. ``` \ No newline at end of file diff --git a/tools/diffscripts/diffapi.ps1 b/tools/diffscripts/diffapi.ps1 index d6ca222165..4fc14c4d0c 100644 --- a/tools/diffscripts/diffapi.ps1 +++ b/tools/diffscripts/diffapi.ps1 @@ -64,7 +64,7 @@ Param( })] # The path of the iot-sdk-internals repository (ex: c:\repo\iot-sdks-internals) [System.IO.FileInfo] $SDKInternalsPath = $null, - # Indicates you will compare the output to the last preview version instead of master + # Indicates you will compare the output to the last preview version instead of main [switch] $IsPreview ) @@ -137,7 +137,7 @@ else Write-Verbose "Using $internalRootPath for the internals sdk repository base directory." # If we specify to use the preview directory on the command line we will set it as such -$compareDirectory = Join-Path -Path $internalRootPath -Child "\sdk_design_docs\CSharp\master" +$compareDirectory = Join-Path -Path $internalRootPath -Child "\sdk_design_docs\CSharp\main" if ($IsPreview) { $compareDirectory = Join-Path -Path $internalRootPath -Child "\sdk_design_docs\CSharp\preview" @@ -302,15 +302,15 @@ Set-Location -Path $compareDirectory # # # https://git-scm.com/docs/git-diff # -# 9 3 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Client.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Client.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Security.Tpm.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Service.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Http.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.md -# 2 0 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.Shared.md -# 7 9 sdk_design_docs/CSharp/master/Microsoft.Azure.Devices.md +# 9 3 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Client.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Client.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Security.Tpm.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Service.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Amqp.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Http.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Provisioning.Transport.Mqtt.md +# 2 0 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.Shared.md +# 7 9 sdk_design_docs/CSharp/main/Microsoft.Azure.Devices.md $gitDiffOutput = git diff --ignore-all-space --numstat Write-Verbose "Output off git diff --ignore-all-space --numstat" diff --git a/vsts/vsts.yaml b/vsts/vsts.yaml index 3e4568ce24..a7f8c7de81 100644 --- a/vsts/vsts.yaml +++ b/vsts/vsts.yaml @@ -4,7 +4,7 @@ trigger: batch: true branches: include: - - master + - main paths: exclude: - docs/* @@ -332,7 +332,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