Создание пользовательского соединителя Microsoft Graph в C#

В этой статье описывается использование пакета SDK соединителя Microsoft Graph для создания пользовательского соединителя в C#.

Предварительные требования

  1. Скачайте, установите и завершите установку агента соединителя Microsoft Graph.
  2. Установите Visual Studio 2019 или более поздней версии с помощью пакета SDK для .NET 7.0.
  3. Скачайте файл ApplianceParts.csv из репозитория примера пользовательского соединителя.

Установка расширения

  1. Откройте Visual Studio и перейдитев раздел Расширения >Управление расширениями.

  2. Поиск расширения GraphConnectorsTemplate и скачайте его.

  3. Закройте и запустите Visual Studio, чтобы установить шаблон.

  4. Перейдите в файл>Новый>проект и найдите GraphConnectorsTemplate. Выберите шаблон и нажмите кнопку Далее. Снимок экрана: страница

  5. Укажите имя проекта и нажмите кнопку Далее.

  6. Выберите .NET Core 3.1, назовите соединитель CustomConnector и нажмите кнопку Создать.

  7. Теперь создается проект шаблона пользовательского соединителя.

    Снимок экрана: структура проекта CustomConnector в Visual Studio

Создание пользовательского соединителя

Перед сборкой соединителя выполните следующие действия, чтобы установить пакеты NuGet и создать модели данных, которые будут использоваться.

Установка пакетов Nuget

  1. Щелкните проект правой кнопкой мыши и выберите команду Открыть в терминале.

  2. Выполните следующую команду.

    dotnet add package CsvHelper --version 27.2.1
    

Создание моделей данных

  1. Создайте папку Models в разделе CustomConnector и создайте файл с именем AppliancePart.cs в папке .

  2. Вставьте следующий код в 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; }
        }
    }
    
    

Обновление ConnectionManagementServiceImpl.cs

В ConnectionManagementServiceImpl.cs будет реализовано три метода.

ValidateAuthentication

Метод ValidateAuthentication используется для проверки учетных данных и предоставленного URL-адреса источника данных. Необходимо подключиться к URL-адресу источника данных, используя предоставленные учетные данные, и вернуть успешное завершение подключения или состояние сбоя проверки подлинности в случае сбоя подключения.

  1. Создайте папку с именем Data в разделе CustomConnector и создайте в ней файл CsvDataLoader.cs.

  2. Скопируйте следующий код в 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>();
            }
        }
    }
    
    

    Метод ReadRecordFromCsv откроет CSV-файл и считывает первую запись из файла. Этот метод можно использовать для проверки допустимости указанного URL-адреса источника данных (пути к CSV-файлу). Этот соединитель использует анонимную проверку подлинности; поэтому учетные данные не проверяются. Если соединитель использует любой другой тип проверки подлинности, подключение к источнику данных должно быть выполнено с использованием учетных данных, предоставленных для проверки проверки подлинности.

  3. Добавьте следующую директиву using в ConnectionManagementServiceImpl.cs.

    using CustomConnector.Data;
    
  4. Обновите метод ValidateAuthentication в ConnectionManagementServiceImpl.cs с помощью следующего кода, чтобы вызвать метод ReadRecordFromCsv класса 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

Метод ValidateCustomConfiguration используется для проверки всех других параметров, необходимых для подключения. Соединитель, который вы создаете, не требует дополнительных параметров; Таким образом, метод проверит, что дополнительные параметры пусты.

  1. Обновите метод ValidateCustomConfiguration в ConnectionManagementServiceImpl.cs с помощью следующего кода.

    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

Метод GetDataSourceSchema используется для получения схемы для соединителя.

  1. Добавьте следующие директивы using в AppliancePart.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    using static Microsoft.Graph.Connectors.Contracts.Grpc.SourcePropertyDefinition.Types;
    
    
  2. Добавьте следующий метод GetSchema в класс 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. Добавьте следующую директиву using в ConnectionManagementServiceImpl.cs.

    using CustomConnector.Models;
    
  4. Обновите метод GetDataSourceSchema в ConnectionManagementServiceImpl.cs с помощью следующего кода.

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

Обновление ConnectorCrawlerServiceImpl.cs

Этот класс содержит методы, которые будут вызываться платформой во время обхода контента.

Метод GetCrawlStream будет вызываться во время полного или периодического полного обхода контента.

  1. Добавьте следующую директиву using в AppliancePart.cs.

    using System.Globalization;
    
  2. Добавьте следующие методы в AppliancePart.cs, чтобы преобразовать запись AppliancePart в 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. Добавьте следующую директиву using в CsvDataLoader.cs.

    using Microsoft.Graph.Connectors.Contracts.Grpc;
    
  4. Добавьте следующий метод в 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. Добавьте следующую директиву using в ConnectorCrawlerServiceImpl.cs.

    using CustomConnector.Data;
    
  6. Добавьте следующий метод в 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. Обновите метод GetCrawlStream следующим образом.

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

Теперь соединитель создан, и вы можете выполнить сборку и запуск проекта.

Дальнейшие действия