Управление графиком цифровых двойников с помощью связей

Основой Azure Digital Twins является граф двойника, представляющий всю среду. Граф двойника состоит из отдельных цифровых двойников, подключенных с помощью связей. Эта статья посвящена управлению связями и графу в целом. Сведения о работе с отдельными цифровыми двойниками см. в разделе Управление цифровыми двойниками.

Создав рабочий экземпляр Azure Digital Twins и настроив код проверки подлинности в клиентском приложении, вы можете создаватЬ, изменять и удалять цифровые двойники и их связи в экземпляре Azure Digital Twins.

Необходимые компоненты

Для работы с Azure Digital Twins в этой статье потребуется экземпляр Azure Digital Twins и необходимые разрешения для его использования. Если у вас уже настроен экземпляр Azure Digital Twins, этот экземпляр можно использовать и перейти к следующему разделу. В противном случае выполните инструкции по настройке экземпляра и аутентификации. В инструкциях приводится информация, которая поможет вам убедиться, что вы успешно выполнили каждый шаг.

После настройки экземпляра запишите имя узла экземпляра. Вы можете найти имя хоста на портале Azure.

Интерфейсы для разработчиков

В этой статье объясняется, как выполнять различные операции управления с помощью .NET (C#) SDK. Вы также можете создавать те же вызовы управления с помощью пакетов SDK для других языков, описанных в статье, посвященной API и пакетам SDK для Azure Digital Twins.

Вот некоторые другие интерфейсы для разработчиков, которые можно использовать для выполнения этих операций:

Визуализация

Azure Digital Twins Explorer — это визуальное средство для изучения данных в графе Azure Digital Twins. С помощью этого обозревателя можно просматривать, запрашивать и изменять модели, двойников и отношения.

Сведения о средстве 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.

Чтобы создать связь, необходимо указать следующее.

  • Исходный идентификатор двойника (srcId в примере кода ниже): исходный идентификатор двойника для связи.
  • Целевой идентификатор двойника (targetId в примере кода ниже): целевой идентификатор двойника для связи.
  • Имя связи (relName в примере кода ниже): универсальный тип связи, подобный contains.
  • Идентификатор связи (relId в примере кода ниже): конкретное имя для этой связи, подобно Relationship1.

Идентификатор связи должен быть уникальным в пределах заданного исходного двойника. Он не обязательно должен быть уникальным глобально. Например, для двойника Foo каждый конкретный идентификатор связи должен быть уникальным. Однако другой двойник Bar может иметь исходящую связь, которая совпадает с идентификатором связи Foo.

В следующем примере кода показано, как создать связь в вашем экземпляре Azure Digital Twins. В нем используется вызов пакета 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 см. в статье об API и пакетах SDK для Azure Digital Twins.

Создание нескольких связей между двойниками

Связи можно классифицировать следующим образом.

  • Исходящие связи: связи, принадлежащие этому двойнику, которые служат для его подключения к другим двойникам. Для получения исходящих связей двойника используется метод GetRelationshipsAsync().
  • Входящие связи: связи, принадлежащие другим двойникам, которые указывают на этот двойник для создания "входящей" связи. Для получения входящих связей двойника используется метод GetIncomingRelationshipsAsync().

Количество связей, которые можно создать между двумя двойниками, не ограничено. Можно создать любое количество связей.

Это означает, что можно одновременно выразить несколько различных типов связей между двумя двойниками. Например, двойник A может иметь как хранимую связь, так и производимую связь с двойником B.

При необходимости можно даже создать несколько экземпляров одного типа связи между одними и теми же двумя двойниками. В этом примере двойник A может иметь две разных хранимых связи с двойником B, если связи имеют разные идентификаторы.

Примечание.

Атрибуты minMultiplicity DTDL и maxMultiplicity связи в настоящее время не поддерживаются в Azure Digital Twins, даже если они определены как часть модели, они не будут применяться службой. Дополнительные сведения см. в заметках DTDL для конкретной службы.

Создание связей в массовом режиме с помощью API импорта заданий

API импорта заданий можно использовать для создания нескольких связей одновременно в одном вызове API. Этот метод требует использования Хранилище BLOB-объектов Azure, а также разрешений на запись в экземпляре Azure Digital Twins для связей и массовых заданий.

Совет

API заданий импорта также позволяет импортировать модели и двойники в одном вызове для создания всех частей графа одновременно. Дополнительные сведения об этом процессе см. в статье "Отправка моделей, двойников и связей" в массовом режиме с помощью API импорта заданий.

Для массового импорта связей необходимо структурировать связи (и любые другие ресурсы, включенные в задание массового импорта) в виде файла NDJSON . Этот Relationships раздел появится после Twins раздела, что делает его последним разделом данных графа в файле. Связи, определенные в файле, могут ссылаться на двойники, определенные в этом файле или уже присутствующих в экземпляре, и при необходимости они могут включать инициализацию любых свойств, имеющих связи.

Вы можете просмотреть пример файла импорта и пример проекта для создания этих файлов в введение в API заданий импорта.

Затем файл необходимо передать в добавочный большой двоичный объект в Хранилище BLOB-объектов Azure. Инструкции по созданию контейнера хранилища Azure см. в статье "Создание контейнера". Затем отправьте файл с помощью предпочтительного метода отправки (некоторые параметры — команда AzCopy, Azure CLI или портал Azure).

После отправки файла NDJSON в контейнер получите ЕГО URL-адрес в контейнере BLOB-объектов . Это значение будет использоваться позже в тексте вызова API массового импорта.

Ниже приведен снимок экрана, показывающий значение URL-адреса файла БОЛЬШОго двоичного объекта в портал Azure:

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

Затем файл можно использовать в вызове API импорта заданий. Вы предоставите URL-адрес хранилища BLOB-объектов входного файла, а также новый URL-адрес хранилища BLOB-объектов, чтобы указать, где должен храниться выходной журнал при создании службы.

Список связей

Список свойств одной связи

Вы всегда можете десериализовать данные связи в выбранный вами тип. Для базового доступа к связи используйте тип 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().

Список входящих связей цифрового двойника

В Azure Digital Twins также выполняется вызов пакета 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.

Примечание.

Этот метод предназначен для обновления свойств связи. Если необходимо изменить исходный двойник или целевой двойник связи, необходимо удалить связь и создать ее повторно с помощью новых двойников.

Для вызова клиента используются следующие обязательные параметры:

  • Идентификатор исходного двойника (двойник, где создана связь).
  • Идентификатор обновляемой связи.
  • Документ Исправление 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);

Удаление отношений

Первый параметр указывает исходного двойника (двойника, от которого исходит связь). Другой параметр — это идентификатор связи. Вам потребуется идентификатор двойника и идентификатор связи, поскольку идентификаторы связей уникальны только в пределах области двойника.

Ниже приведен пример кода, демонстрирующий использование этого метода. В этом примере используется вызов пакета 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, эффективно создавая граф одновременно. Этот метод требует использования Хранилище BLOB-объектов Azure, а также разрешений на запись в экземпляре Azure Digital Twins для элементов графа (моделей, двойников и связей) и массовых заданий.

Чтобы импортировать ресурсы в массовом режиме, сначала создайте файл NDJSON , содержащий сведения о ресурсах. Файл начинается с Header раздела, за которым следует необязательные разделы Models, Twinsа Relationshipsтакже . Вам не нужно включать в файл все три типа данных графа, но все разделы, которые присутствуют, должны соответствовать этому порядку. Двойники, определенные в файле, могут ссылаться на модели, определенные в этом файле или уже присутствующих в экземпляре, и при необходимости они могут включать инициализацию свойств двойника. Связи, определенные в файле, могут ссылаться на двойники, определенные в этом файле или уже присутствующих в экземпляре, и при необходимости они могут включать инициализацию свойств связи.

Вы можете просмотреть пример файла импорта и пример проекта для создания этих файлов в введение в API заданий импорта.

Затем файл необходимо передать в добавочный большой двоичный объект в Хранилище BLOB-объектов Azure. Инструкции по созданию контейнера хранилища Azure см. в статье "Создание контейнера". Затем отправьте файл с помощью предпочтительного метода отправки (некоторые параметры — команда AzCopy, Azure CLI или портал Azure).

После отправки файла NDJSON в контейнер получите ЕГО URL-адрес в контейнере BLOB-объектов . Это значение будет использоваться позже в тексте вызова API массового импорта.

Ниже приведен снимок экрана, показывающий значение URL-адреса файла БОЛЬШОго двоичного объекта в портал Azure:

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

Затем файл можно использовать в вызове API импорта заданий. Вы предоставите URL-адрес хранилища BLOB-объектов входного файла, а также новый URL-адрес хранилища BLOB-объектов, чтобы указать, где должен храниться выходной журнал при создании службы.

Импорт графа с помощью Azure Digital Twins Обозреватель

Azure Digital Twins Обозреватель — это визуальное средство для просмотра и взаимодействия с графом двойников. Он содержит функцию импорта файла графа в формате JSON или Excel, который может содержать несколько моделей, двойников и связей.

Подробные сведения об использовании этой функции см. в статье "Импорт графа" в документации по Azure Digital Twins Обозреватель.

Создание двойников и связей из CSV-файла

Иногда может потребоваться создать иерархии двойников из данных, хранящихся в другой базе данных, или в электронной таблице или CSV-файле. В этом разделе показано, как считать данные из CSV-файла и создать на основе них граф двойника.

Рассмотрим следующую таблицу данных, описывающую набор цифровых двойников и связей. Модели, на которые ссылается этот файл, уже должны существовать в экземпляре Azure Digital Twins.

Model ID Идентификатор двойника (должен быть уникальным) Имя отношения Идентификатор целевого двойника Данные инициализации двойника
dtmi:example:Floor;1 Floor1 содержит Room1
dtmi:example:Floor;1 Этаж0 содержит Room0
dtmi:example:Room;1 Room1 {"Temperature": 80}
dtmi:example:Room;1 Room0 {"Temperature": 70}

Для доставки этих данных в Azure Digital Twins можно преобразовать таблицу в файл CSV. После преобразования таблицы код можно записать, чтобы перевести файл в команды для создания двойников и связей. В следующем примере кода показано чтение данных из CSV-файла и создание графа двойника в Azure Digital Twins.

В приведенном ниже коде CSV-файл называется data.csv, а также имеется заполнитель, представляющий имя узла вашего экземпляра Azure Digital Twins. В примере также используется несколько пакетов, которые можно добавить в проект, чтобы упростить этот процесс.

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.json и Floor.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 Digital Twins.

Настройка проекта

Затем выполните указанные ниже действия, чтобы настроить код своего проекта.

  1. Добавьте скачанные ранее файлы Room.json и Floor.json в проект, а затем замените заполнители <path-to> в коде, чтобы сообщить программе, где их найти.

  2. Замените заполнитель <your-instance-hostname> именем узла своего экземпляра Azure Digital Twins.

  3. Добавьте в проект две зависимости, которые понадобятся для работы с Azure Digital Twins. Первая — это пакет для пакета SDK Azure Digital Twins для .NET, а вторая содержит средства для проверки подлинности в Azure.

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

Кроме того, необходимо настроить локальные учетные данные, если вы хотите выполнить пример напрямую. В следующем разделе приведены пошаговые инструкции для этого процесса.

Настройка локальных учетных данных Azure

В этом примере используется DefaultAzureCredential (часть библиотеки Azure.Identity) для аутентификации пользователей с помощью экземпляра Azure Digital Twins, запускаемого на локальном компьютере. Дополнительные сведения о различных способах аутентификации клиентского приложения в Azure Digital Twins см. в статье о записи кода аутентификации приложения.

При использовании DefaultAzureCredential пример будет искать учетные данные в локальной среде, например имя для входа Azure в локальной версии Azure CLI, в Visual Studio либо Visual Studio Code. Поэтому вам нужно войти в Azure локально с помощью одного из этих механизмов, чтобы настроить учетные данные для примера.

Если вы используете Visual Studio или Visual Studio Code для выполнения примеров кода, убедитесь, что вы вошли в этот редактор с теми же учетными данными Azure, которые вы хотите использовать для доступа к экземпляру Azure Digital Twins. Если вы используете локальное окно CLI, выполните az login команду, чтобы войти в учетную запись Azure. После этого при запуске примера кода необходимо автоматически пройти проверку подлинности.

Запуск примера

После завершения установки можно запустить пример кода проекта.

Ниже приведены выходные данные консоли для вашей программы.

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

Совет

Граф двойника является основой создания связей между двойниками. Чтобы просмотреть визуальное представление графа двойника, обратитесь к разделу Визуализация данной статьи.

Следующие шаги

Дополнительные сведения о запросах к графу двойников Azure Digital Twins