CHAPTER 5
In this chapter, we want to implement a backend infrastructure based on Durable Functions (and durable entities) to manage the actual state of a set of IoT devices.
The following figure shows you the scenario.

Figure 31: The architecture schema for the IoT backend platform
The scenario shown in Figure 31 is a classic IoT scenario. A set of devices produce telemetries (in our example, we suppose that they produce temperature telemetries, but we try to generalize the kind of telemetry). These telemetries must be ingested in our cloud platform to store and use data. We use an IoTHub service to ingest data, and we must implement a dispatcher layer to get telemetries from IoTHub and dispatch them to the correct device in the backend layer.
We don't care about the IoTHub and the dispatcher in this example because they aren't topics related to Durable Functions. You can find several implementations of these components in Microsoft documentation.
Let’s suppose that the backend layer must have these requirements:
During the example explanation, we try to emphasize how we implement every single requirement.
We can design our solution with three layers, as the following figure shows.

Figure 32: The layers of the IoT platform
The front-end layer contains all the REST endpoints to expose all the features requested in the requirements. This layer is composed of durable clients that interact with the stateful layer.
The stateful layer contains all the objects that manage the status of the devices. This layer is composed of durable entities, and each entity manages a device in the field.
Finally, the integration layer contains all the workflows invoked by the stateful layer and manages the integration with the external services. In this layer, you can find orchestrators and activities.
The front-end layer contains the client functions that implement the APIs you can use to send telemetry to the devices or retrieve information from them.
These are the clients in the front-end layer:
Each of those client functions uses one of the previous chapters' communication patterns to communicate with the stateful layer composed of entities.
The following snippet of code shows you the implementation of SendTelemetryToDevice client functions.
Code Listing 46: SendTelemetryToDevice function
[FunctionName(nameof(SendTelemetryToDevice))] public async Task<IActionResult> SendTelemetryToDevice( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "devices/{deviceId}/telemetries")] HttpRequest req, string deviceId, [DurableClient] IDurableEntityClient client, ILogger logger) { var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var telemetry = JsonConvert.DeserializeObject<DeviceTelemetry>(requestBody); telemetry.DeviceId = deviceId; var entityId = await _entityfactory.GetEntityIdAsync(telemetry.DeviceId, telemetry.Type, default); await client.SignalEntityAsync<IDeviceEntity>(entityId, d => d.TelemetryReceived(telemetry)); return new OkObjectResult(telemetry); } |
The TelemetryDispatcher component in our solution retrieves telemetry from the IoTHub, creates a DeviceTelemetry object, and sends it to the front-end layer using the POST API exposed by this function.
In the following figure, you can see the structure of the DeviceTelemetry class.

Figure 33: DeviceTelemetry class structure
You can add all the properties you need to enrich the information to send to the devices:
The SendTelemetryToDevice client is easy, and its flow is the following:
In this scenario, the signaling approach is the right choice because the client must send the telemetry to the device without waiting for the response. This way, it can immediately close and free its resources for the other telemetries sent by the TelemetryDispatcher.
The GetDevices client function allows you to search devices and retrieves information about them. The implementation code of this client is the following.
Code Listing 47: GetDevices function
public async Task<IActionResult> GetDevices( [HttpTrigger(AuthorizationLevel.Function, "get", Route = "devices")] HttpRequest req, [DurableClient] IDurableEntityClient client) { if (!Enum.TryParse(typeof(DeviceType), req.Query["deviceType"], true, out var deviceType)) { return new BadRequestResult(); } var result = new List<DeviceInfoModel>(); EntityQuery queryDefinition = new EntityQuery() { PageSize = 100, FetchState = true, }; queryDefinition.EntityName = await _entityfactory.GetEntityNameAsync((DeviceType)deviceType, default); do { EntityQueryResult queryResult = await client.ListEntitiesAsync(queryDefinition, default); foreach (var item in queryResult.Entities) { DeviceInfoModel model = item.ToDeviceInfoModel(); // If you want to add other filters to your method, // you can add them here before adding the model to the return list. result.Add(model); } queryDefinition.ContinuationToken = queryResult.ContinuationToken; } while (queryDefinition.ContinuationToken != null); return new OkObjectResult(result); } |
The function expects a parameter in the query string of the request to define the device type. If this parameter is not present, the function returns an HTTP 400 (bad request) error.
The function code consists of two main steps.
The first step consists of creating an EntityQuery object to control the behavior of the ListEntitiesAsync method used later in the code. The EntityQuery class allows you to configure the page size of the list of entities for each request (PageSize property); to define if you want to retrieve only the device metadata or also the state (FetchState property); or to add a filter on the entity name (EntityName property).
If you don't set the EntityName, the ListEntitiesAsync method retrieves all the entities in the platform. Our sample generates the entity's name using a factory similar to the factory used in the SendTelemetryToDevice function to generate the EntityId.
The next logical step in the code is retrieving the entities data from the platform. For this purpose, you can use the ListEntitiesAsync method. Every time you call it, you retrieve a page of entities based on the setting you define in the EntityQuery class mentioned earlier. The return value of the ListEntitiesAsync method contains the page of results (property Entities) and the continuation token (ContinuationToken property).
If you want the next page of the query, you need to call the ListEntitiesAsync method again, passing EntityQuery with the continuation token retrieved in the previous call. You reach the last page when the continuation token returned by the ListEntityAsync method is null.
The Entities property is a list that contains the entity information. It includes the entity ID, the last operation timestamp, and the entity's status if you enable the FetchState property in the EntityQuery. This status is a JSON object stored in a JToken class.
In our example, we deserialize the JSON object to fill the list of the result to return to the client caller.
You can add other filters to your client functions (such as the device's name or the last operation timestamp) before adding the state to the return list.
The GetDevice client function allows you to retrieve information about a specific device, and its code is the following.
Code Listing 48: GetDevice function
[FunctionName(nameof(GetDevice))] public async Task<IActionResult> GetDevice( [HttpTrigger(AuthorizationLevel.Function, "get", Route = "devices/{deviceId}")] HttpRequest req, string deviceId, [DurableClient] IDurableEntityClient client) { if (!Enum.TryParse(typeof(DeviceType), req.Query["deviceType"], true, out var deviceType)) { return new BadRequestObjectResult(deviceId); } EntityId entityId = await _entityfactory.GetEntityIdAsync(deviceId, (DeviceType)deviceType, default); EntityStateResponse<JObject> entity = await client.ReadEntityStateAsync<JObject>(entityId); if (entity.EntityExists) { var device = entity.EntityState.ToDeviceDetailModel(); device.DeviceId = deviceId; return new OkObjectResult(device); } return new NotFoundObjectResult(deviceId); } |
The function's URL expects the ID of the device to be recovered and the deviceType field as a parameter in the query string. The code uses device ID and device type to generate the entity ID and uses this value to call the ReadEntityStateAsync method exposed by the IDurableEntityClient.
This method allows you to retrieve the entity's status and serialize it in the object used as generic. The method returns an EntityStateResponse object. This object contains the serialized status (EntityState property) and a Boolean property, which indicates if the entity exists or not (EntityExists property). If the entity exists, we create the response object and return it to the caller. Otherwise, the function returns a NotFoundObjectResult response (Not Found response, error code HTTP 404).
The SetConfiguration function allows you to configure a single device signaling the configuration object to the entity, as shown in the following code.
Code Listing 49: SetConfiguration function
[FunctionName(nameof(SetConfiguration))] public async Task<IActionResult> SetConfiguration( [HttpTrigger(AuthorizationLevel.Function, "put", Route = "devices/{deviceId}/configuration")] HttpRequest req, string deviceId, [DurableClient] IDurableEntityClient client) { if (!Enum.TryParse(typeof(DeviceType), req.Query["deviceType"], true, out var deviceType)) { return new BadRequestObjectResult(deviceId); } EntityId entityId = await _entityfactory.GetEntityIdAsync(deviceId, (DeviceType)deviceType, default); var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); await client.SignalEntityAsync<IDeviceEntity>(entityId, d => d.SetConfiguration(requestBody)); return new OkObjectResult(requestBody); } |
This function is similar to the SendTelemetryToDevice function: it retrieves the configuration object from the request body and signals it to the entity. The configuration object can be different for each device, and in this way, we can support different devices in the future.
Finally, the last client function, the GetDeviceNotifications function, retrieves all the devices' alert notifications. It is like the GetDevices function: it calls the ListEntitiesAsync method, but this time, the EntityName property of the EntityQuery object is always the same, and it is the name of the entity that stores the notifications for each device.
Code Listing 50: GetDeviceNotifications function
[FunctionName(nameof(GetDeviceNotifications))] public async Task<IActionResult> GetDeviceNotifications( [HttpTrigger(AuthorizationLevel.Function, "get", Route = "notifications")] HttpRequest req, [DurableClient] IDurableEntityClient client) { var result = new List<JObject>(); EntityQuery queryDefinition = new EntityQuery() { PageSize = 100, FetchState = true, EntityName = nameof(DeviceNotificationsEntity) }; do { EntityQueryResult queryResult = await client.ListEntitiesAsync(queryDefinition, default); foreach (var item in queryResult.Entities) { result.Add(item.State as JObject); } queryDefinition.ContinuationToken = queryResult.ContinuationToken; } while (queryDefinition.ContinuationToken != null); return new OkObjectResult(result); } |
The stateful layer contains the entities of our solution.
We have two kinds of entities:
The following figure shows the definition of the IDeviceInterface interface.

Figure 34: The IDeviceEntity interface
The interface exposes the following methods:
The following snippet of code shows you an implementation of a device that manages telemetries containing a temperature value.
Code Listing 51: The TemperatureDeviceEntity class
[JsonObject(MemberSerialization.OptIn)] public class TemperatureDeviceEntity : IDeviceEntity { public class DeviceConfiguration { ... } private readonly ILogger logger; public TemperatureDeviceEntity(ILogger logger) { this.logger = logger; EntityConfig = new DeviceConfiguration(); } #region [ State ] [JsonProperty("deviceType")] public string DeviceType { get => Models.DeviceType.Temperature.ToString(); set { } } [JsonProperty("historyData")] public Dictionary<DateTimeOffset, DeviceData> HistoryData { get; set; } [JsonProperty("entityConfig")] public DeviceConfiguration EntityConfig { get; set; } [JsonProperty("deviceName")] public string DeviceName { get; set; } [JsonProperty("lastUpdate")] public DateTimeOffset LastUpdate { get; set; } [JsonProperty("lastData")] public DeviceData LastData { get; set; } [JsonProperty("temperatureHighNotificationFired")] public bool TemperatureHighNotificationFired { get; set; } = false; [JsonProperty("temperatureLowNotificationFired")] public bool TemperatureLowNotificationFired { get; set; } = false; #endregion [ State ] #region [ Behavior ]
public void TelemetryReceived(DeviceTelemetry telemetry) { ... } public Task<IDictionary<DateTimeOffset, DeviceData>> GetLastTelemetries(int numberOfTelemetries = 10) { ... } public void SetConfiguration(string config) { ... } #endregion [ Behavior ] #region [ Private Methods ] #endregion [ Private Methods ] [FunctionName(nameof(TemperatureDeviceEntity))] public static Task Run([EntityTrigger] IDurableEntityContext ctx, ILogger logger) => ctx.DispatchAsync<TemperatureDeviceEntity>(logger); } |
We will analyze the most important part of the code shortly, but you can find the whole class in the GitHub repo of this book.
The inner class, called DeviceConfiguration, contains the configuration you can set using the SetConfiguration method of the entity.
Code Listing 52: The DeviceConfiguration class
public class DeviceConfiguration { [JsonProperty("historyRetention")] public TimeSpan HistoryRetention { get; set; } = TimeSpan.FromMinutes(10); [JsonProperty("temperatureHighThreshold")] public double? TemperatureHighThreshold { get; set; } [JsonProperty("temperatureLowThreshold")] public double? TemperatureLowThreshold { get; set; } [JsonProperty("notificationNumber")] public string NotificationNumber { get; set; } public bool TemperatureHighAlertEnabled() { return TemperatureHighThreshold.HasValue; } public bool TemperatureLowAlertEnabled() { return TemperatureLowThreshold.HasValue; } } |
The properties of this class allow you to configure:
The following snippet of code shows you the implementation of the TelemetryReceived method.
Code Listing 53: The TelemetryReceived method
public void TelemetryReceived(DeviceTelemetry telemetry) { if (HistoryData == null) HistoryData = new Dictionary<DateTimeOffset, DeviceData>(); DeviceName = telemetry.DeviceName; if (telemetry.Timestamp < DateTimeOffset.Now.Subtract(EntityConfig.HistoryRetention)) return; if (telemetry.Data != null) { HistoryData[telemetry.Timestamp] = telemetry.Data; if (LastUpdate < telemetry.Timestamp) { LastUpdate = telemetry.Timestamp; LastData = telemetry.Data; } ClearHistoryData(); CheckAlert(); } } |
The method checks if the telemetry received by the caller must be stored (if it is older than the persistence interval set in the configuration, then it is discarded) and stores it in the HistoryData dictionary.
Finally, the method removes the expired telemetries (ClearHistoryData method) and checks if a notification must call (CheckAlert method).
If the device must throw a notification, it uses a method called SendAlert, which is shown in the following snippet of code.
Code Listing 54: The SendAlert method
private void SendAlert(double lastTemperature) { var notification = new DeviceNotificationInfo() { Timestamp = DateTimeOffset.Now, DeviceId = Entity.Current.EntityKey, DeviceType = Entity.Current.EntityName }; notification.Telemetries.Add("temperature", lastTemperature); notification.Metadata.Add("notificationNumber", EntityConfig?.NotificationNumber); Entity.Current.SignalNotification(notification); if (!string.IsNullOrWhiteSpace(EntityConfig?.NotificationNumber)) { TemperatureNotificationData temperatureAlert = new TemperatureNotificationData() { DeviceName = DeviceName, NotificationNumber = EntityConfig.NotificationNumber, Temperature = lastTemperature }; Entity.Current.StartNewOrchestration("Alerts_SendTemperatureNotification", temperatureAlert); } } |
This method signals to another durable entity, called DeviceNotificationEntity, the information about the notification. Every device has a notification entity that stores all its alerts, even if these didn't generate a real notification against an external service.
We implement the SignalNotification method to centralize the creation of the notification entity and the signal process. This method is an extension method of the IDurableEntityContext interface, and the code is shown in the following snippet.
Code Listing 55: The SignalNotification method
public static void SignalNotification(this IDurableEntityContext context, DeviceNotificationInfo notification) { var notificationEntityId = new EntityId(nameof(DeviceNotificationsEntity), $"{context.EntityName}|{context.EntityKey}"); Entity.Current.SignalEntity<IDeviceNotificationEntity>(notificationEntityId, n => n.NotificationFired(notification)); } |
The name of the notification entity used by the device to store its notifications is generated using the device entity name and the device entity ID. In this way, you have a pair of entities: one for the telemetries (the device), and one for all the notifications thrown by the device (the notification entity).
Using the signal approach, the device sends the information to the notification entity, but it doesn't wait. It continues immediately with the next instruction. This approach helps you in scalability.
The last part of the method starts; if you set the phone number for the notification, an orchestration to send the alarm. In this case, the orchestration start is asynchronous, and the device can immediately manage the next telemetry.
If you change the behavior of the orchestrator, you can change the way the notification will be sent. In the next section, we will analyze the orchestrator.
The following snippet of code shows the DeviceNotificationEntity implementation.
Code Listing 56: The DeviceNotificationEntity class
public class DeviceNotificationsEntity : IDeviceNotificationEntity { private readonly ILogger logger; public DeviceNotificationsEntity(ILogger logger) { this.logger = logger; } #region [ State ] [JsonProperty("notifications")] public List<DeviceNotificationInfo> Notifications { get; set; } [JsonProperty("deviceType")] public string DeviceType { get; set; } [JsonProperty("deviceId")] public string DeviceId { get; set; } #endregion [ State ] #region [ Behavior ] public void NotificationFired(DeviceNotificationInfo notification) { if (notification == null) return; if (Notifications == null) Notifications = new List<DeviceNotificationInfo>(); DeviceType = notification.DeviceType; DeviceId = notification.DeviceId; Notifications.Add(notification); } public Task PurgeAsync() { Notifications?.Clear(); return Task.CompletedTask; } #endregion [ Behavior ] [FunctionName(nameof(DeviceNotificationsEntity))] public static Task Run([EntityTrigger] IDurableEntityContext ctx, ILogger logger) => ctx.DispatchAsync<DeviceNotificationsEntity>(logger); } |
The integration layer contains all the durable functions used for the interaction with external services.
When an entity device needs to interact with external services, it starts an orchestrator. The orchestrator manages the flow among all the activity functions used for interaction between our platform and the external services.
This way, as we saw for the notification approach between device entity and notification entity in the previous paragraph, it allows us to implement an async interaction between the entity and the orchestrator. We don't block the device while we wait for the orchestration to be completed.
In the following snippet, you can see the implementation of the SendTemperatureNotification orchestrator.
Code Listing 57: The SendTemperatureNotification orchestrator
[FunctionName("Alerts_SendTemperatureNotification")] public async Task SendTemperatureNotification( [OrchestrationTrigger] IDurableOrchestrationContext context, ILogger logger) { var notificationdata = context.GetInput<TemperatureNotificationData>(); var smsData = new TwilioActivities.SmsData() { Number = notificationdata.NotificationNumber, Message = $"The temperature for device {notificationdata.DeviceName} is {notificationdata.Temperature}" }; try { await context.CallActivityAsync("TwilioActivities_SendSMS", smsData); } catch (System.Exception ex) { logger.LogError(ex, "Error during TwilioActivity invocation", smsData); } } |
We implement an orchestrator even if we have only one activity in the notification process (as we do in the previous snippet of code).
It is useful if you remember that an entity can signal another entity or start an orchestrator, and you cannot call activity directly from the entity.
The SendTemperatureNotification orchestrator calls a single activity (the TwilioActivities_SendSMS activity) to send an SMS using Twilio. The code for that activity is shown in the following snippet.
Code Listing 58: The SendMessageToTwilio activity
[FunctionName("TwilioActivities_SendSMS")] [return: TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken")] public CreateMessageOptions SendMessageToTwilio([ActivityTrigger] IDurableActivityContext context, ILogger log) { SmsData data = context.GetInput<SmsData>(); log.LogInformation($"Sending message to : {data.Number}"); var fromNumber = this.configuration.GetValue<string>("TwilioFromNumber");
var message = new CreateMessageOptions(new PhoneNumber(data.Number)) { From = new PhoneNumber(fromNumber), Body = data.Message }; return message; } |
All the configurations for the Twilio account are stored in the configuration file, as shown in the following JSON.
Code Listing 59: The configuration file for our function app
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", ... "TwilioAccountSid": "<twilio account SID>", "TwilioAuthToken": "<twilio auth token>", "TwilioFromNumber": "<twilio virtual number>" } } |
Note: Twilio is a full solution for enterprise communications. One of the services provided by Twilio is SMS (Twilio Programmable Messaging). You can find the documentation here.
This concludes our tour of Azure Durable Functions.
Durable Functions is a powerful technology that you can use in different scenarios to solve various problems. As with all technologies, there are scenarios where it makes sense to use this technology, and others where it would be counterproductive. I hope this book gives you the tools to understand if Durable Functions is the right tool for you.
Good luck and buona fortuna!