Поделиться через


Создание пользовательского соединителя 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;
    }
    

    Примечание.

    • Свойство RequiredSearchAnnotations помечает заметки свойства как обязательные и неизменяемые во время установки соединителя. В предыдущем примере все свойства задаются как доступные для поиска и извлечения обязательные. Однако вы можете не задавать requiredSearchAnnotations для одного или нескольких свойств.
    • Свойство DefaultSearchAnnotations помечает заметки свойств как значения по умолчанию, но их можно изменить во время установки соединителя.
  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 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. Добавьте следующую директиву 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);
        }
    }
    

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

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