【C#】Azure CosmosDBのデータ更新についてとdynamicでハマった話

こんにちは！GxP2年目の笠井です。
この記事は[グロースエクスパートナーズ アドベントカレンダー](https://qiita.com/advent-calendar/2020/gxp)の12日目の記事となります。

今回は業務でAzure CosmosDB(以下CosmosDB)のデータメンテナンスツールを作っている際にC#のdynamicで悩んだ話です。
テーマが「今年の学び」ということで、基礎的なところで躓いた話ではありますが自分への戒めとして書いていきます。

## 状況
CosmosDBから取得したデータ内の年月日等の日付に相当する値を結合し、新たにISO 8601形式の日付データとして要素を追加するツールを作る必要がありました。

![CosmosDBJson.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906347/1ae6d6bc-9067-f7ca-772a-c9b3523ba8b9.png)


今回CosmosDBから取得するデータはJSONになっており、１つのドキュメントごとに要素数が異なる半構造化データに対応するためにdynamicで取得することにしました。

## Azure CosmosDB
### CosmosDBとは
CosmosDBはさまざまなタイプのデータモデルに対応可能なNoSQLデータベースで、Key-Value型、Graph型、Document型などのデータモデルとそれぞれのモデルに対応したAPIが提供されています。
今回はそのなかでもSQL API(Document型)を使用しました。

### Cosmosアカウントの要素
Cosmosアカウントの要素は以下のような階層になっています。
![CosmosDBArchi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906347/5b471b37-3eb2-d48c-8fd4-e515aba3df89.png)


## dynamic とは
dynamicがどういったものなのかざっくり説明します。
### 動的型付け変数
実行時に型が決まります。
ですので、以下のようなクラスを定義したとして

```C#
public class Hoge {
   public string A { get; set; } 
   public string B { get; set; } 
}
```
以下のように存在しないプロパティを参照するようなコードを書いた場合、コンパイルは通りますが```RuntimeBinderException```が投げられます。

```C#
dynamic hoge = new Hoge();

// コンソールにAの値が出力される
System.Console.Writeline(hoge.A);

// コンソールにBの値が出力される
System.Console.Writeline(hoge.B);

// コンパイルは通るが実行時エラーとなる
System.Console.Writeline(hoge.C);
```
また、dynamic **型**というものではなく、実体は**Object**です。
なので以下のようなコードはコンパイルエラーになります。

```C#
dynamic A;
if (typeof(A) == typeof(dynamic))
{
    hoge();
}
```



## CosmosDBデータメンテナンスツール
今回は単純に「CosmosDBに接続、全件取得し更新する」というコードです。

```C#
using Microsoft.Azure.Cosmos;
using System;
using System.Threading.Tasks;

namespace Hoge
{
    public class Hoge
    {
        // CosmosDB エンドポイント
        private static readonly string EndpointUri = "todo";
        // プライマリキー 
        private static readonly string PrimaryKey = "todo";

        // CosmosDB クライアント
        public CosmosClient cosmosClient;

        // DB
        public Database database;

        // コンテナ 
        public Container container;

        // DB名 
        private string databaseName = "todo";

        // コンテナ名 
        private string containerName = "todo";


        public static async Task Main(string[] args)
        {
            try
            {
                Console.WriteLine("開始しています...\n");
                Hoge app = new Hoge();

                app.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
            
                // DBへ接続する、存在しなければDBを作成する
                await app.CreateDatabaseAsync();
                // コンテナへ接続する、存在しなければコンテナを作成する
                await app.CreateContainerAsync();
                // SQLを実行し、検索結果の取得と各UPDATEを実行する
                await app.QueryAndUpdateItemsAsync();
            }
            catch (CosmosException de)
            {
                Exception baseException = de.GetBaseException();
                Console.WriteLine("{0} error occurred: {1}", de.StatusCode, de);
            }
            catch (Exception e)
            {
                Console.WriteLine("Error: {0}", e);
            }
            finally
            {
                Console.WriteLine("終了しました、何かキーを押してください。");
                Console.ReadKey();
            }
        }

        /// <summary>
        /// DBの情報を取得、存在しなければDBを作成する
        /// </summary>
        public async Task CreateDatabaseAsync()
        {
            this.database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(databaseName);
            Console.WriteLine("Created Database: {0}\n", this.database.Id);
        }

        /// <summary>
        /// コンテナの情報を取得、存在しなければコンテナを作成する
        /// </summary>
        /// <returns></returns>
        public async Task CreateContainerAsync()
        {
            // コンテナ名とパーティションキーを指定する。
            // 存在しなければ指定したコンテナ名とパーティションキーでコンテナが作成される
            this.container = await this.database.CreateContainerIfNotExistsAsync(containerName, "/partitionKey");
            Console.WriteLine("Created Container: {0}\n", this.container.Id);
        }

        /// <summary>
        /// SQLを実行し、検索結果の取得と各UPDATEを実行する
        /// </summary>
        /// <returns></returns>
        public async Task QueryAndUpdateItemsAsync()
        {
            // クエリ、今回は全件検索
            var sqlQueryText = "SELECT * FROM c ";

            // クエリを実行する
            QueryDefinition queryDefinition = new QueryDefinition(sqlQueryText);
            FeedIterator<dynamic> queryResultSetIterator = 
                                     this.container.GetItemQueryIterator<dynamic>(queryDefinition);


            while (queryResultSetIterator.HasMoreResults)
            {
                FeedResponse<dynamic> currentResultSet = await queryResultSetIterator.ReadNextAsync();
                foreach (var item in currentResultSet)
                {
                   // データを整形する
                   item.Date = example(item);                    

                   // ドキュメントを更新する
                   await this.container.ReplaceItemAsync<dynamic>(
                        item, 
                        item.id.ToString(), 
                        new PartitionKey(item.partitionKey.ToString())
                   );
                }
            }
        }
    }
}
```
### DB、コンテナの情報を取得
- ```CosmosClient.CreateDatabaseIfNotExistsAsync()```で指定したDBが存在するかを確認し、存在すればDBの情報（```Task<DatabaseResponse>```）を返却します。存在しない場合はDBを作成します。
- ```CosmosDatabase.CreateContainerIfNotExistsAsync()```で指定したコンテナが存在するかを確認し、存在すればコンテナの情報（```Task<ContainerResponse>```）を返却します。存在しない場合はコンテナを作成します。

```C#
/// <summary>
/// DBの情報を取得、存在しなければDBを作成する
/// </summary>
public async Task CreateDatabaseAsync()
{
    this.database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(databaseName);
    Console.WriteLine("Created Database: {0}\n", this.database.Id);
}

/// <summary>
/// コンテナの情報を取得、存在しなければコンテナを作成する
/// </summary>
/// <returns></returns>
public async Task CreateContainerAsync()
{
    // コンテナ名とパーティションキーを指定する。
    // 存在しなければ指定したコンテナ名とパーティションキーでコンテナが作成される
    this.container = await this.database.CreateContainerIfNotExistsAsync(containerName, "/partitionKey");
    Console.WriteLine("Created Container: {0}\n", this.container.Id);
}
```

### クエリの実行
- stringのクエリ文字列を```QueryDefinition```クラスに定義します。
定義したクエリを```Container.GetItemQueryIterator()```で実行することで、結果が```FeedIterator<T>```で返却されます。

```C#
// クエリ、今回は全件検索
var sqlQueryText = "SELECT * FROM c ";

// クエリを実行する
QueryDefinition queryDefinition = new QueryDefinition(sqlQueryText);
FeedIterator<dynamic> queryResultSetIterator = this.container.GetItemQueryIterator<dynamic>(queryDefinition);
```

### データを更新
- ```FeedIterator.ReadNextAsync()``` で取得してきた ```FeedResponse<T>```にはクエリで取得してきたデータが入っているのでforeachを使って一つ一つ更新していきます。
- ```Container.ReplaceItemAsync<T>()```に内容を更新した**item**、itemの**id**、**パーティションキー**を渡して更新します。

```C# 
while (queryResultSetIterator.HasMoreResults)
{
    FeedResponse<dynamic> currentResultSet = await queryResultSetIterator.ReadNextAsync();
    foreach (var item in currentResultSet)
    {
        // データを整形する
        item.Date = example(item);                    

        // ドキュメントを更新する
        await this.container.ReplaceItemAsync<dynamic>(
            item, 
            item.id.ToString(), 
            new PartitionKey(item.partitionKey.ToString())
        );
    }
}
```

## 今回のハマりポイント
### ちゃんと型を把握しなければいけない
実行時に型が決まるのでコンパイルエラーが出ません。なのでちゃんと型を把握して使う必要があります。
コンパイルは通ってしまうのであまり意識が出来ておらず苦戦しました。

以下の**item.id**は```Newtonsoft.Json.Linq.JToken```が想定されるので```ToString()```メソッドで```string```に変換して引数に入れています。

```C#
await this.container.ReplaceItemAsync<dynamic>(
    item, 
    item.id.ToString(), 
    new PartitionKey(item.partitionKey.ToString())
);
```

今回事前調査が足りず、CosmosDBから取得できるJSONが``` Newtonsoft.Json.Linq.JObject```であることが実装後にわかったので、結果としてdynamicを使う必要がありませんでした。
以下は修正後のコードです。

```C#
public async Task QueryAndUpdateItemsAsync()
{
    // クエリ、今回は全件検索
    var sqlQueryText = "SELECT * FROM c ";

    // クエリを実行する
    QueryDefinition queryDefinition = new QueryDefinition(sqlQueryText);
    FeedIterator<JObject> queryResultSetIterator = 
                                     this.container.GetItemQueryIterator<JObject>(queryDefinition);


    while (queryResultSetIterator.HasMoreResults)
    {
        FeedResponse<JObject> currentResultSet = await queryResultSetIterator.ReadNextAsync();
        foreach (var item in currentResultSet)
        {
            // データを整形する
            item["Date"] = example(item);                    

            // ドキュメントを更新する
            await this.container.ReplaceItemAsync<dynamic>(
                item, 
                item["id"], 
                new PartitionKey(item["partitionKey"])
            );
        }
    }
}
```
## まとめ
本記事の件で自分の経験の浅さを思い知りました。
**実行時に型が決まる**という仕様上コンパイルしただけではエラーなのかが把握しきれないので、動的型付けや型推論を使う際はちゃんと型を把握して実装するよう注意したいと思いました。
どんな型のデータが入っているかを知る方法として```Object.GetType()```がありますが、一番手っ取り早いのは**ちゃんと公式のドキュメントを読むこと**だと思います。

また、**dynamic**や**var**をむやみに乱用すると他の作業者や未来の自分がソースコードを見た時に理解しづらくなってしまうので**型が容易に想定できる範囲**で使うことを意識するといいと思いました。