DurableFunctions コントリビューションの学び - (1) クエリーの実装

私は定期的に Durable Functions のリポジトリに貢献している。特に DevOps に関する機能に関して貢献している。なぜそんなことをしているか？というと、プロジェクトが気に入っているのもあるし、プロジェクトのオーナーの Chris とも仲がいいし、貢献するととても学びになる。私の前職の以前のキャリアは、大手SI のSE/PM だったり、アジャイルや、超上流のコンサルでプログラマとしては三流。どうやって、スキルを引き上げるか考えた場合、こういった OSS の貢献はガチの本番環境を体験できるし、リリースまでのすべてを学べるし、リポジトリオーナーのレビューも最高なので、大変ありがたい。

ただ、今までは、貢献しっぱなしで、特に振り返りなどをしていなかったので、大きめの貢献をしたら、学んだことを振り返っておこうと思う。

# 今まで貢献した内容

大きな機能でいうと２つぐらいしかまだ貢献していない。大きく分けると EventGrid にオーケストレーションのステータスを送信するようにしたものと、Http/Client 経由でインスタンスの状態を取得できるようにしたものの２つ。Kanio とか 24 commitしていて２位だけど、私は 4 commit で５位だから、もうちょっと頑張ってみたいな。スピードを上げて楽に貢献できるようになりたい。

* [Post orchestration lifecycle events to Azure Event Grid](https://github.com/Azure/azure-functions-durable-extension/pull/220)
* [fix: Event Grid data payload should use lowercase letters.](https://github.com/Azure/azure-functions-durable-extension/pull/259)
* [Durable Functions Automatic Event Grid publishing documentation #38541](https://github.com/MicrosoftDocs/azure-docs-pr/pull/38541)
* [Change custom Event Grid eventType property from an int to a string #294](https://github.com/Azure/azure-functions-durable-extension/pull/294)
* [Change custom Event Grid eventType property from an int to a string #40239](https://github.com/MicrosoftDocs/azure-docs-pr/pull/40239)
* [DurableTask.AzureStorage API to enumerate instances #187](https://github.com/Azure/durabletask/pull/187)
* [APIs to enumerate all orchestration instances #323](https://github.com/Azure/azure-functions-durable-extension/pull/323)
* [Durable Functions Get All orchestration instance statues. #42887](https://github.com/MicrosoftDocs/azure-docs-pr/pull/42887)

# 今回の貢献内容

今回のPR は、[Get all instances status enhancements #358](https://github.com/Azure/azure-functions-durable-extension/issues/358)に対応したもの。前に、ステータスの取得を全件行うAPIを作成したけど、その当時からわかっていたことだけど、検索条件を付けたり、古いデータを削除したりということは当然必要になることはわかっていた。しかし、一気にインプリすると負荷も大きいので、プルリクエストを分けて実施することにした。

今回は、条件検索のコードを追加することにした。むっちゃ簡単そうだ（実際そんなに難しくない）だが、ガチのプロダクトに貢献するとなると、そうは簡単にいかない。今回は２つのリポジトリを修正する必要がある。

* [Azure/durabletask](https://github.com/Azure/durabletask)
* [Azure/azure-functions-durable-extension](https://github.com/Azure/azure-functions-durable-extension)

Durable Functions の本体は後者だが、DurableTask は、Durable Functions の内部で使われているライブラリで、Azure Storage へのアクセスを担っている。Durable Task はロングランニングな永続性のあるワークフローをサポートするライブラリで、Durable だけでなく、マイクロソフトの他のプロジェクトでも使われているもの。今回は、インスタンステーブルという、Durable Functions のオーケストレーションインスタンスの状態を CQRSパターンで保存する部分へのクエリを書くので、こちらのライブラリの修正が必要となる。Durable 側の実装は大したことなくて、今回のPRの最大のポイントはクエリーの組み立てと、そのデザイン部分と思う。だから、今回はクエリーをラップするクエリーオブジェクトというものをTDDで実装してみた。パラメータを雑に渡すと、TableQuery を組み立てて返してくれるので、それを単に実行すればいいようにしてくれる。もともとの発想の元は、J2EE パターンかなんかのQueryObject というパターンがあったので、それを参考にしている。

コードは最終的にこんな感じになった。あとのロジックは、大体委譲なのだけど、Durable の HTTP API のインターフェイスからのパラメータのパースの部分が存在する。

_Query オブジェクト_

```csharp
//  ----------------------------------------------------------------------------------
//  Copyright Microsoft Corporation
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//  ----------------------------------------------------------------------------------

namespace DurableTask.AzureStorage.Tracking
{
    using DurableTask.Core;
    using Microsoft.WindowsAzure.Storage.Table;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;

    /// <summary>
    /// OrchestrationInstanceStatusQueryBuilder is a builder to create a StorageTable Query
    /// </summary>
    internal class OrchestrationInstanceStatusQueryCondition
    {
        public IEnumerable<OrchestrationStatus> RuntimeStatus { get; set; }

        public DateTime CreatedTimeFrom { get; set; }

        public DateTime CreatedTimeTo { get; set; }

        public TableQuery<T> ToTableQuery<T>()
            where T : TableEntity, new()
        {
            var query = new TableQuery<T>();
            if (!((RuntimeStatus == null || (!RuntimeStatus.Any())) && CreatedTimeFrom == default(DateTime) && CreatedTimeTo == default(DateTime)))
            {
                query.Where(
                    GetConditions()
                    );
            }
            return query;
        }

        private string GetConditions()
        {
            var conditions = new List<string>();

            if (default(DateTime) != this.CreatedTimeFrom)
            {
                conditions.Add(TableQuery.GenerateFilterConditionForDate("CreatedTime", QueryComparisons.GreaterThanOrEqual, new DateTimeOffset(this.CreatedTimeFrom)));
            }

            if (default(DateTime) != this.CreatedTimeTo)
            {
                conditions.Add(TableQuery.GenerateFilterConditionForDate("CreatedTime", QueryComparisons.LessThanOrEqual, new DateTimeOffset(this.CreatedTimeTo)));
            }

            if (this.RuntimeStatus != null && this.RuntimeStatus.Any())
            {
                var runtimeCondition = this.RuntimeStatus.Select(x => TableQuery.GenerateFilterCondition("RuntimeStatus", QueryComparisons.Equal, x.ToString()))
                                    .Aggregate((a, b) => TableQuery.CombineFilters(a, TableOperators.Or, b));
                if (runtimeCondition.Count() != 0)
                {
                    conditions.Add(runtimeCondition);
                }
            }

            if (conditions.Count == 1)
            {
                return conditions[0];
            }
            else
            {
                return conditions.Aggregate((a, b) => TableQuery.CombineFilters(a, TableOperators.And, b));
            }

        }

        /// <summary>
        /// Parse is a factory method of the OrchestrationInstanceStatusConditionQuery
        /// </summary>
        /// <param name="createdTimeFrom">CreatedTimeFrom</param>
        /// <param name="createdTimeTo">CreatedTimeTo</param>
        /// <param name="runtimeStatus">RuntimeStatus</param>
        /// <returns></returns>
        public static OrchestrationInstanceStatusQueryCondition Parse(DateTime createdTimeFrom, DateTime? createdTimeTo, IEnumerable<OrchestrationStatus> runtimeStatus)
        {
            var condition = new OrchestrationInstanceStatusQueryCondition();
            condition.CreatedTimeFrom = createdTimeFrom;
            condition.CreatedTimeTo = (createdTimeTo != null) ? (DateTime)createdTimeTo : default(DateTime);
            condition.RuntimeStatus = runtimeStatus;
            return condition;
        }
    }
}

```

_Httpのパース部分_

```csharp
        private async Task<HttpResponseMessage> HandleGetStatusRequestAsync(
            HttpRequestMessage request)
        {
            DurableOrchestrationClientBase client = this.GetClient(request);
            var queryNameValuePairs = request.GetQueryNameValuePairs();
            var createdTimeFrom = GetDateTimeQueryParameterValue(queryNameValuePairs, CreatedTimeFromParameter, default(DateTime));
            var createdTimeTo = GetDateTimeQueryParameterValue(queryNameValuePairs, CreatedTimeToParameter, default(DateTime));
            var runtimeStatus = GetIEnumerableQueryParameterValue(queryNameValuePairs, RuntimeStatusParameter);

            // TODO Step-by-step. After fixing the parameter change, I'll implement multiple parameters.
            IList<DurableOrchestrationStatus> statusForAllInstances = await client.GetStatusAsync(createdTimeFrom, createdTimeTo, runtimeStatus);

            var results = new List<StatusResponsePayload>(statusForAllInstances.Count);
            foreach (var state in statusForAllInstances)
            {
                results.Add(this.ConvertFrom(state));
            }

            return request.CreateResponse(HttpStatusCode.OK, results);
        }
  :
        private static IEnumerable<OrchestrationRuntimeStatus> GetIEnumerableQueryParameterValue(NameValueCollection queryStringNameValueCollection, string queryParameterName)
        {
            var results = new List<OrchestrationRuntimeStatus>();
            var parameters = queryStringNameValueCollection.GetValues(queryParameterName) ?? new string[] { };

            foreach (var value in parameters.SelectMany(x => x.Split(',')))
            {
                if (Enum.TryParse<OrchestrationRuntimeStatus>(value, out OrchestrationRuntimeStatus result))
                {
                    results.Add(result);
                }
            }

            return results;
        }

        private static DateTime GetDateTimeQueryParameterValue(NameValueCollection queryStringNameValueCollection, string queryParameterName, DateTime defaultDateTime)
        {
            var value = queryStringNameValueCollection[queryParameterName];
            return DateTime.TryParse(value, out DateTime dateTime) ? dateTime : defaultDateTime;
        }

```


_Queryのテスト_

```csharp
//  ----------------------------------------------------------------------------------
//  Copyright Microsoft Corporation
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//  http://www.apache.org/licenses/LICENSE-2.0
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//  ----------------------------------------------------------------------------------

namespace DurableTask.AzureStorage.Tests
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using DurableTask.AzureStorage.Tracking;
    using DurableTask.Core;
    using DurableTask.ServiceBus.Tracking;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class OrchestrationInstanceStatusQueryConditionTest
    {
        [TestMethod]
        public void OrchestrationInstanceQuery_RuntimeStatus()
        {
            var runtimeStatus = new OrchestrationStatus[] { OrchestrationStatus.Running };
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                RuntimeStatus = runtimeStatus
            };

            var query = condition.ToTableQuery<OrchestrationInstanceStatus>();
            Assert.AreEqual("RuntimeStatus eq 'Running'", query.FilterString);
        }

        [TestMethod]
        public void OrchestrationInstanceQuery_CreatedTime()
        {
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)

            };

            var result = condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString;
            Assert.AreEqual("(CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z') and (CreatedTime le datetime'2018-01-10T01:10:50.0000000Z')", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);

        }
        [TestMethod]
        public void OrchestrationInstanceQuery_CreatedTimeOnly()
        {
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = default(DateTime),
                RuntimeStatus = new List<OrchestrationStatus>(),
            };

            var result = condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString;
            Assert.AreEqual("CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z'", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);

        }

        [TestMethod]
        public void OrchestrationInstanceQuery_CreatedTimeVariations()
        {
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10)
            };
            Assert.AreEqual("CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z'", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);

            condition = new OrchestrationInstanceStatusQueryCondition
            {
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)
            };
            Assert.AreEqual("CreatedTime le datetime'2018-01-10T01:10:50.0000000Z'", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);
        }

        [TestMethod]
        public void OrchestrationInstanceQuery_Combination()
        {
            var runtimeStatus = new OrchestrationStatus[] { OrchestrationStatus.Running };
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                RuntimeStatus = runtimeStatus,
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)
            };
            Assert.AreEqual("((CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z') and (CreatedTime le datetime'2018-01-10T01:10:50.0000000Z')) and (RuntimeStatus eq 'Running')", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);

        }
        [TestMethod]
        public void OrchestrationInstanceQuery_NoParameter()
        {
            var condition = new OrchestrationInstanceStatusQueryCondition();
            var query = condition.ToTableQuery<OrchestrationInstanceStatus>();
            Assert.IsNull(query.Expression);
        }

        [TestMethod]
        public void OrchestrationInstanceQuery_MultipleRuntimeStatus()
        {
            var runtimeStatus = new OrchestrationStatus[] { OrchestrationStatus.Running , OrchestrationStatus.Completed };
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                RuntimeStatus = runtimeStatus,
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)
            };
            Assert.AreEqual("((CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z') and (CreatedTime le datetime'2018-01-10T01:10:50.0000000Z')) and ((RuntimeStatus eq 'Running') or (RuntimeStatus eq 'Completed'))", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);
        }

        [TestMethod]
        public void OrchestrationInstanceQuery_Parse()
        {
            var runtimeStatus = new List<OrchestrationStatus>();
            runtimeStatus.Add(OrchestrationStatus.Running);
            var condition = OrchestrationInstanceStatusQueryCondition.Parse(new DateTime(2018, 1, 10, 10, 10, 10), new DateTime(2018, 1, 10, 10, 10, 50), runtimeStatus);
            Assert.AreEqual("((CreatedTime ge datetime'2018-01-10T01:10:10.0000000Z') and (CreatedTime le datetime'2018-01-10T01:10:50.0000000Z')) and (RuntimeStatus eq 'Running')", condition.ToTableQuery<OrchestrationInstanceStatus>().FilterString);

        }

        [TestMethod]
        public void OrchestrationInstanceQuery_ParseOptional()
        {
            var runtimeStatus = new List<OrchestrationStatus>();
            runtimeStatus.Add(OrchestrationStatus.Running);
            var condition = OrchestrationInstanceStatusQueryCondition.Parse(default(DateTime), null, runtimeStatus);
            var query = condition.ToTableQuery<OrchestrationInstanceStatus>();
            Assert.AreEqual("RuntimeStatus eq 'Running'", query.FilterString);
        }
    }
}

```

_Httpのテスト_

```csharp
      [Fact]
        public async Task GetAllStatus_is_Success()
        {

            var list = (IList<DurableOrchestrationStatus>)new List<DurableOrchestrationStatus>
                     {
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "01",
                             RuntimeStatus = OrchestrationRuntimeStatus.Running
                         },
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "02",
                             RuntimeStatus = OrchestrationRuntimeStatus.Completed
                         },
                     };

            var clientMock = new Mock<DurableOrchestrationClientBase>();
            clientMock
                .Setup(x => x.GetStatusAsync(default(DateTime), default(DateTime), new List<OrchestrationRuntimeStatus>(), It.IsAny<CancellationToken>()))
                .Returns(Task.FromResult(list));
            var httpApiHandler = new ExtendedHttpApiHandler(clientMock.Object);

            var getStatusRequestUriBuilder = new UriBuilder(TestConstants.NotificationUrl);
            getStatusRequestUriBuilder.Path += $"/Instances/";

            var responseMessage = await httpApiHandler.HandleRequestAsync(
                new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = getStatusRequestUriBuilder.Uri,
                });
            Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
            var actual = JsonConvert.DeserializeObject<IList<StatusResponsePayload>>(await responseMessage.Content.ReadAsStringAsync());

            Assert.Equal("01", actual[0].InstanceId);
            Assert.Equal("Running", actual[0].RuntimeStatus);
            Assert.Equal("02", actual[1].InstanceId);
            Assert.Equal("Completed", actual[1].RuntimeStatus);
        }

        [Fact]
        public async Task GetQueryStatus_is_Success()
        {

            var list = (IList<DurableOrchestrationStatus>)new List<DurableOrchestrationStatus>
                     {
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "01",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 10, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Running,
                         },
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "02",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 6, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Running,
                         },
                     };

            var createdTimeFrom = new DateTime(2018, 3, 10, 10, 1, 0);
            var createdTimeTo = new DateTime(2018, 3, 10, 10, 23, 59);
            var runtimeStatus = new List<OrchestrationRuntimeStatus>();
            runtimeStatus.Add(OrchestrationRuntimeStatus.Running);
            var runtimeStatusString = OrchestrationRuntimeStatus.Running.ToString();

            var clientMock = new Mock<DurableOrchestrationClientBase>();
            clientMock
                .Setup(x => x.GetStatusAsync(createdTimeFrom, createdTimeTo, runtimeStatus, It.IsAny<CancellationToken>()))
                .Returns(Task.FromResult(list));
            var httpApiHandler = new ExtendedHttpApiHandler(clientMock.Object);

            var getStatusRequestUriBuilder = new UriBuilder(TestConstants.NotificationUrl);
            getStatusRequestUriBuilder.Path += $"/Instances/";
            getStatusRequestUriBuilder.Query = $"createdTimeFrom={WebUtility.UrlEncode(createdTimeFrom.ToString())}&createdTimeTo={System.Web.HttpUtility.UrlEncode(createdTimeTo.ToString())}&runtimeStatus={runtimeStatusString}";

            var responseMessage = await httpApiHandler.HandleRequestAsync(
                new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = getStatusRequestUriBuilder.Uri,
                });
            Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
            var actual = JsonConvert.DeserializeObject<IList<StatusResponsePayload>>(await responseMessage.Content.ReadAsStringAsync());
            clientMock.Verify(x => x.GetStatusAsync(createdTimeFrom, createdTimeTo, runtimeStatus, It.IsAny<CancellationToken>()));
            Assert.Equal("01", actual[0].InstanceId);
            Assert.Equal("Running", actual[0].RuntimeStatus);
            Assert.Equal("02", actual[1].InstanceId);
            Assert.Equal("Running", actual[1].RuntimeStatus);
        }

        [Fact]
        public async Task GetQueryMultipleRuntimeStatus_is_Success()
        {

            var list = (IList<DurableOrchestrationStatus>)new List<DurableOrchestrationStatus>
                     {
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "01",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 10, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Running,
                         },
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "02",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 6, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Completed,
                         },
                     };

            var createdTimeFrom = new DateTime(2018, 3, 10, 10, 1, 0);
            var createdTimeTo = new DateTime(2018, 3, 10, 10, 23, 59);
            var runtimeStatus = new List<OrchestrationRuntimeStatus>();
            runtimeStatus.Add(OrchestrationRuntimeStatus.Running);
            runtimeStatus.Add(OrchestrationRuntimeStatus.Completed);

            var runtimeStatusRunningString = OrchestrationRuntimeStatus.Running.ToString();
            var runtimeStatusCompletedString = OrchestrationRuntimeStatus.Completed.ToString();

            var clientMock = new Mock<DurableOrchestrationClientBase>();
            clientMock
                .Setup(x => x.GetStatusAsync(createdTimeFrom, createdTimeTo, runtimeStatus, It.IsAny<CancellationToken>()))
                .Returns(Task.FromResult(list));
            var httpApiHandler = new ExtendedHttpApiHandler(clientMock.Object);

            var getStatusRequestUriBuilder = new UriBuilder(TestConstants.NotificationUrl);
            getStatusRequestUriBuilder.Path += $"/Instances/";
            getStatusRequestUriBuilder.Query = $"createdTimeFrom={WebUtility.UrlEncode(createdTimeFrom.ToString())}&createdTimeTo={WebUtility.UrlEncode(createdTimeTo.ToString())}&runtimeStatus={runtimeStatusRunningString},{runtimeStatusCompletedString}";

            var responseMessage = await httpApiHandler.HandleRequestAsync(
                new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = getStatusRequestUriBuilder.Uri,
                });
            Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
            var actual = JsonConvert.DeserializeObject<IList<StatusResponsePayload>>(await responseMessage.Content.ReadAsStringAsync());
            clientMock.Verify(x => x.GetStatusAsync(createdTimeFrom, createdTimeTo, runtimeStatus, It.IsAny<CancellationToken>()));
            Assert.Equal("01", actual[0].InstanceId);
            Assert.Equal("Running", actual[0].RuntimeStatus);
            Assert.Equal("02", actual[1].InstanceId);
            Assert.Equal("Completed", actual[1].RuntimeStatus);
        }

        [Fact]
        public async Task GetQueryWithoutRuntimeStatus_is_Success()
        {

            var list = (IList<DurableOrchestrationStatus>)new List<DurableOrchestrationStatus>
                     {
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "01",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 10, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Running,
                         },
                         new DurableOrchestrationStatus
                         {
                             InstanceId = "02",
                             CreatedTime = new DateTime(2018, 3, 10, 10, 6, 10),
                             RuntimeStatus = OrchestrationRuntimeStatus.Completed,
                         },
                     };

            var createdTimeFrom = new DateTime(2018, 3, 10, 10, 1, 0);

            var clientMock = new Mock<DurableOrchestrationClientBase>();
            clientMock
                .Setup(x => x.GetStatusAsync(createdTimeFrom, default(DateTime), new List<OrchestrationRuntimeStatus>(), It.IsAny<CancellationToken>()))
                .Returns(Task.FromResult(list));
            var httpApiHandler = new ExtendedHttpApiHandler(clientMock.Object);

            var getStatusRequestUriBuilder = new UriBuilder(TestConstants.NotificationUrl);
            getStatusRequestUriBuilder.Path += $"/Instances/";
            getStatusRequestUriBuilder.Query = $"createdTimeFrom={WebUtility.UrlEncode(createdTimeFrom.ToString())}";

            var responseMessage = await httpApiHandler.HandleRequestAsync(
                new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = getStatusRequestUriBuilder.Uri,
                });
            Assert.Equal(HttpStatusCode.OK, responseMessage.StatusCode);
            var actual = JsonConvert.DeserializeObject<IList<StatusResponsePayload>>(await responseMessage.Content.ReadAsStringAsync());
            clientMock.Verify(x => x.GetStatusAsync(createdTimeFrom, default(DateTime), new List<OrchestrationRuntimeStatus>(), It.IsAny<CancellationToken>()));
            Assert.Equal("01", actual[0].InstanceId);
            Assert.Equal("Running", actual[0].RuntimeStatus);
            Assert.Equal("02", actual[1].InstanceId);
            Assert.Equal("Completed", actual[1].RuntimeStatus);
        }
```

こうして振り返ると全然たいしたことないPRなのに、なんでこんなに時間がかかってるんだよ！というのがこのブログを書いて解明したいポイントではある。

# 師匠の皆様からのコメントと調べたこと

さて、この作成過程でかずき師匠や、Chris からいろいろ良いレビューをいただいたので、その辺を再度見直して、なぜ起こったのか、次回以降の対策を考えていきたい。

## StorageTableのサンプル

最初に困ったのが、StorageTable のサンプル探しだ。そんなのとても簡単そうに思えるのだが、今のStorageTableは、SDKがかわっていて、コードの書き方が全部違っている。そして、今はなんと、Namespace が CosmosDBになっている。

* [Get started with Azure Table storage and the Azure Cosmos DB Table API using .NET](https://docs.microsoft.com/en-us/azure/cosmos-db/table-storage-how-to-use-dotnet)

これが必要になってくる。

```
using Microsoft.Azure; // Namespace for CloudConfigurationManager
using Microsoft.Azure.Storage; // Namespace for StorageAccounts
using Microsoft.Azure.CosmosDB.Table; // Namespace for Table storage types
```

ただ、Durable Functions の場合`Microsoft.WindowsAzure.Storage.Table 7.2.1` を使っているので現在見れるサンプルとはコードの書き方が異なる。仕方がないので自分でスパイクしてサンプルを作ってみた。ここには、Queryオブジェクトの原型のプロトタイプがある。レビュー前なので、現在のと比べると面白い。

* [TsuyoshiUshio/StorageTableQueryCore](https://github.com/TsuyoshiUshio/StorageTableQueryCore)


ちなみに、Durable Functions の instance テーブルのデータを大量につくる簡単なサンプルも書いておいた

* [TsuyoshiUshio/DurablePerformance](https://github.com/TsuyoshiUshio/DurablePerformance/blob/master/RequestClient/Program.cs)

これは古いサンプルを基に実装している。
* [How to write to and read from Windows Azure tables within Web Applications](https://blogs.msdn.microsoft.com/brunoterkaly/2013/11/09/how-to-write-to-and-read-from-windows-azure-tables-within-web-applications/)
* [CloudStorageAccount Class ](https://docs.microsoft.com/en-us/dotnet/api/microsoft.windowsazure.storage.cloudstorageaccount?view=azure-dotnet)

## Linq クエリが使えない

どうせならLinq かっこよく書きたいところだが、調べると、.NET Coreでは、下記の Linq に書いているこのような書き方ができない。

```csharp
var query = from entity in dataServiceContext.CreateQuery<SampleEntity>(tableName)  
                 where entity.PartitionKey == "MyPartitionKey"  
                 select new { entity.RowKey };  
```

* [Writing LINQ Queries Against the Table Service](https://docs.microsoft.com/en-us/rest/api/storageservices/writing-linq-queries-against-the-table-service)

上記にある、`CreateQuery` メソッドが.NetCoreでは使えないからだ。なぜかというと、.NetCore用のライブラリ では、sync オペレーションがサポートされていないからの様子。

> According to this question: Missing syncronous methods for dotnet core?，NetCore/Netstandard support does not yet include Sync implementation of the APIs.

* [Azure Table Storage CreateQuery in .NET Core](https://stackoverflow.com/questions/42937596/azure-table-storage-createquery-in-net-core)
* [Missing syncronous methods for dotnet core?](https://github.com/Azure/azure-storage-net/issues/367)

となれば、Linq 無しで書くことになる。こんな感じ。

```csharp
  
// Retrieve the storage account from the connection string
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
    CloudConfigurationManager.GetSetting("StorageConnectionString"));

// Create the table client
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

// Create the CloudTable object that represents the "items" table
  CloudTable table = client.GetTableReference("items");

TableQuery<Item> itemStockQuery = new TableQuery<Item>().Where(
TableQuery.CombineFilters(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "RawMaterial"),
	TableOperators.And,
TableQuery.GenerateFilterConditionForInt("Stock-in-hand", QueryComparisons.GreaterThan, 0)));

var rawMtlStock = table.ExecuteQuery(itemStockQuery);
if (rawMtlStock.Any())
{
    foreach (ItemEntity  item in rawMtlStock)
    {
      Console.WriteLine("Item: {0} as {1} items in stock", item.Name, item.Stock-in-hand);
    }
}
```

* [Q81: How do I query azure table storage using TableQuery class?](https://www.vkinfotek.com/azureqa/how-do-i-query-azure-table-storage-using-tablequery-class.html)

## クラスの解析

* [OrchestrationClientAttribute.cs](https://github.com/Azure/azure-functions-durable-extension/blob/master/src/WebJobs.Extensions.DurableTask/OrchestrationClientAttribute.cs)

このクラスには、TaskHubなど、Httpリクエストで使っているパラメータが全部ではないが格納されている。しかし、このクラスはキャッシュされるので、頻繁に変わるものに対しては使えないので、[HttpApiHandler.cs](https://github.com/Azure/azure-functions-durable-extension/blob/master/src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs)でパースの処理を実装。[HttpManagementPayload](https://github.com/Azure/azure-functions-durable-extension/blob/master/src/WebJobs.Extensions.DurableTask/HttpManagementPayload.cs)もなにか、URL系の値をもっているが、こちらは、インスタンスを作成したら取得できる３るのURLを保持するためのクラスなので、今回の修正には関係がない。

## APIのデザイン

### Http API

API のデザインも、クリスからアドバイスをもらった。最初に考えていたデザインは次の感じ。

```
http://localhost:7071/runtime/webhooks/DurableTaskExtension/instances/?taskHub={TaskHubName}&connection=Storage&code={SystemKey}&createdTimeFrom=2018-08-10T06%3A44%3A49Z&createdTimeTo=2018-08-10T07%3A44%3A49&runtimeStatus[]=Completed&runtimeStatus[]=Running
```

クリス曰く、こんなデザインはみたことないし、こっちのほうがいいというコメント。たしかにこっちのほうがずっといい。ちなみに上の書き方は、RFCで定義されていないことを確認してから、日本語のブログを検索して採用したんだけど、やっぱり慎重にMicrosoft の他の REST API の書き方を勉強したほうが良かったのかもしれない。[RESTAPIブラウザー](https://docs.microsoft.com/ja-jp/rest/api/)を見ても１項目複数指定できるクエリーは見つけられなかった。

```
http://localhost:7071/runtime/webhooks/DurableTaskExtension/instances/?taskHub={TaskHubName}&connection=Storage&code={SystemKey}&createdTimeFrom=2018-08-10T06%3A44%3A49Z&createdTimeTo=2018-08-10T07%3A44%3A49&runtimeStatus=Completed,Running
```

### OrchestrationClient

他の項目としても、OrchestrationClient に実装されるメソッドのなまえとして、`GetStatus` にするのか、`QueryStatus` にするのかでも迷った。スタイルに合わせるということで、`GetStatus`になったが、Azure の API を見ても、Getか、List しかなくて、Queryというネーミングにはだれもしていないので、妥当と思われる。

#### 最初の API デザイン

最初はこのような感じだったが、createdTimeTo をオプショナルにしたいという話になった。

```csharp
 public override async Task<IList<DurableOrchestrationStatus>> GetStatusAsync(DateTime createdTimeFrom, DateTime createdTimeTo, string runtimeStatus, CancellationToken cancellationToken = default(CancellationToken))
```

ちなみに、本当はクエリーオブジェクトは、すべてオプショナルでも動くようになっているので、問題ないのだが、API としては、作者の「意図」を明示的に示したいのだろう。勉強になる。

```csharp
 public override async Task<IList<DurableOrchestrationStatus>> GetStatusAsync(DateTime createdTimeFrom, DateTime? createdTimeTo, string runtimeStatus, CancellationToken cancellationToken = default(CancellationToken))
```

さらに、runtimeStatus が複数指定できるようにとのことになった。

#### IList より IEnumerable

status の複数形は、status/statuses のどちらでもいいので、同じ形を採用。しかし、IList はいまいちとのことで、IEnumerable に変更。最初は、配列にするか迷ったのだが、C# の場合は、Enumerable が良さげ。Linq も使える。が、IList だと要素の増減ができてしまうので、クエリーの戻り値としてはこちらが良さげ。

かずき師匠にきいても次のような回答。


> 私は特に理由がなければ引数も戻り値も複数件のときはIEnumerableですよ

```csharp
 public override async Task<IList<DurableOrchestrationStatus>> GetStatusAsync(DateTime createdTimeFrom, DateTime createdTimeTo, IList<string> runtimeStatus, CancellationToken cancellationToken = default(CancellationToken))
```

こんな感じに

```csharp
 public override async Task<IList<DurableOrchestrationStatus>> GetStatusAsync(DateTime createdTimeFrom, DateTime createdTimeTo, IEnumerable<string> runtimeStatus, CancellationToken cancellationToken = default(CancellationToken))
```

最終的に、string はいまいちだろうということで、

```csharp
public override async Task<IList<DurableOrchestrationStatus>> GetStatusAsync(DateTime createdTimeFrom, DateTime? createdTimeTo, IEnumerable<OrchestrationRuntimeStatus> runtimeStatus, CancellationToken cancellationToken = default(CancellationToken))
```

になった。こちらのほうが型が明確になっている。

### System.Web.HttpUtility.UrlEncode より、WebUtility.UrlEncode

クリス曰く

> System.Web.HttpUtility.UrlEncodeを使わないでください。WebUtility.UrlEncodeを使ったほうがいいです。

とのこと。

* [Beware: WebUtility.UrlEncode vs HttpUtility.UrlEncode](https://www.samnoble.co.uk/2014/05/21/beware-webutility-urlencode-vs-httputility-urlencode/)

これを読むと挙動の違いはわかるけど、なぜかは不明。聞いてみる。

追記：facebookで尋ねてみると

> Web配下は ASP.NET に依存していて事実上Webプロジェクトじゃないと使えないからだったはず。
> System.webには色々ありすぎだからエンコードするだけにはちょっと的な？
> そですね。System配下のやつができるまでコンソールアプリでUri使うの大変でした

なるほど。ASP.NET 依存だったのですね。


### Enum の型変換

今回学んだことの一つとして、DurableTask と、Durable Functions 側でとても似たようなEnumがあった。意味はほぼ同等といってよくて、実装されている場所が違う程度。`OrchestrationRuntimeStatus` と、DurableTask.Core で実装されている`OrchestrationStatus` もっている項目も同じ。当初は自分で変換しないといけないと思っていたのだが、なんとキャストでいけるようす。

```csharp
OrchestrationStatus status = (OrchestrationStatus)orchestrationRuntimeStatus;
```

みたいな感じ。これにはびっくり。

### 最後でのみ await するメソッドは、async メソッドにしない

![AsyncAwait.JPG](https://qiita-image-store.s3.amazonaws.com/0/3470/4e9238ed-2404-0f2c-708a-c4b33b3d6717.jpeg)

クリスから上記のようなコメントがあり、どういうこと？と思ったんだけど、整理して考えると当たり前で、async/await は、なぜ待ち合わせできるか？というと、Taskを戻しているから。このコードの場合、途中でawait を使っている箇所がなくて、最後の戻りのところで、ほかのasync メソッドを呼び出して待ち受けをしている。でもそれだと、ここは、Task返ってくるだけなので、ここで、特にタスクの待ち受けをしても時間がかかるだけで、ここからTaskを返却してあげると、await の分だけ待たなくても、このメソッドを使う元で、待ち合わせがかかるので問題ないという仕組み。

* [async/awaitと同時実行制御](https://ufcpp.wordpress.com/2012/11/12/asyncawait%E3%81%A8%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E5%88%B6%E5%BE%A1/)

### internal の使用とユニットテスト

当初私は、Queryオブジェクトを、public で実装していたが、クリスの指摘を受けて、internal に変更した。なぜ私がpublic にしたかというと、Unit Testing をしたかったからなのだが、C# の機能で、internal はアセンブリ単位だけど、それを超えたい場合は、AssemblyInfo.cs クラスに参照可能なネームスペースを指定しておけば見れるようになる。

```csharp
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("WebJobs.Extensions.DurableTask.Tests")]

```

* [Visual Studio の単体テストで利用するInternalsVisibleTo属性について](http://moriblog.kit-eng.com/?p=867)
* [実装の隠蔽](https://ufcpp.net/study/csharp/oo_conceal.html)

## Coun() != 0 のかわりにAny()

下記も指摘をうけた。`this.RuntimeStatus.Any()` というメソッドがあり、これがあれば、要素があるか否かを検査してくれる。こっちのほうが素晴らしい。ちなみに、要素がなければというメソッドは存在しなかった。ちゃんと基本的な APIはスピードをあげるためには、一通りどんなメソッドがあるのか読んでおくとよさそう。

```csharp
if(this.RuntimeStatus.Count() != 0)  
   :
``` 

## flatMap 相当の C# 実装

最終的にAPI で、`runtimeStatus=foo,bar` を実装することになった。Request のパラメータは、`string[]` になっているので、ワンライナーで書くために、Scala でいうflatMapを使いたくなった。もちろんリンクにもあってSelectManyだった。まだ改良できそうだけど、`Split()`でカンマ区切りをコレクションに変えて、SelectMany で階層をフラットにする。

```csharp
        private static IEnumerable<OrchestrationRuntimeStatus> GetIEnumerableQueryParameterValue(NameValueCollection queryStringNameValueCollection, string queryParameterName)
        {
            var results = new List<OrchestrationRuntimeStatus>();
            var parameters = queryStringNameValueCollection.GetValues(queryParameterName) ?? new string[] { };

            foreach (var value in parameters.SelectMany(x => x.Split(',')))
            {
                if (Enum.TryParse<OrchestrationRuntimeStatus>(value, out OrchestrationRuntimeStatus result))
                {
                    results.Add(result);
                }
            }

            return results;
        }
```

# エラーの対処

いろいろ起こったエラーのメッセージと対処方法

## Object is not defined in an assembly that is not referenced. 

最初に実行しようとした出たエラー。実際のメッセージはちょっと違うが、DurableTask.Core の2.0.0.6 で PublicKeyTokenがnullのものが無いというエラーだった。普通のものはトークンがあるので、DurableTaskでPack を実行すると、DurableTask.Coreのpackageも作成されるので、そちらを使えばよい。おそらく、この方式だと、publicKeyTokenがnullのものができるのだろう。

```
Error CS0012 The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
```

## Table QueryのWhere句がnull の場合のエラー

```
[05/08/2018 07:43:19] Executed 'Query' (Failed, Id=31af8789-cd6b-4734-aa69-93d8a88dd4f3)
[05/08/2018 07:43:19] System.Private.CoreLib: Exception while executing function: Query. System.Linq: Sequence contains no elements.
```

## NameValueCollection に複数キー入力可能か？

可能

```csharp

            var collection = new NameValueCollection();
            collection.Add("name", "ushio");
            collection.Add("name", "yamazaki");
            var values = collection.GetValues("name");

```

## DurableのHttp APIで 500 エラー

エラーがでると、ログがないので解析不可能。どうやったかというと、再現テストを、Durable Functions 側、DurableTask側で書いてみて、検証した。同じパラメータを、テストとして渡してみる。Durable側では問題ないので、Mock先だとわかる。今回は、DurableTask側のエラーだった。FunctionAppにデプロイしてもエラーは見ることができない。

今回の問題は、Aggregateの箇所。ここで、RuntimeStatus.Select(...) が0件の場合、`Aggregate` は`Sequence Contains no element.` エラーになる。

```csharp
                var runtimeCondition = this.RuntimeStatus.Select(x => TableQuery.GenerateFilterCondition("RuntimeStatus", QueryComparisons.Equal, x.ToString()))
                                    .Aggregate((a, b) => TableQuery.CombineFilters(a, TableOperators.Or, b));
```

今回はこの箇所の、件数０件チェックを`Any`で行うので不要になったが、Aggregateで解決したい場合は

```csharp
.Aggregate("", (a, b) => ...
```

といった具合に初期値を設定するとよい。ただ、今回は初期値を設定すると、それも、`OR`の対象になったので、外した。

* [Sequence contains no elements exception in linq without even using Single](https://stackoverflow.com/questions/8867867/sequence-contains-no-elements-exception-in-linq-without-even-using-single)

# クソコードの消臭

クソコードがあって、そこも、いろいろ相談してみました。

## null を判断してデフォルト値を返す

```csharp
condition.CreatedTimeTo = (createdTimeTo != null) ? (DateTime)createdTimeTo : default(DateTime);
```

みたいなものは、師匠曰く


> nullだったら何か別の値ってパターンは
createdTimeTo ?? default(DateTime) みたいにかけますよ

なるほど、、、

## 

```csharp
var runtimeStatus = new List<string>();
            runtimeStatus.Add("Running");
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                RuntimeStatus = runtimeStatus,
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)
            };

  : 

```

このクソコードですが



とのこと。string は IEnumerableなので、

```
            var condition = new OrchestrationInstanceStatusQueryCondition
            {
                RuntimeStatus = new string[] {"Running"},
                CreatedTimeFrom = new DateTime(2018, 1, 10, 10, 10, 10),
                CreatedTimeTo = new DateTime(2018, 1, 10, 10, 10, 50)
            };
```

```csharp
public static OrchestrationInstanceStatusQueryCondition Parse(DateTime createdTimeFrom, DateTime? createdTimeTo, IEnumerable<string> runtimeStatus)
        {
            var condition = new OrchestrationInstanceStatusQueryCondition();
            condition.CreatedTimeFrom = createdTimeFrom;
            condition.CreatedTimeTo = (createdTimeTo != null) ? (DateTime)createdTimeTo : default(DateTime);
            condition.RuntimeStatus = runtimeStatus;
            return condition;
        }
```

> あとは、どうかなぁ。IE<string>をparams string[] にして可変長引数対応のオーバーロードも作ってもいいかもですねぇ
Parse メソッドはオーバーロード追加してから、オブジェクト初期化子と??演算子で書くとすっきりするかもですね
あとは1ステートメントなので、メソッド本文を=>で書けますね

可変の書き方はためしてないけど、こんな感じでいいのかな。

```csharp
public static OrchestrationInstanceStatusQueryCondition Parse(DateTime createdTimeFrom, DateTime? createdTimeTo, IEnumerable<string> runtimeStatus) => new OrchestrationInstanceStatusQueryCondition {
            condition.CreatedTimeFrom = createdTimeFrom,
            condition.CreatedTimeTo = (DateTime) (createdTimeTo ?? default(DateTime),            condition.RuntimeStatus = runtimeStatus
        }
```


# 今回の成果

* [Enabling conditional query #200](https://github.com/Azure/durabletask/pull/200)
* [Enabling conditional query #399](https://github.com/Azure/azure-functions-durable-extension/pull/399)

# まとめ

次回以降スピードアップするために、学んだことを記録してみたが、ブログを書くのに３時間ぐらいかかってしまった。もっとこまめにアウトプットするほうがよさそう。

今回全体像として学んだのはこういうところ

* 代表的なクラスライブラリのインターフェイスは一通りなめておくこと
* Httpや、ライブラリのインターフェイスの決定は既存のライブラリを参考にするのが良い。普通のブログだとオレオレの記事もあるので、注意
* 見直しをする（すぐ完成にしちゃうけど、しっかりリファクタリング）
* 既存のリポジトリのコードを参考にする。時間をかけて読むほうが結局速いかも（スタイルを合わせるのが重要）
* 技術要素に関してスパイクしておいて、高速に書けるまで練習する
* C#の言語仕様を学ぶほうがいいかも

次はパージの機能いってみよう。