C でカスタム Microsoft Graph コネクタを構築する#
この記事では、Microsoft Graph コネクタ SDK を使用して C# でカスタム コネクタを構築する方法について説明します。
前提条件
- Microsoft Graph コネクタ エージェントのセットアップをダウンロード、インストール、完了します。
- .NET 7.0 SDK を使用して Visual Studio 2019 以降をインストールします。
- カスタム コネクタ サンプル リポジトリから ApplianceParts.csv ファイルをダウンロードします。
拡張機能をインストールする
Visual Studio を開き、[ 拡張機能>管理拡張機能] に移動します。
GraphConnectorsTemplate 拡張機能を検索してダウンロードします。
Visual Studio を閉じて再起動してテンプレートをインストールします。
[ファイル>New>Project] に移動し、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には 3 つのメソッドを実装します。
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 プロパティは、コネクタのセットアップ中に、プロパティの注釈を必須で変更不可としてマークします。 前の例では、すべてのプロパティを 検索可能 と 取得可能 として必須に設定します。ただし、1 つ以上のプロパティで 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 レコードを CrawlItem に変換するには、AppliancePart.csに次のメソッドを追加します。
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); } }
これでコネクタが作成され、プロジェクトをビルドして実行できます。