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.
To parse a DTDL model, you need to instantiate a ModelParser
.
No arguments are required.
var modelParser = new ModelParser();
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""
}
}
]
}";
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);
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
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
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
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: