在 C 中生成自定义 Microsoft Graph 连接器#
本文介绍如何使用 Microsoft Graph 连接器 SDK 在 C# 中生成自定义连接器。
先决条件
- 下载、安装和完成 Microsoft Graph 连接器代理的设置。
- 使用 .NET 7.0 SDK 安装 Visual Studio 2019 或更高版本。
- 从 自定义连接器示例存储库下载 ApplianceParts.csv 文件。
安装扩展
打开 Visual Studio,转到 “扩展>”“管理扩展”。
搜索 GraphConnectorsTemplate 扩展并下载它。
关闭并重新启动 Visual Studio 以安装模板。
转到 “文件>新>项目” 并搜索 GraphConnectorsTemplate。 选择模板,然后选择 “下一步”。
提供项目的名称,然后选择“ 下一步”。
选择“.NET Core 3.1”,将连接器命名为 “CustomConnector”,然后选择“ 创建”。
现在已创建自定义连接器模板项目。
创建自定义连接器
在生成连接器之前,请使用以下步骤安装 NuGet 包并创建要使用的数据模型。
安装 NuGet 包
右键单击项目,然后选择“ 在终端中打开”。
运行以下命令:
dotnet add package CsvHelper --version 27.2.1
创建数据模型
在 CustomConnector 下创建名为 Models 的文件夹,并在 文件夹下创建名为 AppliancePart.cs 的文件。
将以下代码粘贴到 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,如果连接成功,则返回成功;如果连接失败,则返回身份验证失败状态。
在 CustomConnector 下创建名为 Data 的文件夹,并在文件夹中创建CsvDataLoader.cs文件。
将以下代码复制到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 文件路径) 是否有效。 此连接器使用匿名身份验证;因此,不会验证凭据。 如果连接器使用任何其他身份验证类型,则必须使用提供的凭据来验证身份验证,才能连接到数据源。
在 ConnectionManagementServiceImpl.cs 中添加以下 using 指令。
using CustomConnector.Data;
使用以下代码更新 ConnectionManagementServiceImpl.cs 中的 ValidateAuthentication 方法,以调用 CsvDataLoader 类的 ReadRecordFromCsv 方法。
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 方法用于验证连接所需的任何其他参数。 正在生成的连接器不需要任何额外的参数;因此, 方法验证额外参数是否为空。
使用以下代码更新 ConnectionManagementServiceImpl.cs 中的 ValidateCustomConfiguration 方法。
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 方法用于提取连接器的架构。
在 AppliancePart.cs 中添加以下 using 指令。
using Microsoft.Graph.Connectors.Contracts.Grpc; using static Microsoft.Graph.Connectors.Contracts.Grpc.SourcePropertyDefinition.Types;
在 AppliancePart.cs 类中添加以下 GetSchema 方法。
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 属性将属性批注标记为默认值,但在连接器设置期间可以更改这些注释。
在 ConnectionManagementServiceImpl.cs 中添加以下 using 指令。
using CustomConnector.Models;
使用以下代码更新 ConnectionManagementServiceImpl.cs 中的 GetDataSourceSchema 方法。
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 方法。
在 AppliancePart.cs 中添加以下 using 指令。
using System.Globalization;
在 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; }
在 CsvDataLoader.cs 中添加以下 using 指令。
using Microsoft.Graph.Connectors.Contracts.Grpc;
在 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(); } } }
在 ConnectorCrawlerServiceImpl.cs 中添加以下 using 指令。
using CustomConnector.Data;
在 ConnectorCrawlerServiceImpl.cs 中添加以下方法。
private CrawlStreamBit GetCrawlStreamBit(CrawlItem crawlItem) { return new CrawlStreamBit { Status = new OperationStatus { Result = OperationResult.Success, }, CrawlItem = crawlItem, CrawlProgressMarker = new CrawlCheckpoint { CustomMarkerData = crawlItem.ItemId, }, }; }
将 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); } }
现在,连接器已创建,你可以生成并运行项目。