生成你的第一个自定义 Microsoft Graph 连接器
Microsoft Graph 连接器允许将自己的数据添加到 Microsoft Graph 中,并使其为各种Microsoft 365 体验提供支持。
此 .NET 应用程序演示如何使用 Microsoft Graph 连接器 API 创建自定义连接器,并使用它为Microsoft搜索提供支持。 本教程使用 Contoso 设备维修组织的示例数据设备部件清单。
先决条件
在开发计算机上安装的 .NET SDK 。
应具有具有全局管理员角色Microsoft工作或学校帐户。 如果没有 Microsoft 365 租户,则可以通过 Microsoft 365 开发人员计划获得租户;有关详细信息,请参阅 常见问题解答。 或者,可以 注册 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 管理中心。
展开“ 标识 ”菜单 > ,选择“ 应用程序>”“应用注册>”“新建注册”。
输入应用程序的名称,例如
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 标识客户端库 ,用于对用户进行身份验证并获取访问令牌。
- Microsoft Graph .NET 客户端库 调用 Microsoft Graph。
- 用于访问本地数据库的实体框架包。
- 用于读取 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 客户端配置为使用仅限应用的身份验证。
创建帮助程序类
在 PartsInventoryConnector 目录中创建名为 Graph 的新目录。
在 Graph 目录中创建名为 GraphHelper.cs 的文件,并添加以下
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}"); } }
创建数据库
在本部分中,将定义设备部件清单记录和实体框架上下文的模型,并使用 dotnet ef
该工具初始化数据库。
定义模型
在 PartsInventoryConnector 目录中创建名为 Data 的新目录。
在 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; } }
在 Data 目录中创建名为 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; } }
在 Data 目录中创建名为 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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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
GraphHelper.cs 中的 类。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。将所有项推送到当前连接。
在搜索中显示数据
在此步骤中,你将创建垂直搜索和结果类型,以自定义 Microsoft SharePoint、Microsoft Office 中的搜索结果,并在必应中Microsoft搜索。
创建垂直
使用全局管理员角色登录到 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搜索提供支持。
后续步骤
- 若要了解有关自定义连接器的详细信息,请参阅 Microsoft Graph 连接器概述。
- 浏览 示例连接器。
- 了解 社区中的示例连接器。
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。