最初のカスタム Microsoft Graph コネクタを構築する
Microsoft Graph コネクタを使用すると、独自のデータを Microsoft Graph に追加し、さまざまな Microsoft 365 エクスペリエンスに対応できます。
この .NET アプリケーションでは、Microsoft Graph コネクタ API を使用してカスタム コネクタを作成し、それを使用して Microsoft Search の電源を供給する方法について説明します。 このチュートリアルでは、Contoso アプライアンス修復組織のサンプル データ アプライアンス パーツ インベントリを使用します。
前提条件
開発マシンにインストールされている .NET SDK 。
グローバル管理者ロールを持つ Microsoft の職場または学校アカウントが必要です。 Microsoft 365 テナントをお持ちでない場合は、 Microsoft 365 開発者プログラムを通じてテナントの資格を得る可能性があります。詳細については、 FAQ を参照してください。 または、 1 か月間の無料試用版にサインアップするか、Microsoft 365 プランを購入することもできます。
次のコマンドを使用して、 Entity Framework Core Tools をグローバル ツールとしてインストールします。
dotnet tool install --global dotnet-ef
SQLite データベースを更新するツールをインストールします。 たとえば、 SQLite の DB ブラウザーです。
検索コネクタのサンプル リポジトリから ApplianceParts.csv ファイルをダウンロードします。
ポータルでアプリを登録する
この演習では、Microsoft Entra ID に新しいアプリケーションを登録して 、アプリのみの認証を有効にします。 Microsoft Graph コネクタでは、アプリのみの認証を使用してコネクタ API にアクセスします。
アプリケーションをアプリ専用認証に登録する
このセクションでは、 クライアント資格情報フローを使用してアプリのみの認証をサポートするアプリケーションを登録します。
Microsoft Entra 管理センターにサインインします。
[ ID ] メニューを展開 > [ アプリケーション>App registrations>新しい登録] を選択します。
アプリケーションの名前 (例:
Parts Inventory Connector
) を入力します。[ サポートされているアカウントの種類 ] を [この組織のディレクトリ内のアカウントのみ] に設定します。
[リダイレクト URI]を空のままにします。
[登録] を選択します。 アプリケーションの [概要 ] ページで、 アプリケーション (クライアント) ID と ディレクトリ (テナント) ID の値をコピーして保存します。次の手順でこれらの値が必要になります。
[管理] で、[API のアクセス許可] を選択します。
行で省略記号 (...) を選択し、[アクセス許可の削除] を選択して、[構成されたアクセス許可] の下にある既定の User.Readアクセス許可を削除します。
[ アクセス許可の追加] を選択し、[ Microsoft Graph] を選択します。
[アプリケーションのアクセス許可] を選択します。
[ ExternalConnection.ReadWrite.OwnedBy ] と [ExternalItem.ReadWrite.OwnedBy] を選択し、[ アクセス許可の追加] を選択します。
[ 管理者の同意を付与する] を選択し、[ はい ] を選択して、選択したアクセス許可に対して管理者の同意を提供します。
[管理] で [証明書とシークレット] を選択し、[新しいクライアント シークレット] を選択します。
説明を入力し、期間を選択し、[ 追加] を選択します。
[値] 列からシークレットをコピーします。次の手順で必要になります。
重要
このクライアント シークレットは今後表示されないため、この段階で必ずコピーするようにしてください。
アプリケーションを作成する
まず、 .NET CLI を使用して新しい .NET コンソール プロジェクトを作成します。
プロジェクトを作成するディレクトリでコマンド ライン インターフェイス (CLI) を開きます。 次のコマンドを実行します。
dotnet new console -o PartsInventoryConnector
プロジェクトが作成されたら、現在のディレクトリを PartsInventoryConnector ディレクトリに変更し、CLI で次のコマンドを実行して動作することを確認します。
dotnet run
動作する場合、アプリは
Hello, World!
を出力する必要があります。
依存関係のインストール
次に進む前に、後で使用するいくつかの依存関係を追加します。
- appsettings.jsonからアプリケーション構成を読み取る .NET 構成パッケージ。
- ユーザーを認証し、アクセス トークンを取得するための .NET 用の Azure Identity クライアント ライブラリ。
- Microsoft Graph .NET クライアント ライブラリ を使用して、Microsoft Graph を呼び出します。
- ローカル データベースにアクセスするための Entity Framework パッケージ。
- CSV ファイルを読み取るための CsvHelper。
CLI で次のコマンドを実行して、依存関係をインストールします。
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
dotnet add package Azure.Identity
dotnet add package Microsoft.Graph
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package CsvHelper
アプリケーション設定を読み込む
このセクションでは、アプリ登録の詳細をプロジェクトに追加します。
クライアント ID、テナント ID、クライアント シークレットを .NET シークレット マネージャーに追加します。 コマンド ライン インターフェイスで、ディレクトリを PartsInventoryConnector.csproj の場所に変更し、次のコマンドを実行します。<client-id> をアプリ登録のクライアント ID、<tenant-id>テナント ID、<client-secret> に置き換えます。
dotnet user-secrets init dotnet user-secrets set settings:clientId <client-id> dotnet user-secrets set settings:tenantId <tenant-id> dotnet user-secrets set settings:clientSecret <client-secret>
partsInventoryConnector ディレクトリに Settings.cs という名前のファイルを作成し、次のコードを追加します。
using Microsoft.Extensions.Configuration; namespace PartsInventoryConnector; public class Settings { public string? ClientId { get; set; } public string? ClientSecret { get; set; } public string? TenantId { get; set; } public static Settings LoadSettings() { // Load settings IConfiguration config = new ConfigurationBuilder() .AddUserSecrets<Program>() .Build(); return config.GetRequiredSection("Settings").Get<Settings>() ?? throw new Exception("Could not load app settings. See README for configuration instructions."); } }
アプリを設計する
このセクションでは、コンソール ベースのメニューを作成します。
./Program.cs を開き、その内容全体を次のコードに置き換えます。
using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Graph; using Microsoft.Graph.Models.ExternalConnectors; using Microsoft.Graph.Models.ODataErrors; using PartsInventoryConnector; using PartsInventoryConnector.Data; using PartsInventoryConnector.Graph; Console.WriteLine("Parts Inventory Search Connector\n"); var settings = Settings.LoadSettings(); // Initialize Graph InitializeGraph(settings); ExternalConnection? currentConnection = null; int choice = -1; while (choice != 0) { Console.WriteLine($"Current connection: {(currentConnection == null ? "NONE" : currentConnection.Name)}\n"); Console.WriteLine("Please choose one of the following options:"); Console.WriteLine("0. Exit"); Console.WriteLine("1. Create a connection"); Console.WriteLine("2. Select an existing connection"); Console.WriteLine("3. Delete current connection"); Console.WriteLine("4. Register schema for current connection"); Console.WriteLine("5. View schema for current connection"); Console.WriteLine("6. Push updated items to current connection"); Console.WriteLine("7. Push ALL items to current connection"); Console.Write("Selection: "); try { choice = int.Parse(Console.ReadLine() ?? string.Empty); } catch (FormatException) { // Set to invalid value choice = -1; } switch(choice) { case 0: // Exit the program Console.WriteLine("Goodbye..."); break; case 1: currentConnection = await CreateConnectionAsync(); break; case 2: currentConnection = await SelectExistingConnectionAsync(); break; case 3: await DeleteCurrentConnectionAsync(currentConnection); currentConnection = null; break; case 4: await RegisterSchemaAsync(); break; case 5: await GetSchemaAsync(); break; case 6: await UpdateItemsFromDatabaseAsync(true, settings.TenantId); break; case 7: await UpdateItemsFromDatabaseAsync(false, settings.TenantId); break; default: Console.WriteLine("Invalid choice! Please try again."); break; } } static string? PromptForInput(string prompt, bool valueRequired) { string? response; do { Console.WriteLine($"{prompt}:"); response = Console.ReadLine(); if (valueRequired && string.IsNullOrEmpty(response)) { Console.WriteLine("You must provide a value"); } } while (valueRequired && string.IsNullOrEmpty(response)); return response; } static DateTime GetLastUploadTime() { if (File.Exists("lastuploadtime.bin")) { return DateTime.Parse( File.ReadAllText("lastuploadtime.bin")).ToUniversalTime(); } return DateTime.MinValue; } static void SaveLastUploadTime(DateTime uploadTime) { File.WriteAllText("lastuploadtime.bin", uploadTime.ToString("u")); }
ファイルの末尾に次のプレースホルダー メソッドを追加します。 後の手順で実装します。
void InitializeGraph(Settings settings) { // TODO } async Task<ExternalConnection?> CreateConnectionAsync() { // TODO throw new NotImplementedException(); } async Task<ExternalConnection?> SelectExistingConnectionAsync() { // TODO throw new NotImplementedException(); } async Task DeleteCurrentConnectionAsync(ExternalConnection? connection) { // TODO } async Task RegisterSchemaAsync() { // TODO } async Task GetSchemaAsync() { // TODO } async Task UpdateItemsFromDatabaseAsync(bool uploadModifiedOnly, string? tenantId) { // TODO }
これにより、基本的なメニューが実装され、コマンド ラインからユーザーの選択が読み取ります。
Microsoft Graph の構成
このセクションでは、アプリ専用認証を使用するように Microsoft Graph SDK クライアントを構成します。
ヘルパー クラスを作成する
Graph という名前の PartsInventoryConnector ディレクトリに新しいディレクトリを作成します。
GraphHelper.cs という名前の Graph ディレクトリにファイル を 作成し、次の
using
ステートメントを追加します。using Azure.Identity; using Microsoft.Graph; using Microsoft.Graph.Models.ExternalConnectors; using Microsoft.Kiota.Authentication.Azure;
名前空間とクラス定義を追加します。
namespace PartsInventoryConnector.Graph; public static class GraphHelper { }
次のコードを
GraphHelper
クラスに追加します。これにより、アプリ専用認証でGraphServiceClient
が構成されます。private static GraphServiceClient? graphClient; private static HttpClient? httpClient; public static void Initialize(Settings settings) { // Create a credential that uses the client credentials // authorization flow var credential = new ClientSecretCredential( settings.TenantId, settings.ClientId, settings.ClientSecret); // Create an HTTP client httpClient = GraphClientFactory.Create(); // Create an auth provider var authProvider = new AzureIdentityAuthenticationProvider( credential, scopes: new[] { "https://graph.microsoft.com/.default" }); // Create a Graph client using the credential graphClient = new GraphServiceClient(httpClient, authProvider); }
Program.csの空の
InitializeGraph
関数 を 次のように置き換えます。void InitializeGraph(Settings settings) { try { GraphHelper.Initialize(settings); } catch (Exception ex) { Console.WriteLine($"Error initializing Graph: {ex.Message}"); } }
データベースを作成する
このセクションでは、アプライアンス パーツ インベントリ レコードと Entity Framework コンテキストのモデルを定義し、 dotnet ef
ツールを使用してデータベースを初期化します。
モデルを定義する
PartsInventoryConnector ディレクトリに Data という名前の新しいディレクトリを作成します。
AppliancePart.cs という名前の データ ディレクトリにファイル を 作成し、次のコードを追加します。
using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Microsoft.Graph.Models.ExternalConnectors; namespace PartsInventoryConnector.Data; public class AppliancePart { [JsonPropertyName("appliances@odata.type")] private const string AppliancesODataType = "Collection(String)"; [Key] public int PartNumber { get; set; } public string? Name { get; set; } public string? Description { get; set; } public double Price { get; set; } public int Inventory { get; set; } public List<string>? Appliances { get; set; } public Properties AsExternalItemProperties() { _ = Name ?? throw new MemberAccessException("Name cannot be null"); _ = Description ?? throw new MemberAccessException("Description cannot be null"); _ = Appliances ?? throw new MemberAccessException("Appliances cannot be null"); var properties = new Properties { AdditionalData = new Dictionary<string, object> { { "partNumber", PartNumber }, { "name", Name }, { "description", Description }, { "price", Price }, { "inventory", Inventory }, { "appliances@odata.type", "Collection(String)" }, { "appliances", Appliances } } }; return properties; } }
ApplianceDbContext.cs という名前のデータ ディレクトリにファイルを作成し、次のコードを追加します。
using System.Text.Json; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace PartsInventoryConnector.Data; public class ApplianceDbContext : DbContext { public DbSet<AppliancePart> Parts => Set<AppliancePart>(); public void EnsureDatabase() { if (Database.EnsureCreated() || !Parts.Any()) { // File was just created (or is empty), // seed with data from CSV file var parts = CsvDataLoader.LoadPartsFromCsv("ApplianceParts.csv"); Parts.AddRange(parts); SaveChanges(); } } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseSqlite("Data Source=parts.db"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // EF Core can't store lists, so add a converter for the Appliances // property to serialize as a JSON string on save to DB modelBuilder.Entity<AppliancePart>() .Property(ap => ap.Appliances) .HasConversion( v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), v => JsonSerializer.Deserialize<List<string>>(v, JsonSerializerOptions.Default) ); // Add LastUpdated and IsDeleted shadow properties modelBuilder.Entity<AppliancePart>() .Property<DateTime>("LastUpdated") .HasDefaultValueSql("datetime()") .ValueGeneratedOnAddOrUpdate(); modelBuilder.Entity<AppliancePart>() .Property<bool>("IsDeleted") .IsRequired() .HasDefaultValue(false); // Exclude any soft-deleted items (IsDeleted = 1) from // the default query sets modelBuilder.Entity<AppliancePart>() .HasQueryFilter(a => !EF.Property<bool>(a, "IsDeleted")); } public override int SaveChanges() { // Prevent deletes of data, instead mark the item as deleted // by setting IsDeleted = true. foreach(var entry in ChangeTracker.Entries() .Where(e => e.State == EntityState.Deleted)) { if (entry.Entity.GetType() == typeof(AppliancePart)) { SoftDelete(entry); } } return base.SaveChanges(); } private void SoftDelete(EntityEntry entry) { var partNumber = new SqliteParameter("@partNumber", entry.OriginalValues["PartNumber"]); Database.ExecuteSqlRaw( "UPDATE Parts SET IsDeleted = 1 WHERE PartNumber = @partNumber", partNumber); entry.State = EntityState.Detached; } }
CsvDataLoader.cs という名前の データ ディレクトリにファイル を 作成し、次のコードを追加します。
using System.Globalization; using CsvHelper; using CsvHelper.Configuration; using CsvHelper.TypeConversion; namespace PartsInventoryConnector.Data; public static class CsvDataLoader { public static List<AppliancePart> LoadPartsFromCsv(string filePath) { using var reader = new StreamReader(filePath); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); csv.Context.RegisterClassMap<AppliancePartMap>(); return new List<AppliancePart>(csv.GetRecords<AppliancePart>()); } } public class ApplianceListConverter : DefaultTypeConverter { public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData) { var appliances = text?.Split(';') ?? Array.Empty<string>(); return new List<string>(appliances); } } public class AppliancePartMap : ClassMap<AppliancePart> { public AppliancePartMap() { Map(m => m.PartNumber); Map(m => m.Name); Map(m => m.Description); Map(m => m.Price); Map(m => m.Inventory); Map(m => m.Appliances).TypeConverter<ApplianceListConverter>(); } }
データベースを初期化する
PartsInventoryConnector.csproj があるディレクトリでコマンド ライン インターフェイス (CLI) を開きます。
以下のコマンドを実行します。
dotnet ef migrations add InitialCreate dotnet ef database update
注:
CSV ファイルでスキーマが変更された場合は、次のコマンドを実行し、それらの変更を SQLite データベースに反映します。
dotnet ef database drop
dotnet ef database update
接続を管理する
このセクションでは、 外部接続を管理するメソッドを追加します。
接続を作成する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task<ExternalConnection?> CreateConnectionAsync(string id, string name, string? description) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); var newConnection = new ExternalConnection { Id = id, Name = name, Description = description, }; return await graphClient.External.Connections.PostAsync(newConnection); }
Program.csのプレースホルダー関数
CreateConnectionAsync
を次のように置き換えます。async Task<ExternalConnection?> CreateConnectionAsync() { var connectionId = PromptForInput( "Enter a unique ID for the new connection (3-32 characters)", true) ?? "ConnectionId"; var connectionName = PromptForInput( "Enter a name for the new connection", true) ?? "ConnectionName"; var connectionDescription = PromptForInput( "Enter a description for the new connection", false); try { // Create the connection var connection = await GraphHelper.CreateConnectionAsync( connectionId, connectionName, connectionDescription); Console.WriteLine($"New connection created - Name: {connection?.Name}, Id: {connection?.Id}"); return connection; } catch (ODataError odataError) { Console.WriteLine($"Error creating connection: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); return null; } }
既存の接続を選択する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task<ExternalConnectionCollectionResponse?> GetExistingConnectionsAsync() { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); return await graphClient.External.Connections.GetAsync(); }
Program.csのプレースホルダー関数
SelectExistingConnectionAsync
を次のように置き換えます。async Task<ExternalConnection?> SelectExistingConnectionAsync() { // TODO Console.WriteLine("Getting existing connections..."); try { var response = await GraphHelper.GetExistingConnectionsAsync(); var connections = response?.Value ?? new List<ExternalConnection>(); if (connections.Count <= 0) { Console.WriteLine("No connections exist. Please create a new connection"); return null; } // Display connections Console.WriteLine("Choose one of the following connections:"); var menuNumber = 1; foreach(var connection in connections) { Console.WriteLine($"{menuNumber++}. {connection.Name}"); } ExternalConnection? selection = null; do { try { Console.Write("Selection: "); var choice = int.Parse(Console.ReadLine() ?? string.Empty); if (choice > 0 && choice <= connections.Count) { selection = connections[choice - 1]; } else { Console.WriteLine("Invalid choice."); } } catch (FormatException) { Console.WriteLine("Invalid choice."); } } while (selection == null); return selection; } catch (ODataError odataError) { Console.WriteLine($"Error getting connections: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); return null; } }
接続を削除する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task DeleteConnectionAsync(string? connectionId) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); _ = connectionId ?? throw new ArgumentException("connectionId is required"); await graphClient.External.Connections[connectionId].DeleteAsync(); }
Program.csのプレースホルダー関数
DeleteCurrentConnectionAsync
を次のように置き換えます。async Task DeleteCurrentConnectionAsync(ExternalConnection? connection) { if (connection == null) { Console.WriteLine( "No connection selected. Please create a new connection or select an existing connection."); return; } try { await GraphHelper.DeleteConnectionAsync(connection.Id); Console.WriteLine($"{connection.Name} deleted successfully."); } catch (ODataError odataError) { Console.WriteLine($"Error deleting connection: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); } }
スキーマを管理する
このセクションでは、コネクタの スキーマを登録 するメソッドを追加します。
スキーマを登録する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task RegisterSchemaAsync(string? connectionId, Schema schema) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); _ = httpClient ?? throw new MemberAccessException("httpClient is null"); _ = connectionId ?? throw new ArgumentException("connectionId is required"); // Use the Graph SDK's request builder to generate the request URL var requestInfo = graphClient.External .Connections[connectionId] .Schema .ToGetRequestInformation(); requestInfo.SetContentFromParsable(graphClient.RequestAdapter, "application/json", schema); // Convert the SDK request to an HttpRequestMessage var requestMessage = await graphClient.RequestAdapter .ConvertToNativeRequestAsync<HttpRequestMessage>(requestInfo); _ = requestMessage ?? throw new Exception("Could not create native HTTP request"); requestMessage.Method = HttpMethod.Post; requestMessage.Headers.Add("Prefer", "respond-async"); // Send the request var responseMessage = await httpClient.SendAsync(requestMessage) ?? throw new Exception("No response returned from API"); if (responseMessage.IsSuccessStatusCode) { // The operation ID is contained in the Location header returned // in the response var operationId = responseMessage.Headers.Location?.Segments.Last() ?? throw new Exception("Could not get operation ID from Location header"); await WaitForOperationToCompleteAsync(connectionId, operationId); } else { throw new ServiceException("Registering schema failed", responseMessage.Headers, (int)responseMessage.StatusCode); } } private static async Task WaitForOperationToCompleteAsync(string connectionId, string operationId) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); do { var operation = await graphClient.External .Connections[connectionId] .Operations[operationId] .GetAsync(); if (operation?.Status == ConnectionOperationStatus.Completed) { return; } else if (operation?.Status == ConnectionOperationStatus.Failed) { throw new ServiceException($"Schema operation failed: {operation?.Error?.Code} {operation?.Error?.Message}"); } // Wait 5 seconds and check again await Task.Delay(5000); } while (true); }
Program.csのプレースホルダー関数
RegisterSchemaAsync
を次のように置き換えます。async Task RegisterSchemaAsync() { if (currentConnection == null) { Console.WriteLine("No connection selected. Please create a new connection or select an existing connection."); return; } Console.WriteLine("Registering schema, this may take a moment..."); try { // Create the schema var schema = new Schema { BaseType = "microsoft.graph.externalItem", Properties = new List<Property> { new Property { Name = "partNumber", Type = PropertyType.Int64, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true }, new Property { Name = "name", Type = PropertyType.String, IsQueryable = true, IsSearchable = true, IsRetrievable = true, IsRefinable = false, Labels = new List<Label?>() { Label.Title }}, new Property { Name = "description", Type = PropertyType.String, IsQueryable = false, IsSearchable = true, IsRetrievable = true, IsRefinable = false }, new Property { Name = "price", Type = PropertyType.Double, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true }, new Property { Name = "inventory", Type = PropertyType.Int64, IsQueryable = true, IsSearchable = false, IsRetrievable = true, IsRefinable = true }, new Property { Name = "appliances", Type = PropertyType.StringCollection, IsQueryable = true, IsSearchable = true, IsRetrievable = true, IsRefinable = false } }, }; await GraphHelper.RegisterSchemaAsync(currentConnection.Id, schema); Console.WriteLine("Schema registered successfully"); } catch (ServiceException serviceException) { Console.WriteLine($"Error registering schema: {serviceException.ResponseStatusCode} {serviceException.Message}"); } catch (ODataError odataError) { Console.WriteLine($"Error registering schema: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); } }
接続のスキーマを取得する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task<Schema?> GetSchemaAsync(string? connectionId) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); _ = connectionId ?? throw new ArgumentException("connectionId is null"); return await graphClient.External .Connections[connectionId] .Schema .GetAsync(); }
Program.csのプレースホルダー関数
GetSchemaAsync
を次のように置き換えます。async Task GetSchemaAsync() { if (currentConnection == null) { Console.WriteLine("No connection selected. Please create a new connection or select an existing connection."); return; } try { var schema = await GraphHelper.GetSchemaAsync(currentConnection.Id); Console.WriteLine(JsonSerializer.Serialize(schema)); } catch (ODataError odataError) { Console.WriteLine($"Error getting schema: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); } }
アイテムを管理する
このセクションでは、コネクタに 項目を追加または削除 するメソッドを追加します。
アイテムをアップロードまたは削除する
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task AddOrUpdateItemAsync(string? connectionId, ExternalItem item) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); _ = connectionId ?? throw new ArgumentException("connectionId is null"); await graphClient.External .Connections[connectionId] .Items[item.Id] .PutAsync(item); }
GraphHelper.csの
GraphHelper
クラスに次の関数 を追加します。public static async Task DeleteItemAsync(string? connectionId, string? itemId) { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); _ = connectionId ?? throw new ArgumentException("connectionId is null"); _ = itemId ?? throw new ArgumentException("itemId is null"); await graphClient.External .Connections[connectionId] .Items[itemId] .DeleteAsync(); }
Program.csのプレースホルダー関数
UpdateItemsFromDatabaseAsync
を次のように置き換えます。async Task UpdateItemsFromDatabaseAsync(bool uploadModifiedOnly, string? tenantId) { if (currentConnection == null) { Console.WriteLine("No connection selected. Please create a new connection or select an existing connection."); return; } _ = tenantId ?? throw new ArgumentException("tenantId is null"); List<AppliancePart>? partsToUpload = null; List<AppliancePart>? partsToDelete = null; var newUploadTime = DateTime.UtcNow; var partsDb = new ApplianceDbContext(); partsDb.EnsureDatabase(); if (uploadModifiedOnly) { var lastUploadTime = GetLastUploadTime(); Console.WriteLine($"Uploading changes since last upload at {lastUploadTime.ToLocalTime()}"); partsToUpload = partsDb.Parts .Where(p => EF.Property<DateTime>(p, "LastUpdated") > lastUploadTime) .ToList(); partsToDelete = partsDb.Parts .IgnoreQueryFilters() .Where(p => EF.Property<bool>(p, "IsDeleted") && EF.Property<DateTime>(p, "LastUpdated") > lastUploadTime) .ToList(); } else { partsToUpload = partsDb.Parts.ToList(); partsToDelete = partsDb.Parts .IgnoreQueryFilters() .Where(p => EF.Property<bool>(p, "IsDeleted")) .ToList(); } Console.WriteLine($"Processing {partsToUpload.Count} add/updates, {partsToDelete.Count} deletes."); var success = true; foreach (var part in partsToUpload) { var newItem = new ExternalItem { Id = part.PartNumber.ToString(), Content = new ExternalItemContent { Type = ExternalItemContentType.Text, Value = part.Description }, Acl = new List<Acl> { new Acl { AccessType = AccessType.Grant, Type = AclType.Everyone, Value = tenantId, } }, Properties = part.AsExternalItemProperties(), }; try { Console.Write($"Uploading part number {part.PartNumber}..."); await GraphHelper.AddOrUpdateItemAsync(currentConnection.Id, newItem); Console.WriteLine("DONE"); } catch (ODataError odataError) { success = false; Console.WriteLine("FAILED"); Console.WriteLine($"Error: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); } } foreach (var part in partsToDelete) { try { Console.Write($"Deleting part number {part.PartNumber}..."); await GraphHelper.DeleteItemAsync(currentConnection.Id, part.PartNumber.ToString()); Console.WriteLine("DONE"); } catch (ODataError odataError) { success = false; Console.WriteLine("FAILED"); Console.WriteLine($"Error: {odataError.ResponseStatusCode}: {odataError.Error?.Code} {odataError.Error?.Message}"); } } // If no errors, update our last upload time if (success) { SaveLastUploadTime(newUploadTime); } }
アプリケーションを実行する
この手順では、サンプルをビルドして実行します。 このコード サンプルでは、新しい接続を作成し、スキーマを登録してから、 ApplianceParts.csv ファイルからその接続に項目をプッシュします。
- PartsInventoryConnector ディレクトリでコマンド ライン インターフェイス (CLI) を開きます。
- コマンド
dotnet build
を使用して、サンプルをビルドします。 - コマンド
dotnet run
を使用して、サンプルを実行します。 - [1] を選択 します。接続を作成します。 その接続の一意の識別子、名前、説明を入力します。
- [4] を選択 します。現在の接続のスキーマを登録し、操作が完了するまで待ちます。
- [7] を選択 します。すべての項目を現在の接続にプッシュします。
注:
手順 5 でエラーが発生した場合は、数分待ってから 7 を選択します 。すべての項目を現在の接続にプッシュします。
検索中のデータを表示する
この手順では、Bingで Microsoft SharePoint、Microsoft Office、Microsoft Search の検索結果をカスタマイズするために、検索の垂直方向と結果の種類を作成します。
垂直を作成する
グローバル管理者ロールを使用して Microsoft 365 管理センター にサインインし、次の操作を行います。
[設定>検索 & インテリジェンス>カスタマイズ] に移動します。
[縦] に移動し、[追加] を選択します。
[名前] フィールドに「
Appliance Parts
」と入力し、[次へ] を選択します。[ コネクタ] を選択し、[ パーツ インベントリ ] コネクタを選択します。 [次へ] を選択します。
[ クエリの追加] ページで、クエリ を空白のままにします。 [次へ] を選択します。
[フィルター] ページ で 、[ 次へ] を選択します。
[ 垂直の追加] を選択します。
[ 垂直を有効にする] を選択し、[完了] を選択 します。
結果の種類を作成する
結果の種類を作成するには:
[設定>検索 & インテリジェンス>カスタマイズ] に移動します。
[結果の 種類 ] タブに移動し、[ 追加] を選択します。
[名前] フィールドに「
Appliance Part
」と入力し、[次へ] を選択します。[ コンテンツ ソース ] ページで、[ パーツ コネクタ] を選択します。 [次へ] を選択します。
[ ルール ] ページで、[ 次へ] を選択します。
[ レイアウトのデザイン ] ページで、次の JSON を貼り付け、[ 次へ] を選択します。
{ "type": "AdaptiveCard", "version": "1.3", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "width": 6, "items": [ { "type": "TextBlock", "text": "__${name} (Part #${partNumber})__", "color": "accent", "size": "medium", "spacing": "none", "$when": "${name != \"\"}" }, { "type": "TextBlock", "text": "${description}", "wrap": true, "maxLines": 3, "$when": "${description != \"\"}" } ], "horizontalAlignment": "Center", "spacing": "none" }, { "type": "Column", "width": 2, "items": [ { "type": "FactSet", "facts": [ { "title": "Price", "value": "$${price}" }, { "title": "Current Inventory", "value": "${inventory} units" } ] } ], "spacing": "none", "horizontalAlignment": "right" } ] } ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json" }
[ 結果の種類の追加] を選択し、[完了] を選択 します。
検索結果を検索する
この手順では、SharePoint でパーツを検索します。
テナントのルート SharePoint サイトに移動します。
ページの上部にある検索ボックスを使用して、ヒンジを検索 します。
0 件の結果で検索が完了したら、[ アプライアンス パーツ ] タブを選択します。コネクタからの結果が表示されます。
おめでとうございます。
.NET Microsoft Graph コネクタのチュートリアルが正常に完了しました。カスタム コネクタを作成し、それを使用して Microsoft Search の電源を供給しました。
次の手順
- カスタム コネクタの詳細については、「 Microsoft Graph コネクタの概要」を参照してください。
- サンプル コネクタを参照します。
- コミュニティのサンプル コネクタについて説明します。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。