Créer un connecteur Microsoft Graph personnalisé en C#

Cet article explique comment utiliser le Kit de développement logiciel (SDK) du connecteur Microsoft Graph pour créer un connecteur personnalisé en C#.

Configuration requise

  1. Téléchargez, installez et terminez l’installation de l’agent de connecteur Microsoft Graph.
  2. Installez Visual Studio 2019 ou version ultérieure avec le SDK .NET 7.0.
  3. Téléchargez le fichier ApplianceParts.csv à partir de l’exemple de référentiel de connecteur personnalisé.

Installer l’extension

  1. Ouvrez Visual Studio et accédez à Extensions>Gérer les extensions.

  2. Recherchez l’extension GraphConnectorsTemplate et téléchargez-la.

  3. Fermez et relancez Visual Studio pour installer le modèle.

  4. Accédez à Fichier>Nouveau>projet et recherchez GraphConnectorsTemplate. Sélectionnez le modèle, puis choisissez Suivant. Capture d’écran de la page Créer un projet à partir d’un modèle dans Visual Studio

  5. Fournissez un nom pour le projet, puis choisissez Suivant.

  6. Choisissez .NET Core 3.1, nommez le connecteur CustomConnector, puis choisissez Créer.

  7. Le projet de modèle de connecteur personnalisé est maintenant créé.

    Capture d’écran de la structure du projet CustomConnector dans Visual Studio

Créer le connecteur personnalisé

Avant de générer le connecteur, procédez comme suit pour installer les packages NuGet et créer les modèles de données qui seront utilisés.

Installation de packages NuGet

  1. Cliquez avec le bouton droit sur le projet et choisissez Ouvrir dans le terminal.

  2. Exécutez la commande suivante :

    dotnet add package CsvHelper --version 27.2.1
    

Créer des modèles de données

  1. Créez un dossier appelé Models sous CustomConnector et créez un fichier nommé AppliancePart.cs sous le dossier .

  2. Collez le code suivant dans 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; }
        }
    }
    

Mettre à jour ConnectionManagementServiceImpl.cs

Vous allez implémenter trois méthodes dans ConnectionManagementServiceImpl.cs.

ValidateAuthentication

La méthode ValidateAuthentication est utilisée pour valider les informations d’identification et l’URL de source de données fournies. Vous devez vous connecter à l’URL de la source de données à l’aide des informations d’identification fournies et retourner la réussite si la connexion réussit ou l’échec de l’authentification status en cas d’échec de la connexion.

  1. Créez un dossier appelé Données sous CustomConnector et créez un fichier CsvDataLoader.cs dans le dossier.

  2. Copiez le code suivant dans 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>();
            }
        }
    }
    

    La méthode ReadRecordFromCsv ouvre le fichier CSV et lit le premier enregistrement du fichier. Nous pouvons utiliser cette méthode pour vérifier que l’URL de source de données fournie (chemin d’accès du fichier CSV) est valide. Ce connecteur utilise l’authentification anonyme ; par conséquent, les informations d’identification ne sont pas validées. Si le connecteur utilise un autre type d’authentification, la connexion à la source de données doit être établie à l’aide des informations d’identification fournies pour valider l’authentification.

  3. Ajoutez la directive using suivante dans ConnectionManagementServiceImpl.cs.

    using CustomConnector.Data;
    
  4. Mettez à jour la méthode ValidateAuthentication dans ConnectionManagementServiceImpl.cs avec le code suivant pour appeler la méthode ReadRecordFromCsv de la classe 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

La méthode ValidateCustomConfiguration est utilisée pour valider tous les autres paramètres requis pour la connexion. Le connecteur que vous créez ne nécessite aucun paramètre supplémentaire ; par conséquent, la méthode valide que les paramètres supplémentaires sont vides.

  1. Mettez à jour la méthode ValidateCustomConfiguration dans ConnectionManagementServiceImpl.cs avec le code suivant.

    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

La méthode GetDataSourceSchema est utilisée pour extraire le schéma du connecteur.

  1. Ajoutez les directives using suivantes dans AppliancePart.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    using static Microsoft.Graph.Connectors.Contracts.Grpc.SourcePropertyDefinition.Types;
    
  2. Ajoutez la méthode GetSchema suivante dans la classe 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;
    }
    

    Remarque

    • La propriété RequiredSearchAnnotations marque les annotations de propriété comme obligatoires et immuables pendant la configuration du connecteur. L’exemple précédent définit toutes les propriétés comme pouvant faire l’objet d’une recherche et récupérables obligatoirement ; Toutefois, vous pouvez choisir de ne pas définir requiredSearchAnnotations sur une ou plusieurs propriétés.
    • La propriété DefaultSearchAnnotations marque les annotations de propriété comme étant par défaut, mais elles peuvent être modifiées lors de la configuration du connecteur.
  3. Ajoutez la directive using suivante dans ConnectionManagementServiceImpl.cs.

    using CustomConnector.Models;
    
  4. Mettez à jour la méthode GetDataSourceSchema dans ConnectionManagementServiceImpl.cs avec le code suivant.

    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);
    }
    

Mettre à jour ConnectorCrawlerServiceImpl.cs

Cette classe possède les méthodes appelées par la plateforme pendant les analyses.

La méthode GetCrawlStream est appelée pendant les analyses complètes ou périodiques.

  1. Ajoutez la directive using suivante dans AppliancePart.cs.

    using System.Globalization;
    
  2. Ajoutez les méthodes suivantes dans AppliancePart.cs pour convertir l’enregistrement 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. Ajoutez la directive using suivante dans CsvDataLoader.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    
  4. Ajoutez la méthode suivante dans 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 returns an IEnumerable<T> that yields 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. Ajoutez la directive using suivante dans ConnectorCrawlerServiceImpl.cs.

    using CustomConnector.Data;
    
  6. Ajoutez la méthode suivante dans 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. Mettez à jour la méthode GetCrawlStream comme suit.

    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);
        }
    }
    

À présent, le connecteur est créé et vous pouvez générer et exécuter le projet.

Étapes suivantes