Crear el primer conector de Microsoft Graph personalizado
Los conectores de Microsoft Graph le permiten agregar sus propios datos a Microsoft Graph y hacer que puedan alimentar varias experiencias de Microsoft 365.
Esta aplicación .NET muestra cómo usar la API de conectores de Microsoft Graph para crear un conector personalizado y usarlo para impulsar Microsoft Search. En este tutorial se usa un inventario de piezas de dispositivo de datos de ejemplo para la organización Contoso Appliance Repair.
Requisitos previos
SDK de .NET instalado en la máquina de desarrollo.
Debe tener una cuenta profesional o educativa de Microsoft con el rol de administrador global. Si no tiene un inquilino de Microsoft 365, puede calificar para uno a través del Programa para desarrolladores de Microsoft 365; Para obtener más información, consulte las preguntas más frecuentes. Como alternativa, puede registrarse para obtener una evaluación gratuita de 1 mes o comprar un plan de Microsoft 365.
Instale Entity Framework Core Tools como una herramienta global mediante el siguiente comando:
dotnet tool install --global dotnet-ef
Instale una herramienta para actualizar una base de datos SQLite. Por ejemplo, el explorador de base de datos para SQLite.
Descargue el archivo ApplianceParts.csv del repositorio de ejemplo del conector de búsqueda.
Registrar la aplicación en el portal
En este ejercicio registrará una nueva aplicación en Microsoft Entra ID para habilitar la autenticación de solo aplicación. Los conectores de Microsoft Graph usan la autenticación de solo aplicación para acceder a las API del conector.
Registro de la aplicación para la autenticación de solo aplicación
En esta sección registrará una aplicación que admita la autenticación de solo aplicación mediante el flujo de credenciales de cliente.
Inicie sesión en el Centro de administración Microsoft Entra.
Expanda el menú >Identidad y seleccioneRegistros> de aplicaciones>Nuevo registro.
Escriba un nombre para la aplicación, por ejemplo,
Parts Inventory Connector
.Establezca Tipos de cuenta admitidosen Solo cuentas en este directorio organizativo.
Deje URI de redireccionamiento vacía.
Seleccione Registrar. En la página Información general de la aplicación, copie el valor del identificador de aplicación (cliente) y el identificador de directorio (inquilino) y guárdelos, necesitará estos valores en el paso siguiente.
Seleccione Permisos de las API en Administrar.
Quite el permiso User.Read predeterminado en Permisos configurados ; para ello, seleccione los puntos suspensivos (...) de su fila y seleccione Quitar permiso.
Seleccione Agregar un permiso y, a continuación, Microsoft Graph.
Seleccione Permisos de aplicación.
Seleccione ExternalConnection.ReadWrite.OwnedBy y ExternalItem.ReadWrite.OwnedBy y, a continuación, seleccione Agregar permisos.
Seleccione Conceder consentimiento de administrador para...y, a continuación, seleccione Sí para proporcionar el consentimiento del administrador para el permiso seleccionado.
Seleccione Certificados y secretos en Administrar y, a continuación, seleccione Nuevo secreto de cliente.
Escriba una descripción, elija una duración y seleccione Agregar.
Copie el secreto de la columna Valor ; lo necesitará en los pasos siguientes.
Importante
El secreto de cliente no se vuelve a mostrar, así que asegúrese de copiarlo en este momento.
Crear la aplicación
Empiece por crear un nuevo proyecto de consola de .NET mediante la CLI de .NET.
Abra la interfaz de línea de comandos (CLI) en un directorio donde quiera crear el proyecto. Ejecuta el siguiente comando.
dotnet new console -o PartsInventoryConnector
Una vez creado el proyecto, compruebe que funciona cambiando el directorio actual al directorio PartsInventoryConnector y ejecutando el siguiente comando en la CLI.
dotnet run
Si funciona, la aplicación debe generar
Hello, World!
.
Instalar dependencias
Antes de continuar, agregue algunas dependencias adicionales que use más adelante.
- Paquetes de configuración de .NET para leer la configuración de la aplicación desde appsettings.json.
- Biblioteca cliente de Azure Identity para .NET para autenticar al usuario y adquirir tokens de acceso.
- Biblioteca cliente de Microsoft Graph .NET para realizar llamadas a Microsoft Graph.
- Paquetes de Entity Framework para acceder a una base de datos local.
- CsvHelper para leer archivos CSV.
Ejecute los siguientes comandos en la CLI para instalar las dependencias.
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
Cargar la configuración de la aplicación
En esta sección, agregará los detalles del registro de la aplicación al proyecto.
Agregue el identificador de cliente, el identificador de inquilino y el secreto de cliente al Administrador de secretos de .NET. En la interfaz de la línea de comandos, cambie el directorio a la ubicación de PartsInventoryConnector.csproj y ejecute los siguientes comandos, reemplazando <client-id> por el identificador de cliente del registro de la aplicación, <tenant-id> por el identificador de inquilino y <client-secret> por el secreto de 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>
Cree un archivo en el directorio PartsInventoryConnector denominado Settings.cs y agregue el código siguiente.
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."); } }
Diseñar la aplicación
En esta sección creará un menú basado en consola.
Abra ./Program.cs y reemplace todo su contenido por el código siguiente.
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")); }
Agregue los siguientes métodos de marcador de posición al final del archivo. Las implementará en pasos 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 }
Esto implementa un menú básico y lee la elección del usuario desde la línea de comandos.
Configuración de Microsoft Graph
En esta sección, configurará el cliente del SDK de Microsoft Graph para usar la autenticación de solo aplicación.
Creación de una clase auxiliar
Cree un nuevo directorio en el directorio PartsInventoryConnector denominado Graph.
Cree un archivo en el directorio de Graph denominado GraphHelper.cs y agregue las siguientes
using
instrucciones.using Azure.Identity; using Microsoft.Graph; using Microsoft.Graph.Models.ExternalConnectors; using Microsoft.Kiota.Authentication.Azure;
Agregue un espacio de nombres y una definición de clase.
namespace PartsInventoryConnector.Graph; public static class GraphHelper { }
Agregue el código siguiente a la
GraphHelper
clase , que configura unaGraphServiceClient
con autenticación de solo aplicación.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); }
Reemplace la función vacía
InitializeGraph
en Program.cs por lo siguiente.void InitializeGraph(Settings settings) { try { GraphHelper.Initialize(settings); } catch (Exception ex) { Console.WriteLine($"Error initializing Graph: {ex.Message}"); } }
Creación de la base de datos
En esta sección, definirá el modelo para los registros de inventario de elementos del dispositivo y el contexto de Entity Framework y usará la dotnet ef
herramienta para inicializar la base de datos.
Definición del modelo
Cree un nuevo directorio en el directorio PartsInventoryConnector denominado Data.
Cree un archivo en el directorio Data denominado AppliancePart.cs y agregue el código siguiente.
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; } }
Cree un archivo en el directorio Data denominado ApplianceDbContext.cs y agregue el código siguiente.
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; } }
Cree un archivo en el directorio Data denominado CsvDataLoader.cs y agregue el código siguiente.
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 la base de datos
Abra la interfaz de línea de comandos (CLI) en el directorio donde se encuentra PartsInventoryConnector.csproj .
Ejecute los comandos siguientes:
dotnet ef migrations add InitialCreate dotnet ef database update
Nota:
Ejecute los siguientes comandos si un esquema cambia en el archivo CSV y refleje esos cambios en la base de datos SQLite.
dotnet ef database drop
dotnet ef database update
Administrar conexiones
En esta sección agregará métodos para administrar conexiones externas.
Crear una conexión
Agregue la siguiente función a la
GraphHelper
clase en 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); }
Reemplace la función
CreateConnectionAsync
de marcador de posición de Program.cs por lo siguiente.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; } }
Selección de una conexión existente
Agregue la siguiente función a la
GraphHelper
clase en GraphHelper.cs.public static async Task<ExternalConnectionCollectionResponse?> GetExistingConnectionsAsync() { _ = graphClient ?? throw new MemberAccessException("graphClient is null"); return await graphClient.External.Connections.GetAsync(); }
Reemplace la función
SelectExistingConnectionAsync
de marcador de posición de Program.cs por lo siguiente.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; } }
Eliminar una conexión
Agregue la siguiente función a la
GraphHelper
clase en 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(); }
Reemplace la función
DeleteCurrentConnectionAsync
de marcador de posición de Program.cs por lo siguiente.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}"); } }
Administrar esquema
En esta sección, agregará métodos para registrar el esquema del conector.
Registro del esquema
Agregue las siguientes funciones a la
GraphHelper
clase en 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); }
Reemplace la función
RegisterSchemaAsync
de marcador de posición de Program.cs por lo siguiente.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}"); } }
Obtener el esquema de una conexión
Agregue la siguiente función a la
GraphHelper
clase en 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(); }
Reemplace la función
GetSchemaAsync
de marcador de posición de Program.cs por lo siguiente.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}"); } }
Administrar elementos
En esta sección, agregará métodos para agregar o eliminar elementos al conector.
Cargar o eliminar elementos
Agregue la siguiente función a la
GraphHelper
clase en 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); }
Agregue la siguiente función a la
GraphHelper
clase en 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(); }
Reemplace la función
UpdateItemsFromDatabaseAsync
de marcador de posición de Program.cs por lo siguiente.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); } }
Ejecución de la aplicación
En este paso, compilará y ejecutará el ejemplo. En este ejemplo de código se crea una nueva conexión, se registra el esquema y, a continuación, se insertan elementos del archivo ApplianceParts.csv en esa conexión.
- Abra la interfaz de línea de comandos (CLI) en el directorio PartsInventoryConnector .
- Use el comando
dotnet build
para compilar el ejemplo. - Use el comando
dotnet run
para ejecutar el ejemplo. - Seleccione 1. Cree una conexión. Escriba un identificador, un nombre y una descripción únicos para esa conexión.
- Seleccione 4. Registre el esquema para la conexión actual y espere a que se complete la operación.
- Seleccione 7. Inserte todos los elementos en la conexión actual.
Nota:
Si el paso 5 produce un error, espere unos minutos y seleccione 7. Inserte todos los elementos en la conexión actual.
Exponer los datos en la búsqueda
En este paso, creará verticales de búsqueda y tipos de resultados para personalizar los resultados de búsqueda en Microsoft SharePoint, Microsoft Office y Microsoft Search en Bing.
Creación de un vertical
Inicie sesión en el Centro de administración de Microsoft 365 con el rol de administrador global y haga lo siguiente:
Vaya a Configuración>Buscar &personalizaciones de inteligencia>.
Vaya a Verticales y seleccione Agregar.
Escriba
Appliance Parts
en el campo Nombre y seleccione Siguiente.Seleccione Conectores y, a continuación, seleccione el conector inventario de piezas . Seleccione Siguiente.
En la página Agregar una consulta , deje la consulta en blanco. Seleccione Siguiente.
En la página Filtros , seleccione Siguiente.
Seleccione Agregar vertical.
Seleccione Habilitar vertical y, a continuación, haga clic en Listo.
Creación de un tipo de resultado
Para crear un tipo de resultado:
Vaya a Configuración>Buscar &personalizaciones de inteligencia>.
Vaya a la pestaña Tipo de resultado y seleccione Agregar.
Escriba
Appliance Part
en el campo Nombre y seleccione Siguiente.En la página Origen del contenido , seleccione Conector de piezas. Seleccione Siguiente.
En la página Reglas , seleccione Siguiente.
En la página Diseñar el diseño , pegue el siguiente CÓDIGO JSON y, a continuación, seleccione Siguiente.
{ "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" }
Seleccione Agregar tipo de resultado y, a continuación, haga clic en Listo.
Búsqueda de resultados
En este paso, buscará elementos en SharePoint.
Vaya al sitio raíz de SharePoint del inquilino.
Con el cuadro de búsqueda de la parte superior de la página, busque bisagra.
Cuando la búsqueda se complete con 0 resultados, seleccione la pestaña Piezas del dispositivo . Se muestran los resultados del conector.
¡Enhorabuena!
Ha completado correctamente el tutorial de conectores de Microsoft Graph de .NET: ha creado un conector personalizado y lo ha usado para impulsar Microsoft Search.
Siguientes pasos
- Para obtener más información sobre los conectores personalizados, consulte Introducción a los conectores de Microsoft Graph.
- Examine nuestros conectores de ejemplo.
- Explore los conectores de ejemplo de la comunidad.
¿Tiene algún problema con esta sección? Si es así, envíenos sus comentarios para que podamos mejorarla.