Skip to content

Latest commit

 

History

History
230 lines (183 loc) · 8.13 KB

Tutorial10_ResolveExternalReferencesAsync.md

File metadata and controls

230 lines (183 loc) · 8.13 KB

Resolve external references asynchronously

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.

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 that references another model

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.

Submit the JSON text to the ModelParser

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;
    }
}

Observe resolution exception

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.

Obtain the JSON text of referenced DTDL model

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""
    }
  ]
}";

Register a DtmiResolverAsync with the ModelParser

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);
}});

Resubmit the original JSON text to the ModelParser

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!

Inspect the Interface hierarchy via DTDL properties

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

Inspect the Interface hierarchy via synthetic properties

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