你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

使用关系管理数字孪生图

Azure 数字孪生的核心是代表整个环境的孪生图。 孪生图由通过关系连接的各个数字孪生组成。 本文重点介绍如何将关系和图作为一个整体进行管理;若要单独处理数字孪生体,请参阅管理数字孪生体

拥有有效的 Azure 数字孪生实例并在客户端应用中设置了验证码后,就可在 Azure 数字孪生实例中创建、修改和删除数字孪生体及其关系。

先决条件

若要在本文中使用 Azure 数字孪生,你需要有一个 Azure 数字孪生实例,还需具备使用它所必需的权限。 如果你已设置了一个 Azure 数字孪生实例,则可以使用该实例并跳到下一部分。 如果没有,请按照设置实例和身份验证中的说明操作。 该说明中包含可帮助你验证是否已成功完成每个步骤的信息。

设置实例后,记下实例的主机名。 可以在 Azure 门户中找到该主机名

开发人员接口

本文重点介绍如何使用 .NET (C#) SDK 完成不同的管理操作。 还可使用 Azure 数字孪生 API 和 SDK 中所述的其他语言 SDK 创建这些管理调用。

其他可用于完成这些操作的开发人员接口包括:

可视化效果

Azure Digital Twins Explorer 是一种可视化工具,用于浏览 Azure 数字孪生图中的数据。 你可以使用此资源管理器查看、查询和编辑模型、孪生和关系。

若要了解 Azure Digital Twins Explorer 工具,请参阅 Azure Digital Twins Explorer。 如需各项功能的详细使用步骤,请参阅使用 Azure Digital Twins Explorer

下面展示了可视化效果:

Screenshot of Azure Digital Twins Explorer showing sample models and twins.

创建关系

关系可描述不同数字孪生体是如何相互连接的(这是构成孪生图的基础)。

将那些可以创建的一个孪生体(源)与另一个孪生体(目标)的关系类型定义为源孪生体的 DTDL 模型的一部分。 可以通过将 CreateOrReplaceRelationshipAsync() SDK 调用与遵循 DTDL 定义的孪生体和关系详细信息一起使用来创建关系的实例。

若要创建关系,需要指定:

  • 源孪生体 ID(以下代码示例中为 srcId):关系起源的孪生体的 ID。
  • 目标孪生体 ID(以下代码示例中为 targetId):关系到达的孪生体的 ID。
  • 关系名称(以下代码示例中为 relName):关系的泛型类型,如 contains
  • 关系 ID(以下代码示例中为 relId):此关系的特定名称,如 Relationship1

关系 ID 在给定的源孪生体中必须是唯一的。 无需全局唯一。 例如,对于孪生体 Foo,每个特定的关系 ID 必须是唯一的。 但是,另一个孪生体 Bar 可以具有与 Foo 关系的相同 ID 匹配的传出关系。

下面的代码示例演示如何在 Azure 数字孪生实例中创建关系。 在可能出现在较大程序上下文中的自定义方法中,它使用 SDK 调用(已突出显示)。

private async static Task CustomMethod_CreateRelationshipAsync(DigitalTwinsClient client, string srcId, string targetId, string relName, IDictionary<string,object> inputProperties)
{
    var relationship = new BasicRelationship
    {
        TargetId = targetId,
        Name = relName,
        Properties = inputProperties
    };

    try
    {
        string relId = $"{srcId}-{relName}->{targetId}";
        await client.CreateOrReplaceRelationshipAsync<BasicRelationship>(srcId, relId, relationship);
        Console.WriteLine($"Created {relName} relationship successfully. Relationship ID is {relId}.");
    }
    catch (RequestFailedException rex)
    {
        Console.WriteLine($"Create relationship error: {rex.Status}:{rex.Message}");
    }

}

现在可调用此自定义函数来创建 contains 关系,方法如下

await CustomMethod_CreateRelationshipAsync(client, srcId, targetId, "contains", properties);

如果希望创建多个关系,可重复调用同一方法,将不同的关系类型传递到参数。

有关帮助程序类 BasicRelationship 的详细信息,请参阅 Azure 数字孪生 API 和 SDK

在孪生体之间创建多种关系

关系可以分为以下两类:

  • 传出关系:属于此孪生体的关系,此类关系指向外部以将孪生体与其他孪生体相连接。 GetRelationshipsAsync() 方法用于获取孪生体的传出关系。
  • 传入关系:属于其他孪生体的关系,此类关系指向此孪生体以创建“传入”链接。 GetIncomingRelationshipsAsync() 方法用于获取孪生体的传入关系。

两个孪生体间的关系数量没有限制,你可根据需要在孪生体之间建立任意数量的关系。

此情况意味着,可一次表达两个孪生体间的多种关系。 例如,孪生体 A 可与孪生体 B 既有 stored 关系又有 manufactured 关系

如果需要,甚至可在相同的两个孪生体之间创建具有同一种关系的多个实例。 在此示例中,孪生体 A 可与孪生体 B 具有两种不同的 stored 关系,只要这两种关系具有不同的关系 ID 即可

注意

Azure 数字孪生目前不支持关系的 minMultiplicitymaxMultiplicity 的 DTDL 特性,即使它们已被定义为模型的一部分,服务也不会强制实现它们。 有关详细信息,请参阅特定于服务的 DTDL 说明

使用导入作业 API 批量创建关系

可以使用 导入作业 API 在单个 API 调用中一次性创建多个关系。 此方法需要使用 Azure Blob 存储,以及 Azure 数字孪生实例中的写入权限,以用于关系和批量作业。

提示

导入作业 API 还允许在同一调用中导入模型和孪生,以一次性创建图形的所有部分。 有关此过程的详细信息,请参阅 使用导入作业 API 批量上传模型、孪生体和关系。

若要批量导入关系,需要将关系(以及批量导入作业中包含的任何其他资源)结构为 NDJSON 文件。 该 Relationships 部分位于该节之后 Twins ,使其成为文件中的最后一个图形数据部分。 文件中定义的关系可以引用在此文件中定义的孪生体,也可以引用实例中已存在的孪生体,并且可以选择性地包括关系具有的任何属性的初始化。

可以在导入作业 API 简介查看示例导入文件和用于创建这些文件的示例项目。

接下来,需要将文件上传到 Azure Blob 存储 中的追加 blob 中。 有关如何创建 Azure 存储容器的说明,请参阅 “创建容器”。 然后,使用首选上传方法上传文件(某些选项是 AzCopy 命令Azure CLIAzure 门户)。

将 NDJSON 文件上传到容器后,请在 Blob 容器中获取其 URL 。 稍后将在批量导入 API 调用正文中使用此值。

下面是显示Azure 门户中 blob 文件的 URL 值的屏幕截图:

Screenshot of the Azure portal showing the URL of a file in a storage container.

然后,可以在导入作业 API 调用中使用该文件。 你将提供输入文件的 Blob 存储 URL,以及新的 Blob 存储 URL,以指示希望在服务创建输出日志时存储的位置。

列出关系

列出单个关系的属性

始终可将关系数据反序列化为你选择的类型。 若要对关系进行基本访问,请使用类型 BasicRelationship。 使用 BasicRelationship 帮助程序类,还可通过 IDictionary<string, object> 访问关系上定义的属性。 若要列出属性,可使用:

public async Task ListRelationshipProperties(DigitalTwinsClient client, string twinId, string relId, BasicDigitalTwin twin)
{

    var res = await client.GetRelationshipAsync<BasicRelationship>(twinId, relId);
    BasicRelationship rel = res.Value;
    Console.WriteLine($"Relationship Name: {rel.Name}");
    foreach (string prop in rel.Properties.Keys)
    {
        if (twin.Contents.TryGetValue(prop, out object value))
        {
            Console.WriteLine($"Property '{prop}': {value}");
        }
    }
}

列出数字孪生体中传出的关系

若要访问图中给定孪生体的传出关系列表,可使用如下所示的 GetRelationships() 方法

AsyncPageable<BasicRelationship> rels = client.GetRelationshipsAsync<BasicRelationship>(dtId);

此方法会返回 Azure.Pageable<T>Azure.AsyncPageable<T>,具体取决于你使用的是同步还是异步版本的调用。

以下为检索关系列表的示例。 在可能出现在较大程序上下文中的自定义方法中,它使用 SDK 调用(已突出显示)。

private static async Task<List<BasicRelationship>> CustomMethod_FindOutgoingRelationshipsAsync(DigitalTwinsClient client, string dtId)
{
    // Find the relationships for the twin
    
    try
    {
        // GetRelationshipsAsync will throw if an error occurs
        AsyncPageable<BasicRelationship> rels = client.GetRelationshipsAsync<BasicRelationship>(dtId);
        var results = new List<BasicRelationship>();
        await foreach (BasicRelationship rel in rels)
        {
            results.Add(rel);
            Console.WriteLine($"Found relationship: {rel.Id}");

            //Print its properties
            Console.WriteLine($"Relationship properties:");
            foreach(KeyValuePair<string, object> property in rel.Properties)
            {
                Console.WriteLine("{0} = {1}", property.Key, property.Value);
            }
        }

        return results;
    }
    catch (RequestFailedException ex)
    {
        Console.WriteLine($"*** Error {ex.Status}/{ex.ErrorCode} retrieving relationships for {dtId} due to {ex.Message}");
        return null;
    }
}

现在可调用此自定义方法来查看孪生体的传出关系,如下所示:

await CustomMethod_FindOutgoingRelationshipsAsync(client, twin_Id);

可以使用检索到的关系导航到图中的其他孪生体,方法如下:从返回的关系中读取 target 字段,并将它用作下次调用 GetDigitalTwin() 的 ID。

列出传入数字孪生体的关系

Azure 数字孪生还提供了一个 SDK 调用,用于查找传入到给定孪生体的所有关系。 此 SDK 在反向导航或删除孪生体时通常很有用。

注意

IncomingRelationship 调用不返回关系的完整正文。 有关 IncomingRelationship 类的更多信息,请参阅其参考文档

上一节的代码示例重点介绍了如何查找孪生体的传出关系。 以下示例的结构类似,但用于查找孪生体的传入关系。 在可能出现在较大程序上下文中的自定义方法中,此示例还使用 SDK 调用(已突出显示)。

private static async Task<List<IncomingRelationship>> CustomMethod_FindIncomingRelationshipsAsync(DigitalTwinsClient client, string dtId)
{
    // Find the relationships for the twin
    
    try
    {
        // GetRelationshipsAsync will throw an error if a problem occurs
        AsyncPageable<IncomingRelationship> incomingRels = client.GetIncomingRelationshipsAsync(dtId);

        var results = new List<IncomingRelationship>();
        await foreach (IncomingRelationship incomingRel in incomingRels)
        {
            results.Add(incomingRel);
            Console.WriteLine($"Found incoming relationship: {incomingRel.RelationshipId}");

            //Print its properties
            Response<BasicRelationship> relResponse = await client.GetRelationshipAsync<BasicRelationship>(incomingRel.SourceId, incomingRel.RelationshipId);
            BasicRelationship rel = relResponse.Value;
            Console.WriteLine($"Relationship properties:");
            foreach(KeyValuePair<string, object> property in rel.Properties)
            {
                Console.WriteLine("{0} = {1}", property.Key, property.Value);
            }
        }
        return results;
    }
    catch (RequestFailedException ex)
    {
        Console.WriteLine($"*** Error {ex.Status}/{ex.ErrorCode} retrieving incoming relationships for {dtId} due to {ex.Message}");
        return null;
    }
}

现在可调用此自定义方法来查看孪生体的传入关系,如下所示:

await CustomMethod_FindIncomingRelationshipsAsync(client, twin_Id);

列出所有的孪生体属性和关系

通过使用上述方法列出孪生体的传出和传入关系,可以创建一种方法来输出完整的孪生体信息,包括孪生体的属性以及这两种关系。 下面是一个自定义方法示例,展现了如何组合上述自定义方法来实现此目的。

private static async Task CustomMethod_FetchAndPrintTwinAsync(string twin_Id, DigitalTwinsClient client)
{
    Response<BasicDigitalTwin> res = await client.GetDigitalTwinAsync<BasicDigitalTwin>(twin_Id);
    await CustomMethod_FindOutgoingRelationshipsAsync(client, twin_Id);
    await CustomMethod_FindIncomingRelationshipsAsync(client, twin_Id);

    return;
}

现在可调用此自定义函数,如下所示:

await CustomMethod_FetchAndPrintTwinAsync(srcId, client);

更新关系

使用 UpdateRelationship 方法更新关系。

注意

此方法用于更新关系的属性。 如果需要更改关系的源孪生体或目标孪生体,则需要删除该关系,然后使用新的孪生体重新创建关系

客户端调用所需的参数包括:

  • 源孪生体(关系起源的孪生体)的 ID。
  • 要更新的关系的 ID。
  • 包含要更新的属性和新值的 JSON 修补程序文档。

下面是展现如何使用此方法的示例代码片段。 在可能出现在较大程序上下文中的自定义方法中,此示例使用 SDK 调用(已突出显示)。

private async static Task CustomMethod_UpdateRelationshipAsync(DigitalTwinsClient client, string srcId, string relId, Azure.JsonPatchDocument updateDocument)
{

    try
    {
        await client.UpdateRelationshipAsync(srcId, relId, updateDocument);
        Console.WriteLine($"Successfully updated {relId}");
    }
    catch (RequestFailedException rex)
    {
        Console.WriteLine($"Update relationship error: {rex.Status}:{rex.Message}");
    }

}

以下示例展现了如何调用此自定义方法,传入包含更新属性所需信息的 JSON 修补程序文档。

var updatePropertyPatch = new JsonPatchDocument();
updatePropertyPatch.AppendAdd("/ownershipUser", "ownershipUser NEW value");
await CustomMethod_UpdateRelationshipAsync(client, srcId, $"{srcId}-contains->{targetId}", updatePropertyPatch);

删除关系

第一个参数指定源孪生体(关系起源的孪生体)。 另一个参数是关系 ID。 同时需要孪生体 ID 和关系 ID,因为关系 ID 只在孪生体范围内唯一。

下面是展现如何使用此方法的示例代码。 在可能出现在较大程序上下文中的自定义方法中,此示例使用 SDK 调用(已突出显示)。

private static async Task CustomMethod_DeleteRelationshipAsync(DigitalTwinsClient client, string srcId, string relId)
{
    try
    {
        Response response = await client.DeleteRelationshipAsync(srcId, relId);
        await CustomMethod_FetchAndPrintTwinAsync(srcId, client);
        Console.WriteLine("Deleted relationship successfully");
    }
    catch (RequestFailedException e)
    {
        Console.WriteLine($"Error {e.ErrorCode}");
    }
}

现在可调用此自定义方法来删除关系,如下所示:

await CustomMethod_DeleteRelationshipAsync(client, srcId, $"{srcId}-contains->{targetId}");

注意

如果要一次性删除 实例中的所有 模型、孪生体和关系,请使用 “删除作业”API

一次性创建多个图形元素

本部分介绍用于同时创建具有多个元素的图形的策略,而不是使用单个 API 调用来上传模型、孪生体和关系,以逐个上传它们。

使用导入作业 API 批量上传模型、孪生体和关系

可以使用 导入作业 API 在单个 API 调用中将多个模型、孪生体和关系上传到实例,从而有效地一次性创建图形。 此方法需要对图形元素(模型、孪生和关系)和批量作业使用 Azure Blob 存储,以及 Azure 数字孪生实例中的写入权限

若要批量导入资源,请先创建包含 资源详细信息的 NDJSON 文件。 该文件以节 Header 开头,后跟可选节 ModelsTwins以及 Relationships。 无需在文件中包括所有三种类型的图形数据,但存在的任何部分都必须遵循该顺序。 文件中定义的孪生可以引用在此文件中定义的模型,也可以引用实例中已存在的模型,并且可以选择性地包括孪生体属性的初始化。 文件中定义的关系可以引用在此文件中定义的孪生体,也可以引用实例中已存在的孪生体,并且可以选择性地包括关系属性的初始化。

可以在导入作业 API 简介查看示例导入文件和用于创建这些文件的示例项目。

接下来,需要将文件上传到 Azure Blob 存储 中的追加 blob 中。 有关如何创建 Azure 存储容器的说明,请参阅 “创建容器”。 然后,使用首选上传方法上传文件(某些选项是 AzCopy 命令Azure CLIAzure 门户)。

将 NDJSON 文件上传到容器后,请在 Blob 容器中获取其 URL 。 稍后将在批量导入 API 调用正文中使用此值。

下面是显示Azure 门户中 blob 文件的 URL 值的屏幕截图:

Screenshot of the Azure portal showing the URL of a file in a storage container.

然后,可以在导入作业 API 调用中使用该文件。 你将提供输入文件的 Blob 存储 URL,以及新的 Blob 存储 URL,以指示希望在服务创建输出日志时存储的位置。

使用 Azure 数字孪生资源管理器导入图形

Azure 数字孪生资源管理器 是用于查看和交互孪生图的可视化工具。 它包含一项功能,用于以 JSON 或 Excel 格式导入图形文件,其中包含多个模型、孪生体和关系。

有关使用此功能的详细信息,请参阅 Azure 数字孪生资源管理器文档中的“导入”图形

从 CSV 文件创建孪生体和关系

有时,可能需要根据存储在不同数据库或电子表格或 CSV 文件中的数据创建孪生层次结构。 本部分说明如何从 CSV 文件中读取数据并根据这些数据创建孪生图。

请看以下数据表,其中描述了一组数字孪生体和关系。 此文件中引用的模型必须已存在于 Azure 数字孪生实例中。

模型 ID 孪生体 ID(必须唯一) 关系名称 目标孪生体 ID 孪生体初始化数据
dtmi:example:Floor;1 Floor1 contains Room1
dtmi:example:Floor;1 楼层0 contains Room0
dtmi:example:Room;1 Room1 {"Temperature": 80}
dtmi:example:Room;1 Room0 {"Temperature": 70}

若要将此数据导入到 Azure 数字孪生,可以将表转换为 CSV 文件。 转换表后,可以编写代码来将文件解释为命令以创建孪生体和关系。 下面的代码示例演示如何从 CSV 文件读取数据并在 Azure 数字孪生中创建孪生图。

在下面的代码中,CSV 文件名为 data.csv,并且还有一个占位符,表示 Azure 数字孪生实例的主机名。 该示例还利用了多个包,你可将这些包添加到项目中来帮助完成此过程。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Azure;
using Azure.DigitalTwins.Core;
using Azure.Identity;

namespace creating_twin_graph_from_csv
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var relationshipRecordList = new List<BasicRelationship>();
            var twinList = new List<BasicDigitalTwin>();
            List<List<string>> data = ReadData();
            DigitalTwinsClient client = CreateDtClient();

            // Interpret the CSV file data, by each row
            foreach (List<string> row in data)
            {
                string modelID = row.Count > 0 ? row[0].Trim() : null;
                string srcID = row.Count > 1 ? row[1].Trim() : null;
                string relName = row.Count > 2 ? row[2].Trim() : null;
                string targetID = row.Count > 3 ? row[3].Trim() : null;
                string initProperties = row.Count > 4 ? row[4].Trim() : null;
                Console.WriteLine($"ModelID: {modelID}, TwinID: {srcID}, RelName: {relName}, TargetID: {targetID}, InitData: {initProperties}");
                var props = new Dictionary<string, object>();
                // Parse properties into dictionary (left out for compactness)
                // ...

                // Null check for source and target IDs
                if (!string.IsNullOrWhiteSpace(srcID) && !string.IsNullOrWhiteSpace(targetID) && !string.IsNullOrWhiteSpace(relName))
                {
                    relationshipRecordList.Add(
                        new BasicRelationship
                        {
                            SourceId = srcID,
                            TargetId = targetID,
                            Name = relName,
                        });
                }

                if (!string.IsNullOrWhiteSpace(srcID) && !string.IsNullOrWhiteSpace(modelID))
                twinList.Add(
                    new BasicDigitalTwin
                    {
                        Id = srcID,
                        Metadata = { ModelId = modelID },
                        Contents = props,
                    });
            }

            // Create digital twins
            foreach (BasicDigitalTwin twin in twinList)
            {
                try
                {
                    await client.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(twin.Id, twin);
                    Console.WriteLine("Twin is created");
                }
                catch (RequestFailedException ex)
                {
                    Console.WriteLine($"Error {ex.Status}: {ex.Message}");
                }
            }

            // Create relationships between the twins
            foreach (BasicRelationship rec in relationshipRecordList)
            {
                string relId = $"{rec.SourceId}-{rec.Name}->{rec.TargetId}";
                try
                {
                    await client.CreateOrReplaceRelationshipAsync<BasicRelationship>(rec.SourceId, relId, rec);
                    Console.WriteLine($"Relationship {relId} is created");
                }
                catch (RequestFailedException ex)
                {
                    Console.WriteLine($"Error creating relationship {relId}. {ex.Status}: {ex.Message}");
                }
            }
        }

        // Method to ingest data from the CSV file
        public static List<List<string>> ReadData()
        {
            string path = "<path-to>/data.csv";
            string[] lines = System.IO.File.ReadAllLines(path);
            var data = new List<List<string>>();
            int count = 0;
            foreach (string line in lines)
            {
                if (count++ == 0)
                    continue;
                var cols = new List<string>();
                string[] columns = line.Split(',');
                foreach (string column in columns)
                {
                    cols.Add(column);
                }
                data.Add(cols);
            }
            return data;
        }

        // Method to create the digital twins client
        private static DigitalTwinsClient CreateDtClient()
        {
            string adtInstanceUrl = "https://<your-instance-hostname>";
            var credentials = new DefaultAzureCredential();
            return new DigitalTwinsClient(new Uri(adtInstanceUrl), credentials);
        }
    }
}

可运行的孪生图示例

以下可运行代码片段使用本文中的关系操作,根据数字孪生体和关系创建孪生图。

设置示例项目文件

该代码片段使用两个示例模型定义,Room.jsonFloor.json。 若要下载模型文件以便可在代码中使用,请使用以下链接直接转到 GitHub 中的这些文件。 然后,右键单击屏幕上的任意位置,在浏览器的右击菜单中选择“另存为”,并使用“另存为”窗口将文件另存为 Room.json 和 Floor.json

接下来,在 Visual Studio 或所选的编辑器中创建新的控制台应用项目

然后,将可运行示例的以下代码复制到你的项目中

using System;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;
using Azure;
using Azure.DigitalTwins.Core;
using Azure.Identity;

namespace DigitalTwins_Samples
{
    public class GraphOperationsSample
    {
        public static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            // Create the Azure Digital Twins client for API calls
            DigitalTwinsClient client = createDtClient();
            Console.WriteLine($"Service client created – ready to go");
            Console.WriteLine();

            // Upload models
            Console.WriteLine($"Upload models");
            Console.WriteLine();
            string dtdl = File.ReadAllText("<path-to>/Room.json");
            string dtdl1 = File.ReadAllText("<path-to>/Floor.json");
            var models = new List<string>
            {
                dtdl,
                dtdl1,
            };
            // Upload the models to the service
            await client.CreateModelsAsync(models);

            // Create new (Floor) digital twin
            var floorTwin = new BasicDigitalTwin();
            string srcId = "myFloorID";
            floorTwin.Metadata.ModelId = "dtmi:example:Floor;1";
            // Floor twins have no properties, so nothing to initialize
            // Create the twin
            await client.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(srcId, floorTwin);
            Console.WriteLine("Twin created successfully");

            // Create second (Room) digital twin
            var roomTwin = new BasicDigitalTwin();
            string targetId = "myRoomID";
            roomTwin.Metadata.ModelId = "dtmi:example:Room;1";
            // Initialize properties
            roomTwin.Contents.Add("Temperature", 35.0);
            roomTwin.Contents.Add("Humidity", 55.0);
            // Create the twin
            await client.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(targetId, roomTwin);
            
            // Create relationship between them
            var properties = new Dictionary<string, object>
            {
                { "ownershipUser", "ownershipUser original value" },
            };
            // <UseCreateRelationship>
            await CustomMethod_CreateRelationshipAsync(client, srcId, targetId, "contains", properties);
            // </UseCreateRelationship>
            Console.WriteLine();

            // Update relationship's Name property
            // <UseUpdateRelationship>
            var updatePropertyPatch = new JsonPatchDocument();
            updatePropertyPatch.AppendAdd("/ownershipUser", "ownershipUser NEW value");
            await CustomMethod_UpdateRelationshipAsync(client, srcId, $"{srcId}-contains->{targetId}", updatePropertyPatch);
            // </UseUpdateRelationship>
            Console.WriteLine();

            //Print twins and their relationships
            Console.WriteLine("--- Printing details:");
            Console.WriteLine($"Outgoing relationships from source twin, {srcId}:");
            // <UseFetchAndPrint>
            await CustomMethod_FetchAndPrintTwinAsync(srcId, client);
            // </UseFetchAndPrint>
            Console.WriteLine();
            Console.WriteLine($"Incoming relationships to target twin, {targetId}:");
            await CustomMethod_FetchAndPrintTwinAsync(targetId, client);
            Console.WriteLine("--------");
            Console.WriteLine();

            // Delete the relationship
            // <UseDeleteRelationship>
            await CustomMethod_DeleteRelationshipAsync(client, srcId, $"{srcId}-contains->{targetId}");
            // </UseDeleteRelationship>
            Console.WriteLine();

            // Print twins and their relationships again
            Console.WriteLine("--- Printing details (after relationship deletion):");
            Console.WriteLine("Outgoing relationships from source twin:");
            await CustomMethod_FetchAndPrintTwinAsync(srcId, client);
            Console.WriteLine();
            Console.WriteLine("Incoming relationships to target twin:");
            await CustomMethod_FetchAndPrintTwinAsync(targetId, client);
            Console.WriteLine("--------");
            Console.WriteLine();
        }

        private static DigitalTwinsClient createDtClient()
        {
            string adtInstanceUrl = "https://<your-instance-hostname>";
            var credentials = new DefaultAzureCredential();
            var client = new DigitalTwinsClient(new Uri(adtInstanceUrl), credentials);
            return client;
        }

        // <CreateRelationshipMethod>
        private async static Task CustomMethod_CreateRelationshipAsync(DigitalTwinsClient client, string srcId, string targetId, string relName, IDictionary<string,object> inputProperties)
        {
            var relationship = new BasicRelationship
            {
                TargetId = targetId,
                Name = relName,
                Properties = inputProperties
            };

            try
            {
                string relId = $"{srcId}-{relName}->{targetId}";
                await client.CreateOrReplaceRelationshipAsync<BasicRelationship>(srcId, relId, relationship);
                Console.WriteLine($"Created {relName} relationship successfully. Relationship ID is {relId}.");
            }
            catch (RequestFailedException rex)
            {
                Console.WriteLine($"Create relationship error: {rex.Status}:{rex.Message}");
            }

        }
        // </CreateRelationshipMethod>

        // <UpdateRelationshipMethod>
        private async static Task CustomMethod_UpdateRelationshipAsync(DigitalTwinsClient client, string srcId, string relId, Azure.JsonPatchDocument updateDocument)
        {

            try
            {
                await client.UpdateRelationshipAsync(srcId, relId, updateDocument);
                Console.WriteLine($"Successfully updated {relId}");
            }
            catch (RequestFailedException rex)
            {
                Console.WriteLine($"Update relationship error: {rex.Status}:{rex.Message}");
            }

        }
        // </UpdateRelationshipMethod>

        // <FetchAndPrintMethod>
        private static async Task CustomMethod_FetchAndPrintTwinAsync(string twin_Id, DigitalTwinsClient client)
        {
            Response<BasicDigitalTwin> res = await client.GetDigitalTwinAsync<BasicDigitalTwin>(twin_Id);
            // <UseFindOutgoingRelationships>
            await CustomMethod_FindOutgoingRelationshipsAsync(client, twin_Id);
            // </UseFindOutgoingRelationships>
            // <UseFindIncomingRelationships>
            await CustomMethod_FindIncomingRelationshipsAsync(client, twin_Id);
            // </UseFindIncomingRelationships>

            return;
        }
        // </FetchAndPrintMethod>

        // <FindOutgoingRelationshipsMethod>
        private static async Task<List<BasicRelationship>> CustomMethod_FindOutgoingRelationshipsAsync(DigitalTwinsClient client, string dtId)
        {
            // Find the relationships for the twin
            
            try
            {
                // GetRelationshipsAsync will throw if an error occurs
                // <GetRelationshipsCall>
                AsyncPageable<BasicRelationship> rels = client.GetRelationshipsAsync<BasicRelationship>(dtId);
                // </GetRelationshipsCall>
                var results = new List<BasicRelationship>();
                await foreach (BasicRelationship rel in rels)
                {
                    results.Add(rel);
                    Console.WriteLine($"Found relationship: {rel.Id}");

                    //Print its properties
                    Console.WriteLine($"Relationship properties:");
                    foreach(KeyValuePair<string, object> property in rel.Properties)
                    {
                        Console.WriteLine("{0} = {1}", property.Key, property.Value);
                    }
                }

                return results;
            }
            catch (RequestFailedException ex)
            {
                Console.WriteLine($"*** Error {ex.Status}/{ex.ErrorCode} retrieving relationships for {dtId} due to {ex.Message}");
                return null;
            }
        }
        // </FindOutgoingRelationshipsMethod>

        // <FindIncomingRelationshipsMethod>
        private static async Task<List<IncomingRelationship>> CustomMethod_FindIncomingRelationshipsAsync(DigitalTwinsClient client, string dtId)
        {
            // Find the relationships for the twin
            
            try
            {
                // GetRelationshipsAsync will throw an error if a problem occurs
                AsyncPageable<IncomingRelationship> incomingRels = client.GetIncomingRelationshipsAsync(dtId);

                var results = new List<IncomingRelationship>();
                await foreach (IncomingRelationship incomingRel in incomingRels)
                {
                    results.Add(incomingRel);
                    Console.WriteLine($"Found incoming relationship: {incomingRel.RelationshipId}");

                    //Print its properties
                    Response<BasicRelationship> relResponse = await client.GetRelationshipAsync<BasicRelationship>(incomingRel.SourceId, incomingRel.RelationshipId);
                    BasicRelationship rel = relResponse.Value;
                    Console.WriteLine($"Relationship properties:");
                    foreach(KeyValuePair<string, object> property in rel.Properties)
                    {
                        Console.WriteLine("{0} = {1}", property.Key, property.Value);
                    }
                }
                return results;
            }
            catch (RequestFailedException ex)
            {
                Console.WriteLine($"*** Error {ex.Status}/{ex.ErrorCode} retrieving incoming relationships for {dtId} due to {ex.Message}");
                return null;
            }
        }
        // </FindIncomingRelationshipsMethod>

        // <DeleteRelationshipMethod>
        private static async Task CustomMethod_DeleteRelationshipAsync(DigitalTwinsClient client, string srcId, string relId)
        {
            try
            {
                Response response = await client.DeleteRelationshipAsync(srcId, relId);
                await CustomMethod_FetchAndPrintTwinAsync(srcId, client);
                Console.WriteLine("Deleted relationship successfully");
            }
            catch (RequestFailedException e)
            {
                Console.WriteLine($"Error {e.ErrorCode}");
            }
        }
        // </DeleteRelationshipMethod>
    }
}

注意

当前存在影响 DefaultAzureCredential 包装类的已知问题,该问题在进行身份验证时可能会导致错误。 如果遇到此问题,可以尝试使用以下可选参数实例化 DefaultAzureCredential 来解决问题:new DefaultAzureCredential(new DefaultAzureCredentialOptions { ExcludeSharedTokenCacheCredential = true });

有关此问题的详细信息,请参阅 Azure 数字孪生已知问题

配置项目

接下来,完成以下步骤来配置项目代码:

  1. 将之前下载的 Room.json 和 Floor.json 文件添加到项目中,并替换代码中的 <path-to> 占位符,以告知程序可在何处找到它们

  2. 将占位符 <your-instance-hostname> 替换为你的 Azure 数字孪生实例的主机名。

  3. 将两个依赖项添加到你的项目,这是与 Azure 数字孪生结合使用所必需的。 第一个是适用于 .NET 的 Azure 数字孪生 SDK 的包,第二个提供工具来帮助向 Azure 进行身份验证。

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

如果要直接运行该示例,还需要设置本地凭据。 下一节将详细介绍这一过程。

设置本地 Azure 凭据

当你在本地计算机上运行示例时,此示例使用 DefaultAzureCredential(属于 Azure.Identity 库的一部分)对用户进行 Azure 数字孪生实例验证。 若要详细了解客户端应用可向 Azure 数字孪生进行身份验证的不同方法,请参阅编写应用身份验证代码

使用 DefaultAzureCredential,此示例将在本地环境中搜索凭据,如本地 DefaultAzureCredential 或 Visual Studio/Visual Studio Code 中的 Azure 登录。 因此,应该通过这些机制在本地登录 Azure,以便设置示例的凭据。

如果使用 Visual Studio 或 Visual Studio Code 运行代码示例,请确保使用要用于访问 Azure 数字孪生实例的相同 Azure 凭据登录到该编辑器。 如果使用本地 CLI 窗口,请运行 az login 命令来登录到你的 Azure 帐户。 此后,当你运行代码示例时,系统应自动对你进行身份验证。

运行示例

现已完成设置,可以运行示例代码项目。

下面是该程序的控制台输出:

Screenshot of the console output showing the twin details with incoming and outgoing relationships of the twins.

提示

孪生图是在孪生体之间创建关系的概念。 若要查看孪生图的可视化表示形式,请参阅本文的可视化效果部分

后续步骤

了解如何查询 Azure 数字孪生孪生图: