Tutorial: Coding with the Azure Digital Twins SDK

Developers working with Azure Digital Twins commonly write client applications for interacting with their instance of the Azure Digital Twins service. This developer-focused tutorial provides an introduction to programming against the Azure Digital Twins service, using the Azure Digital Twins SDK for .NET (C#). It walks you through writing a C# console client app step by step, starting from scratch.

  • Set up project
  • Get started with project code
  • Complete code sample
  • Clean up resources
  • Next steps

Prerequisites

This Azure Digital Twins tutorial uses the command line for setup and project work. As such, you can use any code editor to walk through the exercises.

What you need to begin:

  • Any code editor
  • .NET Core 3.1 on your development machine. You can download this version of the .NET Core SDK for multiple platforms from Download .NET Core 3.1.

Prepare an Azure Digital Twins instance

To work with Azure Digital Twins in this article, you'll need an Azure Digital Twins instance and the required permissions for using it. If you already have an Azure Digital Twins instance set up, you can use that instance and skip to the next section. Otherwise, follow the instructions in Set up an instance and authentication. The instructions contain information to help you verify that you've completed each step successfully.

After you set up your instance, make a note of the instance's host name. You can find the host name in the Azure portal.

Set up local Azure credentials

This sample uses DefaultAzureCredential (part of the Azure.Identity library) to authenticate users with the Azure Digital Twins instance when you run it on your local machine. For more information on different ways a client app can authenticate with Azure Digital Twins, see Write app authentication code.

With DefaultAzureCredential, the sample will search for credentials in your local environment, like an Azure sign-in in a local Azure CLI or in Visual Studio or Visual Studio Code. For this reason, you should sign in to Azure locally through one of these mechanisms to set up credentials for the sample.

If you're using Visual Studio or Visual Studio Code to run code samples, make sure you're signed in to that editor with the same Azure credentials that you want to use to access your Azure Digital Twins instance. If you're using a local CLI window, run the az login command to sign in to your Azure account. After this, when you run your code sample, you should be authenticated automatically.

Set up project

Once you're ready to go with your Azure Digital Twins instance, start setting up the client app project.

Open a console window on your machine, and create an empty project directory where you want to store your work during this tutorial. Name the directory whatever you want (for example, DigitalTwinsCodeTutorial).

Navigate into the new directory.

Once in the project directory, create an empty .NET console app project. In the command window, you can run the following command to create a minimal C# project for the console:

dotnet new console

This command will create several files inside your directory, including one called Program.cs where you'll write most of your code.

Keep the command window open, as you'll continue to use it throughout the tutorial.

Next, add two dependencies to your project that will be needed to work with Azure Digital Twins. The first is the package for the Azure Digital Twins SDK for .NET, the second provides tools to help with authentication against Azure.

dotnet add package Azure.DigitalTwins.Core
dotnet add package Azure.Identity

Get started with project code

In this section, you'll begin writing the code for your new app project to work with Azure Digital Twins. The actions covered include:

  • Authenticating against the service
  • Uploading a model
  • Catching errors
  • Creating digital twins
  • Creating relationships
  • Querying digital twins

There's also a section showing the complete code at the end of the tutorial. You can use this section as a reference to check your program as you go.

To begin, open the file Program.cs in any code editor. You'll see a minimal code template that looks something like this:

Screenshot of a snippet of sample code in a code editor.

First, add some using lines at the top of the code to pull in necessary dependencies.

using Azure.DigitalTwins.Core;
using Azure.Identity;

Next, you'll add code to this file to fill out some functionality.

Authenticate against the service

The first thing your app will need to do is authenticate against the Azure Digital Twins service. Then, you can create a service client class to access the SDK functions.

To authenticate, you need the host name of your Azure Digital Twins instance.

In Program.cs, paste the following code below the "Hello, World!" print line in the Main method. Set the value of adtInstanceUrl to your Azure Digital Twins instance host name.

string adtInstanceUrl = "https://<your-Azure-Digital-Twins-instance-hostName>"; 

var credential = new DefaultAzureCredential();
var client = new DigitalTwinsClient(new Uri(adtInstanceUrl), credential);
Console.WriteLine($"Service client created – ready to go");

Save the file.

In your command window, run the code with this command:

dotnet run

This command will restore the dependencies on first run, and then execute the program.

  • If no error occurs, the program will print: "Service client created - ready to go".
  • Since there isn't yet any error handling in this project, if there are any issues, you'll see an exception thrown by the code.

Note

There's currently a known issue affecting the DefaultAzureCredential wrapper class that may result in an error while authenticating. If you encounter this issue, you can try instantiating DefaultAzureCredential with the following optional parameter to resolve it: new DefaultAzureCredential(new DefaultAzureCredentialOptions { ExcludeSharedTokenCacheCredential = true });

For more information about this issue, see Azure Digital Twins known issues.

Upload a model

Azure Digital Twins has no intrinsic domain vocabulary. The types of elements in your environment that you can represent in Azure Digital Twins are defined by you, using models. Models are similar to classes in object-oriented programming languages; they provide user-defined templates for digital twins to follow and instantiate later. They're written in a JSON-like language called Digital Twins Definition Language (DTDL).

The first step in creating an Azure Digital Twins solution is defining at least one model in a DTDL file.

In the directory where you created your project, create a new .json file called SampleModel.json. Paste in the following file body:

{
  "@id": "dtmi:example:SampleModel;1",
  "@type": "Interface",
  "displayName": "SampleModel",
  "contents": [
    {
      "@type": "Relationship",
      "name": "contains"
    },
    {
      "@type": "Property",
      "name": "data",
      "schema": "string"
    }
  ],
  "@context": "dtmi:dtdl:context;3"
}

Tip

If you're using Visual Studio for this tutorial, you may want to select the newly-created JSON file and set the Copy to Output Directory property in the Property inspector to Copy if Newer or Copy Always. This will enable Visual Studio to find the JSON file with the default path when you run the program with F5 during the rest of the tutorial.

Tip

You can check model documents to make sure the DTDL is valid using the DTDLParser library. For more about using this library, see Parse and validate models.

Next, add some more code to Program.cs to upload the model you've created into your Azure Digital Twins instance.

First, add a few using statements to the top of the file:

using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;
using Azure;

Next, prepare to use the asynchronous methods in the C# service SDK, by changing the Main method signature to allow for async execution.

static async Task Main(string[] args)
{

Note

Using async is not strictly required, as the SDK also provides synchronous versions of all calls. This tutorial practices using async.

Next comes the first bit of code that interacts with the Azure Digital Twins service. This code loads the DTDL file you created from your disk, and then uploads it to your Azure Digital Twins service instance.

Paste in the following code under the authorization code you added earlier.

Console.WriteLine();
Console.WriteLine($"Upload a model");
string dtdl = File.ReadAllText("SampleModel.json");
var models = new List<string> { dtdl };
// Upload the model to the service
await client.CreateModelsAsync(models);

In your command window, run the program with this command:

dotnet run

"Upload a model" will be printed in the output, indicating that this code was reached, but there's no output yet to indicate whether the upload was successful.

To add a print statement showing all models that have been successfully uploaded to the instance, add the following code right after the previous section:

// Read a list of models back from the service
AsyncPageable<DigitalTwinsModelData> modelDataList = client.GetModelsAsync();
await foreach (DigitalTwinsModelData md in modelDataList)
{
    Console.WriteLine($"Model: {md.Id}");
}

Before you run the program again to test this new code, recall that the last time you ran the program, you uploaded your model already. Azure Digital Twins won't let you upload the same model twice, so if you attempt to upload the same model again, the program should throw an exception.

With this information in mind, run the program again with this command in your command window:

dotnet run

The program should throw an exception. When you attempt to upload a model that has been uploaded already, the service returns a "bad request" error via the REST API. As a result, the Azure Digital Twins client SDK will in turn throw an exception, for every service return code other than success.

The next section talks about exceptions like this and how to handle them in your code.

Catch errors

To keep the program from crashing, you can add exception code around the model upload code. Wrap the existing client call await client.CreateModelsAsync(typeList) in a try/catch handler, like this:

try
{
    await client.CreateModelsAsync(models);
    Console.WriteLine("Models uploaded to the instance:");
}
catch (RequestFailedException e)
{
    Console.WriteLine($"Upload model error: {e.Status}: {e.Message}");
}

Run the program again with dotnet run in your command window. You'll see that you get back more details about the model upload issue, including an error code stating that ModelIdAlreadyExists.

From this point forward, the tutorial will wrap all calls to service methods in try/catch handlers.

Create digital twins

Now that you've uploaded a model to Azure Digital Twins, you can use this model definition to create digital twins. Digital twins are instances of a model, and represent the entities within your business environment—things like sensors on a farm, rooms in a building, or lights in a car. This section creates a few digital twins based on the model you uploaded earlier.

Add the following code to the end of the Main method to create and initialize three digital twins based on this model.

var twinData = new BasicDigitalTwin();
twinData.Metadata.ModelId = "dtmi:example:SampleModel;1";
twinData.Contents.Add("data", $"Hello World!");

string prefix = "sampleTwin-";
for (int i = 0; i < 3; i++)
{
    try
    {
        twinData.Id = $"{prefix}{i}";
        await client.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(twinData.Id, twinData);
        Console.WriteLine($"Created twin: {twinData.Id}");
    }
    catch(RequestFailedException e)
    {
        Console.WriteLine($"Create twin error: {e.Status}: {e.Message}");
    }
}

In your command window, run the program with dotnet run. In the output, look for the print messages that sampleTwin-0, sampleTwin-1, and sampleTwin-2 were created.

Then, run the program again.

Notice that no error is thrown when the twins are created the second time, even though the twins already exist after the first run. Unlike model creation, twin creation is, on the REST level, a PUT call with upsert semantics. Using this kind of REST call means that if a twin already exists, an attempt to create the same twin again will just replace the original twin. No error is thrown.

Create relationships

Next, you can create relationships between the twins you've created, to connect them into a twin graph. Twin graphs are used to represent your entire environment.

Add a new static method to the Program class, underneath the Main method (the code now has two methods):

public async static Task CreateRelationshipAsync(DigitalTwinsClient client, string srcId, string targetId)
{
    var relationship = new BasicRelationship
    {
        TargetId = targetId,
        Name = "contains"
    };

    try
    {
        string relId = $"{srcId}-contains->{targetId}";
        await client.CreateOrReplaceRelationshipAsync(srcId, relId, relationship);
        Console.WriteLine("Created relationship successfully");
    }
    catch (RequestFailedException e)
    {
        Console.WriteLine($"Create relationship error: {e.Status}: {e.Message}");
    }
}

Next, add the following code to the end of the Main method, to call the CreateRelationship method and use the code you just wrote:

// Connect the twins with relationships
await CreateRelationshipAsync(client, "sampleTwin-0", "sampleTwin-1");
await CreateRelationshipAsync(client, "sampleTwin-0", "sampleTwin-2");

In your command window, run the program with dotnet run. In the output, look for print statements saying that the two relationships were created successfully.

Azure Digital Twins won't let you create a relationship if another relationship with the same ID already exists—so if you run the program multiple times, you'll see exceptions on relationship creation. This code catches the exceptions and ignores them.

List relationships

The next code you'll add allows you to see the list of relationships you've created.

Add the following new method to the Program class:

public async static Task ListRelationshipsAsync(DigitalTwinsClient client, string srcId)
{
    try
    {
        AsyncPageable<BasicRelationship> results = client.GetRelationshipsAsync<BasicRelationship>(srcId);
        Console.WriteLine($"Twin {srcId} is connected to:");
        await foreach (BasicRelationship rel in results)
        {
            Console.WriteLine($" -{rel.Name}->{rel.TargetId}");
        }
    }
    catch (RequestFailedException e)
    {
        Console.WriteLine($"Relationship retrieval error: {e.Status}: {e.Message}");
    }
}

Then, add the following code to the end of the Main method to call the ListRelationships code:

//List the relationships
await ListRelationshipsAsync(client, "sampleTwin-0");

In your command window, run the program with dotnet run. You should see a list of all the relationships you've created in an output statement that looks like this:

Screenshot of a console showing the program output, which results in a message that lists the twin relationships.

Query digital twins

A main feature of Azure Digital Twins is the ability to query your twin graph easily and efficiently to answer questions about your environment.

The last section of code to add in this tutorial runs a query against the Azure Digital Twins instance. The query used in this example returns all the digital twins in the instance.

Add this using statement to enable use of the JsonSerializer class to help present the digital twin information:

using System.Text.Json;

Then, add the following code to the end of the Main method:

// Run a query for all twins
string query = "SELECT * FROM digitaltwins";
AsyncPageable<BasicDigitalTwin> queryResult = client.QueryAsync<BasicDigitalTwin>(query);

await foreach (BasicDigitalTwin twin in queryResult)
{
    Console.WriteLine(JsonSerializer.Serialize(twin));
    Console.WriteLine("---------------");
}

In your command window, run the program with dotnet run. You should see all the digital twins in this instance in the output.

Note

After making a change to the data in your graph, there may be a latency of up to 10 seconds before the changes will be reflected in queries.

The DigitalTwins API reflects changes immediately, so if you need an instant response, use an API request (DigitalTwins GetById) or an SDK call (GetDigitalTwin) to get twin data instead of a query.

Complete code example

At this point in the tutorial, you have a complete client app that can perform basic actions against Azure Digital Twins. For reference, the full code of the program in Program.cs is listed below:

using System;
// <Azure_Digital_Twins_dependencies>
using Azure.DigitalTwins.Core;
using Azure.Identity;
// </Azure_Digital_Twins_dependencies>
// <Model_dependencies>
using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;
using Azure;
// </Model_dependencies>
// <Query_dependencies>
using System.Text.Json;
// </Query_dependencies>

namespace DigitalTwins_Samples
{
    class DigitalTwinsClientAppSample
    {
        // <Async_signature>
        static async Task Main(string[] args)
        {
        // </Async_signature>
            Console.WriteLine("Hello World!");
            // <Authentication_code>
            string adtInstanceUrl = "https://<your-Azure-Digital-Twins-instance-hostName>"; 
            
            var credential = new DefaultAzureCredential();
            var client = new DigitalTwinsClient(new Uri(adtInstanceUrl), credential);
            Console.WriteLine($"Service client created – ready to go");
            // </Authentication_code>

            // <Model_code>
            Console.WriteLine();
            Console.WriteLine("Upload a model");
            string dtdl = File.ReadAllText("SampleModel.json");
            var models = new List<string> { dtdl };

            // Upload the model to the service
            // <Model_try_catch>
            try
            {
                await client.CreateModelsAsync(models);
                Console.WriteLine("Models uploaded to the instance:");
            }
            catch (RequestFailedException e)
            {
                Console.WriteLine($"Upload model error: {e.Status}: {e.Message}");
            }
            // </Model_try_catch>

            // <Print_model>
            // Read a list of models back from the service
            AsyncPageable<DigitalTwinsModelData> modelDataList = client.GetModelsAsync();
            await foreach (DigitalTwinsModelData md in modelDataList)
            {
                Console.WriteLine($"Model: {md.Id}");
            }
            // </Print_model>
            // </Model_code>

            // <Initialize_twins>
            var twinData = new BasicDigitalTwin();
            twinData.Metadata.ModelId = "dtmi:example:SampleModel;1";
            twinData.Contents.Add("data", $"Hello World!");
            
            string prefix = "sampleTwin-";
            for (int i = 0; i < 3; i++)
            {
                try
                {
                    twinData.Id = $"{prefix}{i}";
                    await client.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(twinData.Id, twinData);
                    Console.WriteLine($"Created twin: {twinData.Id}");
                }
                catch(RequestFailedException e)
                {
                    Console.WriteLine($"Create twin error: {e.Status}: {e.Message}");
                }
            }
            // </Initialize_twins>

            // <Use_create_relationship>
            // Connect the twins with relationships
            await CreateRelationshipAsync(client, "sampleTwin-0", "sampleTwin-1");
            await CreateRelationshipAsync(client, "sampleTwin-0", "sampleTwin-2");
            // </Use_create_relationship>

            // <Use_list_relationships>
            //List the relationships
            await ListRelationshipsAsync(client, "sampleTwin-0");
            // </Use_list_relationships>

            // <Query_twins>
            // Run a query for all twins
            string query = "SELECT * FROM digitaltwins";
            AsyncPageable<BasicDigitalTwin> queryResult = client.QueryAsync<BasicDigitalTwin>(query);
            
            await foreach (BasicDigitalTwin twin in queryResult)
            {
                Console.WriteLine(JsonSerializer.Serialize(twin));
                Console.WriteLine("---------------");
            }
            // </Query_twins>
        }

        // <Create_relationship>
        public async static Task CreateRelationshipAsync(DigitalTwinsClient client, string srcId, string targetId)
        {
            var relationship = new BasicRelationship
            {
                TargetId = targetId,
                Name = "contains"
            };
        
            try
            {
                string relId = $"{srcId}-contains->{targetId}";
                await client.CreateOrReplaceRelationshipAsync(srcId, relId, relationship);
                Console.WriteLine("Created relationship successfully");
            }
            catch (RequestFailedException e)
            {
                Console.WriteLine($"Create relationship error: {e.Status}: {e.Message}");
            }
        }
        // </Create_relationship>
        
        // <List_relationships>
        public async static Task ListRelationshipsAsync(DigitalTwinsClient client, string srcId)
        {
            try
            {
                AsyncPageable<BasicRelationship> results = client.GetRelationshipsAsync<BasicRelationship>(srcId);
                Console.WriteLine($"Twin {srcId} is connected to:");
                await foreach (BasicRelationship rel in results)
                {
                    Console.WriteLine($" -{rel.Name}->{rel.TargetId}");
                }
            }
            catch (RequestFailedException e)
            {
                Console.WriteLine($"Relationship retrieval error: {e.Status}: {e.Message}");
            }
        }
        // </List_relationships>
    }
}

Clean up resources

After completing this tutorial, you can choose which resources you want to remove, depending on what you want to do next.

  • If you plan to continue to the next tutorial, the instance used in this tutorial can be reused in the next one. You can keep the Azure Digital Twins resources you set up here and skip the rest of this section.
  • If you want to continue using the Azure Digital Twins instance from this article, but clear out all of its models, twins, and relationships, run the following az dt job deletion CLI command:

    az dt job deletion create -n <name-of-Azure-Digital-Twins-instance> -y
    

    If you only want to delete some of these elements, you can use the az dt twin relationship delete, az dt twin delete, and az dt model delete commands to selectively delete only the elements you want to remove.

  • If you do not need any of the resources you created in this tutorial, you can delete the Azure Digital Twins instance and all other resources from this article with the az group delete CLI command. This deletes all Azure resources in a resource group, as well as the resource group itself.

    Important

    Deleting a resource group is irreversible. The resource group and all the resources contained in it are permanently deleted. Make sure that you don't accidentally delete the wrong resource group or resources.

    Open Azure Cloud Shell or a local CLI window, and run the following command to delete the resource group and everything it contains.

    az group delete --name <your-resource-group>
    

You may also want to delete the project folder from your local machine.

Next steps

In this tutorial, you created a .NET console client application from scratch. You wrote code for this client app to perform the basic actions on an Azure Digital Twins instance.

Continue to the next tutorial to explore the things you can do with such a sample client app: