2020年から始める Azure Cosmos DB - .NET (V3) Labs Part.1

# 2020/8/23 追記

2020/8/22 に、本国の Azure Cosmos DB チームに所属する MS 社員によって、.NET Core 3.1 \(C# 8.0\) 対応版にラボ内容が更新されました。 
そのため、公開時から一部記述内容を変更しています。

~~**なお、2020/8/23 現在、本家のリポジトリに記載の内容に従って作業を進めると、コードは動きません！！**~~

~~本記事内にて、該当箇所には修正の解説を入れていますので、合わせてご確認ください。~~

*2020/8/25 追記: コードは修正されたようです。*

# はじめに

## 記事について

この記事は、**[Azure Cosmos DB Lab][1]** の **.NET (V3) Labs** の以下の章を解説した記事です。

- [Lab 1: Creating a container in Azure Cosmos DB][2]

また、フォークした GitHub リポジトリにて、日本語訳を用意していますので、合わせてご確認ください。

- GitHub: [ymasaoka/labs][3]

## ラボ実施について

2020/8/22 時点では、**本記事の内容を実施するには、お金がかかります**。
※現在 Free Tier の適用など、少しでも身銭を切らないで良くなる手順を調査中です
身銭を切れる人は、一緒に行ってみてください。身銭を切れない人は、読むだけに留めてください。
※記載の内容を踏まえて、先にアプリのコードを作成し、環境のデプロイ->実行を行ったところ、私の環境では90円程度の課金でした。(あくまで課金額の一例です)

![スクリーンショット 2020-08-16 11.03.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/96322b6c-ea01-eec1-a115-24ce6ff541da.png)

## 実行環境

ラボの進行には、以下の環境を使用して確認しました。

- **OS**: Windows 10 Pro 10.0.19042 N/A ビルド 19042


# 環境作成

まずは、ラボ用の環境を作成します。

## Azure サブスクリプションの作成

まだ、Azure の環境を用意していない場合は、以下のリンクより Azure サブスクリプションを作成してください。

- [https://azure.microsoft.com/ja-jp/free/][4]

Azure ポータルから [サブスクリプション] を選択し、**サブスクリプションID** を確認します。この値は、この後の作業で使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/8eb4f902-fa50-067a-fc63-87e371a7aac7.png)

## Azure PowerShell のインストール

環境の作成には、Azure PowerShell を使用します。詳細は以下のページを参照します。

- [Install Azure PowerShell][5]

PowerShell 7 が未インストールの場合は、こちらも参照してください。

- [PowerShell/PowerShell][6]

## GitHub リポジトリのクローン

以下のリポジトリをローカルの任意のディレクトリにクローンします。
クローンするのは本家のリポジトリでも日本語訳版のリポジトリでも OK です。

- 本家(英語): [AzureCosmosDB/labs][7]
- 日本語訳版: [ymasaoka/labs][3]

```powershell
git clone https://github.com/ymasaoka/labs.git
```

## Azure リソースの作成

[/dotnet/labs/00-account_setup.md](https://github.com/ymasaoka/labs/blob/ja-jp/dotnet/labs/00-account_setup.md) の内容をもとに進めていきます。
まずは、Azure PowerShell で Azure アカウントに接続します。

```powershell
Connect-AzAccount -subscription <subscription id>
```

\<subscription id\> には、[Azure サブスクリプションの作成]() で確認した値を入力します。

```powershell
cd .\dotnet\setup\
.\labSetup.ps1
```

_labSetup.ps1_ を実行すると、以下のリソースが作成されます。

- Azure CosmosDB Account
- Stream Analytics Job
- Azure Data Factory
- Event Hubs Namespace

Part 1 では、Azure Cosmos DB アカウントのみ使用します。
Part 1 のみの実行であれば、Azure Cosmos DB アカウントのみ作成できればよいので、以下の部分は不要です。`Ctrl+K+C`(macOSの場合は Command+K+C) でコメントアウトしても問題ありません。

```powershell:labSetup.ps1
# 413 行目以降
New-Database $resourceGroupName $accountName "FinancialDatabase"
New-Container $resourceGroupName $accountName "FinancialDatabase" "PeopleCollection" "/accountHolder/LastName"
New-Container $resourceGroupName $accountName "FinancialDatabase" "TransactionCollection" "/costCenter" 10000

New-Database $resourceGroupName $accountName "NutritionDatabase"
New-Container $resourceGroupName $accountName "NutritionDatabase" "FoodCollection" "/foodGroup" 11000

#Lab08
New-Database $resourceGroupName $accountName "StoreDatabase"
New-Container $resourceGroupName $accountName "StoreDatabase" "CartContainer" "/Item"
New-Container $resourceGroupName $accountName "StoreDatabase" "CartContainerByState" "/BuyerState"
New-Container $resourceGroupName $accountName "StoreDatabase" "StateSales" "/State"

Add-EventHub $resourceGroupName $location $eventHubNS "CartEventHub" 1
Add-StreamProcessor $resourceGroupName $location $eventHubNS "CartStreamProcessor"
#END Lab08

Add-DataSet $resourceGroupName "importNutritionData$randomNum" $location $accountName "NutritionDatabase" "FoodCollection"
```

なお、同様に Stream Analytics Job、Azure Data Factory、Event Hubs Namespace についても、Part 1 のみの実行であれば不要なので、_labSetup.ps1_ のコードからコメントアウトしても問題ありませんが、コード量が多いため、この記事内では割愛します。

デプロイは、最大10分程度の時間がかかるため、完了するまで待ちます。
また、デプロイがうまくいかない場合があります。エラーの原因として、Azure PowerShell のリソースプロバイダーが登録されていない可能性があります。
`Get-AzResourceProvider -ListAvailable`コマンドで、デプロイに必要なリソースプロバイダーが **登録済み(RegistrationState : Registered)** になっているか確認してください。

```powershell
# リソースプロバイダーの確認
Get-AzResourceProvider -ListAvailable
# 例: Microsoft.DocumentDB を登録する場合のコマンド
Register-AzResourceProvider -ProviderNamespace Microsoft.DocumentDB
```


# .NET SDK を使用したパーティション化されたコンテナーの作成

## Azure Cosmos DB アカウント資格情報を取得

Azure ポータルより、Azure Cosmos DB の **URI**、**プライマリキー** を取得します。
詳細は、[/dotnet/labs/00-account_setup.md](https://github.com/ymasaoka/labs/blob/ja-jp/dotnet/labs/00-account_setup.md) に記載の内容を確認してください。

## コーディング

[dotnet/labs/01-creating_partitioned_collection.md](https://github.com/ymasaoka/labs/blob/ja-jp/dotnet/labs/01-creating_partitioned_collection.md) の記載の通りに進めてください。

プロジェクトを作成し、コードの実行に必要となるライブラリは`dotnet`コマンドでインストールします。

```powershell
dotnet new console --output .
dotnet add package Microsoft.Azure.Cosmos --version 3.12.0
dotnet add package Bogus --version 30.0.2
dotnet restore
```

Lab01 の内容すべてを一度に実行するための Program.cs は、以下の通りです。

```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;

namespace Lab01
{
    public class Program
    {
        private static readonly string _endpointUri = "<作成した Cosmos DB アカウントの URI>";
        private static readonly string _primaryKey = "<作成した Cosmos DB アカウントの PRIMARY KEY>";


        public static async Task Main(string[] args)
        {
            using CosmosClient client = new CosmosClient(_endpointUri, _primaryKey);
            Database database = await InitializeDatabase(client, "EntertainmentDatabase");
            Container container = await InitializeContainer(database, "EntertainmentContainer");
            // await LoadFoodAndBeverage(container);
            // await LoadTelevision(container);
            await LoadMapViews(container);
        }

        private static async Task<Database> InitializeDatabase(CosmosClient client, string databaseId)
        {
            DatabaseResponse databaseResponse = await client.CreateDatabaseIfNotExistsAsync(databaseId);
            Database targetDatabase = databaseResponse.Database;
            await Console.Out.WriteLineAsync($"Database Id:\t{targetDatabase.Id}");
            return targetDatabase;
        }

        private static async Task<Container> InitializeContainer(Database database, string containerId)
        {
            IndexingPolicy indexingPolicy = new IndexingPolicy
            {
                IndexingMode = IndexingMode.Consistent,
                Automatic = true,
                IncludedPaths =
                {
                    new IncludedPath
                    {
                        Path = "/*"
                    }
                },
                ExcludedPaths =
                {
                    new ExcludedPath
                    {
                        Path = "/\"_etag\"/?"
                    }
                }
            };

            ContainerProperties containerProperties = new ContainerProperties(containerId, "/type")
            {
                IndexingPolicy = indexingPolicy,
            };

            ContainerResponse containerResponse = await database.CreateContainerIfNotExistsAsync(containerProperties, 400);
            Container container = containerResponse.Container;

            await Console.Out.WriteLineAsync($"Container Id:\t{container.Id}");
            return container;
        }

        private static async Task LoadFoodAndBeverage(Container container)
        {
            var foodInteractions = new Bogus.Faker<PurchaseFoodOrBeverage>()
                .RuleFor(i => i.id, (fake) => Guid.NewGuid().ToString())
                .RuleFor(i => i.type, (fake) => nameof(PurchaseFoodOrBeverage))
                .RuleFor(i => i.unitPrice, (fake) => Math.Round(fake.Random.Decimal(1.99m, 15.99m), 2))
                .RuleFor(i => i.quantity, (fake) => fake.Random.Number(1, 5))
                .RuleFor(i => i.totalPrice, (fake, user) => Math.Round(user.unitPrice * user.quantity, 2))
                .GenerateLazy(100);
            
            foreach(var interaction in foodInteractions)
            {
                ItemResponse<PurchaseFoodOrBeverage> result = await container.CreateItemAsync(interaction, new PartitionKey(interaction.type));
                await Console.Out.WriteLineAsync($"Item Created\t{result.Resource.id}");
            }
        }

        private static async Task LoadTelevision(Container container)
        {
            var tvInteractions = new Bogus.Faker<WatchLiveTelevisionChannel>()
                .RuleFor(i => i.id, (fake) => Guid.NewGuid().ToString())
                .RuleFor(i => i.type, (fake) => nameof(WatchLiveTelevisionChannel))
                .RuleFor(i => i.minutesViewed, (fake) => fake.Random.Number(1, 45))
                .RuleFor(i => i.channelName, (fake) => fake.PickRandom(new List<string> { "NEWS-6", "DRAMA-15", "ACTION-12", "DOCUMENTARY-4", "SPORTS-8" }))
                .GenerateLazy(100);
            
            foreach (var interaction in tvInteractions)
            {
                ItemResponse<WatchLiveTelevisionChannel> result = await container.CreateItemAsync(interaction, new PartitionKey(interaction.type));
                await Console.Out.WriteLineAsync($"Item Created\t{result.Resource.id}");
            }
        }

        private static async Task LoadMapViews(Container container)
        {
            var mapInteractions = new Bogus.Faker<ViewMap>()
                .RuleFor(i => i.id, (fake) => Guid.NewGuid().ToString())
                .RuleFor(i => i.type, (fake) => nameof(ViewMap))
                .RuleFor(i => i.minutesViewed, (fake) => fake.Random.Number(1, 45))
                .GenerateLazy(100);

            foreach (var interaction in mapInteractions)
            {
                ItemResponse<ViewMap> result = await container.CreateItemAsync(interaction);
                await Console.Out.WriteLineAsync($"Item Created\t{result.Resource.id}");
            }
        }
    }
}
```

~~2020/8/25 時点での注意事項としては、~~

~~1. 本家リポジトリだと、**EntertainmentContainer** コンテナー のスループットは 10000 の設定になっていると思います。
これは、Program.cs 内にある~~

<details>
```csharp
ContainerResponse containerResponse = await database.CreateContainerIfNotExistsAsync(containerProperties, 10000); 
```
</details>

~~にて、10000 RU/s を設定しているためです。
上記の Program.cs は本家のコードを踏襲して実行していますが、コード内でスループットの値を設定することも可能です。Part 1 の実行のみであれば 10000 RU/s は無駄になりますので、10000 を 400 に変えてしまっても問題ありません。アプリ処理は正常に動作できることを確認済みです。~~

*2020/08/29 追記: コードは修正され、400 RU/s で作成されるように変更されました。上の記載は無視してください。*

コーディングが終了したら、アプリを実行して動作を確認します。

```powershell
dotnet restore
dotnet build
dotnet run
```

アプリの正常実行および完了を確認したら、Azure ポータルから Azure Cosmos DB にデータが投入されたか確認してみましょう。
*PurchaseFoodOrBeverage*、*WatchLiveTelevisionChannel*、*ViewMap* それぞれのデータが **EntertainmentDatabase** の中にある **CustomCollection** コンテナーの中に登録されていることが確認できるはずです。Azure Cosmos DB がいかにスキーマレスであるかを確認することができると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/8cd92d85-e97a-20f0-3943-e1754a665dde.png)

また、SDK で設定した **Partition key** と **includedPaths** も正しく設定されていることを確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348116/10e1016e-14fe-73c9-8803-326aa9267ddd.png)

SDK で設定した通りの値が表示されていることを確認できます。

# 環境のクリーンアップ

**次回以降のラボを実施しない場合**は、動作を確認出来たら、不要な課金を抑えるため、環境を削除します。
[/dotnet/labs/11-cleaning_up.md](https://github.com/ymasaoka/labs/blob/ja-jp/dotnet/labs/11-cleaning_up.md) に記載の内容に従って、Azure リソースを削除してください。

**無駄な課金はお財布に優しくないのできちんと削除確認しましょう。**


# 次のラボ

- [2020年から始める Azure Cosmos DB - .NET (V3) Labs Part.2][8]

---

[1]:https://azurecosmosdb.github.io/labs/
[2]:https://azurecosmosdb.github.io/labs/dotnet/labs/01-creating_partitioned_collection.html
[3]:https://github.com/ymasaoka/labs/tree/ja-jp
[4]:https://azure.microsoft.com/ja-jp/free/
[5]:https://docs.microsoft.com/ja-jp/powershell/azure/install-az-ps
[6]:https://github.com/PowerShell/PowerShell/releases
[7]:https://github.com/AzureCosmosDB/labs
[8]:https://qiita.com/ymasaoka/items/63d39b64d3705e56e453