Сборка вашего первого пользовательского соединителя Microsoft Graph
Соединители Microsoft Graph позволяют добавлять собственные данные в Microsoft Graph и использовать различные возможности Microsoft 365.
В этом приложении .NET показано, как использовать API соединителей Microsoft Graph для создания пользовательского соединителя и его использования для работы с поиском (Майкрософт). В этом руководстве используется пример инвентаризации частей устройства данных для организации Contoso Appliance Repair.
Предварительные условия
Пакет SDK для .NET, установленный на компьютере разработки.
У вас должна быть рабочая или учебная учетная запись Майкрософт с ролью глобального администратора. Если у вас нет клиента Microsoft 365, вы можете претендовать на него в рамках Программы разработчиков Microsoft 365. Дополнительные сведения см. в разделе Вопросы и ответы. Кроме того, вы можете зарегистрироваться для получения бесплатной пробной версии на 1 месяц или приобрести план Microsoft 365.
Установите Entity Framework Core Tools в качестве глобального средства с помощью следующей команды:
dotnet tool install --global dotnet-ef
Установите средство для обновления базы данных SQLite. Например, браузер базы данных для SQLite.
Скачайте файлApplianceParts.csv из репозитория примера соединителя поиска.
Регистрация приложения на портале
В этом упражнении вы зарегистрируете новое приложение в Microsoft Entra ID, чтобы включить проверку подлинности только для приложений. Соединители Microsoft Graph используют проверку подлинности только для приложений для доступа к API соединителя.
Регистрация приложения для проверки подлинности только для приложений
В этом разделе описано, как зарегистрировать приложение, которое поддерживает проверку подлинности только для приложений с помощью потока учетных данных клиента.
Войдите в центр администрирования Microsoft Entra .
Разверните меню >Удостоверениевыберите Приложения>Регистрация приложений>Новая регистрация.
Введите имя приложения, например
Parts Inventory Connector
.Задайте для параметра Поддерживаемые типы учетных записейзначение Учетные записи только в этом каталоге организации.
Оставьте поле URI перенаправления пустым.
Нажмите Зарегистрировать. На странице обзор приложения скопируйте значение идентификатора приложения (клиента) и идентификатора каталога (клиента) и сохраните их. Эти значения потребуются на следующем шаге.
Выберите Разрешения API в разделе Управление.
Удалите разрешение User.Read по умолчанию в разделе Настроенные разрешения , выбрав многоточие (...) в строке и выбрав Удалить разрешение.
Выберите Добавить разрешение, а затем — Microsoft Graph.
Выберите Разрешения приложения.
Выберите ExternalConnection.ReadWrite.OwnedBy и ExternalItem.ReadWrite.OwnedBy, а затем выберите Добавить разрешения.
Выберите Предоставить согласие администратора для..., а затем нажмите кнопку Да , чтобы предоставить согласие администратора для выбранного разрешения.
Выберите Сертификаты и секреты в разделе Управление, а затем выберите Новый секрет клиента.
Введите описание, выберите длительность и нажмите кнопку Добавить.
Скопируйте секрет из столбца Значение . Он понадобится на следующих шагах.
Важно!
Система никогда не покажет секрет клиента повторно, поэтому убедитесь, что вы скопировали его.
Создание приложения
Начните с создания консольного проекта .NET с помощью интерфейса командной строки .NET.
Откройте интерфейс командной строки (CLI) в каталоге, в котором вы хотите создать проект. Выполните следующую команду.
dotnet new console -o PartsInventoryConnector
После создания проекта убедитесь, что он работает, изменив текущий каталог на каталог PartsInventoryConnector и выполнив следующую команду в интерфейсе командной строки.
dotnet run
Если это работает, приложение должно вывести .
Hello, World!
Установка зависимостей
Прежде чем переходить дальше, добавьте некоторые дополнительные зависимости, которые вы будете использовать позже.
- Пакеты конфигурации .NET для чтения конфигурации приложения из appsettings.json.
- Клиентская библиотека удостоверений Azure для .NET для проверки подлинности пользователя и получения маркеров доступа.
- Клиентская библиотека Microsoft Graph .NET для выполнения вызовов к Microsoft Graph.
- Пакеты Entity Framework для доступа к локальной базе данных.
- CsvHelper для чтения CSV-файлов.
Выполните следующие команды в интерфейсе командной строки, чтобы установить зависимости.
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
Загрузка параметров приложения
В этом разделе вы добавите в проект сведения о регистрации приложения.
Добавьте идентификатор клиента, идентификатор клиента и секрет клиента в диспетчер секретов .NET. В интерфейсе командной строки измените каталог на расположение PartsInventoryConnector.csproj и выполните следующие команды, заменив <идентификатор клиента> идентификатором клиента из регистрации приложения, <идентификатор клиента> — идентификатором клиента, а <секрет> клиента — секретом клиента.
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
В этом разделе описана настройка клиента пакета SDK Microsoft Graph для использования проверки подлинности только для приложений.
Создание вспомогательного класса
Создайте новый каталог в каталоге 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); }
Замените пустую
InitializeGraph
функцию в Program.cs на следующую.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("[email protected]")] 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 }, { "[email protected]", "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; } }
Создайте файл в каталоге Данныхс именем 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>(); } }
Инициализация базы данных
Откройте интерфейс командной строки (CLI) в каталоге, где находится PartsInventoryConnector.csproj .
Выполните следующие команды:
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); }
Замените функцию-заполнитель
CreateConnectionAsync
в Program.cs следующим кодом.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(); }
Замените функцию-заполнитель
SelectExistingConnectionAsync
в Program.cs следующим кодом.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(); }
Замените функцию-заполнитель
DeleteCurrentConnectionAsync
в Program.cs следующим кодом.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); }
Замените функцию-заполнитель
RegisterSchemaAsync
в Program.cs следующим кодом.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(); }
Замените функцию-заполнитель
GetSchemaAsync
в Program.cs следующим кодом.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(); }
Замените функцию-заполнитель
UpdateItemsFromDatabaseAsync
в Program.cs следующим кодом.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 в это соединение.
- Откройте интерфейс командной строки (CLI) в каталоге PartsInventoryConnector .
- Используйте команду
dotnet build
для сборки примера. - Используйте команду
dotnet run
для запуска примера. - Выберите 1. Создайте подключение. Введите уникальный идентификатор, имя и описание для этого подключения.
- Выберите 4. Зарегистрируйте схему для текущего подключения, а затем дождитесь завершения операции.
- Выберите 7. Отправка всех элементов в текущее подключение.
Примечание.
Если шаг 5 приводит к ошибке, подождите несколько минут и выберите 7. Отправка всех элементов в текущее подключение.
Поверхности данных в поиске
На этом шаге вы создадите вертикали поиска и типы результатов для настройки результатов поиска в Microsoft SharePoint, Microsoft Office и Поиске (Майкрософт) в Bing.
Создание вертикали
Войдите в Центр администрирования 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 результатов, перейдите на вкладку Части устройства . Отображаются результаты соединителя.
Поздравляем!
Вы успешно завершили работу с руководством по соединителям Microsoft Graph для .NET: вы создали пользовательский соединитель и использовали его для работы с поиском (Майкрософт).
Дальнейшие действия
- Дополнительные сведения о пользовательских соединителях см. в статье Общие сведения о соединителях Microsoft Graph.
- Просмотрите примеры соединителей.
- Изучите примеры соединителей от сообщества.
Возникла проблема с этим разделом? Если это так, отправьте нам отзыв, чтобы мы исправили этот раздел.