├── .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() { Label.Title }}, 255 | new Property { Name = "description", Type = PropertyType.String, IsQueryable = false, IsSearchable = true, IsRetrievable = true, IsRefinable = false }, 256 | new Property { Name = "price", Type = PropertyType.Double, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true }, 257 | new Property { Name = "inventory", Type = PropertyType.Int64, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true }, 258 | new Property { Name = "appliances", Type = PropertyType.StringCollection, IsQueryable = true, IsSearchable = true, IsRetrievable = true, IsRefinable = false } 259 | }, 260 | }; 261 | 262 | await GraphHelper.RegisterSchemaAsync(currentConnection.Id, schema); 263 | Console.WriteLine("Schema registered successfully"); 264 | } 265 | catch (ServiceException serviceException) 266 | { 267 | Console.WriteLine($"Error registering schema: {serviceException.ResponseStatusCode} {serviceException.Message}"); 268 | } 269 | catch (ODataError odataError) 270 | { 271 | Console.WriteLine($"Error registering schema: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); 272 | } 273 | } 274 | // 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 | [![dotnet build](https://github.com/microsoftgraph/msgraph-search-connector-sample/actions/workflows/dotnet.yml/badge.svg)](https://github.com/microsoftgraph/msgraph-search-connector-sample/actions/workflows/dotnet.yml) ![License.](https://img.shields.io/badge/license-MIT-green.svg) 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 | > ![DB Browser](images/dbbrowser.png) 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 | ![Product photo](images/1000.png) 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 | ![Product photo](images/1001.png) 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 | ![Product photo](images/1002.png) 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 | ![Product photo](images/1003.png) 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 | ![Product photo](images/1004.png) 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 | ![Product photo](images/1005.png) 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 | ![Product photo](images/1006.png) 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 | ![Product photo](images/1007.png) 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 | ![Product photo](images/1008.png) 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 | ![Product photo](images/1009.png) 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 | --------------------------------------------------------------------------------