概要
Power Apps/Automateのカスタムコネクタがアップデートされ、C#のコードでレスポンスを変形したり、処理内容そのものをオーバーライドできるようになりました。
ただし、現状コンパイルエラーのトレースができないためローカルの環境でテストしてから登録する必要があります。
そこで、公式サンプルに従いC#のコードをテストできる環境を作ってみました。
※エンジニアが言うTestではありません。
素人が考えたものなので間違っているかも知れません。
カスタムコネクタに記述する公式サンプルコードの一例
public override async Task<HttpResponseMessage> ExecuteAsync() { // Create a new response var response = new HttpResponseMessage(); // Set the content // Initialize a new JObject and call .ToString() to get the serialized JSON response.Content = CreateJsonContent(new JObject { ["greeting"] = "Hello World!", }.ToString()); return response; }
準備
まずは、VisualStudioの新規プロジェクトで、空の.NET Coreのコンソールアプリを作成します。
今回は.NET5の機能を試したかったので、バージョンは.NET5 C# 9.0作ってみます。
ただしカスタムコネクタのScriptクラス本体は.NET Core3.1 C#8.0相当のコードでないと動きませんので注意が必要です。
実際に開発される場合は.NET Core3.1相当で作成してください。
構成
- Program.cs:MainメソッドとScript(カスタムコード)を呼び出すメソッドを記述。
- Script.cs:カスタムコードの本体部分
- CodeBase.cs:公式サンプルのScriptBaseやIScriptContextインターフェースを定義
CodeBase.csの作成
適当な場所に新規クラスを作成し、公式のテスト用サンプルから以下をコピーして貼り付けます。
そのままでは動かないので少し修正して使用します。
赤線が出る部分はCtrl + . でusingやパッケージ追加を行います。
public abstract class ScriptBase { // Context object public IScriptContext Context { get; init; } // CancellationToken for the execution public CancellationToken CancellationToken { get; init; } // Helper: Creates a StringContent object from the serialized JSON public static StringContent CreateJsonContent(string serializedJson) { return new StringContent(serializedJson, Encoding.UTF8, "application/json"); } // Abstract method for your code public abstract Task<HttpResponseMessage> ExecuteAsync(); } public interface IScriptContext { // Correlation Id string CorrelationId { get; } // Connector Operation Id string OperationId { get; } // Incoming request HttpRequestMessage Request { get; } // Logger instance ILogger Logger { get; } // Used to send an HTTP request // Use this method to send requests instead of HttpClient.SendAsync Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken); }
- CreateJsonContentの実装がなかったので作成しました。
- ContexやCancellationTokenはテスト用に任意で書き換えるため init;をつけて初期化できるように。
※initは.NET5の機能です。
Program.cs
①静的メンバーとMainメソッド
- HttpClient とCancellationTokenSource を初期化。
- メインメソッドをasync Taskに変更して非同期に。
- あとで作成するScriptTest1を呼び出し。
static HttpClient _client = new(); static CancellationTokenSource _cts = new(); static async Task Main(string[] args) { await ScriptTest1(); }
②ScriptTest1(テスト呼び出し部分)
- ScriptTest1メソッドを作成。
- ContextのMockを作成し、任意のrequestMessage やOperationIdを実装します。
- そしてScriptインスタンスの初期化時にContextとCancellationToken(一応)をセットします。
※ScriptBaseクラスのプロパティにinitアクセサを付けため、オブジェクト初期化子からでも初期化できます。
※new()の記述は.NET5の機能です。
static async Task ScriptTest1() { //Contextの実装 var moq = new Mock<IScriptContext>(); moq.Setup(x => x.OperationId).Returns("GetUser"); moq.Setup(x => x.CorrelationId).Returns("123456"); //Request var json = new JObject { ["greeting"] = "Hello World!" }.ToString(); HttpRequestMessage requestMessage = new(HttpMethod.Post, "https://api.contoso.com/Hello") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; moq.Setup(x => x.Request).Returns(requestMessage); //SendAsync moq.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())) .Returns(async (HttpRequestMessage r, CancellationToken c) => await _client.SendAsync(r, c)); //Scriptの呼び出し Script script = new() { CancellationToken = _cts.Token, Context = moq.Object }; var response = await script.ExecuteAsync(); Console.WriteLine(await response.Content.ReadAsStringAsync()); }
又は、 MockではなくIScriptContextインターフェースを実装したクラスを作ってContextにセットしてもOKです。
public class MockContext : IScriptContext { public static HttpClient _client = new(); public string CorrelationId => "123456"; public string OperationId => "GetUser"; public HttpRequestMessage Request { get;} public ILogger Logger => throw new NotImplementedException(); public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return await _client.SendAsync(request, cancellationToken); } public MockContext() { var json = new JObject { ["greeting"] = "Hello World!" }.ToString(); Request = new HttpRequestMessage(HttpMethod.Post, "https://api.contoso.com/Hello") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; } }
Script.cs
- コード記述の本体となる部分、Script.csを作成。
- 公式サンプルに従いScriptBase を継承したクラスを作成し、ExecuteAsyncを実装します。
- こちらはC#8.0のコードで記述します。上記の「initアクセサ」や、「T hoge = new();」みたいなのはC#9.0なのでカスタムコネクタ上では動きません。
ExecuteAsync内がカスタムコネクタで編集するコードです。
今回はMockの内容が反映されているかを確認するためのコードとなっています。
//この中にテストコード public class Script:ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Create a new response var response = new HttpResponseMessage(); var contentAsJson = JObject.Parse(await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false)); // Set the content // Initialize a new JObject and call .ToString() to get the serialized JSON response.Content = CreateJsonContent(new JObject { ["greeting"] = (string)contentAsJson["greeting"], ["RequestUri"] = Context.Request.RequestUri, ["OperationId"] = Context.OperationId }.ToString()); return response; } }
ローカルでのテスト
テストを実行すると、コンソールにresponseのbodyが表示されます。
カスタムコネクタ上でのテスト
Power Appのカスタムコネクタを作成し、テストコードに合わせてアクションや要求を作成。
最初のAPIのURLもapi.contoso.comなど適当なものでOKです。
※誤って要求されても大丈夫そうなURLで
Script.csに書いたコードをコピーして貼り付けます。
貼り付けたらコネクタの更新をクリックします。
他のタブで情報を更新して「コネクタの更新」をするとコードが反映されず元のURIに要求されてしまうため、最後はコードの画面でコードを編集してから「コネクタの更新」をクリックします。
保存のときに画像のような表示になればOKです。
実行して確認。
テストに成功したら、応答スキーマを登録します。
これをやらないとPower Appsから参照できず、カスタムコネクタの応答がbool値になってしまいます。
応答を編集した後は、再度コードを空編集→「コネクタの更新」をクリックしてコードを更新しますw
分かっている制約
- コードはすべてScriptクラス内に記述する必要がある。
- よってメソッドの追加でコードを書いていく必要があり手続き的な処理となる。
- classを入れ子にしてScriptクラスに貼り付けたらコンパイルも通って動きました。
- 関数の起動初回はAzure Functionの無料枠と同じく数秒のインスタンス起動時間がかかっている
- 裏側はFunctionsなのでしょう
- これにより、カスタムコード本体は、.Net Core3.1相当で作成する必要があります!
- カスタムコネクタの保存≒コンパイルは通ったから5が使えるものだと思ってました。
- その他、実行時間、ファイルサイズ、使用できるnamespaceの制約などがあり公式参照。
GitHubリポジトリ
カスタムコードで遊んでみた例
Qiitaの方に投稿しました。 良かったらLGTM頂けると喜びます。
はてな初投稿でした。
お勉強ノート
ラムダ式でasyncを記述する場合は async () => await DoAsync();
UIスレッド以外から呼ぶTask.ConfigureAwait(false)とは?
非同期メソッドをWaitしてデッドロックが発生するのはUIスレッドから呼んだ場合だけ。
公式ソースではawaitで待機してさらにConfigureAwait(false)のはなぜ?
このあたりが理由なのか?はっきりとは分かりません・・
c# - ConfigureAwait(false) not needed in Console/Win service apps, right? - Stack Overflow
→コードが他のアプリケーションでも利用される可能性があり、どのアプリケーションでも安全に動作させるため
'21/8/20 追記
- mockにSendAsyncとCancellationTokenの実装を追加
- 分かっている制約を追加
'21/8/23 修正
- Program.csのコードを修正。ScriptクラスのMockをオブジェクト初期化子内で設定、
- Scriptクラスのpartialを不要にした。
'21/8/27
- カスタムコード本体は.Net Core3.1で作成するよう追記。