├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── auto-merge-dependabot.yml
│ └── dotnet.yml
├── .gitignore
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── PartsInventoryConnector
├── ApplianceParts.csv
├── Data
│ ├── ApplianceDbContext.cs
│ ├── AppliancePart.cs
│ └── CsvDataLoader.cs
├── Graph
│ └── GraphHelper.cs
├── Migrations
│ └── ApplianceDbContextModelSnapshot.cs
├── PartsInventoryConnector.csproj
├── Program.cs
└── Settings.cs
├── README.md
├── SECURITY.md
├── docs
├── 1000.md
├── 1001.md
├── 1002.md
├── 1003.md
├── 1004.md
├── 1005.md
├── 1006.md
├── 1007.md
├── 1008.md
├── 1009.md
└── images
│ ├── 1000.png
│ ├── 1001.png
│ ├── 1002.png
│ ├── 1003.png
│ ├── 1004.png
│ ├── 1005.png
│ ├── 1006.png
│ ├── 1007.png
│ ├── 1008.png
│ └── 1009.png
├── images
└── dbbrowser.png
└── result-type.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "nuget"
4 | directory: "/PartsInventoryConnector/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: "github-actions"
8 | directory: /
9 | schedule:
10 | interval: "weekly"
11 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: Auto-merge dependabot updates
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 |
7 | permissions:
8 | pull-requests: write
9 | contents: write
10 |
11 | jobs:
12 |
13 | dependabot-merge:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | if: ${{ github.actor == 'dependabot[bot]' }}
18 |
19 | steps:
20 | - name: Dependabot metadata
21 | id: metadata
22 | uses: dependabot/fetch-metadata@v1.6.0
23 | with:
24 | github-token: "${{ secrets.GITHUB_TOKEN }}"
25 |
26 | - name: Enable auto-merge for Dependabot PRs
27 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
28 | run: gh pr merge --auto --merge "$PR_URL"
29 | env:
30 | PR_URL: ${{github.event.pull_request.html_url}}
31 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
32 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: dotnet build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: PartsInventoryConnector/
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 7.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
3 |
4 | parts.db
5 | parts.db-shm
6 | parts.db-wal
7 | lastuploadtime.bin
8 |
9 | OLD/
10 | Migrations/
11 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/PartsInventoryConnector/bin/Debug/net7.0/PartsInventoryConnector.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/PartsInventoryConnector",
16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17 | "console": "integratedTerminal",
18 | "internalConsoleOptions": "neverOpen",
19 | "stopAtEntry": false
20 | },
21 | {
22 | "name": ".NET Core Attach",
23 | "type": "coreclr",
24 | "request": "attach",
25 | "processId": "${command:pickProcess}"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "datetime",
4 | "lastuploadtime",
5 | "MSRC",
6 | "Refinable"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/PartsInventoryConnector/PartsInventoryConnector.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/PartsInventoryConnector/PartsInventoryConnector.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "${workspaceFolder}/PartsInventoryConnector/PartsInventoryConnector.csproj",
36 | "/property:GenerateFullPaths=true",
37 | "/consoleloggerparameters:NoSummary"
38 | ],
39 | "problemMatcher": "$msCompile"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 | - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2023 Microsoft
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/ApplianceParts.csv:
--------------------------------------------------------------------------------
1 | PartNumber,Name,Description,Price,Inventory,Appliances
2 | 1000,Door hinge,Door hinge for refrigerators,19.99,350,Contoso Fridge;Contoso Sub-zero Fridge;Contoso XL Fridge
3 | 1001,Door handle (stainless steel),Door handle for refrigerators,49.99,200,Contoso Fridge;Contoso Sub-zero Fridge;Contoso XL Fridge
4 | 1002,Door handle (black),Door handle for refrigerators,39.99,300,Contoso Fridge;Contoso Sub-zero Fridge;Contoso XL Fridge
5 | 1003,Door handle (white),Door handle for refrigerators,39.99,275,Contoso Fridge;Contoso Sub-zero Fridge;Contoso XL Fridge
6 | 1004,Compressor,Compressor for refrigerators,129.99,120,Contoso Fridge;Contoso Sub-zero Fridge;Contoso XL Fridge
7 | 1005,Door hinge,Door hinge for dishwashers,17.99,388,Contoso Diswasher;Contoso Super-silent Diswasher
8 | 1006,Door handle (stainless steel),Door handle for dishwashers,48.99,129,Contoso Diswasher;Contoso Super-silent Diswasher
9 | 1007,Door handle (black),Door handle for dishwashers,37.99,47,Contoso Diswasher;Contoso Super-silent Diswasher
10 | 1008,Door handle (white),Door handle for dishwashers,37.99,103,Contoso Diswasher;Contoso Super-silent Diswasher
11 | 1009,Water pump,Water pump for dishwashers,89.99,42,Contoso Diswasher;Contoso Super-silent Diswasher
12 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Data/ApplianceDbContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //
5 | using System.Text.Json;
6 | using Microsoft.Data.Sqlite;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.EntityFrameworkCore.ChangeTracking;
9 |
10 | namespace PartsInventoryConnector.Data;
11 |
12 | public class ApplianceDbContext : DbContext
13 | {
14 | public DbSet Parts => Set();
15 |
16 | public void EnsureDatabase()
17 | {
18 | if (Database.EnsureCreated() || !Parts.Any())
19 | {
20 | // File was just created (or is empty),
21 | // seed with data from CSV file
22 | var parts = CsvDataLoader.LoadPartsFromCsv("ApplianceParts.csv");
23 | Parts.AddRange(parts);
24 | SaveChanges();
25 | }
26 | }
27 |
28 | protected override void OnConfiguring(DbContextOptionsBuilder options)
29 | {
30 | options.UseSqlite("Data Source=parts.db");
31 | }
32 |
33 | protected override void OnModelCreating(ModelBuilder modelBuilder)
34 | {
35 | // EF Core can't store lists, so add a converter for the Appliances
36 | // property to serialize as a JSON string on save to DB
37 | modelBuilder.Entity()
38 | .Property(ap => ap.Appliances)
39 | .HasConversion(
40 | v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
41 | v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default)
42 | );
43 |
44 | // Add LastUpdated and IsDeleted shadow properties
45 | modelBuilder.Entity()
46 | .Property("LastUpdated")
47 | .HasDefaultValueSql("datetime()")
48 | .ValueGeneratedOnAddOrUpdate();
49 | modelBuilder.Entity()
50 | .Property("IsDeleted")
51 | .IsRequired()
52 | .HasDefaultValue(false);
53 |
54 | // Exclude any soft-deleted items (IsDeleted = 1) from
55 | // the default query sets
56 | modelBuilder.Entity()
57 | .HasQueryFilter(a => !EF.Property(a, "IsDeleted"));
58 | }
59 |
60 | public override int SaveChanges()
61 | {
62 | // Prevent deletes of data, instead mark the item as deleted
63 | // by setting IsDeleted = true.
64 | foreach(var entry in ChangeTracker.Entries()
65 | .Where(e => e.State == EntityState.Deleted))
66 | {
67 | if (entry.Entity.GetType() == typeof(AppliancePart))
68 | {
69 | SoftDelete(entry);
70 | }
71 |
72 | }
73 |
74 | return base.SaveChanges();
75 | }
76 |
77 | private void SoftDelete(EntityEntry entry)
78 | {
79 | var partNumber = new SqliteParameter("@partNumber",
80 | entry.OriginalValues["PartNumber"]);
81 |
82 | Database.ExecuteSqlRaw(
83 | "UPDATE Parts SET IsDeleted = 1 WHERE PartNumber = @partNumber",
84 | partNumber);
85 |
86 | entry.State = EntityState.Detached;
87 | }
88 | }
89 | //
90 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Data/AppliancePart.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //
5 | using System.ComponentModel.DataAnnotations;
6 | using System.Text.Json.Serialization;
7 | using Microsoft.Graph.Models.ExternalConnectors;
8 |
9 | namespace PartsInventoryConnector.Data;
10 |
11 | public class AppliancePart
12 | {
13 | [JsonPropertyName("appliances@odata.type")]
14 | private const string AppliancesODataType = "Collection(String)";
15 |
16 | [Key]
17 | public int PartNumber { get; set; }
18 | public string? Name { get; set; }
19 | public string? Description { get; set; }
20 | public double Price { get; set; }
21 | public int Inventory { get; set; }
22 | public List? Appliances { get; set; }
23 |
24 | public Properties AsExternalItemProperties()
25 | {
26 | _ = Name ?? throw new MemberAccessException("Name cannot be null");
27 | _ = Description ?? throw new MemberAccessException("Description cannot be null");
28 | _ = Appliances ?? throw new MemberAccessException("Appliances cannot be null");
29 |
30 | var properties = new Properties
31 | {
32 | AdditionalData = new Dictionary
33 | {
34 | { "partNumber", PartNumber },
35 | { "name", Name },
36 | { "description", Description },
37 | { "price", Price },
38 | { "inventory", Inventory },
39 | { "appliances@odata.type", "Collection(String)" },
40 | { "appliances", Appliances }
41 | }
42 | };
43 |
44 | return properties;
45 | }
46 | }
47 | //
48 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Data/CsvDataLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //
5 | using System.Globalization;
6 | using CsvHelper;
7 | using CsvHelper.Configuration;
8 | using CsvHelper.TypeConversion;
9 |
10 | namespace PartsInventoryConnector.Data;
11 |
12 | public static class CsvDataLoader
13 | {
14 | public static List LoadPartsFromCsv(string filePath)
15 | {
16 | using var reader = new StreamReader(filePath);
17 | using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
18 | csv.Context.RegisterClassMap();
19 |
20 | return new List(csv.GetRecords());
21 | }
22 | }
23 |
24 | public class ApplianceListConverter : DefaultTypeConverter
25 | {
26 | public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
27 | {
28 | var appliances = text?.Split(';') ?? Array.Empty();
29 | return new List(appliances);
30 | }
31 | }
32 |
33 | public class AppliancePartMap : ClassMap
34 | {
35 | public AppliancePartMap()
36 | {
37 | Map(m => m.PartNumber);
38 | Map(m => m.Name);
39 | Map(m => m.Description);
40 | Map(m => m.Price);
41 | Map(m => m.Inventory);
42 | Map(m => m.Appliances).TypeConverter();
43 | }
44 | }
45 | //
46 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Graph/GraphHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | //
5 | using Azure.Identity;
6 | using Microsoft.Graph;
7 | using Microsoft.Graph.Models.ExternalConnectors;
8 | using Microsoft.Kiota.Authentication.Azure;
9 | //
10 |
11 | namespace PartsInventoryConnector.Graph;
12 |
13 | public static class GraphHelper
14 | {
15 | //
16 | private static GraphServiceClient? graphClient;
17 | private static HttpClient? httpClient;
18 | public static void Initialize(Settings settings)
19 | {
20 | // Create a credential that uses the client credentials
21 | // authorization flow
22 | var credential = new ClientSecretCredential(
23 | settings.TenantId, settings.ClientId, settings.ClientSecret);
24 |
25 | // Create an HTTP client
26 | httpClient = GraphClientFactory.Create();
27 |
28 | // Create an auth provider
29 | var authProvider = new AzureIdentityAuthenticationProvider(
30 | credential, scopes: new[] { "https://graph.microsoft.com/.default" });
31 |
32 | // Create a Graph client using the credential
33 | graphClient = new GraphServiceClient(httpClient, authProvider);
34 | }
35 | //
36 |
37 | //
38 | public static async Task CreateConnectionAsync(string id, string name, string? description)
39 | {
40 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
41 |
42 | var newConnection = new ExternalConnection
43 | {
44 | Id = id,
45 | Name = name,
46 | Description = description,
47 | };
48 |
49 | return await graphClient.External.Connections.PostAsync(newConnection);
50 | }
51 | //
52 |
53 | //
54 | public static async Task GetExistingConnectionsAsync()
55 | {
56 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
57 |
58 | return await graphClient.External.Connections.GetAsync();
59 | }
60 | //
61 |
62 | //
63 | public static async Task DeleteConnectionAsync(string? connectionId)
64 | {
65 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
66 | _ = connectionId ?? throw new ArgumentException("connectionId is required");
67 |
68 | await graphClient.External.Connections[connectionId].DeleteAsync();
69 | }
70 | //
71 |
72 | //
73 | public static async Task RegisterSchemaAsync(string? connectionId, Schema schema)
74 | {
75 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
76 | _ = httpClient ?? throw new MemberAccessException("httpClient is null");
77 | _ = connectionId ?? throw new ArgumentException("connectionId is required");
78 | // Use the Graph SDK's request builder to generate the request URL
79 | var requestInfo = graphClient.External
80 | .Connections[connectionId]
81 | .Schema
82 | .ToGetRequestInformation();
83 |
84 | requestInfo.SetContentFromParsable(graphClient.RequestAdapter, "application/json", schema);
85 |
86 | // Convert the SDK request to an HttpRequestMessage
87 | var requestMessage = await graphClient.RequestAdapter
88 | .ConvertToNativeRequestAsync(requestInfo);
89 | _ = requestMessage ?? throw new Exception("Could not create native HTTP request");
90 | requestMessage.Method = HttpMethod.Post;
91 | requestMessage.Headers.Add("Prefer", "respond-async");
92 |
93 | // Send the request
94 | var responseMessage = await httpClient.SendAsync(requestMessage) ??
95 | throw new Exception("No response returned from API");
96 |
97 | if (responseMessage.IsSuccessStatusCode)
98 | {
99 | // The operation ID is contained in the Location header returned
100 | // in the response
101 | var operationId = responseMessage.Headers.Location?.Segments.Last() ??
102 | throw new Exception("Could not get operation ID from Location header");
103 | await WaitForOperationToCompleteAsync(connectionId, operationId);
104 | }
105 | else
106 | {
107 | throw new ServiceException("Registering schema failed",
108 | responseMessage.Headers, (int)responseMessage.StatusCode);
109 | }
110 | }
111 |
112 | private static async Task WaitForOperationToCompleteAsync(string connectionId, string operationId)
113 | {
114 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
115 |
116 | do
117 | {
118 | var operation = await graphClient.External
119 | .Connections[connectionId]
120 | .Operations[operationId]
121 | .GetAsync();
122 |
123 | if (operation?.Status == ConnectionOperationStatus.Completed)
124 | {
125 | return;
126 | }
127 | else if (operation?.Status == ConnectionOperationStatus.Failed)
128 | {
129 | throw new ServiceException($"Schema operation failed: {operation?.Error?.Code} {operation?.Error?.Message}");
130 | }
131 |
132 | // Wait 5 seconds and check again
133 | await Task.Delay(5000);
134 | } while (true);
135 | }
136 | //
137 |
138 | //
139 | public static async Task GetSchemaAsync(string? connectionId)
140 | {
141 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
142 | _ = connectionId ?? throw new ArgumentException("connectionId is null");
143 |
144 | return await graphClient.External
145 | .Connections[connectionId]
146 | .Schema
147 | .GetAsync();
148 | }
149 | //
150 |
151 | //
152 | public static async Task AddOrUpdateItemAsync(string? connectionId, ExternalItem item)
153 | {
154 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
155 | _ = connectionId ?? throw new ArgumentException("connectionId is null");
156 |
157 | await graphClient.External
158 | .Connections[connectionId]
159 | .Items[item.Id]
160 | .PutAsync(item);
161 | }
162 | //
163 |
164 | //
165 | public static async Task DeleteItemAsync(string? connectionId, string? itemId)
166 | {
167 | _ = graphClient ?? throw new MemberAccessException("graphClient is null");
168 | _ = connectionId ?? throw new ArgumentException("connectionId is null");
169 | _ = itemId ?? throw new ArgumentException("itemId is null");
170 |
171 | await graphClient.External
172 | .Connections[connectionId]
173 | .Items[itemId]
174 | .DeleteAsync();
175 | }
176 | //
177 | }
178 |
179 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Migrations/ApplianceDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using PartsInventoryConnector.Data;
7 |
8 | #nullable disable
9 |
10 | namespace PartsInventoryConnector.Migrations
11 | {
12 | [DbContext(typeof(ApplianceDbContext))]
13 | partial class ApplianceDbContextModelSnapshot : ModelSnapshot
14 | {
15 | protected override void BuildModel(ModelBuilder modelBuilder)
16 | {
17 | #pragma warning disable 612, 618
18 | modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
19 |
20 | modelBuilder.Entity("PartsInventoryConnector.Data.AppliancePart", b =>
21 | {
22 | b.Property("PartNumber")
23 | .ValueGeneratedOnAdd()
24 | .HasColumnType("INTEGER");
25 |
26 | b.Property("Appliances")
27 | .HasColumnType("TEXT");
28 |
29 | b.Property("Description")
30 | .HasColumnType("TEXT");
31 |
32 | b.Property("Inventory")
33 | .HasColumnType("INTEGER");
34 |
35 | b.Property("IsDeleted")
36 | .ValueGeneratedOnAdd()
37 | .HasColumnType("INTEGER")
38 | .HasDefaultValue(false);
39 |
40 | b.Property("LastUpdated")
41 | .ValueGeneratedOnAddOrUpdate()
42 | .HasColumnType("TEXT")
43 | .HasDefaultValueSql("datetime()");
44 |
45 | b.Property("Name")
46 | .HasColumnType("TEXT");
47 |
48 | b.Property("Price")
49 | .HasColumnType("REAL");
50 |
51 | b.HasKey("PartNumber");
52 |
53 | b.ToTable("Parts");
54 | });
55 | #pragma warning restore 612, 618
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/PartsInventoryConnector.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | enable
7 | enable
8 | fe0ecd18-91b6-44dd-8fd3-59aafa1a9e6b
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT license.
3 |
4 | //
5 | using System.Text.Json;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Graph;
8 | using Microsoft.Graph.Models.ExternalConnectors;
9 | using Microsoft.Graph.Models.ODataErrors;
10 | using PartsInventoryConnector;
11 | using PartsInventoryConnector.Data;
12 | using PartsInventoryConnector.Graph;
13 |
14 | Console.WriteLine("Parts Inventory Search Connector\n");
15 |
16 | var settings = Settings.LoadSettings();
17 |
18 | // Initialize Graph
19 | InitializeGraph(settings);
20 |
21 | ExternalConnection? currentConnection = null;
22 | int choice = -1;
23 |
24 | while (choice != 0)
25 | {
26 | Console.WriteLine($"Current connection: {(currentConnection == null ? "NONE" : currentConnection.Name)}\n");
27 | Console.WriteLine("Please choose one of the following options:");
28 | Console.WriteLine("0. Exit");
29 | Console.WriteLine("1. Create a connection");
30 | Console.WriteLine("2. Select an existing connection");
31 | Console.WriteLine("3. Delete current connection");
32 | Console.WriteLine("4. Register schema for current connection");
33 | Console.WriteLine("5. View schema for current connection");
34 | Console.WriteLine("6. Push updated items to current connection");
35 | Console.WriteLine("7. Push ALL items to current connection");
36 | Console.Write("Selection: ");
37 |
38 | try
39 | {
40 | choice = int.Parse(Console.ReadLine() ?? string.Empty);
41 | }
42 | catch (FormatException)
43 | {
44 | // Set to invalid value
45 | choice = -1;
46 | }
47 |
48 | switch(choice)
49 | {
50 | case 0:
51 | // Exit the program
52 | Console.WriteLine("Goodbye...");
53 | break;
54 | case 1:
55 | currentConnection = await CreateConnectionAsync();
56 | break;
57 | case 2:
58 | currentConnection = await SelectExistingConnectionAsync();
59 | break;
60 | case 3:
61 | await DeleteCurrentConnectionAsync(currentConnection);
62 | currentConnection = null;
63 | break;
64 | case 4:
65 | await RegisterSchemaAsync();
66 | break;
67 | case 5:
68 | await GetSchemaAsync();
69 | break;
70 | case 6:
71 | await UpdateItemsFromDatabaseAsync(true, settings.TenantId);
72 | break;
73 | case 7:
74 | await UpdateItemsFromDatabaseAsync(false, settings.TenantId);
75 | break;
76 | default:
77 | Console.WriteLine("Invalid choice! Please try again.");
78 | break;
79 | }
80 | }
81 |
82 | static string? PromptForInput(string prompt, bool valueRequired)
83 | {
84 | string? response;
85 |
86 | do
87 | {
88 | Console.WriteLine($"{prompt}:");
89 | response = Console.ReadLine();
90 | if (valueRequired && string.IsNullOrEmpty(response))
91 | {
92 | Console.WriteLine("You must provide a value");
93 | }
94 | } while (valueRequired && string.IsNullOrEmpty(response));
95 |
96 | return response;
97 | }
98 |
99 | static DateTime GetLastUploadTime()
100 | {
101 | if (File.Exists("lastuploadtime.bin"))
102 | {
103 | return DateTime.Parse(
104 | File.ReadAllText("lastuploadtime.bin")).ToUniversalTime();
105 | }
106 |
107 | return DateTime.MinValue;
108 | }
109 |
110 | static void SaveLastUploadTime(DateTime uploadTime)
111 | {
112 | File.WriteAllText("lastuploadtime.bin", uploadTime.ToString("u"));
113 | }
114 | //
115 |
116 | //
117 | void InitializeGraph(Settings settings)
118 | {
119 | try
120 | {
121 | GraphHelper.Initialize(settings);
122 | }
123 | catch (Exception ex)
124 | {
125 | Console.WriteLine($"Error initializing Graph: {ex.Message}");
126 | }
127 | }
128 | //
129 |
130 | //
131 | async Task CreateConnectionAsync()
132 | {
133 | var connectionId = PromptForInput(
134 | "Enter a unique ID for the new connection (3-32 characters)", true) ?? "ConnectionId";
135 | var connectionName = PromptForInput(
136 | "Enter a name for the new connection", true) ?? "ConnectionName";
137 | var connectionDescription = PromptForInput(
138 | "Enter a description for the new connection", false);
139 |
140 | try
141 | {
142 | // Create the connection
143 | var connection = await GraphHelper.CreateConnectionAsync(
144 | connectionId, connectionName, connectionDescription);
145 | Console.WriteLine($"New connection created - Name: {connection?.Name}, Id: {connection?.Id}");
146 | return connection;
147 | }
148 | catch (ODataError odataError)
149 | {
150 | Console.WriteLine($"Error creating connection: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
151 | return null;
152 | }
153 | }
154 | //
155 |
156 | //
157 | async Task SelectExistingConnectionAsync()
158 | {
159 | // TODO
160 | Console.WriteLine("Getting existing connections...");
161 | try
162 | {
163 | var response = await GraphHelper.GetExistingConnectionsAsync();
164 | var connections = response?.Value ?? new List();
165 | if (connections.Count <= 0)
166 | {
167 | Console.WriteLine("No connections exist. Please create a new connection");
168 | return null;
169 | }
170 |
171 | // Display connections
172 | Console.WriteLine("Choose one of the following connections:");
173 | var menuNumber = 1;
174 | foreach(var connection in connections)
175 | {
176 | Console.WriteLine($"{menuNumber++}. {connection.Name}");
177 | }
178 |
179 | ExternalConnection? selection = null;
180 |
181 | do
182 | {
183 | try
184 | {
185 | Console.Write("Selection: ");
186 | var choice = int.Parse(Console.ReadLine() ?? string.Empty);
187 | if (choice > 0 && choice <= connections.Count)
188 | {
189 | selection = connections[choice - 1];
190 | }
191 | else
192 | {
193 | Console.WriteLine("Invalid choice.");
194 | }
195 | }
196 | catch (FormatException)
197 | {
198 | Console.WriteLine("Invalid choice.");
199 | }
200 | } while (selection == null);
201 |
202 | return selection;
203 | }
204 | catch (ODataError odataError)
205 | {
206 | Console.WriteLine($"Error getting connections: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
207 | return null;
208 | }
209 | }
210 | //
211 |
212 | //
213 | async Task DeleteCurrentConnectionAsync(ExternalConnection? connection)
214 | {
215 | if (connection == null)
216 | {
217 | Console.WriteLine(
218 | "No connection selected. Please create a new connection or select an existing connection.");
219 | return;
220 | }
221 |
222 | try
223 | {
224 | await GraphHelper.DeleteConnectionAsync(connection.Id);
225 | Console.WriteLine($"{connection.Name} deleted successfully.");
226 | }
227 | catch (ODataError odataError)
228 | {
229 | Console.WriteLine($"Error deleting connection: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
230 | }
231 | }
232 | //
233 |
234 | //
235 | async Task RegisterSchemaAsync()
236 | {
237 | if (currentConnection == null)
238 | {
239 | Console.WriteLine("No connection selected. Please create a new connection or select an existing connection.");
240 | return;
241 | }
242 |
243 | Console.WriteLine("Registering schema, this may take a moment...");
244 |
245 | try
246 | {
247 | // Create the schema
248 | var schema = new Schema
249 | {
250 | BaseType = "microsoft.graph.externalItem",
251 | Properties = new List
252 | {
253 | new Property { Name = "partNumber", Type = PropertyType.Int64, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true },
254 | new Property { Name = "name", Type = PropertyType.String, IsQueryable = true, IsSearchable = true, IsRetrievable = true, IsRefinable = false, Labels = new List
275 |
276 | //
277 | async Task GetSchemaAsync()
278 | {
279 | if (currentConnection == null)
280 | {
281 | Console.WriteLine("No connection selected. Please create a new connection or select an existing connection.");
282 | return;
283 | }
284 |
285 | try
286 | {
287 | var schema = await GraphHelper.GetSchemaAsync(currentConnection.Id);
288 | Console.WriteLine(JsonSerializer.Serialize(schema));
289 |
290 | }
291 | catch (ODataError odataError)
292 | {
293 | Console.WriteLine($"Error getting schema: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
294 | }
295 | }
296 | //
297 |
298 | //
299 | async Task UpdateItemsFromDatabaseAsync(bool uploadModifiedOnly, string? tenantId)
300 | {
301 | if (currentConnection == null)
302 | {
303 | Console.WriteLine("No connection selected. Please create a new connection or select an existing connection.");
304 | return;
305 | }
306 |
307 | _ = tenantId ?? throw new ArgumentException("tenantId is null");
308 |
309 | List? partsToUpload = null;
310 | List? partsToDelete = null;
311 |
312 | var newUploadTime = DateTime.UtcNow;
313 |
314 | var partsDb = new ApplianceDbContext();
315 | partsDb.EnsureDatabase();
316 |
317 | if (uploadModifiedOnly)
318 | {
319 | var lastUploadTime = GetLastUploadTime();
320 | Console.WriteLine($"Uploading changes since last upload at {lastUploadTime.ToLocalTime()}");
321 |
322 | partsToUpload = partsDb.Parts
323 | .Where(p => EF.Property(p, "LastUpdated") > lastUploadTime)
324 | .ToList();
325 |
326 | partsToDelete = partsDb.Parts
327 | .IgnoreQueryFilters()
328 | .Where(p => EF.Property(p, "IsDeleted")
329 | && EF.Property(p, "LastUpdated") > lastUploadTime)
330 | .ToList();
331 | }
332 | else
333 | {
334 | partsToUpload = partsDb.Parts.ToList();
335 |
336 | partsToDelete = partsDb.Parts
337 | .IgnoreQueryFilters()
338 | .Where(p => EF.Property(p, "IsDeleted"))
339 | .ToList();
340 | }
341 |
342 | Console.WriteLine($"Processing {partsToUpload.Count} add/updates, {partsToDelete.Count} deletes.");
343 | var success = true;
344 |
345 | foreach (var part in partsToUpload)
346 | {
347 | var newItem = new ExternalItem
348 | {
349 | Id = part.PartNumber.ToString(),
350 | Content = new ExternalItemContent
351 | {
352 | Type = ExternalItemContentType.Text,
353 | Value = part.Description
354 | },
355 | Acl = new List
356 | {
357 | new Acl
358 | {
359 | AccessType = AccessType.Grant,
360 | Type = AclType.Everyone,
361 | Value = tenantId,
362 | }
363 | },
364 | Properties = part.AsExternalItemProperties(),
365 | };
366 |
367 | try
368 | {
369 | Console.Write($"Uploading part number {part.PartNumber}...");
370 | await GraphHelper.AddOrUpdateItemAsync(currentConnection.Id, newItem);
371 | Console.WriteLine("DONE");
372 | }
373 | catch (ODataError odataError)
374 | {
375 | success = false;
376 | Console.WriteLine("FAILED");
377 | Console.WriteLine($"Error: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
378 | }
379 | }
380 |
381 | foreach (var part in partsToDelete)
382 | {
383 | try
384 | {
385 | Console.Write($"Deleting part number {part.PartNumber}...");
386 | await GraphHelper.DeleteItemAsync(currentConnection.Id, part.PartNumber.ToString());
387 | Console.WriteLine("DONE");
388 | }
389 | catch (ODataError odataError)
390 | {
391 | success = false;
392 | Console.WriteLine("FAILED");
393 | Console.WriteLine($"Error: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}");
394 | }
395 | }
396 |
397 | // If no errors, update our last upload time
398 | if (success)
399 | {
400 | SaveLastUploadTime(newUploadTime);
401 | }
402 | }
403 | //
404 |
--------------------------------------------------------------------------------
/PartsInventoryConnector/Settings.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation.
2 | // Licensed under the MIT License.
3 |
4 | //
5 | using Microsoft.Extensions.Configuration;
6 |
7 | namespace PartsInventoryConnector;
8 |
9 | public class Settings
10 | {
11 | public string? ClientId { get; set; }
12 | public string? ClientSecret { get; set; }
13 | public string? TenantId { get; set; }
14 |
15 | public static Settings LoadSettings()
16 | {
17 | // Load settings
18 | IConfiguration config = new ConfigurationBuilder()
19 | .AddUserSecrets()
20 | .Build();
21 |
22 | return config.GetRequiredSection("Settings").Get() ??
23 | throw new Exception("Could not load app settings. See README for configuration instructions.");
24 | }
25 | }
26 | //
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | page_type: sample
3 | description: This sample demonstrates how to use the Microsoft Graph .NET SDK to implement a custom search connector.
4 | products:
5 | - ms-graph
6 | - microsoft-search
7 | languages:
8 | - csharp
9 | ---
10 |
11 | # Microsoft Graph search connector sample
12 |
13 | [](https://github.com/microsoftgraph/msgraph-search-connector-sample/actions/workflows/dotnet.yml) 
14 |
15 | This .NET sample application demonstrates how to build a custom [Microsoft Graph connector](https://learn.microsoft.com/graph/connecting-external-content-connectors-overview) using Microsoft Graph APIs to index items from a sample appliance parts inventory, and have that data appear in [Microsoft Search](https://learn.microsoft.com/microsoftsearch/) results.
16 |
17 | ## Prerequisites
18 |
19 | - [.NET 7.x](https://dotnet.microsoft.com/download)
20 | - [Entity Framework Core Tools](https://learn.microsoft.com/ef/core/miscellaneous/cli/dotnet) (`dotnet tool install --global dotnet-ef`)
21 | - Some way to update a SQLite database. For example, the [DB Browser for SQLite](https://sqlitebrowser.org/).
22 |
23 | ## Register an app in Azure portal
24 |
25 | In this step you will register an application that supports app-only authentication using [client credentials flow](/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow).
26 |
27 | 1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com) and login using a Global administrator account.
28 |
29 | 1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations** under **Manage**.
30 |
31 | 1. Select **New registration**. Enter a name for your application, for example, `Parts Inventory Connector`.
32 |
33 | 1. Set **Supported account types** to **Accounts in this organizational directory only**.
34 |
35 | 1. Leave **Redirect URI** empty.
36 |
37 | 1. Select **Register**. On the application's **Overview** page, copy the value of the **Application (client) ID** and **Directory (tenant) ID** and save them, you will need these values in the next step.
38 |
39 | 1. Select **API permissions** under **Manage**.
40 |
41 | 1. Remove the default **User.Read** permission under **Configured permissions** by selecting the ellipses (**...**) in its row and selecting **Remove permission**.
42 |
43 | 1. Select **Add a permission**, then **Microsoft Graph**.
44 |
45 | 1. Select **Application permissions**.
46 |
47 | 1. Select **ExternalConnection.ReadWrite.OwnedBy** and **ExternalItem.ReadWrite.OwnedBy**, then select **Add permissions**.
48 |
49 | 1. Select **Grant admin consent for...**, then select **Yes** to provide admin consent for the selected permission.
50 |
51 | 1. Select **Certificates and secrets** under **Manage**, then select **New client secret**.
52 |
53 | 1. Enter a description, choose a duration, and select **Add**.
54 |
55 | 1. Copy the secret from the **Value** column, you will need it in the next steps.
56 |
57 | > **IMPORTANT**
58 | > This client secret is never shown again, so make sure you copy it now.
59 |
60 | ## Configure the app
61 |
62 | 1. Open your command line interface (CLI) in the directory where **PartsInventoryConnector.csproj** is located.
63 | 1. Run the following command to initialize [user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets) for the project.
64 |
65 | ```dotnetcli
66 | dotnet user-secrets init
67 | ```
68 |
69 | 1. Run the following commands to store your app ID, app secret, and tenant ID in the user secret store.
70 |
71 | ```dotnetcli
72 | dotnet user-secrets set settings:clientId
73 | dotnet user-secrets set settings:tenantId
74 | dotnet user-secrets set settings:clientSecret
75 | ```
76 |
77 | ## Initialize the database
78 |
79 | ```dotnetcli
80 | dotnet ef database update
81 | ```
82 |
83 | ### Delete and reset database
84 |
85 | ```dotnetcli
86 | dotnet ef database drop
87 | dotnet ef database update
88 | ```
89 |
90 | ## Run the app
91 |
92 | In this step you'll build and run the sample. This will create a new connection, register the schema, then push items from the [ApplianceParts.csv](PartsInventoryConnector/ApplianceParts.csv) file into the connection.
93 |
94 | 1. Open your command-line interface (CLI) in the **PartsInventoryConnector** directory.
95 | 1. Use the `dotnet build` command to build the sample.
96 | 1. Use the `dotnet run` command to run the sample.
97 | 1. Select the **1. Create a connection** option. Enter a unique identifier, name, and description for the connection.
98 | 1. Select the **4. Register schema for current connection** option. Wait for the operation to complete.
99 |
100 | > **Note:** If this steps results in an error, wait a few minutes and then select the **5. View schema for current connection** option. If a schema is returned, the operation completed successfully. If no schema is returned, you may need to try registering the schema again.
101 |
102 | 1. Select the **6. Push updated items to current connection** option.
103 |
104 | ## Create a vertical
105 |
106 | Create and enable a search vertical at the organization level following the instructions in [Manage Verticals](https://learn.microsoft.com/microsoftsearch/manage-verticals).
107 |
108 | - **Name:** Appliance Parts
109 | - **Content source:** the connector created with the app
110 | - **Add a query:** leave blank
111 | - **Filter:** none
112 |
113 | ## Create a result type
114 |
115 | Create a result type at the organization level following the instructions in [Manage Result Types](https://learn.microsoft.com/microsoftsearch/manage-result-types).
116 |
117 | - **Name:** Appliance Part
118 | - **Content source:** the connector created with the app
119 | - **Rules:** None
120 | - Paste contents of [result-type.json](result-type.json) into layout
121 |
122 | ## Search for results
123 |
124 | In this step you'll search for parts in SharePoint.
125 |
126 | 1. Go to your root SharePoint site for your tenant.
127 | 1. Using the search box at the top of the page, search for `hinge`.
128 | 1. When the search completes with 0 results, select the **Appliance Parts** tab.
129 | 1. Results from the connector are displayed.
130 |
131 | ## Updating records in the database
132 |
133 | Use your favorite tool to update records in the database. The **Push updated items** menu choice will only push the items you update.
134 |
135 | > **NOTE**
136 | > Do not delete records from the database. To "delete" an item, set the IsDeleted property to 1.
137 | >
138 | > 
139 |
140 | ## Code of conduct
141 |
142 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
143 |
144 | ## Disclaimer
145 |
146 | **THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
147 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/1000.md:
--------------------------------------------------------------------------------
1 | # Door hinge
2 |
3 | 
4 |
5 | Door hinge for refrigerators.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1000 |
10 | | Price | $19.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Fridge
15 | - Contoso Sub-zero Fridge
16 | - Contoso XL Fridge
17 |
--------------------------------------------------------------------------------
/docs/1001.md:
--------------------------------------------------------------------------------
1 | # Door handle (stainless steel)
2 |
3 | 
4 |
5 | Door handle for refrigerators.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1001 |
10 | | Price | $49.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Fridge
15 | - Contoso Sub-zero Fridge
16 | - Contoso XL Fridge
17 |
--------------------------------------------------------------------------------
/docs/1002.md:
--------------------------------------------------------------------------------
1 | # Door handle (black)
2 |
3 | 
4 |
5 | Door handle for refrigerators.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1002 |
10 | | Price | $39.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Fridge
15 | - Contoso Sub-zero Fridge
16 | - Contoso XL Fridge
17 |
--------------------------------------------------------------------------------
/docs/1003.md:
--------------------------------------------------------------------------------
1 | # Door handle (white)
2 |
3 | 
4 |
5 | Door handle for refrigerators.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1003 |
10 | | Price | $39.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Fridge
15 | - Contoso Sub-zero Fridge
16 | - Contoso XL Fridge
17 |
--------------------------------------------------------------------------------
/docs/1004.md:
--------------------------------------------------------------------------------
1 | # Compressor
2 |
3 | 
4 |
5 | Compressor for refrigerators.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1004 |
10 | | Price | $129.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Fridge
15 | - Contoso Sub-zero Fridge
16 | - Contoso XL Fridge
17 |
--------------------------------------------------------------------------------
/docs/1005.md:
--------------------------------------------------------------------------------
1 | # Door hinge
2 |
3 | 
4 |
5 | Door hinge for dishwashers.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1005 |
10 | | Price | $17.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Dishwasher
15 | - Contoso Super-silent Dishwasher
16 |
--------------------------------------------------------------------------------
/docs/1006.md:
--------------------------------------------------------------------------------
1 | # Door handle (stainless steel)
2 |
3 | 
4 |
5 | Door handle for dishwashers.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1006 |
10 | | Price | $48.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Dishwasher
15 | - Contoso Super-silent Dishwasher
16 |
--------------------------------------------------------------------------------
/docs/1007.md:
--------------------------------------------------------------------------------
1 | # Door handle (black)
2 |
3 | 
4 |
5 | Door handle for dishwashers.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1007 |
10 | | Price | $37.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Dishwasher
15 | - Contoso Super-silent Dishwasher
16 |
--------------------------------------------------------------------------------
/docs/1008.md:
--------------------------------------------------------------------------------
1 | # Door handle (white)
2 |
3 | 
4 |
5 | Door handle for dishwashers.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1008 |
10 | | Price | $37.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Dishwasher
15 | - Contoso Super-silent Dishwasher
16 |
--------------------------------------------------------------------------------
/docs/1009.md:
--------------------------------------------------------------------------------
1 | # Water pump
2 |
3 | 
4 |
5 | Water pump for dishwashers.
6 |
7 | | | |
8 | |-|-|
9 | | Part number | 1009 |
10 | | Price | $89.99 |
11 |
12 | ## Compatible with
13 |
14 | - Contoso Dishwasher
15 | - Contoso Super-silent Dishwasher
16 |
--------------------------------------------------------------------------------
/docs/images/1000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1000.png
--------------------------------------------------------------------------------
/docs/images/1001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1001.png
--------------------------------------------------------------------------------
/docs/images/1002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1002.png
--------------------------------------------------------------------------------
/docs/images/1003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1003.png
--------------------------------------------------------------------------------
/docs/images/1004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1004.png
--------------------------------------------------------------------------------
/docs/images/1005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1005.png
--------------------------------------------------------------------------------
/docs/images/1006.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1006.png
--------------------------------------------------------------------------------
/docs/images/1007.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1007.png
--------------------------------------------------------------------------------
/docs/images/1008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1008.png
--------------------------------------------------------------------------------
/docs/images/1009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/docs/images/1009.png
--------------------------------------------------------------------------------
/images/dbbrowser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftgraph/msgraph-search-connector-sample/54d31d926c2c0cb212021b3d4ef7f7a6195b8ccf/images/dbbrowser.png
--------------------------------------------------------------------------------
/result-type.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "AdaptiveCard",
3 | "version": "1.3",
4 | "body": [
5 | {
6 | "type": "ColumnSet",
7 | "columns": [
8 | {
9 | "type": "Column",
10 | "width": 6,
11 | "items": [
12 | {
13 | "type": "TextBlock",
14 | "text": "__${name} (Part #${partNumber})__",
15 | "color": "accent",
16 | "size": "medium",
17 | "spacing": "none",
18 | "$when": "${name != \"\"}"
19 | },
20 | {
21 | "type": "TextBlock",
22 | "text": "${description}",
23 | "wrap": true,
24 | "maxLines": 3,
25 | "$when": "${description != \"\"}"
26 | }
27 | ],
28 | "horizontalAlignment": "Center",
29 | "spacing": "none"
30 | },
31 | {
32 | "type": "Column",
33 | "width": 2,
34 | "items": [
35 | {
36 | "type": "FactSet",
37 | "facts": [
38 | {
39 | "title": "Price",
40 | "value": "$${price}"
41 | },
42 | {
43 | "title": "Current Inventory",
44 | "value": "${inventory} units"
45 | }
46 | ]
47 | }
48 | ],
49 | "spacing": "none",
50 | "horizontalAlignment": "right"
51 | }
52 | ]
53 | }
54 | ],
55 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
56 | }
57 |
--------------------------------------------------------------------------------