Criar seu primeiro conector Microsoft Graph personalizado
Os conectores do Microsoft Graph permitem-lhe adicionar os seus próprios dados ao Microsoft Graph e dar-lhe energia a várias experiências do Microsoft 365.
Esta aplicação .NET mostra-lhe como utilizar a API de conectores do Microsoft Graph para criar um conector personalizado e utilizá-lo para ligar o Microsoft Search. Este tutorial utiliza um inventário de peças da aplicação de dados de exemplo para a organização de Reparação da Aplicação Contoso.
Pré-requisitos
O SDK .NET instalado no seu computador de desenvolvimento.
Deve ter uma conta escolar ou profissional da Microsoft com a função de Administrador global. Se não tiver um inquilino do Microsoft 365, poderá qualificar-se para um através do Programa para Programadores do Microsoft 365; para obter detalhes, veja as FAQ. Em alternativa, pode inscrever-se numa avaliação gratuita de um mês ou comprar um plano do Microsoft 365.
Instale o Entity Framework Core Tools como uma ferramenta global com o seguinte comando:
dotnet tool install --global dotnet-ef
Instale uma ferramenta para atualizar uma base de dados SQLite. Por exemplo, o Browser DB para SQLite.
Transfira o ficheiro deApplianceParts.csv a partir do repositório de exemplo do conector de pesquisa.
Registrar o aplicativo no portal
Neste exercício, irá registar uma nova aplicação no Microsoft Entra ID para ativar a autenticação apenas de aplicações. Os conectores do Microsoft Graph utilizam a autenticação apenas de aplicações para aceder às APIs do conector.
Registar aplicação para autenticação apenas de aplicação
Nesta secção, irá registar uma aplicação que suporta a autenticação apenas da aplicação através do fluxo de credenciais de cliente.
Expanda o menu >Identidade selecione Registos de aplicações>Registos de aplicações>Novo registo.
Introduza um nome para a sua aplicação, por exemplo,
Parts Inventory Connector
.Defina Tipos de conta suportados como Contas apenas neste diretório organizacional.
Deixe o URI de Redirecionamento vazio.
Selecione Registrar. Na página Descrição Geral da aplicação, copie o valor do ID da Aplicação (cliente) e do ID do Diretório (inquilino) e guarde-os, irá precisar destes valores no próximo passo.
Selecione Permissões de API em Gerenciar.
Remova a permissão User.Read predefinida em Permissões configuradas ao selecionar as reticências (...) na respetiva linha e selecionar Remover permissão.
Selecione Adicionar uma permissão e, em seguida, Microsoft Graph.
Selecione Permissões de aplicativos.
Selecione ExternalConnection.ReadWrite.OwnedBy e ExternalItem.ReadWrite.OwnedBy e, em seguida, selecione Adicionar permissões.
Selecione Conceder consentimento do administrador para...e, em seguida, selecione Sim para dar consentimento do administrador para a permissão selecionada.
Selecione Certificados e segredos em Gerir e, em seguida, selecione Novo segredo do cliente.
Introduza uma descrição, escolha uma duração e selecione Adicionar.
Copie o segredo da coluna Valor . Irá precisar dele nos próximos passos.
Importante
Este segredo do cliente nunca é mostrado novamente, portanto, certifique-se de copiá-lo agora.
Criar o aplicativo
Comece por criar um novo projeto de consola .NET com a CLI de .NET.
Abra a interface de linha de comandos (CLI) num diretório onde pretende criar o projeto. Execute o seguinte comando:
dotnet new console -o PartsInventoryConnector
Assim que o projeto for criado, verifique se funciona ao alterar o diretório atual para o diretório PartsInventoryConnector e ao executar o seguinte comando na CLI.
dotnet run
Se funcionar, a aplicação deverá produzir
Hello, World!
.
Instalar dependências
Antes de continuar, adicione algumas dependências adicionais que utiliza mais tarde.
- Pacotes de configuração .NET para ler a configuração da aplicação a partir de appsettings.json.
- Biblioteca de cliente da Identidade do Azure para .NET para autenticar o utilizador e adquirir tokens de acesso.
- Biblioteca de cliente .NET do Microsoft Graph para fazer chamadas para o Microsoft Graph.
- Pacotes do Entity Framework para aceder a uma base de dados local.
- CsvHelper para ler ficheiros CSV.
Execute os seguintes comandos na CLI para instalar as dependências.
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
Carregar as definições da aplicação
Nesta secção, vai adicionar os detalhes do registo da aplicação ao projeto.
Adicione o ID de cliente, o ID do inquilino e o segredo do cliente ao Gestor de Segredos do .NET. Na sua interface de linha de comandos, altere o diretório para a localização de PartsInventoryConnector.csproj e execute os seguintes comandos, substituindo <client-id> pelo ID de cliente do registo da aplicação, <id> do inquilino pelo ID do inquilino e <segredo> do cliente pelo segredo do cliente.
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>
Crie um ficheiro no diretório PartsInventoryConnector com o nome Settings.cs e adicione o seguinte código.
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."); } }
Design do aplicativo
Nesta secção, vai criar um menu baseado na consola.
Abra ./Program.cs e substitua todo o respetivo conteúdo pelo seguinte código.
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")); }
Adicione os seguintes métodos de marcador de posição no final do ficheiro. Pode implementá-los em passos posteriores.
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 }
Esta ação implementa um menu básico e lê a escolha do utilizador a partir da linha de comandos.
Configurar o Microsoft Graph
Nesta secção, vai configurar o cliente do SDK do Microsoft Graph para utilizar a autenticação apenas de aplicações.
Criar uma classe auxiliar
Crie um novo diretório no diretório PartsInventoryConnector com o nome Graph.
Crie um ficheiro no diretório do Graph com o nome GraphHelper.cs e adicione as seguintes
using
instruções.using Azure.Identity; using Microsoft.Graph; using Microsoft.Graph.Models.ExternalConnectors; using Microsoft.Kiota.Authentication.Azure;
Adicione um espaço de nomes e uma definição de classe.
namespace PartsInventoryConnector.Graph; public static class GraphHelper { }
Adicione o seguinte código à
GraphHelper
classe , que configura umaGraphServiceClient
com autenticação apenas de aplicação.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); }
Substitua a função empty
InitializeGraph
no Program.cs pelo seguinte.void InitializeGraph(Settings settings) { try { GraphHelper.Initialize(settings); } catch (Exception ex) { Console.WriteLine($"Error initializing Graph: {ex.Message}"); } }
Criar a base de dados
Nesta secção, vai definir o modelo para os registos de inventário de peças da aplicação e o contexto do Entity Framework e utilizar a dotnet ef
ferramenta para inicializar a base de dados.
Definir o modelo
Crie um novo diretório no diretório PartsInventoryConnector denominado Dados.
Crie um ficheiro no diretório Dados com o nome AppliancePart.cs e adicione o seguinte código.
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; } }
Crie um ficheiro no diretório Dados com o nome ApplianceDbContext.cs e adicione o seguinte código.
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; } }
Crie um ficheiro no diretório Dados com o nome CsvDataLoader.cs e adicione o seguinte código.
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>(); } }
Inicializar a base de dados
Abra a interface de linha de comandos (CLI) no diretório onde se encontra PartsInventoryConnector.csproj .
Execute os seguintes comandos:
dotnet ef migrations add InitialCreate dotnet ef database update
Observação
Execute os seguintes comandos se um esquema for alterado no ficheiro CSV e reflita essas alterações na base de dados SQLite.
dotnet ef database drop
dotnet ef database update
Gerenciar conexões
Nesta secção, vai adicionar métodos para gerir ligações externas.
Criar uma conexão
Adicione a seguinte função à
GraphHelper
classe no 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); }
Substitua a função
CreateConnectionAsync
de marcador de posição no Program.cs pelo seguinte.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; } }
Selecionar uma ligação existente
Adicione a seguinte função à
GraphHelper
classe no GraphHelper.cs.public static async Task<ExternalConnectionCollectionResponse?> GetExistingConnectionsAsync() { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); return await graphClient.External.Connections.GetAsync(); }
Substitua a função
SelectExistingConnectionAsync
de marcador de posição no Program.cs pelo seguinte.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; } }
Excluir uma conexão
Adicione a seguinte função à
GraphHelper
classe no 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(); }
Substitua a função
DeleteCurrentConnectionAsync
de marcador de posição no Program.cs pelo seguinte.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}"); } }
Gerenciar esquema
Nesta secção, irá adicionar métodos para registar o esquema do conector.
Registar o esquema
Adicione as seguintes funções à
GraphHelper
classe no 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); }
Substitua a função
RegisterSchemaAsync
de marcador de posição no Program.cs pelo seguinte.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}"); } }
Obter o esquema de uma ligação
Adicione a seguinte função à
GraphHelper
classe no 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(); }
Substitua a função
GetSchemaAsync
de marcador de posição no Program.cs pelo seguinte.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}"); } }
Gerenciar itens
Nesta secção, irá adicionar métodos para adicionar ou eliminar itens ao conector.
Carregar ou eliminar itens
Adicione a seguinte função à
GraphHelper
classe no 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); }
Adicione a seguinte função à
GraphHelper
classe no 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(); }
Substitua a função
UpdateItemsFromDatabaseAsync
de marcador de posição no Program.cs pelo seguinte.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); } }
Executar a aplicação
Neste passo, irá criar e executar o exemplo. Este exemplo de código cria uma nova ligação, regista o esquema e, em seguida, envia itens do ficheiro ApplianceParts.csv para essa ligação.
- Abra a interface de linha de comandos (CLI) no diretório PartsInventoryConnector .
- Utilize o comando
dotnet build
para criar o exemplo. - Utilize o comando
dotnet run
para executar o exemplo. - Selecione 1. Crie uma ligação. Introduza um identificador, nome e descrição exclusivos para essa ligação.
- Selecione 4. Registe o esquema para a ligação atual e, em seguida, aguarde que a operação seja concluída.
- Selecione 7. Emita TODOS os itens para a ligação atual.
Observação
Se o passo 5 resultar num erro, aguarde alguns minutos e, em seguida, selecione 7. Emita TODOS os itens para a ligação atual.
Surface os dados em pesquisa
Neste passo, irá criar verticais de pesquisa e tipos de resultados para personalizar os resultados da pesquisa no Microsoft SharePoint, Microsoft Office e Microsoft Search no Bing.
Criar uma vertical
Inicie sessão no centro de administração do Microsoft 365 com a função de administrador global e faça o seguinte:
Aceda a Definições>Pesquisa &personalizações de inteligência>.
Aceda a Verticais e, em seguida, selecione Adicionar.
Introduza
Appliance Parts
no campo Nome e selecione Seguinte.Selecione Conectores e, em seguida, selecione o conector Inventário de Peças . Selecione Avançar.
Na página Adicionar uma consulta , deixe a consulta em branco. Selecione Avançar.
Na página Filtros , selecione Seguinte.
Selecione Adicionar Vertical.
Selecione Ativar vertical e, em seguida, selecione Concluído.
Criar um tipo de resultado
Para criar um tipo de resultado:
Aceda a Definições>Pesquisa &personalizações de inteligência>.
Aceda ao separador Tipo de resultado e, em seguida, selecione Adicionar.
Introduza
Appliance Part
no campo Nome e selecione Seguinte.Na página Origem do conteúdo , selecione Conector de Peças. Selecione Avançar.
Na página Regras , selecione Seguinte.
Na página Estruturar o esquema , cole o seguinte JSON e, em seguida, selecione Seguinte.
{ "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" }
Selecione Adicionar tipo de resultado e, em seguida, selecione Concluído.
Procurar resultados
Neste passo, vai procurar partes no SharePoint.
Aceda ao site do SharePoint de raiz do seu inquilino.
Ao utilizar a caixa de pesquisa na parte superior da página, procure dobradiças.
Quando a pesquisa for concluída com 0 resultados, selecione o separador Peças da Aplicação . São apresentados os resultados do conector.
Parabéns!
Concluiu com êxito o tutorial de conectores .NET do Microsoft Graph: criou um conector personalizado e utilizou-o para ligar o Microsoft Search.
Próximas etapas
- Para saber mais sobre conectores personalizados, consulte Descrição geral dos conectores do Microsoft Graph.
- Procure os nossos conectores de exemplo.
- Explore os conectores de exemplo da comunidade.
Tem algum problema com essa seção? Se tiver, envie seus comentários para que possamos melhorar esta seção.