リレーションシップを使ってデジタル ツインのグラフを管理する

Azure Digital Twins の中核は、環境全体を表すツイン グラフです。 ツイン グラフは、リレーションシップを介して接続された個々のデジタル ツインで構成されています。 この記事では、リレーションシップとグラフ全体の管理に焦点を当てます。個々のデジタル ツインの操作については、「デジタル ツインを管理する」を参照してください。

機能する Azure Digital Twins インスタンスがあり、クライアント アプリで認証コードを設定すると、Azure Digital Twins インスタンス内のデジタル ツインとそのリレーションシップを作成、変更、削除することができるようになります。

前提条件

この記事で Azure Digital Twins を操作するには、まず Azure Digital Twins インスタンスとそれを使用するために必要なアクセス許可が必要です。 Azure Digital Twins インスタンスが既に設定されている場合は、そのインスタンスを使用し、次のセクションに進むことができます。 それ以外の場合は、「インスタンスと認証を設定する」の手順に従います。 手順には、各ステップを正常に完了したことを確認するために役立つ情報が含まれています。

インスタンスの設定後、インスタンスのホスト名を書き留めておきます。 ホスト名は Azure portal で確認できます。

開発者インターフェイス

この記事では、.NET (C#) SDK を使用して、さまざまな管理操作を実行する方法について説明します。 また、「Azure Digital Twins API および SDK」で説明されているその他の言語の SDK を使用して、これらと同じ管理呼び出しを作成することもできます。

これらの操作を完了するために使用できるその他の開発者インターフェイスは次のとおりです。

ビジュアル化

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 モデルの一部として定義されています。 リレーションシップのインスタンスは、DTDL 定義に従うツインとリレーションシップの詳細とともに CreateOrReplaceRelationshipAsync() SDK 呼び出しを使用することで作成できます。

リレーションシップを作成するには、以下を指定する必要があります。

  • ソース ツイン ID (srcId 以下のコード サンプル内): リレーションシップが発生するツインの ID。
  • ターゲット ツイン ID (targetId 以下のコード サンプル内): リレーションシップが到着するツインの ID。
  • リレーションシップの名前 (以下のコード サンプルの relName): ジェネリック型のリレーションシップ (contains など)。
  • リレーションシップ ID (以下のコード サンプルの relId): このリレーションシップの具体的な名前 (Relationship1 など)。

リレーションシップ ID は、指定されたソース ツイン内で一意である必要があります。 グローバルに一意である必要はありません。 たとえば、ツイン Foo の場合、それぞれのリレーションシップ ID は一意である必要があります。 ただし、別のツインである Bar は、Foo リレーションシップの同じ ID と一致する発信リレーションシップを持つことができます。

次のコード サンプルは、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}");
    }

}

このカスタム関数を呼び出して、次の方法で包含関係を作成できます。

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

複数のリレーションシップを作成する場合は、同じメソッドの呼び出しを繰り返して、異なるリレーションシップの種類を引数に渡すことができます。

ヘルパー クラス BasicRelationship の詳細については、Azure Digital Twins API および SDK に関するページを参照してください。

ツイン間でリレーションシップの作成

リレーションシップは次のいずれかに分類できます。

  • 発信リレーションシップ:他のツインに接続するために外側を指す、このツインに属するリレーションシップ。 GetRelationshipsAsync() メソッドは、ツインの発信リレーションシップを取得するために使用されます。
  • 受信リレーションシップ:"受信" リンクを作成するためにこのツインを指す、他のツインに属するリレーションシップ。 GetIncomingRelationshipsAsync() メソッドは、ツインの受信リレーションシップを取得するために使用されます。

2 つのツイン間のリレーションシップの数には制限がありません。ツイン間には、必要な数のリレーションシップを含めることができます。

つまり、2 つのツイン間に、さまざまな種類のリレーションシップを同時に複数表すことができることを意味します。 たとえば、ツイン A とツイン B の間に、"格納された" リレーションシップと "製造された" リレーションシップを含めることができます。

必要に応じて、同じ 2 つのツイン間に、同じ種類のリレーションシップのインスタンスを複数作成することもできます。 この例では、リレーションシップのリレーションシップ ID が異なる限り、ツイン A はツイン B と 2 つの異なる "格納された" リレーションシップを持つことができます。

Note

リレーションシップの minMultiplicitymaxMultiplicity の DTDL 属性は現在、Azure Digital Twins ではサポートされていません。モデルの一部として定義されていても、これらはサービスによって適用されません。 詳細については、「サービス固有の DTDL に関する注意事項」を参照してください。

Import Jobs API を使用してリレーションシップを一括で作成する

Import Jobs API を使用すると、1 回の API 呼び出しで多数のリレーションシップを一度に作成できます。 この方法では、Azure Blob Storage と、リレーションシップと一括ジョブに対する Azure Digital Twins インスタンスでの書き込みアクセス許可を使用する必要があります。

ヒント

Import Jobs API では、モデルとツインを同じ呼び出しでインポートして、グラフのすべての部分を一度に作成することもできます。 このプロセスの詳細については、「Import Jobs API を使用してモデル、ツイン、リレーションシップを一括でアップロードする」を参照してください

リレーションシップを一括インポートするには、リレーションシップ (および一括インポート ジョブに含まれるその他のリソース) を NDJSON ファイルとして構成する必要があります。 セクションは Relationships セクションの後に Twins 表示され、ファイル内の最後のグラフ データ セクションになります。 ファイルで定義されているリレーションシップは、このファイルで定義されているか、インスタンスに既に存在するツインを参照でき、必要に応じて、リレーションシップに含まれるすべてのプロパティの初期化を含めることができます。

インポート ファイルの例と、これらのファイルを作成するためのサンプル プロジェクトについては、インポート ジョブ API の 概要を参照してください。

次に、Azure Blob Storage 内の追加 BLOB にファイルをアップロードする必要があります。 Azure ストレージ コンテナーを作成する方法の手順については、「コンテナーを作成する」を参照してください。 次に、ご希望のアップロード方法を使用してファイルをアップロードします (AzCopy コマンドAzure CLIAzure portal などのオプションがあります)。

NDJSON ファイルがコンテナーにアップロードされたら、BLOB コンテナー内で URL を取得します。 この値は、後で一括インポート API 呼び出しの本文で使用します。

Azure portal 内の BLOB ファイルの URL 値のスクリーンショットを次に示します。

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

その後、インポート ジョブ API 呼び出しでファイルを使用できます。 入力ファイルの BLOB ストレージ URL と、サービスで出力ログが作成される際の保存場所を示す新しい BLOB ストレージ URL を指定します。

リレーションシップ一覧を取得

1 つのリレーションシップのプロパティを一覧表示する

リレーションシップ データはいつでも任意の型に逆シリアル化できます。 リレーションシップへの基本的なアクセスには、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 Digital Twins には、特定のツインとのすべての受信リレーションシップを検索するための API もあります。 この SDK は、逆方向のナビゲーションの場合やツインを削除するときに便利です。

Note

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 メソッドを使用して更新します。

Note

このメソッドは、リレーションシップのプロパティを更新するためのものです。 リレーションシップのソース ツインまたはターゲット ツインを変更する必要がある場合は、リレーションシップを削除し、新しいツインを使用して再作成する必要があります。

クライアント呼び出しに必要なパラメーターは次のとおりです。

  • ソース ツインの 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}");

Note

インスタンス内のすべてのモデル、ツイン、リレーションシップを一度に削除する場合は、Delete Jobs API使用します。

一度に複数のグラフ要素を作成する

このセクションでは、個々の API 呼び出しを使用してモデル、ツイン、リレーションシップをアップロードして 1 つずつアップロードするのではなく、複数の要素を同時に含むグラフを作成する方法について説明します。

Import Jobs API を使用してモデル、ツイン、リレーションシップを一括アップロードする

Import Jobs API を使用すると、1 回の API 呼び出しで複数のモデル、ツイン、リレーションシップをインスタンスにアップロードし、グラフを効果的に一度に作成できます。 この方法では、Azure Blob Storage を使用する必要があります。また、グラフ要素 (モデル、ツイン、リレーションシップ) と一括ジョブに対する Azure Digital Twins インスタンスでの書き込みアクセス許可も必要です。

リソースを一括インポートするには、まず、リソースの詳細を 含む NDJSON ファイルを作成します。 ファイルはセクションで Header 始まり、その後に省略可能な Modelsセクション、 Twinsおよび Relationships. 3 種類のグラフ データをすべてファイルに含める必要はありませんが、存在するセクションはその順序に従う必要があります。 ファイルで定義されているツインは、このファイルで定義されているか、インスタンスに既に存在するモデルを参照でき、必要に応じてツインのプロパティの初期化を含めることができます。 ファイルで定義されているリレーションシップは、このファイルで定義されているか、インスタンスに既に存在するツインを参照でき、必要に応じてリレーションシップ プロパティの初期化を含めることができます。

インポート ファイルの例と、これらのファイルを作成するためのサンプル プロジェクトについては、インポート ジョブ API の 概要を参照してください。

次に、Azure Blob Storage 内の追加 BLOB にファイルをアップロードする必要があります。 Azure ストレージ コンテナーを作成する方法の手順については、「コンテナーを作成する」を参照してください。 次に、ご希望のアップロード方法を使用してファイルをアップロードします (AzCopy コマンドAzure CLIAzure portal などのオプションがあります)。

NDJSON ファイルがコンテナーにアップロードされたら、BLOB コンテナー内で URL を取得します。 この値は、後で一括インポート API 呼び出しの本文で使用します。

Azure portal 内の BLOB ファイルの URL 値のスクリーンショットを次に示します。

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

その後、インポート ジョブ API 呼び出しでファイルを使用できます。 入力ファイルの BLOB ストレージ URL と、サービスで出力ログが作成される際の保存場所を示す新しい BLOB ストレージ URL を指定します。

Azure Digital Twins エクスプローラーを使用してグラフをインポートする

Azure Digital Twins エクスプローラー は、ツイン グラフを表示して操作するためのビジュアル ツールです。 これには、複数のモデル、ツイン、リレーションシップを含むことができる JSON 形式または Excel 形式でグラフ ファイルをインポートするための機能が含まれています。

この機能の使用方法の詳細については、Azure Digital Twins エクスプローラー ドキュメントのグラフのインポートを参照してください。

CSV ファイルからツインとリレーションシップを作成する

場合によっては、別のデータベースまたはスプレッドシートまたは CSV ファイルに格納されているデータからツイン階層を作成することが必要になる場合があります。 このセクションでは、CSV ファイルからデータを読み取り、そこからツイン グラフを作成する方法について説明します。

デジタル ツインのセットとリレーションシップが説明されている次のデータ テーブルがあるとします。 このファイルで参照されるモデルは、Azure Digital Twins インスタンスに既に存在している必要があります。

モデル ID ツイン ID (一意である必要があります) リレーションシップ名 ターゲット ツイン ID ツインの init データ
dtmi:example:Floor;1 Floor1 contains Room1
dtmi:example:Floor;1 Floor0 contains Room0
dtmi:example:Room;1 Room1 {"Temperature": 80}
dtmi:example:Room;1 Room0 {"Temperature": 70}

このデータを Azure Digital Twins に取り込む 1 つの方法は、テーブルを 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.jsonFloor.json という 2 つのサンプル モデル定義を使用します。 コードで使用するモデル ファイルをダウンロードするには、これらのリンクを使用して 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>
    }
}

Note

現在 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 を操作するために必要な 2 つの依存関係をプロジェクトに追加します。 1 つ目は .NET 用 Azure Digital Twins SDK 用のパッケージであり、2 つ目では Azure に対する認証に役立つツールが提供されます。

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

サンプルを直接実行する場合は、ローカルの資格情報も設定する必要があります。 次のセクションでは、このプロセスについて説明します。

ローカルの Azure 資格情報を設定する

このサンプルでは、ローカル コンピューターで実行された Azure Digital Twins インスタンスに対し、(Azure.Identity ライブラリの) DefaultAzureCredential を使用してユーザーを認証します。 Azure Digital Twins に対してクライアント アプリの認証を行う各種の方法について詳しくは、「アプリ認証コードを作成する」を参照してください。

このサンプルでは、DefaultAzureCredential を使用して、ローカル環境から資格情報が検索されます。たとえば、ローカルの DefaultAzureCredential や Visual Studio または Visual Studio Code での Azure サインインなどです。 このため、サンプルの資格情報を設定するために、これらのメカニズムのいずれかを使用して、Azure にローカルでサインインする必要があります。

Visual Studio または Visual Studio Code を使用してコード サンプルを実行する場合は、Azure Digital Twins インスタンスへのアクセスに使用する Azure 資格情報でそのエディターにサインインしていることを確認してください。 ローカル CLI ウィンドウを使用している場合は、az login コマンドを実行して Azure アカウントにサインインします。 その後、コード サンプルを実行すれば、自動的に認証処理が行われます。

サンプルの実行

セットアップが完了したので、これでサンプル コード プロジェクトを実行できます。

このプログラムのコンソール出力は次のようになります。

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

ヒント

ツイン グラフは、ツイン間にリレーションシップを作成する概念です。 ツイン グラフの視覚的表現を表示する場合は、この記事の "視覚化" に関するセクションを参照してください。

次のステップ

Azure Digital Twins ツイン グラフのクエリについて確認します。