Skip to content

Latest commit

 

History

History
296 lines (253 loc) · 13.3 KB

Tutorial05_InspectObjectModelAdvanced.md

File metadata and controls

296 lines (253 loc) · 13.3 KB

Inspect object model using advanced technques

The ModelParser class is used to determine whether one or more DTDL models are valid, to identify specific modeling errors, and to enable inspection of model contents. This tutorial walks through the last of these uses: how to access elements and properties in the object model. This is an advanced addendum to the Inspect object model tutorial. An asynchronous version of this tutorial is also available.

Create a ModelParser

To parse a DTDL model, you need to instantiate a ModelParser. No arguments are required.

var modelParser = new ModelParser();

Obtain the JSON text of a DTDL model

The DTDL language is syntactically JSON. The ModelParser expects a single string or an enumeration of strings. The single string or each value in the enumeration is JSON text of a DTDL model.

string jsonText =
@"{
  ""@context"": ""dtmi:dtdl:context;3"",
  ""@id"": ""dtmi:example:anInterface;1"",
  ""@type"": ""Interface"",
  ""contents"": [
    {
      ""@type"": ""Property"",
      ""name"": ""expectedDistance"",
      ""schema"": ""double""
    },
    {
      ""@type"": ""Telemetry"",
      ""name"": ""currentDistance"",
      ""schema"": ""double""
    },
    {
      ""@type"": ""Command"",
      ""name"": ""setDistance"",
      ""request"": {
        ""name"": ""desiredDistance"",
        ""schema"": ""double""
      },
      ""response"": {
        ""name"": ""reportedDistance"",
        ""schema"": ""double""
      }
    }
  ]
}";

Submit the JSON text to the ModelParser

The main synchronous method on the ModelParser is Parse(). One argument is required, which can be either a string or an enumeration of strings containing the JSON text to parse as DTDL. If the submitted model is complete and valid, no exception will be thrown. Proper code should catch and process exceptions as shown in other tutorials such as this one, but for simplicity the present tutorial omits exception handling.

IReadOnlyDictionary<Dtmi, DTEntityInfo> objectModel = modelParser.Parse(jsonText);

Display elements in object model

The object model is a collection of objects in a class hierarchy rooted at DTEntityInfo. All DTDL elements derive from the DTDL abstract type Entity, and each DTDL type has a corresponding C# class whose name has a prefix of "DT" (for Digital Twins) and a suffix of "Info". The elements in the object model are indexed by their identifiers, which have type Dtmi. The following snippet displays the identifiers of all elements in the object model:

Console.WriteLine($"{objectModel.Count} elements in model:");
foreach (KeyValuePair<Dtmi, DTEntityInfo> modelElement in objectModel)
{
    Console.WriteLine(modelElement.Key);
}

For the JSON text above, this snippet displays:

7 elements in model:
dtmi:example:anInterface:_contents:__expectedDistance;1
dtmi:example:anInterface:_contents:__currentDistance;1
dtmi:example:anInterface:_contents:__setDistance:_request;1
dtmi:example:anInterface:_contents:__setDistance:_response;1
dtmi:example:anInterface:_contents:__setDistance;1
dtmi:example:anInterface;1
dtmi:dtdl:instance:Schema:double;2

Of these seven identifiers, only dtmi:example:anInterface;1 is present in the DTDL source model. The identifiers for the contents named "expectedDistance", "currentDistance", and "setDistance" are auto-generated by the ModelParser, as are identifiers for the "request" and "response" properties of the Command; these identifiers are generated via rules that guarantee their uniqueness. The identifier dtmi:dtdl:instance:Schema:double;2 represents an element in the DTDL language model for the schema 'double', as can be seen by using the ModelParser.GetTermOrUri() static method:

Console.WriteLine(ModelParser.GetTermOrUri(new Dtmi("dtmi:dtdl:instance:Schema:double;2")));

This snippet displays:

double

Drill down on one element and inspect via synthetic properties

An individual element can be looked up in the object model by its identifier:

var anInterfaceId = new Dtmi("dtmi:example:anInterface;1");
var anInterface = (DTInterfaceInfo)objectModel[anInterfaceId];

The .NET property Contents on .NET class DTInterfaceInfo is a direct analogue of the DTDL property 'contents' on DTDL type Interface. The object model exposed via the ModelParser also attaches properties that are not directly represented in the DTDL language but rather are synthesized from DTDL properties. Specifically, values of the 'contents' property are broken out into separate .NET properties according to their subtypes, as shown by the following code snippet.

foreach (KeyValuePair<string, DTPropertyInfo> propertyElement in anInterface.Properties)
{
    Console.WriteLine($"Property '{propertyElement.Value.Name}'");
    Console.WriteLine($"  schema: {propertyElement.Value.Schema?.Id?.ToString() ?? "(none)"}");
    Console.WriteLine($"  writable: {(propertyElement.Value.Writable ? "true" : "false")}");
}

foreach (KeyValuePair<string, DTTelemetryInfo> telemetryElement in anInterface.Telemetries)
{
    Console.WriteLine($"Telemetry '{telemetryElement.Value.Name}'");
    Console.WriteLine($"  schema: {telemetryElement.Value.Schema?.Id?.ToString() ?? "(none)"}");
}

foreach (KeyValuePair<string, DTCommandInfo> commandElement in anInterface.Commands)
{
    Console.WriteLine($"Command '{commandElement.Value.Name}'");
    Console.WriteLine($"  request schema: {commandElement.Value.Request?.Schema?.Id?.ToString() ?? "(none)"}");
    Console.WriteLine($"  response schema: {commandElement.Value.Response?.Schema?.Id?.ToString() ?? "(none)"}");
}

foreach (KeyValuePair<string, DTRelationshipInfo> relationshipElement in anInterface.Relationships)
{
    Console.WriteLine($"Relationship '{relationshipElement.Value.Name}'");
    Console.WriteLine($"  target: {relationshipElement.Value.Target?.ToString() ?? "(none)"}");
    Console.WriteLine($"  writable: {(relationshipElement.Value.Writable ? "true" : "false")}");
}

foreach (KeyValuePair<string, DTComponentInfo> componentElement in anInterface.Components)
{
    Console.WriteLine($"Component '{componentElement.Value.Name}'");
    Console.WriteLine($"  schema: {componentElement.Value.Schema.Id}");
}

For the JSON text above, this snippet displays:

Property 'expectedDistance'
  schema: dtmi:dtdl:instance:Schema:double;2
  writable: false
Telemetry 'currentDistance'
  schema: dtmi:dtdl:instance:Schema:double;2
Command 'setDistance'
  request schema: dtmi:dtdl:instance:Schema:double;2
  response schema: dtmi:dtdl:instance:Schema:double;2

Alternative approach 1 -- inspect property values by subtype

Using synthetic properties, as described above, is the recommended approach for inspecting property values because the types of the elements are returned in concrete form. An alternative approach is to inspect the Contents property directly, which may in some cases be beneficial since it maps to the DTDL representation of the model. The disadvantages of this approach are that (a) it requires switching on subtype and (b) it requires casting to obtain sub-properties of the elements.

The DTDL type of each element is expressed via the property EntityKind on the DTEntityInfo base class, which has type enum DTEntityKind. This can be used to specialize accesses for particular subtypes of DTDL Entities. For example, there are five subtypes of Content, so the following snippet will display the values of appropriate properties for each DTContentInfo subclass:

foreach (KeyValuePair<string, DTContentInfo> contentElement in anInterface.Contents)
{
    switch (contentElement.Value.EntityKind)
    {
        case DTEntityKind.Property:
            var propertyElement = (DTPropertyInfo)contentElement.Value;
            Console.WriteLine($"Property '{propertyElement.Name}'");
            Console.WriteLine($"  schema: {propertyElement.Schema?.Id?.ToString() ?? "(none)"}");
            Console.WriteLine($"  writable: {(propertyElement.Writable ? "true" : "false")}");
            break;
        case DTEntityKind.Telemetry:
            var telemetryElement = (DTTelemetryInfo)contentElement.Value;
            Console.WriteLine($"Telemetry '{telemetryElement.Name}'");
            Console.WriteLine($"  schema: {telemetryElement.Schema?.Id?.ToString() ?? "(none)"}");
            break;
        case DTEntityKind.Command:
            var commandElement = (DTCommandInfo)contentElement.Value;
            Console.WriteLine($"Command '{commandElement.Name}'");
            Console.WriteLine($"  request schema: {commandElement.Request?.Schema?.Id?.ToString() ?? "(none)"}");
            Console.WriteLine($"  response schema: {commandElement.Response?.Schema?.Id?.ToString() ?? "(none)"}");
            break;
        case DTEntityKind.Relationship:
            var relationshipElement = (DTRelationshipInfo)contentElement.Value;
            Console.WriteLine($"Relationship '{relationshipElement.Name}'");
            Console.WriteLine($"  target: {relationshipElement.Target?.ToString() ?? "(none)"}");
            Console.WriteLine($"  writable: {(relationshipElement.Writable ? "true" : "false")}");
            break;
        case DTEntityKind.Component:
            var componentElement = (DTComponentInfo)contentElement.Value;
            Console.WriteLine($"Component '{componentElement.Name}'");
            Console.WriteLine($"  schema: {componentElement.Schema.Id}");
            break;
    }
}

For the JSON text above, this snippet displays:

Property 'expectedDistance'
  schema: dtmi:dtdl:instance:Schema:double;2
  writable: false
Telemetry 'currentDistance'
  schema: dtmi:dtdl:instance:Schema:double;2
Command 'setDistance'
  request schema: dtmi:dtdl:instance:Schema:double;2
  response schema: dtmi:dtdl:instance:Schema:double;2

Alternative approach 2 -- use reflection to inspect property values of elements

Another alternative approach is to access properties via the System.Reflection framework. In some situations, this may be valuable since the code below is agnostic to the subtype of DTEntityInfo being inspected.

The following snippet scans through all elements in the object model, finds all property values that are subclasses of DTEntityInfo, and displays the identifier of each referenced element:

foreach (KeyValuePair<Dtmi, DTEntityInfo> modelElement in objectModel)
{
    Console.WriteLine($"{modelElement.Key} refers to:");

    TypeInfo typeInfo = modelElement.Value.GetType().GetTypeInfo();
    foreach (MemberInfo memberInfo in typeInfo.DeclaredMembers)
    {
        if (memberInfo is PropertyInfo propertyInfo)
        {
            object propertyValue = propertyInfo.GetValue(modelElement.Value);
            if (propertyValue is DTEntityInfo refSingle)
            {
                Console.WriteLine($"  {refSingle.Id} via member {memberInfo.Name}");
            }
            else if (propertyValue is IList refList)
            {
                foreach (object refObj in refList)
                {
                    if (refObj is DTEntityInfo refElement)
                    {
                        Console.WriteLine($"  {refElement.Id} via member {memberInfo.Name}");
                    }
                }
            }
            else if (propertyValue is IDictionary refDict)
            {
                foreach (object refObj in refDict.Values)
                {
                    if (refObj is DTEntityInfo refElement)
                    {
                        Console.WriteLine($"  {refElement.Id} via member {memberInfo.Name}");
                    }
                }
            }
        }
    }
}

For the JSON text above, this snippet displays:

dtmi:example:anInterface:_contents:__expectedDistance;1 refers to:
  dtmi:dtdl:instance:Schema:double;2 via member Schema
dtmi:example:anInterface:_contents:__currentDistance;1 refers to:
  dtmi:dtdl:instance:Schema:double;2 via member Schema
dtmi:example:anInterface:_contents:__setDistance:_request;1 refers to:
dtmi:example:anInterface:_contents:__setDistance:_response;1 refers to:
dtmi:example:anInterface:_contents:__setDistance;1 refers to:
  dtmi:example:anInterface:_contents:__setDistance:_request;1 via member Request
  dtmi:example:anInterface:_contents:__setDistance:_response;1 via member Response
dtmi:example:anInterface;1 refers to:
  dtmi:example:anInterface:_contents:__setDistance;1 via member Commands
  dtmi:example:anInterface:_contents:__expectedDistance;1 via member Contents
  dtmi:example:anInterface:_contents:__currentDistance;1 via member Contents
  dtmi:example:anInterface:_contents:__setDistance;1 via member Contents
  dtmi:example:anInterface:_contents:__expectedDistance;1 via member Properties
  dtmi:example:anInterface:_contents:__currentDistance;1 via member Telemetries
dtmi:dtdl:instance:Schema:double;2 refers to: