Criar seu primeiro conector Microsoft Graph personalizado
Os conectores do Microsoft Graph permitem adicionar seus próprios dados ao Microsoft Graph e tê-los alimentando várias experiências do Microsoft 365.
Este aplicativo .NET mostra como usar a API de conectores do Microsoft Graph para criar um conector personalizado e usá-lo para alimentar o Microsoft Pesquisa. Este tutorial usa um inventário de partes dispositivo dados de exemplo para a organização Contoso Appliance Repair.
Pré-requisitos
O SDK do .NET instalado em seu computador de desenvolvimento.
Você deve ter uma conta de trabalho ou de estudante da Microsoft com a função Administrador global. Se você não tiver um locatário do Microsoft 365, poderá se qualificar para um por meio do Programa de Desenvolvedores do Microsoft 365; para obter detalhes, confira as perguntas frequentes. Como alternativa, você pode se inscrever para uma avaliação gratuita de 1 mês ou comprar um plano do Microsoft 365.
Instale as Ferramentas do Entity Framework Core como uma ferramenta global usando o seguinte comando:
dotnet tool install --global dotnet-ef
Instale uma ferramenta para atualizar um banco de dados SQLite. Por exemplo, o Navegador DB para SQLite.
Baixe o arquivo ApplianceParts.csv do repositório de exemplo do conector Pesquisa.
Registrar o aplicativo no portal
Neste exercício, você registrará um novo aplicativo no Microsoft Entra ID para habilitar a autenticação somente aplicativo. Os conectores do Microsoft Graph usam a autenticação somente aplicativo para acessar as APIs do conector.
Registrar aplicativo para autenticação somente aplicativo
Nesta seção, você registrará um aplicativo que dá suporte à autenticação somente aplicativo usando o fluxo de credenciais do cliente.
Expanda o menu >Identidade selecione Aplicativos>Registros de aplicativo>Novo registro.
Insira um nome para seu aplicativo, por exemplo,
Parts Inventory Connector
.Defina tipos de conta com suporte apenas para contas neste diretório organizacional.
Deixe o URI de Redirecionamento vazio.
Selecione Registrar. Na página Visão geral do aplicativo, copie o valor da ID do Aplicativo (cliente) e da ID do Diretório (locatário) e salve-os, você precisará desses valores na próxima etapa.
Selecione Permissões de API em Gerenciar.
Remova a permissão padrão User.Read em Permissões configuradas selecionando as reticências (...) em sua linha e selecionando 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 selecione Adicionar permissões.
Selecione Conceder consentimento do administrador para...e selecione Sim para fornecer o consentimento do administrador para a permissão selecionada.
Selecione Certificados e segredos em Gerenciar e selecione Novo segredo do cliente.
Insira uma descrição, escolha uma duração e selecione Adicionar.
Copie o segredo da coluna Valor , você precisará dele nas próximas etapas.
Importante
Este segredo do cliente nunca é mostrado novamente, portanto, certifique-se de copiá-lo agora.
Criar o aplicativo
Comece criando um novo projeto de console do .NET usando a CLI do .NET.
Abra sua CLI (interface de linha de comando) em um diretório em que você deseja criar o projeto. Execute o seguinte comando:
dotnet new console -o PartsInventoryConnector
Depois que o projeto for criado, verifique se ele funciona alterando o diretório atual para o diretório PartsInventoryConnector e executando o comando a seguir em sua CLI.
dotnet run
Se funcionar, o aplicativo deverá gerar
Hello, World!
.
Instalar dependências
Antes de seguir em frente, adicione algumas dependências adicionais que você usa posteriormente.
- Pacotes de configuração do .NET para ler a configuração do aplicativo de appsettings.json.
- Biblioteca de clientes do Azure Identity para .NET para autenticar o usuário e adquirir tokens de acesso.
- Biblioteca de clientes do Microsoft Graph .NET para fazer chamadas para o Microsoft Graph.
- Pacotes da Estrutura de Entidades para acessar um banco de dados local.
- CsvHelper para ler arquivos CSV.
Execute os comandos a seguir 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 configurações do aplicativo
Nesta seção, você adiciona os detalhes do registro do aplicativo ao projeto.
Adicione a ID do cliente, a ID do locatário e o segredo do cliente ao .NET Secret Manager. Em sua interface de linha de comando, altere o diretório para o local de PartsInventoryConnector.csproj e execute os comandos a seguir, substituindo <a ID> do cliente por sua ID do cliente de seu registro de aplicativo, <id> de locatário com sua ID de locatário e <segredo> do cliente com o 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 arquivo no diretório PartsInventoryConnector chamado Settings.cs e adicione o código a seguir.
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 seção, você cria um menu baseado em console.
Abra ./Program.cs e substitua todo o conteúdo pelo código a seguir.
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 espaço reservado no final do arquivo. Implemente-os em etapas 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 }
Isso implementa um menu básico e lê a escolha do usuário na linha de comando.
Criar o banco de dados
Nesta seção, você definirá o modelo para os registros de inventário de dispositivo parte e o contexto da Estrutura de Entidades e usará a dotnet ef
ferramenta para inicializar o banco de dados.
Definir o modelo
Crie um novo diretório no diretório PartsInventoryConnector chamado Data.
Crie um arquivo no diretório de dados chamado AppliancePart.cs e adicione o código a seguir.
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 arquivo no diretório De dados chamado ApplianceDbContext.cs e adicione o código a seguir.
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 arquivo no diretório de dados chamado CsvDataLoader.cs e adicione o código a seguir.
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 o banco de dados
Abra sua CLI (interface de linha de comando) no diretório em que PartsInventoryConnector.csproj está localizado.
Execute os seguintes comandos:
dotnet ef migrations add InitialCreate dotnet ef database update
Observação
Execute os comandos a seguir se um esquema for alterado no arquivo CSV e refletir essas alterações no banco de dados SQLite.
dotnet ef database drop
dotnet ef database update
Configurar o Microsoft Graph
Nesta seção, você configura o cliente do SDK do Microsoft Graph para usar a autenticação somente aplicativo.
Criar uma classe auxiliar
Crie um novo diretório no diretório PartsInventoryConnector chamado Graph.
Crie um arquivo no diretório graph chamado GraphHelper.cs e adicione as instruções a seguir
using
.using Azure.Identity; using Microsoft.Graph; using Microsoft.Graph.Models.ExternalConnectors; using Microsoft.Kiota.Authentication.Azure;
Adicione um namespace e uma definição de classe.
namespace PartsInventoryConnector.Graph; public static class GraphHelper { }
Adicione o código a
GraphHelper
seguir à classe para configurar umGraphServiceClient
com autenticação somente aplicativo.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 vazia
InitializeGraph
no Program.cs pelo seguinte.void InitializeGraph(Settings settings) { try { GraphHelper.Initialize(settings); } catch (Exception ex) { Console.WriteLine($"Error initializing Graph: {ex.Message}"); } }
Gerenciar conexões
Nesta seção, você adiciona métodos para gerenciar conexões externas.
Criar uma conexão
Adicione a seguinte função à
GraphHelper
classe em 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 espaço reservado em 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 conexão existente
Adicione a seguinte função à
GraphHelper
classe em 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 espaço reservado em 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 em 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 espaço reservado em 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 seção, você adicionará métodos para registrar o esquema do conector.
Registrar o esquema
Adicione as seguintes funções à
GraphHelper
classe em 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 espaço reservado em 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 conexão
Adicione a seguinte função à
GraphHelper
classe em 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 espaço reservado em 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 seção, você adicionará métodos para adicionar ou excluir itens ao conector.
Carregar ou excluir itens
Adicione a seguinte função à
GraphHelper
classe em 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 em 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 espaço reservado em 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 o aplicativo
Nesta etapa, você criará e executará o exemplo. Esse exemplo de código criará uma nova conexão, registrará o esquema e, em seguida, enviará itens do arquivoApplianceParts.csv para essa conexão.
- Abra sua CLI (interface de linha de comando) no diretório PartsInventoryConnector .
- Use o comando
dotnet build
para criar o exemplo. - Use o comando
dotnet run
para executar o exemplo. - Selecione 1. Crie uma conexão. Insira um identificador, nome e descrição exclusivos para essa conexão.
- Selecione 2. Registre o esquema para a opção de conexão atual e aguarde a conclusão da operação.
- Selecione 3. Envie todos os itens para a conexão atual.
Observação
Se a etapa 5 resultar em um erro, aguarde alguns minutos e selecione 3. Envie todos os itens para a conexão atual.
Surface os dados na pesquisa
Nesta etapa, você criará verticais de pesquisa e tipos de resultado para personalizar os resultados da pesquisa no Microsoft SharePoint, Microsoft Office e Microsoft Pesquisa no Bing.
Criar uma vertical
Entre no Centro de administração do Microsoft 365 usando a função de administrador global e faça o seguinte:
Acesse Configurações> Pesquisa &personalizações deinteligência>.
Vá para Verticais e selecione Adicionar.
Insira
Appliance Parts
no campo Nome e selecione Avançar.Selecione Conectores e 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 Avançar.
Selecione Adicionar Vertical.
Selecione Habilitar vertical e selecione Concluído.
Criar um tipo de resultado
Para criar um tipo de resultado:
Acesse Configurações> Pesquisa &personalizações deinteligência>.
Acesse a guia Tipo de resultado e selecione Adicionar.
Insira
Appliance Part
no campo Nome e selecione Avançar.Na página Fonte de conteúdo , selecione Conector de Partes. Selecione Avançar.
Na página Regras , selecione Avançar.
Na página Projetar seu layout , cole o JSON a seguir e selecione Avançar.
{ "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 selecione Concluído.
Pesquisa para obter resultados
Nesta etapa, você pesquisa peças no SharePoint.
Acesse o site raiz do SharePoint para seu locatário.
Usando a caixa de pesquisa na parte superior da página, pesquise por dobradiça.
Quando a pesquisa for concluída com 0 resultados, selecione a guia Partes do Dispositivo . Os resultados do conector são exibidos.
Parabéns!
Você concluiu com êxito o tutorial de conectores do .NET Microsoft Graph: criou um conector personalizado e o usou para alimentar o Microsoft Pesquisa.
Próximas etapas
- Para saber mais sobre conectores personalizados, confira Visão geral dos conectores do Microsoft Graph.
- Navegue por nossos conectores de exemplo.
- Explore 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.
Comentários
https://aka.ms/ContentUserFeedback.
Em breve: Ao longo de 2024, eliminaremos os problemas do GitHub como o mecanismo de comentários para conteúdo e o substituiremos por um novo sistema de comentários. Para obter mais informações, consulteEnviar e exibir comentários de