Durable Functions の Entity のプログラミング体験が割と辛いのですがクラススタイルのプログラミングモデルを使うと既存のプログラミングと同じような感じでいけて素敵です!
クラススタイルのプログラミングモデルは、2019/08/05 時点では Durable Functions v2.0.0-beta1 で試すことが出来ます。
試してみよう!
早速 Azure Functions のプロジェクトを作ります。そして Microsoft.Azure.WebJobs.Extensions.DurableTask
の 2.0.0-beta1 を追加します。
クラススタイルのエンテティは、普通のクラス!違う点があるとすると、クラス名の FunctionName
属性のついた EntityTrigger
の関数があるところですね。
using Microsoft.Azure.WebJobs; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Threading.Tasks; namespace FunctionApp1 { public interface IPerson { void SetName(string name); Task CurseAsync(); } public class Person : IPerson { [JsonProperty("name")] public string Name { get; set; } [JsonProperty("age")] public int Age { get; set; } public void SetName(string name) => Name = name; // 適当な時間経過後に加齢する呪い public async Task CurseAsync() { var r = new Random(); await Task.Delay(TimeSpan.FromSeconds(r.Next(10))); Age += r.Next(10); } [FunctionName(nameof(Person))] public static Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<Person>(); } }
この EntityTrigger
のついた関数の DispatchAsync
が凄くいい仕事します。Person クラスの関数をいい感じに呼び出してくれます。
HttpTrigger などから呼び出してみよう
HttpTrigger などから Entity を使うには、IDurableOrchestrationClient#SignalEntityAsync
でメソッドを呼び出して、IDurableOrchestrationClient#ReadEntityStateAsync
で Entity のステータスを取得します。ステータスは、先ほど定義した Person クラスのプロパティに値が入ってる感じのものが返ってきます。
例えば…適当に SetName を呼んでステータスを読み取ってみましょう。
[FunctionName("CreatePerson")] public static async Task<IActionResult> CreatePersonAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { return new BadRequestResult(); } await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.SetName(id.Value.EntityKey)); return new OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); } private static EntityId? CreateEntityIdFrom(HttpRequest req) { var name = req.Query["name"]; if (string.IsNullOrEmpty(name)) { return null; } return new EntityId(nameof(Person), name); }
EntityId
の最初の引数が Person クラスに定義した関数の名前です。そして、EntityId
の第二引数に Entity の識別子みたいな感じですね。今回は HttpTrigger の URL のクエリーパラメータの name でを使う感じにしました。
POST http://localhost:7071/api/CreatePerson?name=kazuakix HTTP/1.1
こんな感じのURL を叩くと下のような結果が返ってきます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:30:47 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 41 { "entityExists": false, "entityState": null }
entityState
が null なのが気になりますが、これは SignalEntityAsync
は処理の完了を待たずに、処理の要求がキューに入ったら戻ってくる感じです。ReadEntityStateAsync
も別に SignalEntityAsync
で何か処理が行われるか気にせず結果を返すので、こんな結果になります。
なので、もう一度同じ HttpTrigger の関数を叩くと以下のような結果になります。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:34:36 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 0 } }
他に、Person#CurseAsync
を呼ぶ場合は以下のようなコードになります。
[FunctionName("Curse")] public static async Task<IActionResult> CurseAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { return new BadRequestResult(); } await client.SignalEntityAsync<IPerson>(id.Value, proxy => proxy.CurseAsync()); return new OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); }
この例でも CurseAsync
を処理するという要求をするだけなので、最後の結果の ReadEntityStateAsync
の結果は加齢前のものになります。試しに呼んでみると…
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:36:45 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 0 } }
年齢は変わりません。ちょっと何もせずステータスを返す以下のような関数を作って…
[FunctionName("GetPerson")] public static async Task<IActionResult> GetPersonAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { return new BadRequestResult(); } return new OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); }
適当な時間経過後に呼んでみましょう。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 11:37:46 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 63 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 4 } }
年齢増えてますね。
処理結果を待ちつつ結果を取りたい
そういうときは OrchestrationTrigger
の中で Entity を使うと良さそう。
[FunctionName("CurseAndWait")] public static async Task<IActionResult> CurseAndWaitAsync( [HttpTrigger(AuthorizationLevel.Function, "get", "post")]HttpRequest req, [OrchestrationClient] IDurableOrchestrationClient client) { var id = CreateEntityIdFrom(req); if (!id.HasValue) { return new BadRequestResult(); } var instanceId = await client.StartNewAsync("CurseAndWaitOrchestrator", id.Value); while((await client.GetStatusAsync(instanceId)).RuntimeStatus != OrchestrationRuntimeStatus.Completed) { await Task.Delay(5000); } return new OkObjectResult(await client.ReadEntityStateAsync<Person>(id.Value)); } [FunctionName("CurseAndWaitOrchestrator")] public static async Task CurseAndWaitOrchestratorAsync( [OrchestrationTrigger]IDurableOrchestrationContext context) { var id = context.GetInput<EntityId>(); var proxy = context.CreateEntityProxy<IPerson>(id); await proxy.CurseAsync(); }
オーケストレーター関数の終了待つのに、WaitForCompletionOrCreateCheckStatusResponseAsync
使えばいいやって思ったけど、この関数は、あくまでレスポンス受け取った先の人のためのメソッドであって、HttpTrigger の中でこの関数を await しても必ずしも完了まで待ってくれないんですね。勉強になった。
ということでステータスが完了になるまで自分で待ってます。
そうすると、関数を呼ぶと加齢が完了して、結果に加齢後のオブジェクトが入ってます。 呼び出し前の状態を確認するために GetPerson 関数を呼んで結果を見てます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 12:02:23 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 64 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 47 } }
47 歳ですね。では先ほど作った CurseAndWait
関数をこんな感じ( GET http://localhost:7071/api/CurseAndWait?name=kazuakix HTTP/1.1
)で呼んでみます。
HTTP/1.1 200 OK Connection: close Date: Mon, 05 Aug 2019 12:02:50 GMT Content-Type: application/json; charset=utf-8 Server: Kestrel Content-Length: 64 { "entityExists": true, "entityState": { "name": "kazuakix", "age": 53 } }
53 歳になりましたね。あってるのかな。
まとめ
Durable Functions の Entity が本当に Entity 書いてるみたいになるのでとてもいい。Durable Functions v2 のリリースが楽しみになりますね。