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 illustrates how to deal with models that are not self-contained.
A synchronous 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 asynchronous enumeration of strings.
The single string or each value in the enumeration is JSON text of a DTDL model.
The following model contains an external reference.
string jsonText =
@"{
""@context"": ""dtmi:dtdl:context;3"",
""@id"": ""dtmi:example:anInterface;1"",
""@type"": ""Interface"",
""extends"": ""dtmi:example:anotherInterface;1"",
""contents"": [
{
""@type"": ""Property"",
""name"": ""expectedDistance"",
""schema"": ""double""
}
]
}";
The Interface's "extends" property has value "dtmi:example:anotherInterface;1", which is an identifier that is not defined in the model. The parser is unable to fully validate the model without a definition for this referenced Interface.
The main asynchronous method on the ModelParser
is ParseAsync()
.
One argument is required, which can be either a string or an asynchronous enumeration of strings containing the JSON text to parse as DTDL.
var parseTask = modelParser.ParseAsync(jsonText);
The return value is a Task
, whose completion must be awaited before proceeding.
If the submitted model is invalid or incomplete, an exception will be thrown, wrapped in an AggregateException
by the System.Threading.Tasks
framework.
If the submitted model is invalid, the wrapped exception will be a ParsingException
.
If the submitted model is referentially incomplete, the wrapped exception will be a ResolutionException
.
If no exception is thrown, the model is valid.
try
{
parseTask.Wait();
Console.WriteLine($"DTDL model is valid!");
}
catch (AggregateException ex)
{
if (ex.InnerException is ResolutionException)
{
Console.WriteLine($"DTDL model is referentially incomplete: {ex.InnerException}");
}
else if (ex.InnerException is ParsingException)
{
Console.WriteLine($"DTDL model is invalid: {ex.InnerException}");
}
else
{
throw;
}
}
For the JSON text above, the code snippet above will display:
DTDL model is referentially incomplete: No DtmiResolverAsync provided to resolve requisite reference(s): dtmi:example:anotherInterface;1 (referenced in 1 place)
This exception indicates two things:
First, the submitted model contains a reference to dtmi:example:anotherInterface;1 that requires a definition.
Second, the caller has not registered a DtmiResolverAsync
callback to provide definitions for external references.
The asynchronous ParseAsync()
method expects a DtmiResolverAsync
callback for resolving undefined identifiers to an asynchronous enumeration of JSON text strings.
We will store the JSON text of the referenced model in a dictionary keyed on the identifier.
var otherJsonTexts = new Dictionary<Dtmi, string>();
otherJsonTexts[new Dtmi("dtmi:example:anotherInterface;1")] =
@"{
""@context"": ""dtmi:dtdl:context;3"",
""@id"": ""dtmi:example:anotherInterface;1"",
""@type"": ""Interface"",
""contents"": [
{
""@type"": ""Telemetry"",
""name"": ""currentDistance"",
""schema"": ""double""
}
]
}";
A DtmiResolverAsync
is a delegate that the ModelParser
calls whenever it encounters an external reference to an identifier that requires a definition.
We can write a simple resolver and register it with the parser in the constructor, using a local function to generate an asynchronous enumeration:
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
async IAsyncEnumerable<string> GetJsonTexts(IReadOnlyCollection<Dtmi> dtmis, Dictionary<Dtmi, string> jsonTexts)
{
foreach (Dtmi dtmi in dtmis)
{
if (jsonTexts.TryGetValue(dtmi, out string refJsonText))
{
yield return refJsonText;
}
}
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
modelParser = new ModelParser(new ParsingOptions { DtmiResolverAsync = (IReadOnlyCollection<Dtmi> dtmis, CancellationToken _) =>
{
return GetJsonTexts(dtmis, otherJsonTexts);
}});
With the DtmiResolverAsync
registered, when we call ParseAsync()
again:
parseTask = modelParser.ParseAsync(jsonText);
And then wait on the Task
per the code snippet above, it displays:
DTDL model is valid!
An individual element can be looked up in the object model by its identifier:
IReadOnlyDictionary<Dtmi, DTEntityInfo> objectModel = parseTask.Result;
var anInterface = (DTInterfaceInfo)objectModel[new Dtmi("dtmi:example:anInterface;1")];
var anotherInterface = (DTInterfaceInfo)objectModel[new Dtmi("dtmi:example:anotherInterface;1")];
The .NET property Extends
on .NET class DTInterfaceInfo
is a direct analogue of the DTDL property 'extends' on DTDL type Interface.
Inspecting the Extends
property enables bottom-up traversal of the Interface hierarchy by showing the other Interfaces that each Interface extends, as shown in the following code snippet.
if (anInterface.Extends.Any())
{
Console.WriteLine($"anInterface extends:");
foreach (DTInterfaceInfo extendedInterface in anInterface.Extends)
{
Console.WriteLine($" {extendedInterface.Id}");
}
}
if (anotherInterface.Extends.Any())
{
Console.WriteLine($"anotherInterface extends:");
foreach (DTInterfaceInfo extendedInterface in anotherInterface.Extends)
{
Console.WriteLine($" {extendedInterface.Id}");
}
}
This code snippet displays:
anInterface extends:
dtmi:example:anotherInterface;1
The object model exposed via the ModelParser
attaches properties that are not directly represented in the DTDL language but rather are synthesized from DTDL properties.
Specifically, values of the 'extends' property are also expressed in a reversed form via the synthetic property ExtendedBy
, which enables top-down traversal of the Interface hierarchy by showing the other Interfaces that each Interface is extended by.
This is shown in the following code snippet.
if (anInterface.ExtendedBy.Any())
{
Console.WriteLine($"anInterface is extended by:");
foreach (DTInterfaceInfo extendedInterface in anInterface.ExtendedBy)
{
Console.WriteLine($" {extendedInterface.Id}");
}
}
if (anotherInterface.ExtendedBy.Any())
{
Console.WriteLine($"anotherInterface is extended by:");
foreach (DTInterfaceInfo extendedInterface in anotherInterface.ExtendedBy)
{
Console.WriteLine($" {extendedInterface.Id}");
}
}
This code snippet displays:
anotherInterface is extended by:
dtmi:example:anInterface;1