관계를 사용하여 디지털 트윈의 그래프 관리

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): 관계의 제네릭 형식입니다(예: 포함).
  • 관계 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() 메서드는 트윈의 수신 관계를 얻는 데 사용됩니다.

두 트윈 간에 가질 수 있는 관계의 수에는 제한이 없고 원하는 만큼 트윈 간에 관계를 만들 수 있습니다.

이 사실은 두 트윈 사이의 여러 형식의 관계를 한 번에 표현할 수 있음을 의미합니다. 예를 들어 트윈 A는 트윈 B와 저장됨 관계와 제조됨 관계를 모두 가질 수 있습니다.

원하는 경우 동일한 두 트윈 간에 같은 형식의 관계 인스턴스를 여러 개 만들 수도 있습니다. 이 예제에서 트윈 A는 트윈 B와 관계 ID가 서로 다른 두 개의 저장됨 관계를 가질 수 있습니다.

참고 항목

관계에 대한 minMultiplicitymaxMultiplicity의 DTDL 특성은 현재 Azure Digital Twins에서 지원되지 않습니다. 모델의 일부로 정의되더라도 서비스에서 적용되지 않습니다. 자세한 내용은 서비스별 DTDL 참고 사항을 참조하세요.

작업 가져오기 API를 사용하여 대량으로 관계 만들기

작업 가져오기 API를 사용하면 단일 API 호출로 한 번에 많은 관계를 만들 수 있습니다. 이 방법을 사용하려면 관계 및 대량 작업을 위해 Azure Digital Twins 인스턴스에서 Azure Blob Storage쓰기 권한을 사용해야 합니다.

가져오기 작업 API를 사용하면 모델과 트윈을 동일한 호출로 가져와 한 번에 그래프의 모든 파트를 만들 수 있습니다. 이 프로세스에 대한 자세한 내용은 작업 가져오기 API를 사용하여 모델, 트윈 및 관계 대량 업로드를 참조하세요.

관계를 대량으로 가져오려면 관계(및 대량 가져오기 작업에 포함된 기타 리소스)를 NDJSON 파일로 구성해야 합니다. Relationships 섹션은 Twins 섹션 뒤에 오므로 파일의 마지막 그래프 데이터 섹션이 됩니다. 파일에 정의된 관계는 이 파일에 정의되어 있거나 인스턴스에 이미 있는 트윈을 참조할 수 있으며 선택적으로 관계에 있는 모든 속성의 초기화를 포함할 수 있습니다.

가져오기 작업 API 소개에서 가져오기 파일 예와 이러한 파일을 만들기 위한 프로젝트 샘플을 볼 수 있습니다.

다음으로 파일을 Azure Blob Storage의 추가 Blob에 업로드해야 합니다. Azure Storage 컨테이너를 만드는 방법에 대한 지침은 컨테이너 만들기를 참조하세요. 그런 다음 기본 설정하는 업로드 방법(일부 옵션은 AzCopy 명령, Azure CLI 또는 Azure Portal)을 사용하여 파일을 업로드합니다.

NDJSON 파일이 컨테이너에 업로드되면 Blob 컨테이너 내에서 URL을 가져옵니다. 대량 가져오기 API 호출의 본문에서 나중에 이 값을 사용합니다.

다음은 Azure Portal에 있는 Blob 파일의 URL 값을 보여 주는 스크린샷입니다.

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

그런 다음 해당 파일을 Import Jobs API 호출에 사용할 수 있습니다. 입력 파일의 Blob Storage URL과 서비스에서 만들어질 때 출력 로그를 저장할 위치를 나타내는 새 Blob Storage 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 Digital Twins에는 주어진 트윈의 모든 수신 관계를 찾는 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 Digital Twins 인스턴스에서 Azure Blob Storage쓰기 권한을 사용해야 합니다.

리소스를 대량으로 가져오려면 먼저 리소스 세부 정보가 포함된 NDJSON 파일을 만듭니다. 파일은 Header 섹션으로 시작하고 그 뒤에 선택적 섹션 Models, TwinsRelationships가 옵니다. 파일에 세 가지 형식의 그래프 데이터를 모두 포함할 필요는 없지만 존재하는 모든 섹션은 해당 순서를 따라야 합니다. 파일에 정의된 트윈은 이 파일에 정의되었거나 인스턴스에 이미 있는 모델을 참조할 수 있으며 선택적으로 트윈 속성의 초기화를 포함할 수 있습니다. 파일에 정의된 관계는 이 파일에 정의되어 있거나 인스턴스에 이미 있는 트윈을 참조할 수 있으며 선택적으로 관계 속성의 초기화를 포함할 수 있습니다.

가져오기 작업 API 소개에서 가져오기 파일 예와 이러한 파일을 만들기 위한 프로젝트 샘플을 볼 수 있습니다.

다음으로 파일을 Azure Blob Storage의 추가 Blob에 업로드해야 합니다. Azure Storage 컨테이너를 만드는 방법에 대한 지침은 컨테이너 만들기를 참조하세요. 그런 다음 기본 설정하는 업로드 방법(일부 옵션은 AzCopy 명령, Azure CLI 또는 Azure Portal)을 사용하여 파일을 업로드합니다.

NDJSON 파일이 컨테이너에 업로드되면 Blob 컨테이너 내에서 URL을 가져옵니다. 대량 가져오기 API 호출의 본문에서 나중에 이 값을 사용합니다.

다음은 Azure Portal에 있는 Blob 파일의 URL 값을 보여 주는 스크린샷입니다.

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

그런 다음 해당 파일을 Import Jobs API 호출에 사용할 수 있습니다. 입력 파일의 Blob Storage URL과 서비스에서 만들어질 때 출력 로그를 저장할 위치를 나타내는 새 Blob Storage URL을 제공합니다.

Azure Digital Twins Explorer를 사용하여 그래프 가져오기

Azure Digital Twins Explorer는 트윈 그래프를 보고 상호 작용하기 위한 시각적 도구입니다. 여기에는 여러 모델, 트윈 및 관계를 포함할 수 있는 JSON 또는 Excel 형식의 그래프 파일을 가져오는 기능이 포함되어 있습니다.

이 기능 사용에 대한 자세한 내용은 Azure Digital Twins Explorer 설명서의 그래프 가져오기를 참조하세요.

CSV 파일에서 트윈 및 관계 만들기

경우에 따라 다른 데이터베이스나 스프레드시트 또는 CSV 파일에 저장된 데이터에서 트윈 계층 구조를 만들어야 할 수도 있습니다. 이 섹션에서는 CSV 파일에서 데이터를 읽어 트윈 그래프를 만드는 방법을 보여 줍니다.

디지털 트윈 및 관계 집합을 설명하는 다음 데이터 테이블을 고려합니다. 이 파일에서 참조하는 모델은 Azure Digital Twins 인스턴스에 이미 존재해야 합니다.

Model 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로 가져오는 한 가지 방법은 테이블을 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이라는 두 가지 샘플 모델 정의를 사용합니다. 코드에서 사용할 수 있도록 모델 파일을 다운로드하려면 다음 링크를 사용하여 GitHub의 파일로 직접 이동합니다. 그런 다음, 화면의 아무 곳이나 마우스 오른쪽 단추로 클릭하고 브라우저의 오른쪽 클릭 메뉴에서 다른 이름으로 저장을 선택한 다음, 다른 이름으로 저장 창을 사용하여 파일을 Room.jsonFloor.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.jsonFloor.json 파일을 프로젝트에 추가하고 코드의 <path-to> 자리 표시자를 대체하여 프로그램에서 찾을 위치를 알려줍니다.

  2. <your-instance-hostname> 자리 표시자를 Azure Digital Twins 인스턴스의 호스트 이름으로 바꿉니다.

  3. Azure Digital Twins와 함께 작동하는 데 필요한 두 개의 종속성을 프로젝트에 추가합니다. 첫 번째는 .NET용 Azure Digital Twins SDK의 패키지이고, 두 번째는 Azure에 대한 인증을 지원하는 도구를 제공합니다.

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

또한 샘플을 직접 실행하려면 로컬 자격 증명을 설정해야 합니다. 다음 섹션에서는 이 과정을 안내합니다.

로컬 Azure 자격 증명 설정

이 샘플은 로컬 컴퓨터에서 실행할 때 DefaultAzureCredential(Azure.Identity 라이브러리의 일부)을 사용하여 Azure Digital Twins 인스턴스에서 사용자를 인증합니다. 클라이언트 앱에서 Azure Digital Twins를 사용하여 인증하는 여러 방법에 대한 자세한 내용은 앱 인증 코드 작성을 참조하세요.

DefaultAzureCredential을 사용하면 샘플에서 로컬 Azure CLI 또는 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 트윈 그래프를 쿼리하는 방법을 살펴봅니다.