http://fabric/app/service/#/partitionkey/any|primary|secondary/endpoint-name/api-path
        public async Task<IActionResult> GetQueueLengthAsync()
        {
            ServiceUriBuilder uriBuilder = new ServiceUriBuilder(TenantDataServiceName);
            Uri serviceUri = uriBuilder.Build();

            // service may be partitioned.
            // this will aggregate the queue lengths from each partition
            ServicePartitionList partitions = await this.fabricClient.QueryManager.GetPartitionListAsync(serviceUri);
            
            long count = 0;
            foreach (Partition partition in partitions)
            {
                Uri getUrl = new HttpServiceUriBuilder()
                    .SetServiceName(serviceUri)
                    .SetPartitionKey(((Int64RangePartitionInformation) partition.PartitionInformation).LowKey)
                    .SetServicePathAndQuery($"/api/devices/queue/length")
                    .Build();

                HttpResponseMessage response = await this.httpClient.GetAsync(getUrl, this.serviceCancellationToken);

                if (response.StatusCode != System.Net.HttpStatusCode.OK)
                {
                    return this.StatusCode((int) response.StatusCode);
                }

                string result = await response.Content.ReadAsStringAsync();

                count += Int64.Parse(result);
            }

            return this.Ok(count);
        }
        /// <summary>
        /// http://fabric/app/service/#/partitionkey/any|primary|secondary/endpoint-name/api-path
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        protected override async Task <HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            ServicePartitionResolver resolver   = ServicePartitionResolver.GetDefault();
            ResolvedServicePartition partition  = null;
            HttpServiceUriBuilder    uriBuilder = new HttpServiceUriBuilder(request.RequestUri);

            int  retries        = MaxRetries;
            int  retryDelay     = InitialRetryDelayMs;
            bool resolveAddress = true;

            HttpResponseMessage lastResponse  = null;
            Exception           lastException = null;

            while (retries-- > 0)
            {
                lastResponse = null;
                cancellationToken.ThrowIfCancellationRequested();

                if (resolveAddress)
                {
                    partition = partition != null
                        ? await resolver.ResolveAsync(partition, cancellationToken)
                        : await resolver.ResolveAsync(uriBuilder.ServiceName, uriBuilder.PartitionKey, cancellationToken);

                    string serviceEndpointJson;

                    switch (uriBuilder.Target)
                    {
                    case HttpServiceUriTarget.Default:
                    case HttpServiceUriTarget.Primary:
                        serviceEndpointJson = partition.GetEndpoint().Address;
                        break;

                    case HttpServiceUriTarget.Secondary:
                        serviceEndpointJson = partition.Endpoints.ElementAt(this.random.Next(1, partition.Endpoints.Count)).Address;
                        break;

                    case HttpServiceUriTarget.Any:
                    default:
                        serviceEndpointJson = partition.Endpoints.ElementAt(this.random.Next(0, partition.Endpoints.Count)).Address;
                        break;
                    }

                    string endpointUrl = JObject.Parse(serviceEndpointJson)["Endpoints"][uriBuilder.EndpointName].Value <string>();

                    request.RequestUri = new Uri($"{endpointUrl.TrimEnd('/')}/{uriBuilder.ServicePathAndQuery.TrimStart('/')}", UriKind.Absolute);
                }

                try
                {
                    lastResponse = await base.SendAsync(request, cancellationToken);

                    if (lastResponse.StatusCode == HttpStatusCode.NotFound ||
                        lastResponse.StatusCode == HttpStatusCode.ServiceUnavailable)
                    {
                        resolveAddress = true;
                    }
                    else
                    {
                        return(lastResponse);
                    }
                }
                catch (TimeoutException te)
                {
                    lastException  = te;
                    resolveAddress = true;
                }
                catch (SocketException se)
                {
                    lastException  = se;
                    resolveAddress = true;
                }
                catch (HttpRequestException hre)
                {
                    lastException  = hre;
                    resolveAddress = true;
                }
                catch (Exception ex)
                {
                    lastException = ex;
                    WebException we = ex as WebException;

                    if (we == null)
                    {
                        we = ex.InnerException as WebException;
                    }

                    if (we != null)
                    {
                        HttpWebResponse errorResponse = we.Response as HttpWebResponse;

                        // the following assumes port sharing
                        // where a port is shared by multiple replicas within a host process using a single web host (e.g., http.sys).
                        if (we.Status == WebExceptionStatus.ProtocolError)
                        {
                            if (errorResponse.StatusCode == HttpStatusCode.NotFound ||
                                errorResponse.StatusCode == HttpStatusCode.ServiceUnavailable)
                            {
                                // This could either mean we requested an endpoint that does not exist in the service API (a user error)
                                // or the address that was resolved by fabric client is stale (transient runtime error) in which we should re-resolve.
                                resolveAddress = true;
                            }

                            // On any other HTTP status codes, re-throw the exception to the caller.
                            throw;
                        }

                        if (we.Status == WebExceptionStatus.Timeout ||
                            we.Status == WebExceptionStatus.RequestCanceled ||
                            we.Status == WebExceptionStatus.ConnectionClosed ||
                            we.Status == WebExceptionStatus.ConnectFailure)
                        {
                            resolveAddress = true;
                        }
                    }
                    else
                    {
                        throw;
                    }
                }

                await Task.Delay(retryDelay);

                retryDelay += retryDelay;
            }

            if (lastResponse != null)
            {
                return(lastResponse);
            }
            else
            {
                throw lastException;
            }
        }
        /// <summary>
        /// http://fabric/app/service/#/partitionkey/any|primary|secondary/endpoint-name/api-path
        /// </summary>
        /// <param name="request"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            ServicePartitionResolver resolver = ServicePartitionResolver.GetDefault();
            ResolvedServicePartition partition = null;
            HttpServiceUriBuilder uriBuilder = new HttpServiceUriBuilder(request.RequestUri);

            int retries = MaxRetries;
            int retryDelay = InitialRetryDelayMs;
            bool resolveAddress = true;

            HttpResponseMessage lastResponse = null;
            Exception lastException = null;

            while (retries --> 0)
            {
                lastResponse = null;
                cancellationToken.ThrowIfCancellationRequested();

                if (resolveAddress)
                {
                    partition = partition != null
                        ? await resolver.ResolveAsync(partition, cancellationToken)
                        : await resolver.ResolveAsync(uriBuilder.ServiceName, uriBuilder.PartitionKey, cancellationToken);

                    string serviceEndpointJson;

                    switch (uriBuilder.Target)
                    {
                        case HttpServiceUriTarget.Default:
                        case HttpServiceUriTarget.Primary:
                            serviceEndpointJson = partition.GetEndpoint().Address;
                            break;
                        case HttpServiceUriTarget.Secondary:
                            serviceEndpointJson = partition.Endpoints.ElementAt(this.random.Next(1, partition.Endpoints.Count)).Address;
                            break;
                        case HttpServiceUriTarget.Any:
                        default:
                            serviceEndpointJson = partition.Endpoints.ElementAt(this.random.Next(0, partition.Endpoints.Count)).Address;
                            break;
                    }

                    string endpointUrl = JObject.Parse(serviceEndpointJson)["Endpoints"][uriBuilder.EndpointName].Value<string>();

                    request.RequestUri = new Uri($"{endpointUrl.TrimEnd('/')}/{uriBuilder.ServicePathAndQuery.TrimStart('/')}", UriKind.Absolute);
                }

                try
                {
                    lastResponse = await base.SendAsync(request, cancellationToken);

                    if (lastResponse.StatusCode == HttpStatusCode.NotFound ||
                        lastResponse.StatusCode == HttpStatusCode.ServiceUnavailable)
                    {
                        resolveAddress = true;
                    }
                    else
                    {
                        return lastResponse;
                    }
                }
                catch (TimeoutException te)
                {
                    lastException = te;
                    resolveAddress = true;
                }
                catch (SocketException se)
                {
                    lastException = se;
                    resolveAddress = true;
                }
                catch (HttpRequestException hre)
                {
                    lastException = hre;
                    resolveAddress = true;
                }
                catch (Exception ex)
                {
                    lastException = ex;
                    WebException we = ex as WebException;

                    if (we == null)
                    {
                        we = ex.InnerException as WebException;
                    }

                    if (we != null)
                    {
                        HttpWebResponse errorResponse = we.Response as HttpWebResponse;

                        // the following assumes port sharing
                        // where a port is shared by multiple replicas within a host process using a single web host (e.g., http.sys).
                        if (we.Status == WebExceptionStatus.ProtocolError)
                        {
                            if (errorResponse.StatusCode == HttpStatusCode.NotFound ||
                                errorResponse.StatusCode == HttpStatusCode.ServiceUnavailable)
                            {
                                // This could either mean we requested an endpoint that does not exist in the service API (a user error)
                                // or the address that was resolved by fabric client is stale (transient runtime error) in which we should re-resolve.
                                resolveAddress = true;
                            }

                            // On any other HTTP status codes, re-throw the exception to the caller.
                            throw;
                        }

                        if (we.Status == WebExceptionStatus.Timeout ||
                            we.Status == WebExceptionStatus.RequestCanceled ||
                            we.Status == WebExceptionStatus.ConnectionClosed ||
                            we.Status == WebExceptionStatus.ConnectFailure)
                        {
                            resolveAddress = true;
                        }
                    }
                    else
                    {
                        throw;
                    }
                }

                await Task.Delay(retryDelay);

                retryDelay += retryDelay;
            }

            if (lastResponse != null)
            {
                return lastResponse;
            }
            else
            {
                throw lastException;
            }
        }
        /// <summary>
        /// This is the main entry point for your service replica.
        /// This method executes when this replica of your service becomes primary and has write status.
        /// </summary>
        /// <param name="cancellationToken">Canceled when Service Fabric needs to shut down this service replica.</param>
        protected override async Task RunAsync(CancellationToken cancellationToken)
        {
            // Get the IoT Hub connection string from the Settings.xml config file
            // from a configuration package named "Config"
            string iotHubConnectionString =
                this.Context.CodePackageActivationContext
                    .GetConfigurationPackageObject("Config")
                    .Settings
                    .Sections["IoTHubConfigInformation"]
                    .Parameters["ConnectionString"]
                    .Value;

            // These Reliable Dictionaries are used to keep track of our position in IoT Hub.
            // If this service fails over, this will allow it to pick up where it left off in the event stream.
            IReliableDictionary<string, string> offsetDictionary =
                await this.StateManager.GetOrAddAsync<IReliableDictionary<string, string>>(OffsetDictionaryName);

            IReliableDictionary<string, long> epochDictionary =
                await this.StateManager.GetOrAddAsync<IReliableDictionary<string, long>>(EpochDictionaryName);

            // Each partition of this service corresponds to a partition in IoT Hub.
            // IoT Hub partitions are numbered 0..n-1, up to n = 32.
            // This service needs to use an identical partitioning scheme. 
            // The low key of every partition corresponds to an IoT Hub partition.
            Int64RangePartitionInformation partitionInfo = (Int64RangePartitionInformation)this.Partition.PartitionInfo;
            long servicePartitionKey = partitionInfo.LowKey;

            EventHubReceiver eventHubReceiver = null;
            MessagingFactory messagingFactory = null;

            try
            {
                // Get an EventHubReceiver and the MessagingFactory used to create it.
                // The EventHubReceiver is used to get events from IoT Hub.
                // The MessagingFactory is just saved for later so it can be closed before RunAsync exits.
                Tuple<EventHubReceiver, MessagingFactory> iotHubInfo =
                    await this.ConnectToIoTHubAsync(iotHubConnectionString, servicePartitionKey, epochDictionary, offsetDictionary);

                eventHubReceiver = iotHubInfo.Item1;
                messagingFactory = iotHubInfo.Item2;

                // HttpClient is designed as a shared object. 
                // A single instance should be used throughout the lifetime of RunAsync.
                using (HttpClient httpClient = new HttpClient(new HttpServiceClientHandler()))
                {

                    int offsetIteration = 0;

                    while (true)
                    {
                        cancellationToken.ThrowIfCancellationRequested();

                        try
                        {
                            // It's important to set a low wait time here in lieu of a cancellation token
                            // so that this doesn't block RunAsync from exiting when Service Fabric needs it to complete.
                            // ReceiveAsync is a long-poll operation, so the timeout should not be too low,
                            // yet not too high to block RunAsync from exiting within a few seconds.
                            using (EventData eventData = await eventHubReceiver.ReceiveAsync(TimeSpan.FromSeconds(5)))
                            {
                                if (eventData == null)
                                {
                                    continue;
                                }

                                string tenantId = (string)eventData.Properties["TenantID"];
                                string deviceId = (string)eventData.Properties["DeviceID"];

                                // This is the named service instance of the tenant data service that the event should be sent to.
                                // The tenant ID is part of the named service instance name.
                                // The incoming device data stream specifie which tenant the data belongs to.
                                Uri tenantServiceName = new Uri($"{Names.TenantApplicationNamePrefix}/{tenantId}/{Names.TenantDataServiceName}");
                                long tenantServicePartitionKey = FnvHash.Hash(deviceId);

                                // The tenant data service exposes an HTTP API.
                                // For incoming device events, the URL is /api/events/{deviceId}
                                // This sets up a URL and sends a POST request with the device JSON payload.
                                Uri postUrl = new HttpServiceUriBuilder()
                                    .SetServiceName(tenantServiceName)
                                    .SetPartitionKey(tenantServicePartitionKey)
                                    .SetServicePathAndQuery($"/api/events/{deviceId}")
                                    .Build();

                                // The device stream payload isn't deserialized and buffered in memory here.
                                // Instead, we just can just hook the incoming stream from Iot Hub right into the HTTP request stream.
                                using (Stream eventStream = eventData.GetBodyStream())
                                {
                                    using (StreamContent postContent = new StreamContent(eventStream))
                                    {
                                        postContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                                        HttpResponseMessage response = await httpClient.PostAsync(postUrl, postContent, cancellationToken);

                                        ServiceEventSource.Current.ServiceMessage(
                                            this.Context,
                                            "Sent event data to tenant service '{0}' with partition key '{1}'. Result: {2}",
                                            tenantServiceName,
                                            tenantServicePartitionKey,
                                            response.StatusCode.ToString());

                                        if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
                                        {
                                            // This service expects the receiving tenant service to return HTTP 400 if the device message was malformed.
                                            // In this example, the message is simply logged.
                                            // Your application should handle all possible error status codes from the receiving service
                                            // and treat the message as a "poison" message.
                                            // Message processing should be allowed to continue after a poison message is detected.

                                            string responseContent = await response.Content.ReadAsStringAsync();

                                            ServiceEventSource.Current.ServiceMessage(
                                                this.Context,
                                                "Tenant service '{0}' returned HTTP 400 due to a bad device message from device '{1}'. Error message: '{2}'",
                                                tenantServiceName,
                                                deviceId,
                                                responseContent);
                                        }
                                    }
                                }

                                // Save the current Iot Hub data stream offset.
                                // This will allow the service to pick up from its current location if it fails over.
                                // Duplicate device messages may still be sent to the the tenant service 
                                // if this service fails over after the message is sent but before the offset is saved.
                                if (++offsetIteration % OffsetInterval == 0)
                                {
                                    ServiceEventSource.Current.ServiceMessage(
                                            this.Context,
                                            "Saving offset {0}",
                                            eventData.Offset);

                                    using (ITransaction tx = this.StateManager.CreateTransaction())
                                    {
                                        await offsetDictionary.SetAsync(tx, "offset", eventData.Offset);
                                        await tx.CommitAsync();
                                    }

                                    offsetIteration = 0;
                                }
                            }
                        }
                        catch (TimeoutException te)
                        {
                            // transient error. Retry.
                            ServiceEventSource.Current.ServiceMessage(this.Context, $"TimeoutException in RunAsync: {te.ToString()}");
                        }
                        catch (FabricTransientException fte)
                        {
                            // transient error. Retry.
                            ServiceEventSource.Current.ServiceMessage(this.Context, $"FabricTransientException in RunAsync: {fte.ToString()}");
                        }
                        catch (FabricNotPrimaryException)
                        {
                            // not primary any more, time to quit.
                            return;
                        }
                        catch (Exception ex)
                        {
                            ServiceEventSource.Current.ServiceMessage(this.Context, ex.ToString());

                            throw;
                        }
                    }
                }
            }
            finally
            {
                if (messagingFactory != null)
                {
                    await messagingFactory.CloseAsync();
                }
            }
        }
        public async Task<IActionResult> GetDevicesAsync()
        {
            ServiceUriBuilder uriBuilder = new ServiceUriBuilder(TenantDataServiceName);
            Uri serviceUri = uriBuilder.Build();

            // service may be partitioned.
            // this will aggregate device IDs from all partitions
            ServicePartitionList partitions = await this.fabricClient.QueryManager.GetPartitionListAsync(serviceUri);
            
            List<DeviceViewModel> deviceViewModels = new List<DeviceViewModel>();
            foreach (Partition partition in partitions)
            {
                Uri getUrl = new HttpServiceUriBuilder()
                    .SetServiceName(serviceUri)
                    .SetPartitionKey(((Int64RangePartitionInformation) partition.PartitionInformation).LowKey)
                    .SetServicePathAndQuery($"/api/devices")
                    .Build();

                HttpResponseMessage response = await this.httpClient.GetAsync(getUrl, this.serviceCancellationToken);

                if (response.StatusCode != System.Net.HttpStatusCode.OK)
                {
                    return this.StatusCode((int) response.StatusCode);
                }

                JsonSerializer serializer = new JsonSerializer();
                using (StreamReader streamReader = new StreamReader(await response.Content.ReadAsStreamAsync()))
                {
                    using (JsonTextReader jsonReader = new JsonTextReader(streamReader))
                    {
                        List<DeviceViewModel> result = serializer.Deserialize<List<DeviceViewModel>>(jsonReader);

                        if (result != null)
                        {
                            deviceViewModels.AddRange(result);
                        }
                    }
                }
            }

            return this.Ok(deviceViewModels);
        }