- Overview
- Prerequisites
- Create Sandbox Resource Group
- Azure IoT Hub Instance
- Simulated IoT Devices
- Azure Digital Twins Instance
- Azure TwinSync Functions
- Plug-and-Play IoT Devices
- AnyLogic Simulation
- Databricks Simulation
- Microsoft Bonsai Teaching
In this tutorial, you will create a sandbox environment in which you can further explore the architectural components of a specific Azure IoT architecture aimed at teaching AI through digital twin simulation. You will be completing the following tasks:
- Create an Azure IoT Hub
- Use Azure IoT Explorer to register IoT devices
- Create Simulated IoT Devices
- Create an Azure Digital Twins instance
- Learn to use the Manufacturing Ontology to create digital twins for a typical Manufacturer
- Use Azure Functions to synchronise data between components
- Auto-Provision Simulated Plug-and-Play IoT Devices
- Auto-Provision Physical Plug-and-Play IoT Devices (in this case, an MXCHIP AZ3166 multi-sensor device)
- Auto-Retire IoT Devices
- Create an AnyLogic Simulation
- Create a Databricks Simulation
- Extend Simulations to integrate with Microsoft Bonsai
- Use Bonsai to teach an AI 'Brain' from a Simulation
- Apply the Bonsai Brain to a Simulation for verification
- Apply the Bonsai Brain to a digital twin
Before you begin, you may want to clone this entire repository to your local machine, to make some of the steps below more straightforward. This is a collaborative tutorial and sample. Please use the Issues feature of Github to notify others and get help if you run into issues with the instructions or and of the code samples. Issues can be addressed by anyone on the team. Please feel free to submit improvements to the repository, but please document any changes.
- An Avanade or Accenture domain account
- Authorization to create a sandbox obtained from chris.lowndes@avanade.com
- (Optional) An MXCHIP AZ3166 multi-sensor device
To create your own sandbox environment, you will execute various commands in the Azure Cloud Shell. To access the Azure Cloud Shell, follow these steps:
- Navigate to the Azure Portal at https://portal.azure.com
- Login to the Azure Portal using your usual Avanade or Accenture credentials
- Once logged in, in the main search bar, type 'subscriptions'
- In the list of subscriptions, choose the subscription with ID as provided by the facilitator
- Once switched to the required subscription, select the Cloud Shell button shown circled in red

- Once the Cloud Shell opens at the bottom of your browser, switch from Bash mode to PowerShell mode

To create a resource group to hold all of your sandbox resources, follow these steps:
- Your sandbox name is your choice, but should follow this pattern:
industryx-sandbox-<<your-initials-or-name>> #this is your sandbox name. Please copy it as you will be using it frequently - Execute the following command in the Cloud Shell
az group create --name <<your-sandbox-name>> --location japaneastTo create the IoT Hub instance follow these steps:
- Execute the following command using the Azure Cloud Shell
az deployment group create --resource-group <<your-sandbox-name>> --template-uri https://raw.githubusercontent.com/lowndesc/industryx/main/sandbox/IoTHub/azuredeploy.json- Navigate to your sandbox resource group within the Azure Portal. You should see two resources similar to this image

For our sandbox, we will first create code-based 3 simulated devices, each transmitting the same range of telemetry on a schedule, using randomisation to vary the values transmitted and the scheduling.
To create the simulated devices, follow these steps:
- Navigate to your IoT Hub instance
- On the left-hand menu, under 'Settings', click 'Shared access policies'
- On the list of Shared access policies, click the policy named 'device'
- On the policy keys panel, copy the 'Primary connection string' and keep it for later use

- In the Azure Cloud Shell, click on the 'Upload Files' button

- In the 'Open' dialog, post the following file URL into the 'File name:' field
https://raw.githubusercontent.com/lowndesc/industryx/main/sandbox/SimulatedDevices/SimulatorCloudRunner.ps1- This will copy a PowerShell script file from GitHub to your AZure Cloud Shell session
- Execute the following command in Azure Cloud Shell to run the PowerShell script, using the values copied earler
./SimulatorCloudRunner.ps1 -ResourceGroup <<your-sandbox-name>> -IotHubConnectionString <<your-iothub-connection-string>>- After a few seconds, you should see a JSON confirmation that the process has started
- After around 1 minute, the 3 simulated device containers will have been deployed. Check your resource group to confirm.
To connect the simulated devices to the IoT Hub, follow these steps:
- Navigate to your IoT Hub instance
- In the left-hand menu, under 'Explorers', click 'IoT Devices'
- Click 'New' to add a new device
- For 'Device ID', type 'sim000001'
- Leave all other fields unchanged, and click 'Save'
- Repeat for two more devices, 'sim000002' and 'sim000003'
- Your device list should now look like this

To run the simulated devices, follow these steps:
- Navigate to the first simulated device container
- Click on 'Start' to start the container

- Repeat for each of your simulated device containers
- Once each container has started, the onboard functions will connect a device to the IoTHub using the registered device names you created in the previous task
- To verify that the connection is good. navigate back to the IoT Hub insatnce, and on the 'Overview' pane check the 'Iot Hub Usage' panel and it should show 'Iot Devices: 3' and 'Messages used today:' should be a number greater than 0

For our sandbox environment, we will be implementing digital twins using Microsoft's Azure Digital Twins (ADT). ADT is based on a spatial graph model as opposed to traditional relational database technology. In relational databases, to analyse relationships across different table entities, the time expensive ‘JOIN’ operation is used to combine related data. This operation is expensive as it requires index lookups and matching to related columns in other tables. This is a major advantage of graph data models. Graphs store entities and their relationships as nodes and content which may be augmented with additional attributes. Retrieving the relationship between two entities does not involve expensive ‘join’ operations.
Within our sandbox, we therefore require a single Azure Digital Twins (ADT) instance. Each instance can support many different digital twins based on many different models. For simplicity, it is best practice to use a separate ADT instance for each solution domain. ADT hosts the definitions of digital twins, based on underlying models, as well as the data describing the digital twins, including twin instances, components, relationships and properties. It does not, however, store underlying property and telemetry values, which are accessed from underlying storage. This makes ADT a highly performant abstraction of the digital twins it hosts.
ADT uses models to define digital twin types. Modelling is defined in a JSON-LD-based descriptive language called DTDL (Digital Twins Definition Language). DTDL models are interchangeble between device twins used in IoT Plug-and-Play scenarios and digital twins of those devices within Azure Digital Twins. Each DTDL model is an interface which defines the permissible structure of a digital twin. DTDL supports inheritance, such that one or more interfaces can be a base for other derived interfaces. A model is a generally a definition which can be instantiated as a digital twin. Howevere, a model can also be a component, used to compose other models, but not intended for instantiation by itself. An example would be a smartphone device, defined as containing components defining a front camera and a rear camera.
For manufaturing scenarios, we need a set of base models which can form the foundation for describing manufacturing assets and processes. These base models need to be extendible to problem-specific scenrios, such as production monitoring, optimization and simulation, as well as wider scenarios such as materials handling and supply chain modelling. These base models need to follow industry-specific standards, such as ISA-95 and ISA-88, and be gathered into an overall model known as a manufacturing ontology.
The starting point for our ontology is this basic data model as defined by the Industrial Automation standard ISA-95:

From this, we have derived a basic manufacturing ontology as captured in this diagram. We have incuded a few example Azure IoT Plug-and-PLay devices into our ontology, to illustrate how these interact with the manufacturing assets and processes. We are to use this ontology within our sandbox environments:

Take a moment to examine the manufacturing ontology in its raw JSON DTDL form at this location in this repository:
./sandbox/AzureDigitalTwins/models/manufacturing-ontology This entire folder/file structure is in the clone on your local drive for later use
To create your ADT instance within your sandbox, execute the following steps:
- Execute the following command in the Azure Cloud Shell:
az deployment group create --resource-group <<your-sandbox-name>> --template-uri https://raw.githubusercontent.com/lowndesc/industryx/main/sandbox/AzureDigitalTwins/azuredeploy.json- When execution has finished, navigate to your sandbox ADT instance, on the Overview pain, copy the host name URL. You will need this later. In order to create permissions for you to access your ADT instance, you need to add yourself as an owner of this instance.
- On the left-hand menu, select 'Access Control (IAM)'
- On the 'Access Control (IAM)' pane, click +Add, and then 'Add role sssignment'
- In the 'Add role assignment' panel, in the Role dropdown, select the 'Azure Digital Twins Data Owner' role
- In the 'Select' field, type the start of your name, then select your user account from the list to add your user account to the 'Selected members' list
- Click Save to create the role assignment
- To verify that you can now access your ADT instance, navigate to the online ADT explorer.
- In the pop-up that asks for an Azure Digital Twins URL, type 'https://<<your host name URL from step 2 above>>' and click Save.

- On the top bar, click the cog icon to go to Settings. Set the Console and Output toggles to on.

- Now, when you click 'Run Query' in the top right, you should see a message in the centre pain that states 'No Results Found'.

Execute the following steps to upload the manufacturing ontology into the ADT model library:
- At the top of the left-hand Models panel, click the 'Upload a directory of Models' button:

- Select the folder at the head of your local copy of the manufacturing ontology 'manufacturing-ontology'
- Click Upload, and then OK when the system reminds you that you are uploading 40 models.
- When these have uploaded, on the centre pane, click the 'MODEL GRAPH' tab.
- You should see the entire manufacturing ontology with relationships and inheritance depicted.

We will now create sample digital twins of an entire manufacturing operation.
Execute the following steps to create the sample digital twins:
- In Azure Digital Twins Explorer, at the top of the centre pane, click the 'TWIN GRAPH' tab.
- At the top of the Twin Graph pane, click the 'Import Graph' button

- In the file dialog, navigate to the file:
./sandbox/AzureDigitalTwins/logistics-twin.xlsx
- Click Upload. The initial twin graph should load
- On the 'Graph Preview Only' screen, click the save icon in the top right-hand corner.
- The system will now process the graph, loading the nodes into ADT.
- This should process OK. If there are any errors, use the Output screen to determine where the errors may be occuring.
- Once processed, you can view a visualisation of the digital twins by returning to the 'TWIN GRAPH' tab and clicking 'Run Query' in the top right-hand corner.
- You can switch layout style for your graph using the 'Choose Layout' button at the top of the 'TWIN GRAPH' pane.

- Note that we have included a number of Azure IoT Plug-and-PLay devices into this sample digital twins. One such device is shown in detail on the attached image - an MXCHIP AZ3166 device named 'PnP-Sensors', contains multiple sensors, and will be the subject of auto-provisiioning later.

Now that we have created our digital twins of assets and spaces, we can add digital twins of the processes we wish to model. For our sandbox, we will model an end-to-end supply chain process, incorporating the manufacturing assets and spaces we have previously modelled. To create digital twins of an end-to-end supply chain, execute the following steps:
- In Azure Digital Twins Explorer, in the 'TWIN GRAPH' pane, click the 'Import Graph' button
- Select the file:
./sandbox/AzureDigitalTwins/logistics-supply-chain-twin.xlsx
- After the twins have loaded, click the Save icon in the top right-hand corner
- The system will process the supply chain twins
- To display only the supply chain process twins, copy the following SQL Query into the Azure Digitral Twins Explorer query field. This query is filtering the view to only those nodes and their relationships which inherit from the Supply Chain interface.
SELECT * FROM DIGITALTWINS WHERE IS_OF_MODEL('dtmi:isa95:core:SupplyChain;1')- Run the query by clicking the 'Run Query' button.
To update a twin once it is created, you can either use the tools within the Azure Digital Twins explorer or prefereably refer to this article about using the ADT API Here, we will explain how to use the tools in Azure Digital Twins Explorer to make a simple change to an existing twin. Execute the following steps:
- In Azure Digital Twins Explorer, run the following SQL Query to return only assets and their relationships:
SELECT * FROM DIGITAL TWINS WHERE IS_OF_MODEL('dtmi:isa95:core:Asset;1')- In the results view, find and click on the node for our simulated device 'sim000001'
- Notice in the PROPERTIES panel on the right-hand side, there is a Property of the 'sim000001' twin called 'Name' for which the value is 'undefined'
- Edit the 'Name' property to be 'sim000001'.
- Notice that, after making this change, there is a record of the change added to the digital twin properties. All changes to digital twins are audited.

In order to synchronize data between features within our sandbox, we require some light Azure Functions. These we have grouped under a concept called TwinSync, a general principle to use Azure Functions where possible to execute data transformation code outside of any user context. These functions are descrete, low cost, highly scalable and reliable execution actions. The trigger is normally a standard Azure event occuring, such as a message arriving on an Event Grid topic or Event Hub.
We will use TwinSync functions to achieve the following:
- to sync IoTHub telemetry with Azure Digital Twins
- to sync Azure Digital Twins data with AnyLogic simulations
- to sync Azure Digital Twins data with Databricks simulations
For example, we have the following Function code available in a TwinSync Function named IoTHubtoTwins:
using System;
using Azure;
using System.Net.Http;
using Azure.Core.Pipeline;
using Azure.DigitalTwins.Core;
using Azure.Identity;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Avanade.Japan.Device.Simulation.Sync
{
public class IoTHubtoTwins
{
private static readonly string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL");
private static readonly HttpClient httpClient = new HttpClient();
[FunctionName("IoTHubtoTwins")]
public async void Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
{
if (adtInstanceUrl == null) log.LogError("Application setting \"ADT_SERVICE_URL\" not set");
try
{
// Authenticate with Digital Twins
var cred = new ManagedIdentityCredential("https://digitaltwins.azure.net");
var client = new DigitalTwinsClient(
new Uri(adtInstanceUrl),
cred,
new DigitalTwinsClientOptions { Transport = new HttpClientTransport(httpClient) });
log.LogInformation($"ADT service client connection created.");
if (eventGridEvent != null && eventGridEvent.Data != null)
{
log.LogInformation(eventGridEvent.Data.ToString());
// <Find_device_ID_and_values>
JObject deviceMessage = (JObject)JsonConvert.DeserializeObject(eventGridEvent.Data.ToString());
string deviceId = (string)deviceMessage["systemProperties"]["iothub-connection-device-id"];
var temperature = deviceMessage["body"]["temperature"];
var humidity = deviceMessage["body"]["humidity"];
var power = deviceMessage["body"]["power"];
var vibration = deviceMessage["body"]["vibration"];
var uptime = deviceMessage["body"]["uptime"];
var capacity = deviceMessage["body"]["capacity"];
var wait = deviceMessage["body"]["wait"];
var delay = deviceMessage["body"]["delay"];
var arrivalRate = deviceMessage["body"]["arrivalRate"];
var counter = deviceMessage["body"]["Counter"].Value<int>(); // counter must be int
// </Find_device_ID_and_values>
// Correct integer telemetry values by dividing by 100
Random rnd = new Random();
int randomFactor = rnd.Next(3, 18); // a random integer from 3 to 17
var seed = (counter % 99) % randomFactor; // creates a seed value using the random factor, more frequent occurences at lower end
var t = temperature.Value<double>() / 100;
var h = humidity.Value<double>() / 100;
var p = power.Value<double>() / 100;
var v = vibration.Value<double>() / 100;
var u = uptime.Value<double>() / 100;
var c = (int)Math.Round(capacity.Value<double>() / 100); // round capacity to the nearest integer, even integer if midpoint
var w = wait.Value<double>() / 100;
var d = delay.Value<double>() / 100;
var ar = (17*Math.Sin(arrivalRate.Value<double>() / 100) + seed) / 15; // creates a more realistic arrival rate curve
log.LogInformation($"Device:{deviceId} Counter: [{counter}]");
log.LogInformation($"Device:{deviceId} Temperature: {t}");
log.LogInformation($"Device:{deviceId} Humidity: {h}");
log.LogInformation($"Device:{deviceId} Power: {p}");
log.LogInformation($"Device:{deviceId} Vibration: {v}");
log.LogInformation($"Device:{deviceId} Uptime: {u}");
log.LogInformation($"Device:{deviceId} Capacity: {c}");
log.LogInformation($"Device:{deviceId} Wait: {w}");
log.LogInformation($"Device:{deviceId} Delay: {d}");
log.LogInformation($"Device:{deviceId} ArrivalRate: {ar}");
// <Update_twin_with_device_values>
var updateTwinData = new JsonPatchDocument();
updateTwinData.AppendReplace("/Temperature", t);
updateTwinData.AppendReplace("/Humidity", h);
updateTwinData.AppendReplace("/Power", p);
updateTwinData.AppendReplace("/Vibration", v);
updateTwinData.AppendReplace("/Uptime", u);
updateTwinData.AppendReplace("/Capacity", c);
updateTwinData.AppendReplace("/Wait", w);
updateTwinData.AppendReplace("/Delay", d);
updateTwinData.AppendReplace("/ArrivalRate", ar);
await client.UpdateDigitalTwinAsync(deviceId, updateTwinData);
await client.PublishTelemetryAsync(deviceId,null, new JObject
{
{ "Temperature", t },
{ "Humidity", h },
{ "Power", p },
{ "Vibration", v },
{ "Uptime", u },
{ "Capacity", c },
{ "Wait", w },
{ "Delay", d },
{ "ArrivalRate", ar }
}.ToString());
// </Update_twin_with_device_values>
}
}
catch (Exception ex)
{
log.LogError($"Error in ingest function: {ex.Message} | {ex.InnerException} | {ex.StackTrace}");
}
}
}
}Once the events are flowing to our function app, they should be flowing into our ADT twins. To verify the end-to-end flow, execute the following steps:
- Verify that telemetry events are flowing to our function
- Verify that the function is processing the telemetry events without errors
- Verify that properties of ADT sensor twins are being updated
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Azure;
using Azure.Core.Pipeline;
using Azure.DigitalTwins.Core;
using Azure.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.Devices.Provisioning.Service;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Samples.AdtIothub
{
public static class DpsAdtAllocationFunc
{
private const string adtAppId = "https://digitaltwins.azure.net";
private static string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL");
private static readonly HttpClient singletonHttpClientInstance = new HttpClient();
[FunctionName("DpsAdtAllocationFunc")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
// Get request body
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogDebug($"Request.Body: {requestBody}");
dynamic data = JsonConvert.DeserializeObject(requestBody);
// Get registration ID of the device
string regId = data?.deviceRuntimeContext?.registrationId;
bool fail = false;
string message = "Uncaught error";
var response = new ResponseObj();
// Must have unique registration ID on DPS request
if (regId == null)
{
message = "Registration ID not provided for the device.";
log.LogInformation("Registration ID: NULL");
fail = true;
}
else
{
string[] hubs = data?.linkedHubs.ToObject<string[]>();
// Must have hubs selected on the enrollment
if (hubs == null
|| hubs.Length < 1)
{
message = "No hub group defined for the enrollment.";
log.LogInformation("linkedHubs: NULL");
fail = true;
}
else
{
// Find or create twin based on the provided registration ID and model ID
dynamic payloadContext = data?.deviceRuntimeContext?.payload;
string dtmi = payloadContext.modelId;
log.LogDebug($"payload.modelId: {dtmi}");
string dtId = await FindOrCreateTwinAsync(dtmi, regId, log);
// Get first linked hub (TODO: select one of the linked hubs based on policy)
response.iotHubHostName = hubs[0];
// Specify the initial tags for the device.
var tags = new TwinCollection();
tags["dtmi"] = dtmi;
tags["dtId"] = dtId;
// Specify the initial desired properties for the device.
var properties = new TwinCollection();
// Add the initial twin state to the response.
var twinState = new TwinState(tags, properties);
response.initialTwin = twinState;
}
}
log.LogDebug("Response: " + ((response.iotHubHostName != null)? JsonConvert.SerializeObject(response) : message));
return fail
? new BadRequestObjectResult(message)
: (ActionResult)new OkObjectResult(response);
}
public static async Task<string> FindOrCreateTwinAsync(string dtmi, string regId, ILogger log)
{
// Create Digital Twins client
var cred = new ManagedIdentityCredential(adtAppId);
var client = new DigitalTwinsClient(
new Uri(adtInstanceUrl),
cred,
new DigitalTwinsClientOptions
{
Transport = new HttpClientTransport(singletonHttpClientInstance)
});
// Find existing DigitalTwin with registration ID
try
{
// Get DigitalTwin with Id 'regId'
BasicDigitalTwin existingDt = await client.GetDigitalTwinAsync<BasicDigitalTwin>(regId).ConfigureAwait(false);
// Check to make sure it is of the correct model type
if (StringComparer.OrdinalIgnoreCase.Equals(dtmi, existingDt.Metadata.ModelId))
{
log.LogInformation($"DigitalTwin {existingDt.Id} already exists");
return existingDt.Id;
}
// Found DigitalTwin but it is not of the correct model type
log.LogInformation($"Found DigitalTwin {existingDt.Id} but it is not of model {dtmi}");
}
catch(RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotFound)
{
log.LogDebug($"Did not find DigitalTwin {regId}");
}
// Either the DigitalTwin was not found, or we found it but it is of a different model type
// Create or replace it with what it needs to be, meaning if it was not found a brand new DigitalTwin will be created
// and if it was of a different model, it will replace that existing DigitalTwin
// If it was intended to only create the DigitalTwin if there is no matching DigitalTwin with the same Id,
// ETag.All could have been used as the ifNonMatch parameter to the CreateOrReplaceDigitalTwinAsync method call.
// Read more in the CreateOrReplaceDigitalTwinAsync documentation here:
// https://docs.microsoft.com/en-us/dotnet/api/azure.digitaltwins.core.digitaltwinsclient.createorreplacedigitaltwinasync?view=azure-dotnet
BasicDigitalTwin dt = await client.CreateOrReplaceDigitalTwinAsync(
regId,
new BasicDigitalTwin
{
Metadata = { ModelId = dtmi },
Contents =
{
{ "Temperature", 0.0 },
{
"deviceInformation",
new BasicDigitalTwinComponent
{
Metadata = {},
Contents =
{
{ "manufacturer", "MXCHIP" }
}
}
}
}
}
).ConfigureAwait(false);
log.LogInformation($"Digital Twin {dt.Id} created.");
return dt.Id;
}
}
/// <summary>
/// Expected function result format
/// </summary>
public class ResponseObj
{
public string iotHubHostName { get; set; }
public TwinState initialTwin { get; set; }
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Azure;
using Azure.Core.Pipeline;
using Azure.DigitalTwins.Core;
using Azure.Identity;
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Samples.AdtIothub
{
public static class DeviceTelemetryToTwinFunc
{
private static string adtAppId = "https://digitaltwins.azure.net";
private static readonly string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL", EnvironmentVariableTarget.Process);
private static readonly HttpClient singletonHttpClientInstance = new HttpClient();
[FunctionName("DeviceTelemetryToTwinFunc")]
public static async Task Run(
[EventHubTrigger("deviceevents", Connection = "EVENTHUB_CONNECTIONSTRING")] EventData eventData, ILogger log)
{
log.LogInformation($"C# function triggered to process a message: {eventData}");
log.LogInformation($"C# function triggered to process a message: {Encoding.UTF8.GetString(eventData.Body)}");
// Metadata accessed by binding to EventData
log.LogInformation($"EnqueuedTimeUtc={eventData.SystemProperties.EnqueuedTimeUtc}");
log.LogInformation($"SequenceNumber={eventData.SystemProperties.SequenceNumber}");
log.LogInformation($"Offset={eventData.SystemProperties.Offset}");
//var exceptions = new List<Exception>(events.Length);
// Create Digital Twin client
var cred = new ManagedIdentityCredential(adtAppId);
var client = new DigitalTwinsClient(
new Uri(adtInstanceUrl),
cred,
new DigitalTwinsClientOptions
{
Transport = new HttpClientTransport(singletonHttpClientInstance)
});
try
{
log.LogInformation($"EventData: {System.Text.Json.JsonSerializer.Serialize(eventData)}");
// Get message body
string messageBody = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count);
log.LogInformation($"MessageBody: {messageBody}");
// Reading Device ID from message headers
JObject jbody = (JObject)JsonConvert.DeserializeObject(messageBody);
string deviceId = eventData.SystemProperties["iothub-connection-device-id"].ToString();
log.LogInformation($"DeviceId: {deviceId}");
// Extracting temperature from device telemetry
double temperature = Convert.ToDouble(jbody["temperature"].ToString());
// Update device Temperature property
var updateTwinData = new JsonPatchDocument();
updateTwinData.AppendReplace("/Temperature", temperature);
log.LogInformation($"ADT Patch Document: {updateTwinData.ToString()}");
await client.UpdateDigitalTwinAsync(deviceId, updateTwinData);
log.LogInformation($"Updated Temperature of device Twin {deviceId} to: {temperature}");
}
catch (Exception e)
{
// We need to keep processing the rest of the batch - capture this exception and continue.
log.LogError(e, "Function error");
}
}
}
}using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Azure;
using Azure.Core.Pipeline;
using Azure.DigitalTwins.Core;
using Azure.Identity;
using Microsoft.Azure.EventHubs;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
namespace Samples.AdtIothub
{
public static class DeleteDeviceInTwinFunc
{
private static string adtAppId = "https://digitaltwins.azure.net";
private static readonly string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL", EnvironmentVariableTarget.Process);
private static readonly HttpClient singletonHttpClientInstance = new HttpClient();
[FunctionName("DeleteDeviceInTwinFunc")]
public static async Task Run(
[EventHubTrigger("lifecycleevents", Connection = "EVENTHUB_CONNECTIONSTRING")] EventData[] events, ILogger log)
{
var exceptions = new List<Exception>(events.Length);
// Create Digital Twin client
var cred = new ManagedIdentityCredential(adtAppId);
var client = new DigitalTwinsClient(
new Uri(adtInstanceUrl),
cred,
new DigitalTwinsClientOptions
{
Transport = new HttpClientTransport(singletonHttpClientInstance)
});
foreach (EventData eventData in events)
{
try
{
//log.LogDebug($"EventData: {System.Text.Json.JsonSerializer.Serialize(eventData)}");
string opType = eventData.Properties["opType"] as string;
if (opType == "deleteDeviceIdentity")
{
string deviceId = eventData.Properties["deviceId"] as string;
try
{
// Find twin based on the original Registration ID
BasicDigitalTwin digitalTwin = await client.GetDigitalTwinAsync<BasicDigitalTwin>(deviceId);
// In order to delete the twin, all relationships must first be removed
await DeleteAllRelationshipsAsync(client, digitalTwin.Id, log);
// Delete the twin
await client.DeleteDigitalTwinAsync(digitalTwin.Id, digitalTwin.ETag);
log.LogInformation($"Twin {digitalTwin.Id} deleted in DT");
}
catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound)
{
log.LogWarning($"Twin {deviceId} not found in DT");
}
}
}
catch (Exception e)
{
// We need to keep processing the rest of the batch - capture this exception and continue.
exceptions.Add(e);
}
}
if (exceptions.Count > 1)
throw new AggregateException(exceptions);
if (exceptions.Count == 1)
throw exceptions.Single();
}
/// <summary>
/// Deletes all outgoing and incoming relationships from a specified digital twin
/// </summary>
public static async Task DeleteAllRelationshipsAsync(DigitalTwinsClient client, string dtId, ILogger log)
{
AsyncPageable<BasicRelationship> relationships = client.GetRelationshipsAsync<BasicRelationship>(dtId);
await foreach (BasicRelationship relationship in relationships)
{
await client.DeleteRelationshipAsync(dtId, relationship.Id, relationship.ETag);
log.LogInformation($"Twin {dtId} relationship {relationship.Id} deleted in DT");
}
AsyncPageable<IncomingRelationship> incomingRelationships = client.GetIncomingRelationshipsAsync(dtId);
await foreach (IncomingRelationship incomingRelationship in incomingRelationships)
{
await client.DeleteRelationshipAsync(incomingRelationship.SourceId, incomingRelationship.RelationshipId);
log.LogInformation($"Twin {dtId} incoming relationship {incomingRelationship.RelationshipId} from {incomingRelationship.SourceId} deleted in DT");
}
}
}
}







