iothub/service/src/DigitalTwin/DigitalTwinClient.cs (192 lines of code) (raw):

// 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.Net.Http; using System.Threading; using System.Threading.Tasks; using Azure; using Azure.Core; using Microsoft.Azure.Devices.Authentication; using Microsoft.Azure.Devices.Common; using Microsoft.Azure.Devices.DigitalTwin.Authentication; using Microsoft.Azure.Devices.Extensions; using Microsoft.Azure.Devices.Generated; using Microsoft.Rest; using Newtonsoft.Json; using PnpDigitalTwin = Microsoft.Azure.Devices.Generated.DigitalTwin; namespace Microsoft.Azure.Devices { /// <summary> /// This client contains methods to retrieve and update digital twin information, and invoke commands /// on a digital twin device. /// </summary> /// <remarks> /// For more information, see <see href="https://github.com/Azure/azure-iot-sdk-csharp#iot-hub-service-sdk"/>. /// <para> /// This client creates lifetime long instances of <see cref="HttpClient"/> that are tied to the URI of the /// IoT hub specified, configure any proxy settings, and connection lease timeout. /// For that reason, the instances are not static and an application using this client /// should create and save it for all use. Repeated creation may cause /// <see href="https://docs.microsoft.com/azure/architecture/antipatterns/improper-instantiation/">socket exhaustion</see>. /// </para> /// </remarks> public class DigitalTwinClient : IDisposable { private const string HttpsEndpointPrefix = "https"; private readonly IotHubGatewayServiceAPIs _client; private readonly PnpDigitalTwin _protocolLayer; /// <summary> /// Creates an instance of <see cref="DigitalTwinClient"/>, provided for unit testing purposes only. /// </summary> public DigitalTwinClient() { } /// <summary> /// Creates DigitalTwinClient from an IoT hub connection string. /// </summary> /// <param name="connectionString">The IoT hub's connection string.</param> /// <param name="handlers"> /// 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. /// </param> /// <returns>A DigitalTwinsClient instance.</returns> public static DigitalTwinClient CreateFromConnectionString(string connectionString, params DelegatingHandler[] handlers) { connectionString.ThrowIfNullOrWhiteSpace(nameof(connectionString)); var iotHubConnectionString = IotHubConnectionString.Parse(connectionString); var connectionStringCredential = new DigitalTwinConnectionStringCredential(iotHubConnectionString); return new DigitalTwinClient(iotHubConnectionString.HostName, connectionStringCredential, handlers); } /// <summary> /// Creates DigitalTwinClient, authenticating using an identity in Azure Active Directory (AAD). /// </summary> /// <remarks> /// For more about information on the options of authenticating using a derived instance of <see cref="TokenCredential"/>, see /// <see href="https://docs.microsoft.com/dotnet/api/overview/azure/identity-readme"/>. /// For more information on configuring IoT hub with Azure Active Directory, see /// <see href="https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-azure-ad-rbac"/> /// </remarks> /// <param name="hostName">IoT hub host name.</param> /// <param name="credential">Azure Active Directory (AAD) credentials to authenticate with IoT hub. See <see cref="TokenCredential"/></param> /// <param name="handlers"> /// 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. /// </param> /// <returns>A DigitalTwinsClient instance.</returns> public static DigitalTwinClient Create( string hostName, TokenCredential credential, params DelegatingHandler[] handlers) { return Create(hostName, credential, CommonConstants.IotHubAadTokenScopes, handlers); } /// <summary> /// Creates DigitalTwinClient, authenticating using an identity in Azure Active Directory (AAD). /// </summary> /// <remarks> /// For more about information on the options of authenticating using a derived instance of <see cref="TokenCredential"/>, see /// <see href="https://docs.microsoft.com/dotnet/api/overview/azure/identity-readme"/>. /// For more information on configuring IoT hub with Azure Active Directory, see /// <see href="https://docs.microsoft.com/azure/iot-hub/iot-hub-dev-guide-azure-ad-rbac"/> /// </remarks> /// <param name="hostName">IoT hub host name.</param> /// <param name="credential">Azure Active Directory (AAD) credentials to authenticate with IoT hub. See <see cref="TokenCredential"/></param> /// <param name="handlers"> /// 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. /// </param> /// <param name="scopes">The custom scopes to use when authenticating.</param> /// <returns>A DigitalTwinsClient instance.</returns> public static DigitalTwinClient Create( string hostName, TokenCredential credential, string[] scopes, params DelegatingHandler[] handlers) { if (string.IsNullOrEmpty(hostName)) { throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); } if (credential == null) { throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var tokenCredential = new DigitalTwinTokenCredential(credential); return new DigitalTwinClient(hostName, tokenCredential, handlers); } /// <summary> /// Creates DigitalTwinClient using a shared access signature provided and refreshed as necessary by the caller. /// </summary> /// <remarks> /// Users may wish to build their own shared access signature (SAS) tokens rather than give the shared key to the SDK and let it manage signing and renewal. /// The <see cref="AzureSasCredential"/> object gives the SDK access to the SAS token, while the caller can update it as necessary using the /// <see cref="AzureSasCredential.Update(string)"/> method. /// </remarks> /// <param name="hostName">IoT hub host name.</param> /// <param name="credential">Credential that generates a SAS token to authenticate with IoT hub. See <see cref="AzureSasCredential"/>.</param> /// <param name="handlers">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.</param> /// <returns>A DigitalTwinsClient instance.</returns> public static DigitalTwinClient Create( string hostName, AzureSasCredential credential, params DelegatingHandler[] handlers) { if (string.IsNullOrEmpty(hostName)) { throw new ArgumentNullException($"{nameof(hostName)}, Parameter cannot be null or empty"); } if (credential == null) { throw new ArgumentNullException($"{nameof(credential)}, Parameter cannot be null"); } var sasCredential = new DigitalTwinSasCredential(credential); return new DigitalTwinClient(hostName, sasCredential, handlers); } /// <summary> /// Gets a strongly-typed digital twin. /// </summary> /// <param name="digitalTwinId">The Id of the digital twin.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The application/json digital twin and the http response.</returns> public virtual async Task<HttpOperationResponse<T, DigitalTwinGetHeaders>> GetDigitalTwinAsync<T>(string digitalTwinId, CancellationToken cancellationToken = default) { using HttpOperationResponse<string, DigitalTwinGetHeaders> response = await _protocolLayer.GetDigitalTwinWithHttpMessagesAsync(digitalTwinId, null, cancellationToken) .ConfigureAwait(false); return new HttpOperationResponse<T, DigitalTwinGetHeaders> { Body = typeof(T) == typeof(string) ? (T)(object)response.Body : JsonConvert.DeserializeObject<T>(response.Body, JsonSerializerSettingsInitializer.GetJsonSerializerSettings()), Headers = response.Headers, Request = response.Request, Response = response.Response }; } /// <summary> /// Updates a digital twin. /// </summary> /// <remarks> /// For further information on how to create the json-patch, see <see href="https://docs.microsoft.com/azure/iot-pnp/howto-manage-digital-twin"/>. /// </remarks> /// <param name="digitalTwinId">The Id of the digital twin.</param> /// <param name="digitalTwinUpdateOperations">The application/json-patch+json operations to be performed on the specified digital twin.</param> /// <param name="requestOptions">The optional settings for this request.</param> /// <param name="cancellationToken">The cancellationToken.</param> /// <returns>The http response.</returns> public virtual Task<HttpOperationHeaderResponse<DigitalTwinUpdateHeaders>> UpdateDigitalTwinAsync( string digitalTwinId, string digitalTwinUpdateOperations, DigitalTwinUpdateRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { return _protocolLayer.UpdateDigitalTwinWithHttpMessagesAsync(digitalTwinId, digitalTwinUpdateOperations, requestOptions?.IfMatch, null, cancellationToken); } /// <summary> /// Invoke a command on a digital twin. /// </summary> /// <param name="digitalTwinId">The Id of the digital twin.</param> /// <param name="commandName">The command to be invoked.</param> /// <param name="payload">The command payload.</param> /// <param name="requestOptions">The optional settings for this request.</param> /// <param name="cancellationToken">The cancellationToken.</param> /// <returns>The application/json command invocation response and the http response. </returns> public virtual async Task<HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>> InvokeCommandAsync( string digitalTwinId, string commandName, string payload = default, DigitalTwinInvokeCommandRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { using HttpOperationResponse<string, DigitalTwinInvokeRootLevelCommandHeaders> response = await _protocolLayer.InvokeRootLevelCommandWithHttpMessagesAsync( digitalTwinId, commandName, payload, requestOptions?.ConnectTimeoutInSeconds, requestOptions?.ResponseTimeoutInSeconds, null, cancellationToken) .ConfigureAwait(false); return new HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders> { Body = new DigitalTwinCommandResponse { Status = response.Headers.XMsCommandStatuscode, Payload = response.Body }, Headers = new DigitalTwinInvokeCommandHeaders { RequestId = response.Headers.XMsRequestId }, Request = response.Request, Response = response.Response, }; } /// <summary> /// Invoke a command on a component of a digital twin. /// </summary> /// <param name="digitalTwinId">The Id of the digital twin.</param> /// <param name="componentName">The component name under which the command is defined.</param> /// <param name="commandName">The command to be invoked.</param> /// <param name="payload">The command payload.</param> /// <param name="requestOptions">The optional settings for this request.</param> /// <param name="cancellationToken">The cancellationToken.</param> /// <returns>The application/json command invocation response and the http response.</returns> public virtual async Task<HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders>> InvokeComponentCommandAsync( string digitalTwinId, string componentName, string commandName, string payload = default, DigitalTwinInvokeCommandRequestOptions requestOptions = default, CancellationToken cancellationToken = default) { using HttpOperationResponse<string, DigitalTwinInvokeComponentCommandHeaders> response = await _protocolLayer.InvokeComponentCommandWithHttpMessagesAsync( digitalTwinId, componentName, commandName, payload, requestOptions?.ConnectTimeoutInSeconds, requestOptions?.ResponseTimeoutInSeconds, null, cancellationToken) .ConfigureAwait(false); return new HttpOperationResponse<DigitalTwinCommandResponse, DigitalTwinInvokeCommandHeaders> { Body = new DigitalTwinCommandResponse { Status = response.Headers.XMsCommandStatuscode, Payload = response.Body }, Headers = new DigitalTwinInvokeCommandHeaders { RequestId = response.Headers.XMsRequestId }, Request = response.Request, Response = response.Response, }; } /// <inheritdoc /> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> protected virtual void Dispose(bool disposing) { _client?.Dispose(); } private DigitalTwinClient(string hostName, DigitalTwinServiceClientCredentials credentials, params DelegatingHandler[] handlers) { Uri httpsEndpoint = new UriBuilder(HttpsEndpointPrefix, hostName).Uri; HttpMessageHandler httpMessageHandler = HttpClientHelper.CreateDefaultHttpMessageHandler(null, httpsEndpoint, ServicePointHelpers.DefaultConnectionLeaseTimeout); #pragma warning disable CA2000 // Dispose objects before losing scope (httpMessageHandlerWithDelegatingHandlers is disposed when the http client owning it is disposed) HttpMessageHandler httpMessageHandlerWithDelegatingHandlers = CreateHttpHandlerPipeline(httpMessageHandler, handlers); #pragma warning restore CA2000 // Dispose objects before losing scope #pragma warning disable CA2000 // Dispose objects before losing scope (httpClient is disposed when the protocol layer client owning it is disposed) var httpClient = new HttpClient(httpMessageHandlerWithDelegatingHandlers, true) { BaseAddress = httpsEndpoint }; #pragma warning restore CA2000 // Dispose objects before losing scope #pragma warning restore CA2000 // Dispose objects before losing scope // When this client is disposed, all the http message handlers and delegating handlers will be disposed automatically _client = new IotHubGatewayServiceAPIs(credentials, httpClient, true); _client.BaseUri = httpsEndpoint; _protocolLayer = new PnpDigitalTwin(_client); } // Creates a single HttpMessageHandler to construct a HttpClient with from a base httpMessageHandler and some number of custom delegating handlers // This is almost a copy of the Microsoft.Rest.ClientRuntime library's implementation, but with the return and parameter type HttpClientHandler replaced // with the more abstract HttpMessageHandler in order for us to set the base handler as either a SocketsHttpHandler for .net core or an HttpClientHandler otherwise // https://github.com/Azure/azure-sdk-for-net/blob/99f4da88ab0aa01c79aa291c6c101ab94c4ac940/sdk/mgmtcommon/ClientRuntime/ClientRuntime/ServiceClient.cs#L376 private static HttpMessageHandler CreateHttpHandlerPipeline(HttpMessageHandler httpMessageHandler, params DelegatingHandler[] handlers) { // The RetryAfterDelegatingHandler should be the absolute outermost handler // because it's extremely lightweight and non-interfering HttpMessageHandler currentHandler = #pragma warning disable CA2000 // Dispose objects before losing scope (delegating handler is disposed when the http client that uses it is disposed) new RetryDelegatingHandler(new RetryAfterDelegatingHandler { InnerHandler = httpMessageHandler }); #pragma warning restore CA2000 // Dispose objects before losing scope if (handlers != null) { for (int i = handlers.Length - 1; i >= 0; --i) { DelegatingHandler handler = handlers[i]; // Non-delegating handlers are ignored since we always // have RetryDelegatingHandler as the outer-most handler while (handler.InnerHandler is DelegatingHandler) { handler = handler.InnerHandler as DelegatingHandler; } handler.InnerHandler = currentHandler; currentHandler = handlers[i]; } } return currentHandler; } } }