Compilación de un conector personalizado de Microsoft Graph en C#

En este artículo se describe cómo usar el SDK del conector de Microsoft Graph para compilar un conector personalizado en C#.

Requisitos previos

  1. Descargue, instale y complete la configuración del agente del conector de Microsoft Graph.
  2. Instale Visual Studio 2019 o posterior con el SDK de .NET 7.0.
  3. Descargue el archivo ApplianceParts.csv del repositorio de ejemplo del conector personalizado.

Instalación de la extensión

  1. Abra Visual Studio y vaya a Extensiones>Administrar extensiones.

  2. Búsqueda para la extensión GraphConnectorsTemplate y descárguela.

  3. Cierre y vuelva a iniciar Visual Studio para instalar la plantilla.

  4. Vaya a Archivo>nuevo>proyecto y busque GraphConnectorsTemplate. Seleccione la plantilla y elija Siguiente. Captura de pantalla de la página Crear proyecto a partir de la plantilla en Visual Studio

  5. Proporcione un nombre para el proyecto y elija Siguiente.

  6. Elija .NET Core 3.1, asigne al conector el nombre CustomConnector y elija Crear.

  7. Ahora se crea el proyecto de plantilla de conector personalizado.

    Captura de pantalla de la estructura del proyecto CustomConnector en Visual Studio

Creación del conector personalizado

Antes de compilar el conector, siga estos pasos para instalar paquetes NuGet y crear los modelos de datos que se usarán.

Instalar paquetes NuGet

  1. Haga clic con el botón derecho en el proyecto y elija Abrir en terminal.

  2. Ejecuta el siguiente comando.

    dotnet add package CsvHelper --version 27.2.1
    

Creación de modelos de datos

  1. Cree una carpeta denominada Models (Modelos ) en CustomConnector y cree un archivo denominado AppliancePart.cs en la carpeta .

  2. Pegue el código siguiente en AppliancePart.cs.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Text;
    
    namespace CustomConnector.Models
    {
        public class AppliancePart
        {
            [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; }
        }
    }
    
    

Actualizar ConnectionManagementServiceImpl.cs

Implementará tres métodos en ConnectionManagementServiceImpl.cs.

ValidateAuthentication

El método ValidateAuthentication se usa para validar las credenciales y la dirección URL del origen de datos proporcionada. Debe conectarse a la dirección URL del origen de datos con las credenciales proporcionadas y devolver correctamente si la conexión se realiza correctamente o si se produce un error de autenticación si se produce un error en la conexión.

  1. Cree una carpeta denominada Datos en CustomConnector y cree un archivo CsvDataLoader.cs en la carpeta .

  2. Copie el código siguiente en CsvDataLoader.cs:

    using CsvHelper;
    using CsvHelper.Configuration;
    using CsvHelper.TypeConversion;
    
    using CustomConnector.Models;
    
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    
    namespace CustomConnector.Data
    {
        public static class CsvDataLoader
        {
            public static void ReadRecordFromCsv(string filePath)
            {
                using (var reader = new StreamReader(filePath))
                using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
                {
                    csv.Context.RegisterClassMap<AppliancePartMap>();
                    csv.Read();
                }
            }
        }
    
        public class ApplianceListConverter : DefaultTypeConverter
        {
            public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
            {
                var appliances = text.Split(';');
                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>();
            }
        }
    }
    
    

    El método ReadRecordFromCsv abrirá el archivo CSV y leerá el primer registro del archivo. Podemos usar este método para validar que la dirección URL del origen de datos proporcionada (ruta de acceso del archivo CSV) es válida. Este conector usa la autenticación anónima; por lo tanto, no se validan las credenciales. Si el conector usa cualquier otro tipo de autenticación, la conexión al origen de datos debe realizarse con las credenciales proporcionadas para validar la autenticación.

  3. Agregue la siguiente directiva using en ConnectionManagementServiceImpl.cs.

    using CustomConnector.Data;
    
  4. Actualice el método ValidateAuthentication en ConnectionManagementServiceImpl.cs con el código siguiente para llamar al método ReadRecordFromCsv de la clase CsvDataLoader .

    public override Task<ValidateAuthenticationResponse> ValidateAuthentication(ValidateAuthenticationRequest request, ServerCallContext context)
            {
                try
                {
                    Log.Information("Validating authentication");
                    CsvDataLoader.ReadRecordFromCsv(request.AuthenticationData.DatasourceUrl);
                    return this.BuildAuthValidationResponse(true);
                }
                catch (Exception ex)
                {
                    Log.Error(ex.ToString());
                    return this.BuildAuthValidationResponse(false, "Could not read the provided CSV file with the provided credentials");
                }
            }
    
    

ValidateCustomConfiguration

El método ValidateCustomConfiguration se usa para validar cualquier otro parámetro necesario para la conexión. El conector que está compilando no requiere parámetros adicionales; por lo tanto, el método validará que los parámetros adicionales están vacíos.

  1. Actualice el método ValidateCustomConfiguration en ConnectionManagementServiceImpl.cs con el código siguiente.

    public override Task<ValidateCustomConfigurationResponse> ValidateCustomConfiguration(ValidateCustomConfigurationRequest request, ServerCallContext context)
        {
            Log.Information("Validating custom configuration");
            ValidateCustomConfigurationResponse response;
    
            if (!string.IsNullOrWhiteSpace(request.CustomConfiguration.Configuration))
            {
                response = new ValidateCustomConfigurationResponse()
                {
                    Status = new OperationStatus()
                    {
                        Result = OperationResult.ValidationFailure,
                        StatusMessage = "No additional parameters are required for this connector"
                    },
                };
            }
            else
            {
                response = new ValidateCustomConfigurationResponse()
                {
                    Status = new OperationStatus()
                    {
                        Result = OperationResult.Success,
                    },
                };
            }
    
            return Task.FromResult(response);
        }
    
    

GetDataSourceSchema

El método GetDataSourceSchema se usa para capturar el esquema del conector.

  1. Agregue las siguientes directivas using en AppliancePart.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    using static Microsoft.Graph.Connectors.Contracts.Grpc.SourcePropertyDefinition.Types;
    
    
  2. Agregue el siguiente método GetSchema en la clase AppliancePart.cs.

     public static DataSourceSchema GetSchema()
       {
           DataSourceSchema schema = new DataSourceSchema();
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(PartNumber),
                   Type = SourcePropertyType.Int64,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsQueryable | SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsQueryable | SearchAnnotations.IsRetrievable),
               });
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(Name),
                   Type = SourcePropertyType.String,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
               });
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(Price),
                   Type = SourcePropertyType.Double,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsRetrievable),
               });
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(Inventory),
                   Type = SourcePropertyType.Int64,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsQueryable | SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsQueryable | SearchAnnotations.IsRetrievable),
               });
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(Appliances),
                   Type = SourcePropertyType.StringCollection,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
               });
    
           schema.PropertyList.Add(
               new SourcePropertyDefinition
               {
                   Name = nameof(Description),
                   Type = SourcePropertyType.String,
                   DefaultSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
                   RequiredSearchAnnotations = (uint)(SearchAnnotations.IsSearchable | SearchAnnotations.IsRetrievable),
               });
    
           return schema;
       }
    
    
  3. Agregue la siguiente directiva using en ConnectionManagementServiceImpl.cs.

    using CustomConnector.Models;
    
  4. Actualice el método GetDataSourceSchema en ConnectionManagementServiceImpl.cs con el código siguiente.

    public override Task<GetDataSourceSchemaResponse> GetDataSourceSchema(GetDataSourceSchemaRequest request, ServerCallContext context)
        {
            Log.Information("Trying to fetch datasource schema");
    
            var opStatus = new OperationStatus()
            {
                Result = OperationResult.Success,
            };
    
            GetDataSourceSchemaResponse response = new GetDataSourceSchemaResponse()
            {
                DataSourceSchema = AppliancePart.GetSchema(),
                Status = opStatus,
            };
    
            return Task.FromResult(response);
        }
    
    

Actualizar ConnectorCrawlerServiceImpl.cs

Esta clase tiene los métodos a los que llamará la plataforma durante los rastreos.

Se llamará al método GetCrawlStream durante los rastreos completos completos o periódicos.

  1. Agregue la siguiente directiva using en AppliancePart.cs.

    using System.Globalization;
    
  2. Agregue los métodos siguientes en AppliancePart.cs para convertir el registro AppliancePart en CrawlItem.

    public CrawlItem ToCrawlItem()
        {
            return new CrawlItem
            {
                ItemType = CrawlItem.Types.ItemType.ContentItem,
                ItemId = this.PartNumber.ToString(CultureInfo.InvariantCulture),
                ContentItem = this.GetContentItem(),
            };
        }
    
        private ContentItem GetContentItem()
        {
            return new ContentItem
            {
                AccessList = this.GetAccessControlList(),
                PropertyValues = this.GetSourcePropertyValueMap()
            };
        }
    
        private AccessControlList GetAccessControlList()
        {
            AccessControlList accessControlList = new AccessControlList();
            accessControlList.Entries.Add(this.GetAllowEveryoneAccessControlEntry());
            return accessControlList;
        }
    
        private AccessControlEntry GetAllowEveryoneAccessControlEntry()
        {
            return new AccessControlEntry
            {
                AccessType = AccessControlEntry.Types.AclAccessType.Grant,
                Principal = new Principal
                {
                    Type = Principal.Types.PrincipalType.Everyone,
                    IdentitySource = Principal.Types.IdentitySource.AzureActiveDirectory,
                    IdentityType = Principal.Types.IdentityType.AadId,
                    Value = "EVERYONE",
                }
            };
        }
    
        private SourcePropertyValueMap GetSourcePropertyValueMap()
        {
            SourcePropertyValueMap sourcePropertyValueMap = new SourcePropertyValueMap();
    
            sourcePropertyValueMap.Values.Add(
                nameof(this.PartNumber),
                new GenericType
                {
                    IntValue = this.PartNumber,
                });
    
            sourcePropertyValueMap.Values.Add(
                nameof(this.Name),
                new GenericType
                {
                    StringValue = this.Name,
                });
    
            sourcePropertyValueMap.Values.Add(
                nameof(this.Price),
                new GenericType
                {
                    DoubleValue = this.Price,
                });
    
            sourcePropertyValueMap.Values.Add(
                nameof(this.Inventory),
                new GenericType
                {
                    IntValue = this.Inventory,
                });
    
            var appliancesPropertyValue = new StringCollectionType();
            foreach(var property in this.Appliances)
            {
                appliancesPropertyValue.Values.Add(property);
            }
            sourcePropertyValueMap.Values.Add(
                nameof(this.Appliances),
                new GenericType
                {
                    StringCollectionValue = appliancesPropertyValue,
                });
    
            sourcePropertyValueMap.Values.Add(
                nameof(this.Description),
                new GenericType
                {
                    StringValue = Description,
                });
    
            return sourcePropertyValueMap;
        }
    
    
  3. Agregue la siguiente directiva using en CsvDataLoader.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    
  4. Agregue el siguiente método en CsvDataLoader.cs.

    public static IEnumerable<CrawlItem> GetCrawlItemsFromCsv(string filePath)
        {
            using (var reader = new StreamReader(filePath))
            using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
            {
                csv.Context.RegisterClassMap<AppliancePartMap>();
    
                // The GetRecords<T> method will return an IEnumerable<T> that will yield records. This means that only one record is returned at a time as you iterate the records.
                foreach (var record in csv.GetRecords<AppliancePart>())
                {
                    yield return record.ToCrawlItem();
                }
            }
        }
    
    
  5. Agregue la siguiente directiva using en ConnectorCrawlerServiceImpl.cs.

    using CustomConnector.Data;
    
  6. Agregue el siguiente método en ConnectorCrawlerServiceImpl.cs.

    private CrawlStreamBit GetCrawlStreamBit(CrawlItem crawlItem)
        {
            return new CrawlStreamBit
            {
                Status = new OperationStatus
                {
                    Result = OperationResult.Success,
                },
                CrawlItem = crawlItem,
                CrawlProgressMarker = new CrawlCheckpoint
                {
                    CustomMarkerData = crawlItem.ItemId,
                },
            };
        }
    
    
  7. Actualice el método GetCrawlStream a lo siguiente.

    public override async Task GetCrawlStream(GetCrawlStreamRequest request, IServerStreamWriter<CrawlStreamBit> responseStream, ServerCallContext context)
        {
            try
            {
                Log.Information("GetCrawlStream Entry");
                var crawlItems = CsvDataLoader.GetCrawlItemsFromCsv(request.AuthenticationData.DatasourceUrl);
                foreach (var crawlItem in crawlItems)
                {
                    CrawlStreamBit crawlStreamBit = this.GetCrawlStreamBit(crawlItem);
                    await responseStream.WriteAsync(crawlStreamBit).ConfigureAwait(false);
                }
            }
            catch (Exception ex)
            {
                Log.Error(ex.ToString());
                CrawlStreamBit crawlStreamBit = new CrawlStreamBit
                {
                    Status = new OperationStatus
                    {
                        Result = OperationResult.DatasourceError,
                        StatusMessage = "Fetching items from datasource failed",
                        RetryInfo = new RetryDetails
                        {
                            Type = RetryDetails.Types.RetryType.Standard,
                        },
                    },
                };
                await responseStream.WriteAsync(crawlStreamBit).ConfigureAwait(false);
            }
    
        }
    
    

Ahora se crea el conector y puede compilar y ejecutar el proyecto.

Pasos siguientes