diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore b/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore
new file mode 100644
index 000000000000..031950aa8414
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/.gitignore
@@ -0,0 +1 @@
+launchSettings.json
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln
new file mode 100644
index 000000000000..68ee35711dec
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Azure.Iot.ModelsRepository.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30717.126
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.ModelsRepository", "src\Azure.Iot.ModelsRepository.csproj", "{5E11A377-0D20-49F8-952B-50390196EF4B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.ModelsRepository.Tests", "tests\Azure.Iot.ModelsRepository.Tests.csproj", "{092E6CE2-9998-428C-A801-2BAB4E14A577}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.TestFramework", "..\..\core\Azure.Core.TestFramework\src\Azure.Core.TestFramework.csproj", "{1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5E11A377-0D20-49F8-952B-50390196EF4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E11A377-0D20-49F8-952B-50390196EF4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E11A377-0D20-49F8-952B-50390196EF4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E11A377-0D20-49F8-952B-50390196EF4B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {092E6CE2-9998-428C-A801-2BAB4E14A577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {092E6CE2-9998-428C-A801-2BAB4E14A577}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {092E6CE2-9998-428C-A801-2BAB4E14A577}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {092E6CE2-9998-428C-A801-2BAB4E14A577}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1FC8A3EA-3C0D-4DDF-B710-A7091F2CEBB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {671D1EFB-2BB9-4846-91EF-A3FB1FF9DDA6}
+ EndGlobalSection
+EndGlobal
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md
new file mode 100644
index 000000000000..8f770daed613
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CHANGELOG.md
@@ -0,0 +1,19 @@
+# Release History
+
+## 1.0.0-preview.1 (Unreleased)
+
+### New features
+
+- Initial preview of Azure.IoT.ModelRepository SDK
+
+### Breaking changes
+
+- N/A
+
+### Added
+
+- N/A
+
+### Fixes and improvements
+
+- N/A
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config
new file mode 100644
index 000000000000..5382ce6fd8f0
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/CodeMaid.config
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+ True
+
+
+ True
+
+
+ True
+
+
+ 120
+
+
+ False
+
+
+ // Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+
+
+ True
+
+
+ 1
+
+
+ False
+
+
+
+
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props
new file mode 100644
index 000000000000..1a9611bd4924
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj
new file mode 100644
index 000000000000..f522da43641c
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Azure IoT Models Repository SDK
+ $(RequiredTargetFrameworks)
+
+ true
+
+
+
+
+ IoT;ModelsRepository;Pnp;DigitalTwins$(PackageCommonTags)
+ SDK for the Azure IoT Models Repository
+ 1.0.0-preview.1
+
+
+
+
+
+
+
+
+
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs
new file mode 100644
index 000000000000..13636761c71e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DtmiConventions.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace Azure.Iot.ModelsRepository
+{
+ internal static class DtmiConventions
+ {
+ public static bool IsDtmi(string dtmi) => !string.IsNullOrEmpty(dtmi) && new Regex(@"^dtmi:[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?(?::[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?)*;[1-9][0-9]{0,8}$").IsMatch(dtmi);
+ public static string DtmiToPath(string dtmi) => IsDtmi(dtmi) ? $"{dtmi.ToLowerInvariant().Replace(":", "/").Replace(";", "-")}.json" : null;
+
+ public static string DtmiToQualifiedPath(string dtmi, string basePath, bool fromExpanded = false)
+ {
+ string dtmiPath = DtmiToPath(dtmi);
+ if (dtmiPath == null)
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, StandardStrings.InvalidDtmiFormat, dtmi));
+
+ if (!basePath.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
+ basePath += "/";
+
+ string fullyQualifiedPath = $"{basePath}{dtmiPath}";
+
+ if (fromExpanded)
+ fullyQualifiedPath = fullyQualifiedPath.Replace(".json", ".expanded.json");
+
+ return fullyQualifiedPath;
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs
new file mode 100644
index 000000000000..30dd40088aff
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/FetchResult.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Azure.Iot.ModelsRepository.Fetchers
+{
+ internal class FetchResult
+ {
+ public string Definition { get; set; }
+ public string Path { get; set; }
+ public bool FromExpanded => Path.EndsWith("expanded.json", System.StringComparison.InvariantCultureIgnoreCase);
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs
new file mode 100644
index 000000000000..696ad7417d6f
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/IModelFetcher.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository.Fetchers
+{
+ internal interface IModelFetcher
+ {
+ Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default);
+
+ FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs
new file mode 100644
index 000000000000..4eea403dd414
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using System.Text;
+using System.Collections.Generic;
+using System.Threading;
+using System.Globalization;
+
+namespace Azure.Iot.ModelsRepository.Fetchers
+{
+ internal class LocalModelFetcher : IModelFetcher
+ {
+ private readonly bool _tryExpanded;
+
+ public LocalModelFetcher(ResolverClientOptions clientOptions)
+ {
+ _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded;
+ }
+
+ public Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(Fetch(dtmi, repositoryUri, cancellationToken));
+ }
+
+ public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default)
+ {
+ var work = new Queue();
+
+ if (_tryExpanded)
+ work.Enqueue(GetPath(dtmi, repositoryUri, true));
+
+ work.Enqueue(GetPath(dtmi, repositoryUri, false));
+
+ string fnfError = string.Empty;
+ while (work.Count != 0 && !cancellationToken.IsCancellationRequested)
+ {
+ string tryContentPath = work.Dequeue();
+ ResolverEventSource.Shared.FetchingModelContent(tryContentPath);
+
+ if (File.Exists(tryContentPath))
+ {
+ return new FetchResult()
+ {
+ Definition = File.ReadAllText(tryContentPath, Encoding.UTF8),
+ Path = tryContentPath
+ };
+ }
+
+ ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath);
+ fnfError = string.Format(CultureInfo.InvariantCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath);
+ }
+
+ throw new FileNotFoundException(fnfError);
+ }
+
+ private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false)
+ {
+ string registryPath = repositoryUri.AbsolutePath;
+ return DtmiConventions.DtmiToQualifiedPath(dtmi, registryPath, expanded);
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs
new file mode 100644
index 000000000000..a68ee01bf37c
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core;
+using Azure.Core.Pipeline;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository.Fetchers
+{
+ internal class RemoteModelFetcher : IModelFetcher
+ {
+ private readonly HttpPipeline _pipeline;
+ private readonly bool _tryExpanded;
+
+ public RemoteModelFetcher(ResolverClientOptions clientOptions)
+ {
+ _pipeline = CreatePipeline(clientOptions);
+ _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded;
+ }
+
+ public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default)
+ {
+ Queue work = new Queue();
+
+ if (_tryExpanded)
+ work.Enqueue(GetPath(dtmi, repositoryUri, true));
+
+ work.Enqueue(GetPath(dtmi, repositoryUri, false));
+
+ string remoteFetchError = string.Empty;
+ while (work.Count != 0 && !cancellationToken.IsCancellationRequested)
+ {
+ string tryContentPath = work.Dequeue();
+ ResolverEventSource.Shared.FetchingModelContent(tryContentPath);
+
+ string content = await EvaluatePathAsync(tryContentPath, cancellationToken).ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(content))
+ {
+ return new FetchResult()
+ {
+ Definition = content,
+ Path = tryContentPath
+ };
+ }
+
+ ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath);
+ remoteFetchError = string.Format(CultureInfo.InvariantCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath);
+ }
+
+ throw new RequestFailedException(remoteFetchError);
+ }
+
+ private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false)
+ {
+ string absoluteUri = repositoryUri.AbsoluteUri;
+ return DtmiConventions.DtmiToQualifiedPath(dtmi, absoluteUri, expanded);
+ }
+
+ private async Task EvaluatePathAsync(string path, CancellationToken cancellationToken)
+ {
+ Request request = _pipeline.CreateRequest();
+ request.Method = RequestMethod.Get;
+ request.Uri = new RequestUriBuilder();
+ request.Uri.Reset(new Uri(path));
+
+ Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
+
+ if (response.Status >= 200 && response.Status <= 299)
+ {
+ return await GetContentAsync(response.ContentStream, cancellationToken).ConfigureAwait(false);
+ }
+
+ return null;
+ }
+
+ private static async Task GetContentAsync(Stream content, CancellationToken cancellationToken)
+ {
+ using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false))
+ {
+ JsonElement root = json.RootElement;
+ return root.GetRawText();
+ }
+ }
+
+ private static HttpPipeline CreatePipeline(ResolverClientOptions options)
+ {
+ return HttpPipelineBuilder.Build(options);
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs
new file mode 100644
index 000000000000..bf0cc568bf0f
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelMetadata.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Azure.Iot.ModelsRepository
+{
+ internal class ModelMetadata
+ {
+ public ModelMetadata(string id, IList extends, IList componentSchemas)
+ {
+ Id = id;
+ Extends = extends;
+ ComponentSchemas = componentSchemas;
+ }
+
+ public string Id { get; }
+ public IList Extends { get; }
+ public IList ComponentSchemas { get; }
+ public IList Dependencies => Extends.Union(ComponentSchemas).ToList();
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs
new file mode 100644
index 000000000000..ddbea223d2cb
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ModelQuery.cs
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository
+{
+ internal class ModelQuery
+ {
+ private readonly string _content;
+ private readonly JsonDocumentOptions _parseOptions;
+
+ public ModelQuery(string content)
+ {
+ _content = content;
+ _parseOptions = new JsonDocumentOptions
+ {
+ AllowTrailingCommas = true
+ };
+ }
+
+ public ModelMetadata GetMetadata()
+ {
+ return new ModelMetadata(GetId(), GetExtends(), GetComponentSchemas());
+ }
+
+ public string GetId()
+ {
+ using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions))
+ {
+ JsonElement _root = document.RootElement;
+
+ if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("@id", out JsonElement id))
+ {
+ if (id.ValueKind == JsonValueKind.String)
+ {
+ return id.GetString();
+ }
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public IList GetExtends()
+ {
+ List dependencies = new List();
+
+ using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions))
+ {
+ JsonElement _root = document.RootElement;
+
+ if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("extends", out JsonElement extends))
+ {
+ if (extends.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement extendElement in extends.EnumerateArray())
+ {
+ if (extendElement.ValueKind == JsonValueKind.String)
+ {
+ dependencies.Add(extendElement.GetString());
+ }
+ else if (extendElement.ValueKind == JsonValueKind.Object)
+ {
+ // extends can have multiple levels and can contain components.
+ // TODO: Support object ctor - inefficient serialize.
+ ModelMetadata nested_interface = new ModelQuery(JsonSerializer.Serialize(extendElement)).GetMetadata();
+ dependencies.AddRange(nested_interface.Dependencies);
+ }
+ }
+ }
+ else if (extends.ValueKind == JsonValueKind.String)
+ {
+ dependencies.Add(extends.GetString());
+ }
+ }
+ }
+
+ return dependencies;
+ }
+
+ // TODO: Consider refactor to an object type based processing.
+ public IList GetComponentSchemas()
+ {
+ List componentSchemas = new List();
+
+ using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions))
+ {
+ JsonElement _root = document.RootElement;
+
+ if (_root.ValueKind == JsonValueKind.Object && _root.TryGetProperty("contents", out JsonElement contents))
+ {
+ if (contents.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement element in contents.EnumerateArray())
+ {
+ if (element.TryGetProperty("@type", out JsonElement type))
+ {
+ if (type.ValueKind == JsonValueKind.String && type.GetString() == "Component")
+ {
+ if (element.TryGetProperty("schema", out JsonElement schema))
+ {
+ if (schema.ValueKind == JsonValueKind.String)
+ {
+ componentSchemas.Add(schema.GetString());
+ }
+ else if (schema.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement schemaElement in schema.EnumerateArray())
+ {
+ if (schemaElement.ValueKind == JsonValueKind.String)
+ {
+ componentSchemas.Add(schemaElement.GetString());
+ }
+ }
+ }
+ else if (schema.ValueKind == JsonValueKind.Object)
+ {
+ if (schema.TryGetProperty("extends", out JsonElement schemaObjExtends))
+ {
+ if (schemaObjExtends.ValueKind == JsonValueKind.String)
+ {
+ componentSchemas.Add(schemaObjExtends.GetString());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return componentSchemas;
+ }
+
+ public async Task> ListToDictAsync()
+ {
+ Dictionary result = new Dictionary();
+
+ using (JsonDocument document = JsonDocument.Parse(_content, _parseOptions))
+ {
+ JsonElement _root = document.RootElement;
+
+ if (_root.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement element in _root.EnumerateArray())
+ {
+ if (element.ValueKind == JsonValueKind.Object)
+ {
+ using (MemoryStream stream = new MemoryStream())
+ {
+ await JsonSerializer.SerializeAsync(stream, element).ConfigureAwait(false);
+ stream.Position = 0;
+
+ using (StreamReader streamReader = new StreamReader(stream))
+ {
+ string serialized = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+
+ string id = new ModelQuery(serialized).GetId();
+ result.Add(id, serialized);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..0128a60627e3
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Azure.Iot.ModelsRepository.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")]
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs
new file mode 100644
index 000000000000..9350237b07ed
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs
@@ -0,0 +1,118 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Iot.ModelsRepository.Fetchers;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository
+{
+ internal class RepositoryHandler
+ {
+ private readonly IModelFetcher _modelFetcher;
+ private readonly Guid _clientId;
+
+ public Uri RepositoryUri { get; }
+ public ResolverClientOptions ClientOptions { get; }
+
+ public RepositoryHandler(Uri repositoryUri, ResolverClientOptions options = null)
+ {
+ ClientOptions = options ?? new ResolverClientOptions();
+ RepositoryUri = repositoryUri;
+ _modelFetcher = repositoryUri.Scheme == "file"
+ ? _modelFetcher = new LocalModelFetcher(ClientOptions)
+ : _modelFetcher = new RemoteModelFetcher(ClientOptions);
+ _clientId = Guid.NewGuid();
+ ResolverEventSource.Shared.InitFetcher(_clientId, repositoryUri.Scheme);
+ }
+
+ public async Task> ProcessAsync(string dtmi, CancellationToken cancellationToken)
+ {
+ return await ProcessAsync(new List() { dtmi }, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task> ProcessAsync(IEnumerable dtmis, CancellationToken cancellationToken)
+ {
+ Dictionary processedModels = new Dictionary();
+ Queue toProcessModels = new Queue();
+
+ foreach (string dtmi in dtmis)
+ {
+ if (!DtmiConventions.IsDtmi(dtmi))
+ {
+ ResolverEventSource.Shared.InvalidDtmiInput(dtmi);
+ string invalidArgMsg = string.Format(CultureInfo.InvariantCulture, StandardStrings.InvalidDtmiFormat, dtmi);
+ throw new ResolverException(dtmi, invalidArgMsg, new ArgumentException(invalidArgMsg));
+ }
+
+ toProcessModels.Enqueue(dtmi);
+ }
+
+ while (toProcessModels.Count != 0 && !cancellationToken.IsCancellationRequested)
+ {
+ string targetDtmi = toProcessModels.Dequeue();
+ if (processedModels.ContainsKey(targetDtmi))
+ {
+ ResolverEventSource.Shared.SkippingPreprocessedDtmi(targetDtmi);
+ continue;
+ }
+ ResolverEventSource.Shared.ProcessingDtmi(targetDtmi);
+
+ FetchResult result = await FetchAsync(targetDtmi, cancellationToken).ConfigureAwait(false);
+ if (result.FromExpanded)
+ {
+ Dictionary expanded = await new ModelQuery(result.Definition).ListToDictAsync().ConfigureAwait(false);
+ foreach (KeyValuePair kvp in expanded)
+ {
+ if (!processedModels.ContainsKey(kvp.Key))
+ processedModels.Add(kvp.Key, kvp.Value);
+ }
+
+ continue;
+ }
+
+ ModelMetadata metadata = new ModelQuery(result.Definition).GetMetadata();
+
+ if (ClientOptions.DependencyResolution >= DependencyResolutionOption.Enabled)
+ {
+ IList dependencies = metadata.Dependencies;
+
+ if (dependencies.Count > 0)
+ ResolverEventSource.Shared.DiscoveredDependencies(string.Join("\", \"", dependencies));
+
+ foreach (string dep in dependencies)
+ {
+ toProcessModels.Enqueue(dep);
+ }
+ }
+
+ string parsedDtmi = metadata.Id;
+ if (!parsedDtmi.Equals(targetDtmi, StringComparison.Ordinal))
+ {
+ ResolverEventSource.Shared.IncorrectDtmiCasing(targetDtmi, parsedDtmi);
+ string formatErrorMsg = string.Format(CultureInfo.InvariantCulture, StandardStrings.IncorrectDtmiCasing, targetDtmi, parsedDtmi);
+ throw new ResolverException(targetDtmi, formatErrorMsg, new FormatException(formatErrorMsg));
+ }
+
+ processedModels.Add(targetDtmi, result.Definition);
+ }
+
+ return processedModels;
+ }
+
+ private async Task FetchAsync(string dtmi, CancellationToken cancellationToken)
+ {
+ try
+ {
+ return await _modelFetcher.FetchAsync(dtmi, RepositoryUri, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ throw new ResolverException(dtmi, ex.Message, ex);
+ }
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs
new file mode 100644
index 000000000000..a6bddeb27c52
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository
+{
+ ///
+ /// The ResolverClient class supports DTDL model resolution providing functionality to
+ /// resolve models by retrieving model definitions and their dependencies.
+ ///
+ public class ResolverClient
+ {
+ internal const string DefaultRepository = "https://devicemodels.azure.com";
+ private readonly RepositoryHandler _repositoryHandler;
+
+ ///
+ /// Initializes the ResolverClient with default client options while pointing to
+ /// the Azure IoT Plug and Play Model repository https://devicemodels.azure.com for resolution.
+ ///
+ public ResolverClient() : this(new Uri(DefaultRepository), null) { }
+
+ ///
+ /// Initializes the ResolverClient with default client options while pointing to
+ /// a custom for resolution.
+ ///
+ ///
+ /// The model repository Uri value. This can be a remote endpoint or local directory.
+ ///
+ public ResolverClient(Uri repositoryUri) : this(repositoryUri, null) { }
+
+ ///
+ /// Initializes the ResolverClient with custom client while pointing to
+ /// the Azure IoT Plug and Play Model repository https://devicemodels.azure.com for resolution.
+ ///
+ ///
+ /// ResolverClientOptions to configure resolution and client behavior.
+ ///
+ public ResolverClient(ResolverClientOptions options) : this(new Uri(DefaultRepository), options) { }
+
+ ///
+ /// Initializes the ResolverClient with custom client while pointing to
+ /// a custom for resolution.
+ ///
+ ///
+ /// The model repository Uri. This can be a remote endpoint or local directory.
+ ///
+ ///
+ /// ResolverClientOptions to configure resolution and client behavior.
+ ///
+ public ResolverClient(Uri repositoryUri, ResolverClientOptions options)
+ {
+ _repositoryHandler = new RepositoryHandler(repositoryUri, options);
+ }
+
+ ///
+ /// Initializes the ResolverClient with default client options while pointing to
+ /// a custom for resolution.
+ ///
+ ///
+ /// The model repository Uri in string format. This can be a remote endpoint or local directory.
+ ///
+ public ResolverClient(string repositoryUriStr) : this(repositoryUriStr, null) { }
+
+ ///
+ /// Initializes the ResolverClient with custom client while pointing to
+ /// a custom for resolution.
+ ///
+ ///
+ /// The model repository Uri in string format. This can be a remote endpoint or local directory.
+ ///
+ ///
+ /// ResolverClientOptions to configure resolution and client behavior.
+ ///
+ public ResolverClient(string repositoryUriStr, ResolverClientOptions options) : this(new Uri(repositoryUriStr), options) { }
+
+ ///
+ /// Resolves a model definition identified by and optionally its dependencies.
+ ///
+ ///
+ /// An IDictionary containing the model definition(s) where the key is the dtmi
+ /// and the value is the raw model definition string.
+ ///
+ /// Thrown when a resolution failure occurs.
+ /// A well-formed DTDL model Id. For example 'dtmi:com:example:Thermostat;1'.
+ /// The cancellationToken.
+ [SuppressMessage(
+ "Usage",
+ "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.",
+ Justification = "")]
+ [SuppressMessage(
+ "Usage",
+ "AZC0015:Unexpected client method return type.",
+ Justification = "")]
+ public virtual async Task> ResolveAsync(string dtmi, CancellationToken cancellationToken = default)
+ {
+ return await _repositoryHandler.ProcessAsync(dtmi, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Resolves a collection of model definitions identified by and optionally their dependencies.
+ ///
+ ///
+ /// An IDictionary containing the model definition(s) where the key is the dtmi
+ /// and the value is the raw model definition string.
+ ///
+ /// Thrown when a resolution failure occurs.
+ /// A collection of well-formed DTDL model Ids.
+ /// The cancellationToken.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0004:DO provide both asynchronous and synchronous variants for all service methods.", Justification = "")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "AZC0015:Unexpected client method return type.", Justification = "")]
+ public virtual async Task> ResolveAsync(IEnumerable dtmis, CancellationToken cancellationToken = default)
+ {
+ return await _repositoryHandler.ProcessAsync(dtmis, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Evaluates whether an input is valid.
+ ///
+ public static bool IsValidDtmi(string dtmi) => DtmiConventions.IsDtmi(dtmi);
+
+ ///
+ /// Gets the Uri associated with the ResolverClient instance.
+ ///
+ public Uri RepositoryUri => _repositoryHandler.RepositoryUri;
+
+ ///
+ /// Gets the ResolverClientOptions associated with the ResolverClient instance.
+ ///
+ public ResolverClientOptions ClientOptions => _repositoryHandler.ClientOptions;
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs
new file mode 100644
index 000000000000..f49cb34ce9b0
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClientOptions.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core;
+
+namespace Azure.Iot.ModelsRepository
+{
+ ///
+ /// Options that allow configuration of requests sent to the ModelRepositoryService.
+ ///
+ public class ResolverClientOptions : ClientOptions
+ {
+ internal const ServiceVersion LatestVersion = ServiceVersion.V2021_02_11;
+
+ ///
+ /// The versions of Azure IoT Model Repository by this client
+ /// library.
+ ///
+ public enum ServiceVersion
+ {
+ ///
+ /// 2021_02_11
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "")]
+ V2021_02_11 = 1
+ }
+
+ ///
+ /// Gets the of the service API used when
+ /// making requests.
+ ///
+ public ServiceVersion Version { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The of the service API used when
+ /// making requests.
+ ///
+ /// The dependency processing options.
+ public ResolverClientOptions(ServiceVersion version = LatestVersion, DependencyResolutionOption resolutionOption = DependencyResolutionOption.Enabled)
+ {
+ DependencyResolution = resolutionOption;
+ Version = version;
+ }
+
+ ///
+ /// The dependency processing options.
+ ///
+ public DependencyResolutionOption DependencyResolution { get; }
+ }
+
+ ///
+ /// The dependency processing options.
+ ///
+ public enum DependencyResolutionOption
+ {
+ ///
+ /// Do not process external dependencies.
+ ///
+ Disabled,
+ ///
+ /// Enable external dependencies.
+ ///
+ Enabled,
+ ///
+ /// Try to get external dependencies using .expanded.json.
+ ///
+ TryFromExpanded
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs
new file mode 100644
index 000000000000..62b47ef3e2a1
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core.Diagnostics;
+using System;
+using System.Diagnostics.Tracing;
+
+namespace Azure.Iot.ModelsRepository
+{
+ [EventSource(Name = EventSourceName)]
+ internal sealed class ResolverEventSource : EventSource
+ {
+ // Set EventSource name to package name replacing . with -
+ private const string EventSourceName = "Azure-Iot-ModelsRepository";
+
+ // Event ids defined as constants to makes it easy to keep track of them
+ private const int InitFetcherEventId = 1000;
+ private const int ProcessingDtmiEventId = 2000;
+ private const int FetchingModelContentEventId = 2001;
+ private const int DiscoveredDependenciesEventId = 2002;
+ private const int SkippingPreprocessedDtmiEventId = 2003;
+ private const int InvalidDtmiInputEventId = 4000;
+ private const int ErrorFetchingModelContentEventId = 4004;
+ private const int IncorrectDtmiCasingEventId = 4006;
+
+ public static ResolverEventSource Shared { get; } = new ResolverEventSource();
+
+ private ResolverEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { }
+
+ [Event(InitFetcherEventId, Level = EventLevel.Informational, Message = StandardStrings.ClientInitWithFetcher)]
+ public void InitFetcher(Guid clientId, string scheme)
+ {
+ // We are calling Guid.ToString make sure anyone is listening before spending resources
+ if (IsEnabled(EventLevel.Informational, EventKeywords.None))
+ {
+ WriteEvent(InitFetcherEventId, clientId.ToString("N"), scheme);
+ }
+ }
+
+ [Event(InvalidDtmiInputEventId, Level = EventLevel.Error, Message = StandardStrings.InvalidDtmiFormat)]
+ public void InvalidDtmiInput(string dtmi)
+ {
+ if (IsEnabled(EventLevel.Error, EventKeywords.None))
+ {
+ WriteEvent(InvalidDtmiInputEventId, dtmi);
+ }
+ }
+
+ [Event(SkippingPreprocessedDtmiEventId, Level = EventLevel.Informational, Message = StandardStrings.SkippingPreProcessedDtmi)]
+ public void SkippingPreprocessedDtmi(string dtmi)
+ {
+ if (IsEnabled(EventLevel.Informational, EventKeywords.None))
+ {
+ WriteEvent(SkippingPreprocessedDtmiEventId, dtmi);
+ }
+ }
+
+ [Event(ProcessingDtmiEventId, Level = EventLevel.Informational, Message = StandardStrings.ProcessingDtmi)]
+ public void ProcessingDtmi(string dtmi)
+ {
+ if (IsEnabled(EventLevel.Informational, EventKeywords.None))
+ {
+ WriteEvent(ProcessingDtmiEventId, dtmi);
+ }
+ }
+
+ [Event(DiscoveredDependenciesEventId, Level = EventLevel.Informational, Message = StandardStrings.DiscoveredDependencies)]
+ public void DiscoveredDependencies(string dependencies)
+ {
+ if (IsEnabled(EventLevel.Informational, EventKeywords.None))
+ {
+ WriteEvent(DiscoveredDependenciesEventId, dependencies);
+ }
+ }
+
+ [Event(IncorrectDtmiCasingEventId, Level = EventLevel.Error, Message = StandardStrings.IncorrectDtmiCasing)]
+ public void IncorrectDtmiCasing(string expected, string parsed)
+ {
+ if (IsEnabled(EventLevel.Error, EventKeywords.None))
+ {
+ WriteEvent(IncorrectDtmiCasingEventId, expected, parsed);
+ }
+ }
+
+ [Event(FetchingModelContentEventId, Level = EventLevel.Informational, Message = StandardStrings.FetchingModelContent)]
+ public void FetchingModelContent(string path)
+ {
+ if (IsEnabled(EventLevel.Informational, EventKeywords.None))
+ {
+ WriteEvent(FetchingModelContentEventId, path);
+ }
+ }
+
+ [Event(ErrorFetchingModelContentEventId, Level = EventLevel.Error, Message = StandardStrings.ErrorFetchingModelContent)]
+ public void ErrorFetchingModelContent(string path)
+ {
+ if (IsEnabled(EventLevel.Error, EventKeywords.None))
+ {
+ WriteEvent(ErrorFetchingModelContentEventId, path);
+ }
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs
new file mode 100644
index 000000000000..ceb4217c2d21
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverException.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Globalization;
+
+namespace Azure.Iot.ModelsRepository
+{
+ ///
+ /// TODO: Paymaun: Exception comments.
+ ///
+ public class ResolverException : Exception
+ {
+ ///
+ /// TODO: Paymaun: Exception comments.
+ ///
+ ///
+ public ResolverException(string dtmi) : base(string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi))
+ {
+ }
+
+ ///
+ /// TODO: Paymaun: Exception comments.
+ ///
+ ///
+ ///
+ public ResolverException(string dtmi, string message) :
+ base($"{string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi)}{message}")
+ {
+ }
+
+ ///
+ /// TODO: Paymaun: Exception comments.
+ ///
+ ///
+ ///
+ public ResolverException(string dtmi, Exception innerException) :
+ base(string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi), innerException)
+ {
+ }
+
+ ///
+ /// TODO: Paymaun: Exception comments.
+ ///
+ ///
+ ///
+ ///
+ public ResolverException(string dtmi, string message, Exception innerException) :
+ base($"{string.Format(CultureInfo.InvariantCulture, StandardStrings.GenericResolverError, dtmi)}{message}", innerException)
+ {
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs
new file mode 100644
index 000000000000..24bc475ee9c2
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/StandardStrings.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Azure.Iot.ModelsRepository
+{
+ internal class StandardStrings
+ {
+ public const string GenericResolverError = "Unable to resolve \"{0}\".";
+ public const string InvalidDtmiFormat = "Invalid DTMI format \"{0}\".";
+ public const string ClientInitWithFetcher = "Client session {0} initialized with {1} content fetcher.";
+ public const string ProcessingDtmi = "Processing DTMI \"{0}\". ";
+ public const string SkippingPreProcessedDtmi = "Already processed DTMI \"{0}\". Skipping.";
+ public const string DiscoveredDependencies = "Discovered dependencies \"{0}\".";
+ public const string FetchingModelContent = "Attempting to retrieve model content from \"{0}\".";
+ public const string ErrorFetchingModelContent = "Model file \"{0}\" not found or not accessible in target repository.";
+ public const string IncorrectDtmiCasing =
+ "Retrieved model has incorrect DTMI casing. Expected \"{0}\", parsed \"{1}\".";
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj
new file mode 100644
index 000000000000..3142e95ed80e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/Azure.Iot.ModelsRepository.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ $(RequiredTargetFrameworks)
+ $(DefineConstants);TESTFRAMEWORK
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs
new file mode 100644
index 000000000000..437c0231de9d
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using NUnit.Framework;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Azure.Iot.ModelsRepository.Tests
+{
+ public class ClientTests
+ {
+ [Test]
+ public void CtorOverloads()
+ {
+ string remoteUriStr = "https://dtmi.com";
+ Uri remoteUri = new Uri(remoteUriStr);
+
+ ResolverClientOptions options = new ResolverClientOptions();
+
+ Assert.AreEqual(new Uri(ResolverClient.DefaultRepository), new ResolverClient().RepositoryUri);
+ Assert.AreEqual($"{ResolverClient.DefaultRepository}/", new ResolverClient().RepositoryUri.AbsoluteUri);
+ Assert.AreEqual(new Uri(ResolverClient.DefaultRepository), new ResolverClient(options).RepositoryUri);
+
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUri).RepositoryUri);
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, options).RepositoryUri);
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, null).RepositoryUri);
+
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr).RepositoryUri);
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, options).RepositoryUri);
+ Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, null).RepositoryUri);
+
+ string localUriStr = TestHelpers.TestLocalModelRepository;
+ Uri localUri = new Uri(localUriStr);
+
+ Assert.AreEqual(localUri, new ResolverClient(localUri).RepositoryUri);
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ localUriStr = localUriStr.Replace("\\", "/");
+ }
+
+ Assert.AreEqual(localUriStr, new ResolverClient(localUri).RepositoryUri.AbsolutePath);
+ }
+
+ [TestCase("dtmi:com:example:Thermostat;1", true)]
+ [TestCase("dtmi:contoso:scope:entity;2", true)]
+ [TestCase("dtmi:com:example:Thermostat:1", false)]
+ [TestCase("dtmi:com:example::Thermostat;1", false)]
+ [TestCase("com:example:Thermostat;1", false)]
+ [TestCase("", false)]
+ [TestCase(null, false)]
+ public void ClientIsValidDtmi(string dtmi, bool expected)
+ {
+ Assert.AreEqual(expected, ResolverClient.IsValidDtmi(dtmi));
+ }
+
+ [Test]
+ public void ClientOptions()
+ {
+ DependencyResolutionOption defaultResolutionOption = DependencyResolutionOption.Enabled;
+ ResolverClientOptions customOptions =
+ new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded);
+ int maxRetries = 10;
+ customOptions.Retry.MaxRetries = maxRetries;
+
+ string repositoryUriString = "https://localhost/myregistry/";
+ Uri repositoryUri = new Uri(repositoryUriString);
+
+ ResolverClient defaultClient = new ResolverClient(repositoryUri);
+ Assert.AreEqual(defaultResolutionOption, defaultClient.ClientOptions.DependencyResolution);
+
+ ResolverClient customClient = new ResolverClient(repositoryUriString, customOptions);
+ Assert.AreEqual(DependencyResolutionOption.TryFromExpanded, customClient.ClientOptions.DependencyResolution);
+ Assert.AreEqual(maxRetries, customClient.ClientOptions.Retry.MaxRetries);
+ }
+
+ [Test]
+ public void EvaluateEventSourceKPIs()
+ {
+ Type eventSourceType = typeof(ResolverEventSource);
+
+ Assert.NotNull(eventSourceType);
+ Assert.AreEqual("Azure-Iot-ModelsRepository", ResolverEventSource.GetName(eventSourceType));
+ Assert.AreEqual(Guid.Parse("7678f8d4-81db-5fd2-39fc-23552d86b171"), ResolverEventSource.GetGuid(eventSourceType));
+ Assert.IsNotEmpty(ResolverEventSource.GenerateManifest(eventSourceType, "assemblyPathToIncludeInManifest"));
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs
new file mode 100644
index 000000000000..a8919a1ddcaf
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/DtmiConversionTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using NUnit.Framework;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Azure.Iot.ModelsRepository.Tests
+{
+ public class DtmiConversionTests
+ {
+ [TestCase("dtmi:com:Example:Model;1", "dtmi/com/example/model-1.json")]
+ [TestCase("dtmi:com:example:Model;1", "dtmi/com/example/model-1.json")]
+ [TestCase("dtmi:com:example:Model:1", null)]
+ [TestCase("", null)]
+ [TestCase(null, null)]
+ public void DtmiToPath(string dtmi, string expectedPath)
+ {
+ Assert.AreEqual(expectedPath, DtmiConventions.DtmiToPath(dtmi));
+ }
+
+ [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", "https://localhost/repository")]
+ [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", @"C:\fakeRegistry")]
+ [TestCase("dtmi:com:example:Thermostat;1", "dtmi/com/example/thermostat-1.json", "/me/fakeRegistry")]
+ [TestCase("dtmi:com:example:Thermostat:1", null, "https://localhost/repository")]
+ [TestCase("dtmi:com:example:Thermostat:1", null, "/me/fakeRegistry")]
+ public void DtmiToQualifiedPath(string dtmi, string expectedPath, string repository)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ repository = repository.Replace("\\", "/");
+ }
+
+ if (string.IsNullOrEmpty(expectedPath))
+ {
+ ArgumentException re = Assert.Throws(() => DtmiConventions.DtmiToQualifiedPath(dtmi, repository));
+ Assert.AreEqual(re.Message, string.Format(StandardStrings.InvalidDtmiFormat, dtmi));
+ return;
+ }
+
+ string modelPath = DtmiConventions.DtmiToQualifiedPath(dtmi, repository);
+ Assert.AreEqual($"{repository}/{expectedPath}", modelPath);
+
+ string expandedModelPath = DtmiConventions.DtmiToQualifiedPath(dtmi, repository, true);
+ Assert.AreEqual($"{repository}/{expectedPath.Replace(".json", ".expanded.json")}", expandedModelPath);
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs
new file mode 100644
index 000000000000..674ee96b31c6
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ModelQueryTests.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using NUnit.Framework;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository.Tests
+{
+ public class ModelQueryTests
+ {
+ private readonly string _modelTemplate = @"{{
+ {0}
+ ""@type"": ""Interface"",
+ ""displayName"": ""Phone"",
+ {1}
+ {2}
+ ""@context"": ""dtmi:dtdl:context;2""
+ }}";
+
+ [TestCase("\"@id\": \"dtmi:com:example:thermostat;1\",", "dtmi:com:example:thermostat;1")]
+ [TestCase("\"@id\": \"\",", "")]
+ [TestCase("", "")]
+ public void GetId(string formatId, string expectedId)
+ {
+ string modelContent = string.Format(_modelTemplate, formatId, "", "");
+ ModelQuery query = new ModelQuery(modelContent);
+ Assert.AreEqual(query.GetId(), expectedId);
+ }
+
+ [TestCase(
+ @"
+ ""contents"":
+ [{
+ ""@type"": ""Property"",
+ ""name"": ""capacity"",
+ ""schema"": ""integer""
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""frontCamera"",
+ ""schema"": ""dtmi:com:example:Camera;3""
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""backCamera"",
+ ""schema"": ""dtmi:com:example:Camera;3""
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""deviceInfo"",
+ ""schema"": ""dtmi:azure:DeviceManagement:DeviceInformation;1""
+ }],",
+ "dtmi:com:example:Camera;3,dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1"
+ )]
+ [TestCase(
+ @"
+ ""contents"":
+ [{
+ ""@type"": ""Property"",
+ ""name"": ""capacity"",
+ ""schema"": ""integer""
+ }],", ""
+ )]
+ [TestCase(@"""contents"":[],", "")]
+ [TestCase("", "")]
+ public void GetComponentSchema(string contents, string expected)
+ {
+ string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries);
+ string modelContent = string.Format(_modelTemplate, "", "", contents);
+ ModelQuery query = new ModelQuery(modelContent);
+ IList componentSchemas = query.GetComponentSchemas();
+ Assert.AreEqual(componentSchemas.Count, expectedDtmis.Length);
+
+ foreach (string schema in componentSchemas)
+ {
+ Assert.Contains(schema, expectedDtmis);
+ }
+ }
+
+ [TestCase(
+ "\"extends\": [\"dtmi:com:example:Camera;3\",\"dtmi:azure:DeviceManagement:DeviceInformation;1\"],",
+ "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1"
+ )]
+ [TestCase("\"extends\": [],", "")]
+ [TestCase("\"extends\": \"dtmi:com:example:Camera;3\",", "dtmi:com:example:Camera;3")]
+ [TestCase("", "")]
+ public void GetExtends(string extends, string expected)
+ {
+ string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries);
+ string modelContent = string.Format(_modelTemplate, "", extends, "");
+ ModelQuery query = new ModelQuery(modelContent);
+ IList extendsDtmis = query.GetExtends();
+ Assert.AreEqual(extendsDtmis.Count, expectedDtmis.Length);
+
+ foreach (string dtmi in extendsDtmis)
+ {
+ Assert.Contains(dtmi, expectedDtmis);
+ }
+ }
+
+ [TestCase(
+ "\"@id\": \"dtmi:com:example:thermostat;1\",",
+ "\"extends\": [\"dtmi:com:example:Camera;3\",\"dtmi:azure:DeviceManagement:DeviceInformation;1\"],",
+ @"""contents"":
+ [{
+ ""@type"": ""Property"",
+ ""name"": ""capacity"",
+ ""schema"": ""integer""
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""frontCamera"",
+ ""schema"": ""dtmi:com:example:Camera;3""
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""backCamera"",
+ ""schema"": ""dtmi:com:example:Camera;3""
+ }],",
+ "dtmi:com:example:Camera;3,dtmi:azure:DeviceManagement:DeviceInformation;1"
+ ),
+ TestCase(
+ "\"@id\": \"dtmi:example:Interface1;1\",",
+ @"""extends"": [""dtmi:example:Interface2;1"", {
+ ""@id"": ""dtmi:example:Interface3;1"",
+ ""@type"": ""Interface"",
+ ""contents"": [{
+ ""@type"": ""Component"",
+ ""name"": ""comp1"",
+ ""schema"": [""dtmi:example:Interface4;1""]
+ },
+ {
+ ""@type"": ""Component"",
+ ""name"": ""comp2"",
+ ""schema"": {
+ ""@id"": ""dtmi:example:Interface5;1"",
+ ""@type"": ""Interface"",
+ ""extends"": ""dtmi:example:Interface6;1""
+ }
+ }
+ ]
+ }],",
+ "",
+ "dtmi:example:Interface2;1,dtmi:example:Interface4;1,dtmi:example:Interface6;1"
+ )]
+ public void GetModelDependencies(string id, string extends, string contents, string expected)
+ {
+ string[] expectedDtmis = expected.Split(new[] { "," }, System.StringSplitOptions.RemoveEmptyEntries);
+ string modelContent = string.Format(_modelTemplate, id, extends, contents);
+ ModelMetadata metadata = new ModelQuery(modelContent).GetMetadata();
+
+ IList dependencies = metadata.Dependencies;
+
+ Assert.AreEqual(dependencies.Count, expectedDtmis.Length);
+
+ foreach (string dtmi in dependencies)
+ {
+ Assert.Contains(dtmi, expectedDtmis);
+ }
+ }
+
+ [Test]
+ public async Task ListToDictAsync()
+ {
+ string testRepoPath = TestHelpers.TestLocalModelRepository;
+ string expandedContent = File.ReadAllText(
+ $"{testRepoPath}/dtmi/com/example/temperaturecontroller-1.expanded.json", Encoding.UTF8);
+ ModelQuery query = new ModelQuery(expandedContent);
+ Dictionary transformResult = await query.ListToDictAsync();
+
+ // Assert KPI's for TemperatureController;1.
+ // Ensure transform of expanded content to dictionary is what we'd expect.
+ string[] expectedIds = new string[] {
+ "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "dtmi:com:example:Thermostat;1",
+ "dtmi:com:example:TemperatureController;1" };
+
+ Assert.True(transformResult.Keys.Count == expectedIds.Length);
+
+ foreach (string id in expectedIds)
+ {
+ Assert.True(transformResult.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(transformResult[id]).Equals(id, System.StringComparison.Ordinal));
+ }
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs
new file mode 100644
index 000000000000..4eb95f2bd338
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ResolveIntegrationTests.cs
@@ -0,0 +1,298 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Azure.Iot.ModelsRepository.Tests
+{
+ public class ResolveIntegrationTests
+ {
+ [TestCase("dtmi:com:example:thermostat;1", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:thermostat;1", TestHelpers.ClientType.Remote)]
+ public void ResolveWithWrongCasingThrowsException(string dtmi, TestHelpers.ClientType clientType)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(clientType);
+ string expectedExMsg =
+ string.Format(StandardStrings.GenericResolverError, "dtmi:com:example:thermostat;1") +
+ string.Format(StandardStrings.IncorrectDtmiCasing, "dtmi:com:example:thermostat;1", "dtmi:com:example:Thermostat;1");
+
+ ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi));
+ Assert.AreEqual(re.Message, expectedExMsg);
+ }
+
+ [TestCase("dtmi:com:example:Thermostat:1")]
+ [TestCase("dtmi:com:example::Thermostat;1")]
+ [TestCase("com:example:Thermostat;1")]
+ public void ResolveInvalidDtmiFormatThrowsException(string dtmi)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ string expectedExMsg = $"{string.Format(StandardStrings.GenericResolverError, dtmi)}{string.Format(StandardStrings.InvalidDtmiFormat, dtmi)}";
+ ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi));
+ Assert.AreEqual(re.Message, expectedExMsg);
+ }
+
+ [TestCase("dtmi:com:example:thermojax;999", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:thermojax;999", TestHelpers.ClientType.Remote)]
+ public void ResolveNoneExistentDtmiFileThrowsException(string dtmi, TestHelpers.ClientType clientType)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(clientType);
+ ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi));
+ Assert.True(re.Message.StartsWith($"Unable to resolve \"{dtmi}\""));
+ }
+
+ [TestCase("dtmi:com:example:invalidmodel;1", "dtmi:azure:fakeDeviceManagement:FakeDeviceInformation;2")]
+ public void ResolveInvalidDtmiDepsThrowsException(string dtmi, string invalidDep)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ ResolverException re = Assert.ThrowsAsync(async () => await client.ResolveAsync(dtmi));
+ Assert.True(re.Message.StartsWith($"Unable to resolve \"{invalidDep}\""));
+ }
+
+ [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Remote)]
+ public async Task ResolveSingleModelNoDeps(string dtmi, TestHelpers.ClientType clientType)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(clientType);
+ var result = await client.ResolveAsync(dtmi);
+ Assert.True(result.Keys.Count == 1);
+ Assert.True(result.ContainsKey(dtmi));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi);
+ }
+
+ [TestCase("dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Remote)]
+ public async Task ResolveMultipleModelsNoDeps(string dtmi1, string dtmi2, TestHelpers.ClientType clientType)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(clientType);
+ var result = await client.ResolveAsync(new string[] { dtmi1, dtmi2 });
+ Assert.True(result.Keys.Count == 2);
+ Assert.True(result.ContainsKey(dtmi1));
+ Assert.True(result.ContainsKey(dtmi2));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi1]) == dtmi1);
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi2]) == dtmi2);
+ }
+
+ [TestCase("dtmi:com:example:TemperatureController;1",
+ "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:TemperatureController;1",
+ "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1", TestHelpers.ClientType.Remote)]
+ public async Task ResolveSingleModelWithDeps(string dtmi, string expectedDeps, TestHelpers.ClientType clientType)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(clientType);
+ var result = await client.ResolveAsync(dtmi);
+ var expectedDtmis = $"{dtmi},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+
+ // TODO: Evaluate using Azure.Core.TestFramework in future iteration.
+
+ /*
+ // Verifying log entries for a Process(...) run
+ _logger.ValidateLog($"{StandardStrings.ClientInitWithFetcher(localClient.RepositoryUri.Scheme)}", LogLevel.Trace, Times.Once());
+
+ _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:com:example:TemperatureController;1")}", LogLevel.Trace, Times.Once());
+ _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[0], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once());
+
+ _logger.ValidateLog($"{StandardStrings.DiscoveredDependencies(new List() { "dtmi:com:example:Thermostat;1", "dtmi:azure:DeviceManagement:DeviceInformation;1" })}", LogLevel.Trace, Times.Once());
+
+ _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:com:example:Thermostat;1")}", LogLevel.Trace, Times.Once());
+ _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[1], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once());
+
+ _logger.ValidateLog($"{StandardStrings.ProcessingDtmi("dtmi:azure:DeviceManagement:DeviceInformation;1")}", LogLevel.Trace, Times.Once());
+ _logger.ValidateLog($"{StandardStrings.FetchingContent(DtmiConventions.DtmiToQualifiedPath(expectedDtmis[2], localClient.RepositoryUri.AbsolutePath))}", LogLevel.Trace, Times.Once());
+ */
+ }
+
+ [TestCase("dtmi:com:example:Phone;2",
+ "dtmi:com:example:TemperatureController;1",
+ "dtmi:com:example:Thermostat;1," +
+ "dtmi:azure:DeviceManagement:DeviceInformation;1," +
+ "dtmi:azure:DeviceManagement:DeviceInformation;2," +
+ "dtmi:com:example:Camera;3")]
+ public async Task ResolveMultipleModelsWithDeps(string dtmi1, string dtmi2, string expectedDeps)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 });
+ var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+ }
+
+ [TestCase("dtmi:com:example:TemperatureController;1",
+ "dtmi:com:example:ConferenceRoom;1", // Model uses extends
+ "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1,dtmi:com:example:Room;1")]
+ public async Task ResolveMultipleModelsWithDepsFromExtends(string dtmi1, string dtmi2, string expectedDeps)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 });
+ var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+ }
+
+ [TestCase("dtmi:com:example:TemperatureController;1",
+ "dtmi:com:example:ColdStorage;1", // Model uses extends[]
+ "dtmi:com:example:Thermostat;1," +
+ "dtmi:azure:DeviceManagement:DeviceInformation;1," +
+ "dtmi:com:example:Room;1," +
+ "dtmi:com:example:Freezer;1")]
+ public async Task ResolveMultipleModelsWithDepsFromExtendsVariant(string dtmi1, string dtmi2, string expectedDeps)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(new[] { dtmi1, dtmi2 });
+ var expectedDtmis = $"{dtmi1},{dtmi2},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+ }
+
+ [TestCase("dtmi:com:example:base;1")]
+ public async Task ResolveSingleModelWithDepsFromExtendsInline(string dtmi)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(dtmi);
+
+ Assert.True(result.Keys.Count == 1);
+ Assert.True(result.ContainsKey(dtmi));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi);
+ }
+
+ [TestCase("dtmi:com:example:base;2",
+ "dtmi:com:example:Freezer;1," +
+ "dtmi:com:example:Thermostat;1")]
+ public async Task ResolveSingleModelWithDepsFromExtendsInlineVariant(string dtmi, string expected)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(dtmi);
+ var expectedDtmis = $"{dtmi},{expected}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+ }
+
+ [TestCase("dtmi:azure:DeviceManagement:DeviceInformation;1", "dtmi:azure:DeviceManagement:DeviceInformation;1")]
+ public async Task ResolveEnsuresNoDupes(string dtmiDupe1, string dtmiDupe2)
+ {
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local);
+ var result = await client.ResolveAsync(new[] { dtmiDupe1, dtmiDupe2 });
+ Assert.True(result.Keys.Count == 1);
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmiDupe1]) == dtmiDupe1);
+ }
+
+ [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Local)]
+ [TestCase("dtmi:com:example:Thermostat;1", TestHelpers.ClientType.Remote)]
+ public async Task ResolveSingleModelWithDepsDisableDependencyResolution(string dtmi, TestHelpers.ClientType clientType)
+ {
+ ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.Disabled);
+ ResolverClient client = TestHelpers.GetTestClient(clientType, options);
+
+ IDictionary result = await client.ResolveAsync(dtmi);
+
+ Assert.True(result.Keys.Count == 1);
+ Assert.True(result.ContainsKey(dtmi));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[dtmi]) == dtmi);
+ }
+
+ [TestCase(
+ "dtmi:com:example:TemperatureController;1", // .expanded.json available locally.
+ "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1",
+ TestHelpers.ClientType.Local)]
+ [TestCase(
+ "dtmi:com:example:TemperatureController;1", // .expanded.json available remotely.
+ "dtmi:com:example:Thermostat;1,dtmi:azure:DeviceManagement:DeviceInformation;1",
+ TestHelpers.ClientType.Remote)]
+ public async Task ResolveSingleModelTryFromExpanded(string dtmi, string expectedDeps, TestHelpers.ClientType clientType)
+ {
+ var expectedDtmis = $"{dtmi},{expectedDeps}".Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+
+ ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded);
+ ResolverClient client = TestHelpers.GetTestClient(clientType, options);
+
+ var result = await client.ResolveAsync(dtmi);
+
+ Assert.True(result.Keys.Count == expectedDtmis.Length);
+ foreach (var id in expectedDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+
+ // TODO: Evaluate using Azure.Core.TestFramework in future iteration.
+
+ /*
+ string expectedPath = DtmiConventions.DtmiToQualifiedPath(
+ dtmi,
+ repoType == "local" ? client.RepositoryUri.AbsolutePath : client.RepositoryUri.AbsoluteUri,
+ fromExpanded: true);
+ _logger.ValidateLog(StandardStrings.FetchingContent(expectedPath), LogLevel.Trace, Times.Once());
+ */
+ }
+
+ [TestCase("dtmi:com:example:TemperatureController;1," + // Expanded available.
+ "dtmi:com:example:Thermostat;1," +
+ "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "dtmi:com:example:ColdStorage;1," + // Model uses extends[], No Expanded available.
+ "dtmi:com:example:Room;1," +
+ "dtmi:com:example:Freezer;1")]
+ public async Task ResolveMultipleModelsTryFromExpandedPartial(string dtmisExpanded, string dtmisNonExpanded)
+ {
+ string[] expandedDtmis = dtmisExpanded.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+ string[] nonExpandedDtmis = dtmisNonExpanded.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+ string[] totalDtmis = expandedDtmis.Concat(nonExpandedDtmis).ToArray();
+
+ ResolverClientOptions options = new ResolverClientOptions(resolutionOption: DependencyResolutionOption.TryFromExpanded);
+ ResolverClient client = TestHelpers.GetTestClient(TestHelpers.ClientType.Local, options);
+
+ // Multi-resolve dtmi:com:example:TemperatureController;1 + dtmi:com:example:ColdStorage;1
+ IDictionary result = await client.ResolveAsync(new[] { expandedDtmis[0], nonExpandedDtmis[0] });
+
+ Assert.True(result.Keys.Count == totalDtmis.Length);
+ foreach (string id in totalDtmis)
+ {
+ Assert.True(result.ContainsKey(id));
+ Assert.True(TestHelpers.ParseRootDtmiFromJson(result[id]) == id);
+ }
+
+ // TODO: Evaluate using Azure.Core.TestFramework in future iteration.
+
+ /*
+ string expandedModelPath = DtmiConventions.DtmiToQualifiedPath(expandedDtmis[0], localClient.RepositoryUri.AbsolutePath, fromExpanded: true);
+ _logger.ValidateLog(StandardStrings.FetchingContent(expandedModelPath), LogLevel.Trace, Times.Once());
+
+ foreach (string dtmi in nonExpandedDtmis)
+ {
+ string expectedPath = DtmiConventions.DtmiToQualifiedPath(dtmi, localClient.RepositoryUri.AbsolutePath, fromExpanded: true);
+ _logger.ValidateLog(StandardStrings.FetchingContent(expectedPath), LogLevel.Trace, Times.Once());
+ _logger.ValidateLog(StandardStrings.ErrorAccessLocalRepositoryModel(expectedPath), LogLevel.Warning, Times.Once());
+ }
+ */
+ }
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs
new file mode 100644
index 000000000000..a1e3e3020fb7
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using NUnit.Framework;
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+
+namespace Azure.Iot.ModelsRepository.Tests
+{
+ public class TestHelpers
+ {
+ private static readonly string s_fallbackTestRemoteRepo = "https://devicemodels.azure.com/";
+ public enum ClientType
+ {
+ Local,
+ Remote
+ }
+
+ public static string ParseRootDtmiFromJson(string json)
+ {
+ var options = new JsonDocumentOptions
+ {
+ AllowTrailingCommas = true
+ };
+
+ string dtmi = string.Empty;
+ using (JsonDocument document = JsonDocument.Parse(json, options))
+ {
+ dtmi = document.RootElement.GetProperty("@id").GetString();
+ }
+ return dtmi;
+ }
+
+ public static ResolverClient GetTestClient(ClientType clientType, ResolverClientOptions clientOptions = null)
+ {
+ if (clientType == ClientType.Local)
+ return new ResolverClient(TestLocalModelRepository, clientOptions);
+ if (clientType == ClientType.Remote)
+ return new ResolverClient(TestRemoteModelRepository, clientOptions);
+
+ throw new ArgumentException();
+ }
+
+ public static string TestDirectoryPath => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+ public static string TestLocalModelRepository => Path.Combine(TestDirectoryPath, "TestModelRepo");
+
+ public static string TestRemoteModelRepository => Environment.GetEnvironmentVariable("PNP_TEST_REMOTE_REPO") ?? s_fallbackTestRemoteRepo;
+ }
+}
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json
new file mode 100644
index 000000000000..8a37e6d2c2c3
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-1.json
@@ -0,0 +1,64 @@
+{
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "@type": "Interface",
+ "displayName": "Device Information",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "manufacturer",
+ "displayName": "Manufacturer",
+ "schema": "string",
+ "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso."
+ },
+ {
+ "@type": "Property",
+ "name": "model",
+ "displayName": "Device model",
+ "schema": "string",
+ "description": "Device model name or ID. Ex. Surface Book 2."
+ },
+ {
+ "@type": "Property",
+ "name": "swVersion",
+ "displayName": "Software version",
+ "schema": "string",
+ "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45"
+ },
+ {
+ "@type": "Property",
+ "name": "osName",
+ "displayName": "Operating system name",
+ "schema": "string",
+ "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core."
+ },
+ {
+ "@type": "Property",
+ "name": "processorArchitecture",
+ "displayName": "Processor architecture",
+ "schema": "string",
+ "description": "Architecture of the processor on the device. Ex. x64 or ARM."
+ },
+ {
+ "@type": "Property",
+ "name": "processorManufacturer",
+ "displayName": "Processor manufacturer",
+ "schema": "string",
+ "description": "Name of the manufacturer of the processor on the device. Ex. Intel."
+ },
+ {
+ "@type": "Property",
+ "name": "totalStorage",
+ "displayName": "Total storage",
+ "schema": "double",
+ "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes."
+ },
+ {
+ "@type": "Property",
+ "name": "totalMemory",
+ "displayName": "Total memory",
+ "schema": "double",
+ "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json
new file mode 100644
index 000000000000..d35b8a3e3a1d
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/azure/devicemanagement/deviceinformation-2.json
@@ -0,0 +1,16 @@
+{
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:azure:DeviceManagement:DeviceInformation;2",
+ "@type": "Interface",
+ "extends": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "displayName": "Device Information",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "osKernelVersion",
+ "displayName": "OS Kernel Version",
+ "schema": "string",
+ "description": "OS Kernel Version. Ex. Linux 4.15.0-54-generic x86_64."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json
new file mode 100644
index 000000000000..85424e8a229e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-1.json
@@ -0,0 +1,56 @@
+{
+ "@id": "dtmi:com:example:base;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "baseSerialNumber",
+ "schema": "string"
+ }
+ ],
+ "displayName": {
+ "en": "mybaseProp"
+ },
+ "extends": [
+ {
+ "@id": "dtmi:com:example:basic;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "serialNumber",
+ "schema": "string",
+ "writable": false
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Temperature"
+ ],
+ "displayName": {
+ "en": "temperature"
+ },
+ "name": "temperature",
+ "schema": "double",
+ "unit": "degreeCelsius"
+ },
+ {
+ "@type": "Property",
+ "displayName": {
+ "en": "targetTemperature"
+ },
+ "name": "targetTemperature",
+ "schema": "double",
+ "writable": true
+ }
+ ],
+ "displayName": {
+ "en": "Basic"
+ }
+ }
+ ],
+ "@context": [
+ "dtmi:iotcentral:context;2",
+ "dtmi:dtdl:context;2"
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json
new file mode 100644
index 000000000000..29accafcc252
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/base-2.json
@@ -0,0 +1,57 @@
+{
+ "@id": "dtmi:com:example:base;2",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "baseSerialNumber",
+ "schema": "string"
+ }
+ ],
+ "displayName": {
+ "en": "mybaseProp"
+ },
+ "extends": [
+ {
+ "@id": "dtmi:com:example:basic;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "serialNumber",
+ "schema": "string",
+ "writable": false
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "Temperature"
+ ],
+ "displayName": {
+ "en": "temperature"
+ },
+ "name": "temperature",
+ "schema": "double",
+ "unit": "degreeCelsius"
+ },
+ {
+ "@type": "Property",
+ "displayName": {
+ "en": "targetTemperature"
+ },
+ "name": "targetTemperature",
+ "schema": "double",
+ "writable": true
+ }
+ ],
+ "displayName": {
+ "en": "Basic"
+ }
+ },
+ "dtmi:com:example:Freezer;1"
+ ],
+ "@context": [
+ "dtmi:iotcentral:context;2",
+ "dtmi:dtdl:context;2"
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json
new file mode 100644
index 000000000000..c8b6bc6f7375
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/building-1.json
@@ -0,0 +1,19 @@
+{
+ "@id": "dtmi:com:example:Building;1",
+ "@type": "Interface",
+ "displayName": "Building",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "name",
+ "schema": "string",
+ "writable": true
+ },
+ {
+ "@type": "Relationship",
+ "name": "contains",
+ "target": "dtmi:com:example:Room;1"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json
new file mode 100644
index 000000000000..f912746c0040
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/camera-3.json
@@ -0,0 +1,13 @@
+{
+ "@id": "dtmi:com:example:Camera;3",
+ "@type": "Interface",
+ "displayName": "Phone",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json
new file mode 100644
index 000000000000..a3b8466118a9
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/coldstorage-1.json
@@ -0,0 +1,13 @@
+{
+ "@id": "dtmi:com:example:ColdStorage;1",
+ "@type": "Interface",
+ "extends": ["dtmi:com:example:Room;1", "dtmi:com:example:Freezer;1"],
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "capacity",
+ "schema": "integer"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json
new file mode 100644
index 000000000000..2e756ee73b6e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/conferenceroom-1.json
@@ -0,0 +1,13 @@
+{
+ "@id": "dtmi:com:example:ConferenceRoom;1",
+ "@type": "Interface",
+ "extends": "dtmi:com:example:Room;1",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "capacity",
+ "schema": "integer"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json
new file mode 100644
index 000000000000..6006b6673299
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/freezer-1.json
@@ -0,0 +1,12 @@
+{
+ "@id": "dtmi:com:example:Freezer;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:com:example:Thermostat;1"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json
new file mode 100644
index 000000000000..e91626b56e44
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/incompleteexpanded-1.expanded.json
@@ -0,0 +1,151 @@
+[
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:com:example:IncompleteExpanded;1",
+ "@type": "Interface",
+ "displayName": "Incomplete Expanded Temperature Controller",
+ "description": "Device with two thermostats and remote reboot.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "DataSize"
+ ],
+ "name": "workingSet",
+ "displayName": "Working Set",
+ "description": "Current working set of the device memory in KiB.",
+ "schema": "double",
+ "unit": "kibibyte"
+ },
+ {
+ "@type": "Property",
+ "name": "serialNumber",
+ "displayName": "Serial Number",
+ "description": "Serial number of the device.",
+ "schema": "string"
+ },
+ {
+ "@type": "Command",
+ "name": "reboot",
+ "displayName": "Reboot",
+ "description": "Reboots the device after waiting the number of seconds specified.",
+ "request": {
+ "name": "delay",
+ "displayName": "Delay",
+ "description": "Number of seconds to wait before rebooting the device.",
+ "schema": "integer"
+ }
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat1",
+ "displayName": "Thermostat One",
+ "description": "Thermostat One of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat2",
+ "displayName": "Thermostat Two",
+ "description": "Thermostat Two of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "name": "deviceInformation",
+ "displayName": "Device Information interface",
+ "description": "Optional interface with basic device hardware information."
+ }
+ ]
+ },
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:com:example:Thermostat;1",
+ "@type": "Interface",
+ "displayName": "Thermostat",
+ "description": "Reports current temperature and provides desired temperature control.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Temperature"
+ ],
+ "name": "temperature",
+ "displayName": "Temperature",
+ "description": "Temperature in degrees Celsius.",
+ "schema": "double",
+ "unit": "degreeCelsius"
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "targetTemperature",
+ "schema": "double",
+ "displayName": "Target Temperature",
+ "description": "Allows to remotely specify the desired target temperature.",
+ "unit": "degreeCelsius",
+ "writable": true
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "maxTempSinceLastReboot",
+ "schema": "double",
+ "unit": "degreeCelsius",
+ "displayName": "Max temperature since last reboot.",
+ "description": "Returns the max temperature since last device reboot."
+ },
+ {
+ "@type": "Command",
+ "name": "getMaxMinReport",
+ "displayName": "Get Max-Min report.",
+ "description": "This command returns the max, min and average temperature from the specified time to the current time.",
+ "request": {
+ "name": "since",
+ "displayName": "Since",
+ "description": "Period to return the max-min report.",
+ "schema": "dateTime"
+ },
+ "response": {
+ "name": "tempReport",
+ "displayName": "Temperature Report",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "maxTemp",
+ "displayName": "Max temperature",
+ "schema": "double"
+ },
+ {
+ "name": "minTemp",
+ "displayName": "Min temperature",
+ "schema": "double"
+ },
+ {
+ "name": "avgTemp",
+ "displayName": "Average Temperature",
+ "schema": "double"
+ },
+ {
+ "name": "startTime",
+ "displayName": "Start Time",
+ "schema": "dateTime"
+ },
+ {
+ "name": "endTime",
+ "displayName": "End Time",
+ "schema": "dateTime"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json
new file mode 100644
index 000000000000..4f18d7b17658
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-1.json
@@ -0,0 +1,13 @@
+{
+ "@id": "dtmi:com:example:invalidmodel;1",
+ "@type": "Interface",
+ "displayName": "Phone",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:azure:fakeDeviceManagement:FakeDeviceInformation;2"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json
new file mode 100644
index 000000000000..61443734cd90
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/invalidmodel-2.json
@@ -0,0 +1,23 @@
+{
+ "@id": "dtmi:com:example:Phone;2",
+ "@type": "Interfacez",
+ "displayName": "Phone",
+ "contentsz": [
+ {
+ "@type": "Component",
+ "name": "frontCamera",
+ "schema": "dtmi:com:example:Camera;3"
+ },
+ {
+ "@type": "Component",
+ "name": "backCamera",
+ "schema": "dtmi:com:example:Camera;3"
+ },
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:azure:deviceManagement:DeviceInformation;2"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json
new file mode 100644
index 000000000000..26c7efbdedc0
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/phone-2.json
@@ -0,0 +1,23 @@
+{
+ "@id": "dtmi:com:example:Phone;2",
+ "@type": "Interface",
+ "displayName": "Phone",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "frontCamera",
+ "schema": "dtmi:com:example:Camera;3"
+ },
+ {
+ "@type": "Component",
+ "name": "backCamera",
+ "schema": "dtmi:com:example:Camera;3"
+ },
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;2"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json
new file mode 100644
index 000000000000..1a07edec4d98
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/room-1.json
@@ -0,0 +1,12 @@
+{
+ "@id": "dtmi:com:example:Room;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "occupied",
+ "schema": "boolean"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json
new file mode 100644
index 000000000000..14e8e294189e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.expanded.json
@@ -0,0 +1,215 @@
+[
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:com:example:TemperatureController;1",
+ "@type": "Interface",
+ "displayName": "Temperature Controller",
+ "description": "Device with two thermostats and remote reboot.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "DataSize"
+ ],
+ "name": "workingSet",
+ "displayName": "Working Set",
+ "description": "Current working set of the device memory in KiB.",
+ "schema": "double",
+ "unit": "kibibyte"
+ },
+ {
+ "@type": "Property",
+ "name": "serialNumber",
+ "displayName": "Serial Number",
+ "description": "Serial number of the device.",
+ "schema": "string"
+ },
+ {
+ "@type": "Command",
+ "name": "reboot",
+ "displayName": "Reboot",
+ "description": "Reboots the device after waiting the number of seconds specified.",
+ "request": {
+ "name": "delay",
+ "displayName": "Delay",
+ "description": "Number of seconds to wait before rebooting the device.",
+ "schema": "integer"
+ }
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat1",
+ "displayName": "Thermostat One",
+ "description": "Thermostat One of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat2",
+ "displayName": "Thermostat Two",
+ "description": "Thermostat Two of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "name": "deviceInformation",
+ "displayName": "Device Information interface",
+ "description": "Optional interface with basic device hardware information."
+ }
+ ]
+ },
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:com:example:Thermostat;1",
+ "@type": "Interface",
+ "displayName": "Thermostat",
+ "description": "Reports current temperature and provides desired temperature control.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Temperature"
+ ],
+ "name": "temperature",
+ "displayName": "Temperature",
+ "description": "Temperature in degrees Celsius.",
+ "schema": "double",
+ "unit": "degreeCelsius"
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "targetTemperature",
+ "schema": "double",
+ "displayName": "Target Temperature",
+ "description": "Allows to remotely specify the desired target temperature.",
+ "unit": "degreeCelsius",
+ "writable": true
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "maxTempSinceLastReboot",
+ "schema": "double",
+ "unit": "degreeCelsius",
+ "displayName": "Max temperature since last reboot.",
+ "description": "Returns the max temperature since last device reboot."
+ },
+ {
+ "@type": "Command",
+ "name": "getMaxMinReport",
+ "displayName": "Get Max-Min report.",
+ "description": "This command returns the max, min and average temperature from the specified time to the current time.",
+ "request": {
+ "name": "since",
+ "displayName": "Since",
+ "description": "Period to return the max-min report.",
+ "schema": "dateTime"
+ },
+ "response": {
+ "name": "tempReport",
+ "displayName": "Temperature Report",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "maxTemp",
+ "displayName": "Max temperature",
+ "schema": "double"
+ },
+ {
+ "name": "minTemp",
+ "displayName": "Min temperature",
+ "schema": "double"
+ },
+ {
+ "name": "avgTemp",
+ "displayName": "Average Temperature",
+ "schema": "double"
+ },
+ {
+ "name": "startTime",
+ "displayName": "Start Time",
+ "schema": "dateTime"
+ },
+ {
+ "name": "endTime",
+ "displayName": "End Time",
+ "schema": "dateTime"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "@type": "Interface",
+ "displayName": "Device Information",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "manufacturer",
+ "displayName": "Manufacturer",
+ "schema": "string",
+ "description": "Company name of the device manufacturer. This could be the same as the name of the original equipment manufacturer (OEM). Ex. Contoso."
+ },
+ {
+ "@type": "Property",
+ "name": "model",
+ "displayName": "Device model",
+ "schema": "string",
+ "description": "Device model name or ID. Ex. Surface Book 2."
+ },
+ {
+ "@type": "Property",
+ "name": "swVersion",
+ "displayName": "Software version",
+ "schema": "string",
+ "description": "Version of the software on your device. This could be the version of your firmware. Ex. 1.3.45"
+ },
+ {
+ "@type": "Property",
+ "name": "osName",
+ "displayName": "Operating system name",
+ "schema": "string",
+ "description": "Name of the operating system on the device. Ex. Windows 10 IoT Core."
+ },
+ {
+ "@type": "Property",
+ "name": "processorArchitecture",
+ "displayName": "Processor architecture",
+ "schema": "string",
+ "description": "Architecture of the processor on the device. Ex. x64 or ARM."
+ },
+ {
+ "@type": "Property",
+ "name": "processorManufacturer",
+ "displayName": "Processor manufacturer",
+ "schema": "string",
+ "description": "Name of the manufacturer of the processor on the device. Ex. Intel."
+ },
+ {
+ "@type": "Property",
+ "name": "totalStorage",
+ "displayName": "Total storage",
+ "schema": "double",
+ "description": "Total available storage on the device in kilobytes. Ex. 2048000 kilobytes."
+ },
+ {
+ "@type": "Property",
+ "name": "totalMemory",
+ "displayName": "Total memory",
+ "schema": "double",
+ "description": "Total available memory on the device in kilobytes. Ex. 256000 kilobytes."
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json
new file mode 100644
index 000000000000..c455ddf8bae6
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/temperaturecontroller-1.json
@@ -0,0 +1,60 @@
+{
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:com:example:TemperatureController;1",
+ "@type": "Interface",
+ "displayName": "Temperature Controller",
+ "description": "Device with two thermostats and remote reboot.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "DataSize"
+ ],
+ "name": "workingSet",
+ "displayName": "Working Set",
+ "description": "Current working set of the device memory in KiB.",
+ "schema": "double",
+ "unit": "kibibyte"
+ },
+ {
+ "@type": "Property",
+ "name": "serialNumber",
+ "displayName": "Serial Number",
+ "description": "Serial number of the device.",
+ "schema": "string"
+ },
+ {
+ "@type": "Command",
+ "name": "reboot",
+ "displayName": "Reboot",
+ "description": "Reboots the device after waiting the number of seconds specified.",
+ "request": {
+ "name": "delay",
+ "displayName": "Delay",
+ "description": "Number of seconds to wait before rebooting the device.",
+ "schema": "integer"
+ }
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat1",
+ "displayName": "Thermostat One",
+ "description": "Thermostat One of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:com:example:Thermostat;1",
+ "name": "thermostat2",
+ "displayName": "Thermostat Two",
+ "description": "Thermostat Two of Two."
+ },
+ {
+ "@type": "Component",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1",
+ "name": "deviceInformation",
+ "displayName": "Device Information interface",
+ "description": "Optional interface with basic device hardware information."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json
new file mode 100644
index 000000000000..315a307bbcb3
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/com/example/thermostat-1.json
@@ -0,0 +1,19 @@
+{
+ "@id": "dtmi:com:example:Thermostat;1",
+ "@type": "Interface",
+ "displayName": "Thermostat",
+ "contents": [
+ {
+ "@type": "Telemetry",
+ "name": "temp",
+ "schema": "double"
+ },
+ {
+ "@type": "Property",
+ "name": "setPointTemp",
+ "writable": true,
+ "schema": "double"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json
new file mode 100644
index 000000000000..33c9554664a6
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-1.json
@@ -0,0 +1,31 @@
+{
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:company:demodevice;1",
+ "@type": "Interface",
+ "displayName": "demodevice",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "c1",
+ "schema": "dtmi:azure:deviceManagement:DeviceInformation;1"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "temperature",
+ "schema": "double"
+ },
+ {
+ "@type": "Property",
+ "name": "deviceStatus",
+ "schema": "string"
+ },
+ {
+ "@type": "Command",
+ "name": "reboot",
+ "request": {
+ "name": "delay",
+ "schema": "integer"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json
new file mode 100644
index 000000000000..9d9b3bd2322a
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/company/demodevice-2.json
@@ -0,0 +1,31 @@
+{
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:company:demodevice;1",
+ "@type": "Interface",
+ "displayName": "demodevice",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "c1",
+ "schema": "dtmi:azure:DeviceManagement:DeviceInformation;1"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "temperature",
+ "schema": "double"
+ },
+ {
+ "@type": "Property",
+ "name": "deviceStatus",
+ "schema": "string"
+ },
+ {
+ "@type": "Command",
+ "name": "reboot",
+ "request": {
+ "name": "delay",
+ "schema": "integer"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json
new file mode 100644
index 000000000000..6006b6673299
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/badfilepath-1.json
@@ -0,0 +1,12 @@
+{
+ "@id": "dtmi:com:example:Freezer;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Component",
+ "name": "deviceInfo",
+ "schema": "dtmi:com:example:Thermostat;1"
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json
new file mode 100644
index 000000000000..0637a088a01e
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/emptyarray-1.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json
new file mode 100644
index 000000000000..6f2df812a67f
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/namespaceconflict-1.json
@@ -0,0 +1,37 @@
+{
+ "@id": "dtmi:strict:namespaceconflict;1",
+ "@type": "Interface",
+ "contents": [
+ {
+ "@type": "Telemetry",
+ "name": "accelerometer1",
+ "schema": "dtmi:com:example:acceleration;1"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "accelerometer2",
+ "schema": "dtmi:com:example:acceleration;1"
+ }
+ ],
+ "schemas": [
+ {
+ "@id": "dtmi:com:example:acceleration;1",
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "x",
+ "schema": "double"
+ },
+ {
+ "name": "y",
+ "schema": "double"
+ },
+ {
+ "name": "z",
+ "schema": "double"
+ }
+ ]
+ }
+ ],
+ "@context": "dtmi:dtdl:context;2"
+}
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json
new file mode 100644
index 000000000000..b25ba2ac3af6
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/nondtdl-1.json
@@ -0,0 +1 @@
+"content"
\ No newline at end of file
diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json
new file mode 100644
index 000000000000..1f282307a8c3
--- /dev/null
+++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestModelRepo/dtmi/strict/unsupportedrootarray-1.json
@@ -0,0 +1,91 @@
+[
+ {
+ "@context": "dtmi:dtdl:context;2",
+ "@id": "dtmi:strict:unsupportedrootarray;1",
+ "@type": "Interface",
+ "displayName": "Thermostat",
+ "description": "Reports current temperature and provides desired temperature control.",
+ "contents": [
+ {
+ "@type": [
+ "Telemetry",
+ "Temperature"
+ ],
+ "name": "temperature",
+ "displayName": "Temperature",
+ "description": "Temperature in degrees Celsius.",
+ "schema": "double",
+ "unit": "degreeCelsius"
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "targetTemperature",
+ "schema": "double",
+ "displayName": "Target Temperature",
+ "description": "Allows to remotely specify the desired target temperature.",
+ "unit": "degreeCelsius",
+ "writable": true
+ },
+ {
+ "@type": [
+ "Property",
+ "Temperature"
+ ],
+ "name": "maxTempSinceLastReboot",
+ "schema": "double",
+ "unit": "degreeCelsius",
+ "displayName": "Max temperature since last reboot.",
+ "description": "Returns the max temperature since last device reboot."
+ },
+ {
+ "@type": "Command",
+ "name": "getMaxMinReport",
+ "displayName": "Get Max-Min report.",
+ "description": "This command returns the max, min and average temperature from the specified time to the current time.",
+ "request": {
+ "name": "since",
+ "displayName": "Since",
+ "description": "Period to return the max-min report.",
+ "schema": "dateTime"
+ },
+ "response": {
+ "name": "tempReport",
+ "displayName": "Temperature Report",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "maxTemp",
+ "displayName": "Max temperature",
+ "schema": "double"
+ },
+ {
+ "name": "minTemp",
+ "displayName": "Min temperature",
+ "schema": "double"
+ },
+ {
+ "name": "avgTemp",
+ "displayName": "Average Temperature",
+ "schema": "double"
+ },
+ {
+ "name": "startTime",
+ "displayName": "Start Time",
+ "schema": "dateTime"
+ },
+ {
+ "name": "endTime",
+ "displayName": "End Time",
+ "schema": "dateTime"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/sdk/modelsrepository/ci.yml b/sdk/modelsrepository/ci.yml
new file mode 100644
index 000000000000..49b8441618fc
--- /dev/null
+++ b/sdk/modelsrepository/ci.yml
@@ -0,0 +1,32 @@
+# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.
+
+trigger:
+ branches:
+ include:
+ - master
+ - hotfix/*
+ - release/*
+ paths:
+ include:
+ - sdk/modelRepository/
+
+pr:
+ branches:
+ include:
+ - master
+ - feature/*
+ - hotfix/*
+ - release/*
+ paths:
+ include:
+ - sdk/modelRepository/
+
+extends:
+ template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml
+ parameters:
+ ServiceDirectory: modelRepository
+ ArtifactName: packages
+ Artifacts:
+ - name: Azure.IoT.ModelRepository
+ safeName: AzureIoTModelRepository
+
diff --git a/sdk/modelsrepository/tests.yml b/sdk/modelsrepository/tests.yml
new file mode 100644
index 000000000000..c88bde775291
--- /dev/null
+++ b/sdk/modelsrepository/tests.yml
@@ -0,0 +1,8 @@
+trigger: none
+
+extends:
+ template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml
+ parameters:
+ ServiceDirectory: modelrepository
+ Location: westus2
+ Clouds: Preview