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 and that have modeling errors.
A synchronous version of this tutorial is also available.
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"": ""currentDistance"",
""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.
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""
}
]
}";As described and illustrated in the Resolve external references asynchronously tutorial, 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 an instance of ModelParser, 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
var modelParser = new ModelParser(
new ParsingOptions
{
DtmiResolverAsync = (IReadOnlyCollection<Dtmi> dtmis, CancellationToken _) =>
{
return GetJsonTexts(dtmis, otherJsonTexts);
}
});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.
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 pe)
{
Console.WriteLine("DTDL model is invalid:");
foreach (ParsingError err in pe.Errors)
{
Console.WriteLine(err);
}
}
else
{
throw;
}
}The ParsingException has a property named Errors that is a collection of ParsingError objects, each of which provides details about one error in the submitted model.
The ParsingError class has several properties that can be programmatically inspected to obtain details of the error.
This class also overrides the ToString() method to provide a human-readable description of the error.
For the JSON text above, the code snippet above will display a single error:
DTDL model is invalid:
dtmi:example:anInterface;1, because it transitively 'extends' dtmi:example:anotherInterface;1, has property 'contents' that contains more than one element for which property 'name' has value 'currentDistance'. Either change the value of property 'name' to a value that is unique across all values of 'contents', or remove one or more 'extends' properties so that 'contents' will not be imported.This exception indicates two things:
- In the Interface dtmi:example:anInterface;1, property "contents" contains duplicate uses of name "currentDistance", which is invalid.
- This duplication is a result of the fact that dtmi:example:anInterface;1 extends dtmi:example:anotherInterface;1.
Looking at the JSON texts above, we see that elements dtmi:example:anInterface;1 and dtmi:example:anotherInterface;1 both have "contents" values with name "currentDistance". Since dtmi:example:anInterface;1 extends dtmi:example:anotherInterface;1, the contents of the latter are imported into the former, so there is a duplication among the contents values of dtmi:example:anInterface;1.
The error message above is quite specific, but it required some effort to track down the locations of the duplicate values, and this effort would have been greater if a larger number of models had been parsed and/or resolved.
Without support from the calling code, the ModelParser can only report an error's location with reference to a DTDL element with an "@id" property, which may not even be the element immediately containing the error, since this element might not have an "@id" property.
In many cases, the ModelParser can identify which of the submitted JSON text strings contains the error, as well as the line number or range within the text string.
As described in the Pinpoint errors in an invalid DTDL model asynchronously tutorial, the calling code can provide a callback delegate that converts this internal location into a location that is understandable by the user, which can greatly improve the clarity of the error description.
An example of such a delegate is the following:
DtdlParseLocator parseLocator = (
int parseIndex,
int parseLine,
out string sourceName,
out int sourceLine) =>
{
sourceName = "string variable 'jsonText'";
sourceLine = parseLine;
return true;
};When the ModelParser encounters an error in JSON text submitted to ParseAsync(), it calls the delegate with a parseIndex indicating the index of a submitted text string and a parseLine indicating a line number within this string.
If the delegate is able to convert these values into a user-meaningful location, it should set the out-parameter sourceName to the name of the appropriate source file, URL, etc.; set the out-parameter sourceLine to the corresponding line number within this source; and return a value of true.
If it is not able to perform the conversion, it should return a value of false.
The ModelParser.ParseAsync() method optionally takes other arguments, one of which is a DtdlParseLocator, such as the one defined above.
parseTask = modelParser.ParseAsync(jsonText, parseLocator);When the original JSON text is resubmitted via the code snippet above, followed by the previously shown code snippet that calls parseTask.Wait() in a try-catch, it displays the same output as before:
DTDL model is invalid:
dtmi:example:anInterface;1, because it transitively 'extends' dtmi:example:anotherInterface;1, has property 'contents' that contains more than one element for which property 'name' has value 'currentDistance'. Either change the value of property 'name' to a value that is unique across all values of 'contents', or remove one or more 'extends' properties so that 'contents' will not be imported.There is no improvement in the description because the ModelParser cannot fully locate all relevant lines of the error.
The error occurred partly due to the name "currentDistance" in the submitted JSON text, but also due to the name "currentDistance" in the resolved definition.
To enable the parser to locate all relevant lines, we can define a DtdlResolveLocator and register it with the parser in the constructor.
In many cases, the ModelParser can identify which of the externally resolved identifier has a definition that contains the error, as well as the line number or range within the text string returned by the resolver.
The calling code can provide a callback delegate that converts this internal location into a location that is understandable by the user, which can greatly improve the clarity of the error description.
We can write such a delegate and register it with the parser as follows:
DtdlResolveLocator resolveLocator = (
Dtmi resolveDtmi,
int resolveLine,
out string sourceName,
out int sourceLine) =>
{
sourceName = $"dictionary entry 'otherJsonTexts[new Dtmi(\"{resolveDtmi}\")]'";
sourceLine = resolveLine;
return true;
};
modelParser = new ModelParser(
new ParsingOptions()
{
DtmiResolverAsync = (IReadOnlyCollection<Dtmi> dtmis, CancellationToken _) =>
{
return GetJsonTexts(dtmis, otherJsonTexts);
},
DtdlResolveLocator = resolveLocator,
});``When the ModelParser encounters an error in a definition returned by the `DtmiResolverAsync`, it calls the delegate with a `resolveDtmi` indicating the identifer that was resolved with the relevant definition and a `resolveLine` indicating a line number within this definition.
If the delegate is able to convert these values into a user-meaningful location, it should set the out-parameter `sourceName` to the name of the appropriate source file, URL, etc.; set the out-parameter `sourceLine` to the corresponding line number within this source; and return a value of true.
If it is not able to perform the conversion, it should return a value of false.
With the DtdlResolveLocator registered, we can resubmit the original JSON text to the ModelParser by re-executing the code snippet that calls ParseAsync() with the DtdlParseLocator, followed by the previously shown code snippet that calls parseTask.Wait() in a try-catch.
With both locators now available, the code snippet displays:
DTDL model is invalid:
Property 'name' has value 'currentDistance' on line 9 in string variable 'jsonText' and on line 8 in dictionary entry 'otherJsonTexts[new Dtmi("dtmi:example:anotherInterface;1")]', which is a uniqueness violation because dtmi:example:anInterface;1 transitively 'extends' dtmi:example:anotherInterface;1. Either change the value of property 'name' to a value that is unique across all values of 'contents', or remove one or more 'extends' properties so that 'contents' will not be imported.Looking at the JSON texts above, we see that the text in string variable jsonText has a "name" property on line 9.
We also see that the otherJsonTexts dictionary entry for key dtmi:example:anotherInterface;1" has text with a "name" property on line 8.
Furthermore, both of these properties have the value "currentDistance".
In addition, we see that the text in string variable jsonText has an "extends" property on line 5.
Relative to the previous error message, the location information in this message more directly pinpoints the location of the error.
To correct this error, we can change the value "currentDistance" in the submitted JSON text to "expectedDistance":
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""
}
]
}";When this JSON text is submitted to the code snippets above, it displays:
DTDL model is valid!