Dapr を使ってみる - State management 編

この記事は Dapr に関する一連の記事の一部です。

- [Dapr って何？ - 概要編](https://qiita.com/takashiuesaka/items/ecb8d232d00f0a6591a5)
- [Dapr を使ってみる - 入門編](https://qiita.com/takashiuesaka/items/bdae30de41b0daab881f)
- Dapr を使ってみる - State management 編

# 開発環境

- Windows11/Mac Sonoma
- Docker Desktop 4.29.0 (145265)
- Visual Studio 17.10.0 Preview 6.0
    - .NET Aspire Preview 6.0
- Visual Studio Code
- Dapr CLI 1.13.0

# 環境準備

Dapr CLI をインストールし、dapr init の実行を完了しておきます。詳しくはこちらをご覧ください。

[Dapr を使ってみる - 入門編 # 環境準備](https://qiita.com/takashiuesaka/items/bdae30de41b0daab881f#%E7%92%B0%E5%A2%83%E6%BA%96%E5%82%99)

# State management を構成する

Dapr の State management の概要についてはこちらをご覧ください。

[Dapr って何？ - 概要編 # Dapr による状態保存](https://qiita.com/takashiuesaka/items/ecb8d232d00f0a6591a5#dapr-%E3%81%AB%E3%82%88%E3%82%8B%E7%8A%B6%E6%85%8B%E4%BF%9D%E5%AD%98)

dpar init で構成されたローカル環境では、既定で State management を使用するセットアップが行われています。

<dl>
  <dt>Windows の場合の yaml ファイルパス</dt>
  <dd>%userprofile%\.dapr\components\statestore.yaml</dd>
  <dt>Mac OSの場合の yaml ファイルパス</dt>
  <dd>$HOME/.dapr/components/statestore.yaml</dd>
</dl>

```yaml:statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
```

この Statestore を使うときは、 metadata.name に書いてある statestore という名前を使うことになります。また、spec.type に state.redis と記載があることから、データは Redis に保存されます。

## 既定の Redis 以外を使いたい場合

本番環境では State store として Redis 以外を使用する場合も多いでしょう。使用可能なリソースはこちらのページに一覧があります。使用方法は、該当のリソースをクリックすると表示されます。

[State store component specs](https://docs.dapr.io/reference/components-reference/supported-state-stores/)

例えば [Azure Blob Storage](https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-azure-blobstorage/) を使う場合、次のような yaml を作成すれば良いことがわかります。

```yaml:azureblob.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: <NAME>
spec:
  type: state.azure.blobstorage
  # Supports v1 and v2. Users should always use v2 by default. There is no
  # migration path from v1 to v2, see `versioning` below.
  version: v2
  metadata:
  - name: accountName
    value: "[your_account_name]"
  - name: accountKey
    value: "[your_account_key]"
  - name: containerName
    value: "[your_container_name]"
```

例えば、ローカルで稼働する Azure Storage エミューレーター [Azurite](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage) を状態保存先にする場合は次の構成になります。

```yaml:.dapr/components/azureblob.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azureblob
spec:
  type: state.azure.blobstorage
  version: v2
  metadata:
    - name: accountName
      value: "devstoreaccount1"
    - name: accountKey
      value: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
    - name: containerName
      value: "statestore"
    - name: endpoint
      value: "http://127.0.0.1:10000"
```

サービスプリンシパルを使用してよりセキュアに使用する方法についても記載がありますので、本番で使用する場合はリンク先の情報を参考にしてください。

[Azure Blob Storage](https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-azure-blobstorage/)

# Redis に対して状態を保存・読み出しする

今回はサンプルアプリケーションとして Blazor Web App を使用しますが、Blazor を知らなくても大丈夫ですので心配しないでください。Blazor 特有の箇所については説明します。

## サンプルアプリケーションを作成する

Visual Studio 2022 Preview を使用して、プロジェクトを新規作成します。Blazor Web App を選択します。ダイアログで右上のプロジェクトの種類を選択するドロップダウンで Web を選択すると探しやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/a65cb1be-9b4c-3fd2-c33f-bbe4f3781844.png)

追加情報画面では、Include sample pages はオンにしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3ca7b815-4f95-7e25-7850-71aae13c15db.png)

作成できたら実行します。サンプル画面が立ち上がるので、左メニューの Weather をクリックするとこのように天気予報データが表示されます。この天気予報データは毎回ランダム生成したものを表示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/3c6192b7-d281-a7a0-3423-6b2948ef2e5b.png)

MacOSの場合、または Visual Stduio を使用しない場合は.NET 8 をインストールしておいた上で次のコマンドを実行します。

```bash
dotnet new blazor -n DaprStatestore -o DaprStatestore
```

## Dapr 対応する

Dapr 用の SDK をプロジェクトに追加します。Balzor プロジェクトの依存関係を右クリック→　Nuget パッケージの管理を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/6492767a-fb7a-73de-7f5e-4b38fc1fb691.png)

立ち上がってきた Nuget パッケージマネージャーの検索ボックスに Dapr.AspNetCore　と入力して、一覧の中から Dapr.AspNetCore　を選択、インストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7837133d-16a2-8cab-c9fa-2f9ed94f46a2.png)

MacOS または Visual Studio 以外の場合は次のコマンドを実行してインストールします。実行する時は *****.csproj ファイルが存在するパスに移動してから実行することをお忘れなく。

```bash
dotnet add package Dapr.AspNetCore --version 1.13.0
```

では実装していきましょう。まず Program.cs を開いて次の行を追加します。

```diff_c_sharp:Program.cs
+ builder.Services.AddDaprClient();

var app = builder.Build();
```

必ず var app = builder.Build();行より上に実装してください。この実装により、使用したいクラスに DaprClient を Injection してくれるようになります。

次は天気予報ページを修正します。既定の実装では天気予報データを毎回ランダムに生成していますが、初回アクセス時に天気予報データを生成した時に Dapr を通じて保存します。２回目以降は保存した天気予報データを表示するようにします。

プロジェクトの中の Components/Pages フォルダの中に Weather.razor というファイルがありますので、開きます。Weather ページにアクセスすると、このファイルが実行されると考えてください。まずはファイルの先頭に DaprClient を DI で受け取る実装を追加します。通常、C# ではクラスのコンストラクタで DI されるオブジェクトを受け取りますが、 Blazor のファイルではこのように実装します。

```diff_c_sharp:Components/Pages/Weather.razor
@page "/weather"
@attribute [StreamRendering]
+ @inject Dapr.Client.DaprClient DaprClient

```

次に OnInitializedAsync メソッドの中身を次の実装に全て入れ替えます。

```diff_c_sharp:Components/Pages/Weather.razor
protected override async Task OnInitializedAsync()
{
    forecasts = await DaprClient.GetStateAsync<WeatherForecast[]>("statestore", "forecast");

    if (forecasts == null)
    {
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();

        await DaprClient.SaveStateAsync<WeatherForecast[]>("statestore", "forecast", forecasts);
    }
}
```

DI　で受け取った DaprClient オブジェクトを使用して、 statestore という名前がついた Statestore　に対して Key名が forecast という値を読み、それが null だったらランダムに天気予報データを生成してから保存する実装です。

これを実行する前に Dapr Sidecar プロセスを起動しておく必要があります。次のコマンドを実行します。

```bash
dapr run --app-id web --dapr-http-port 3500  --dapr-grpc-port 50001
```

--app-id オプションに渡す web は自由に名前をつけて構いませんが、--dapr-http-port と --dapr-grpc-port　オプションのポート番号は変えてはダメです。今回動かす Dapr Sidecar は1つだけのため、既定のポート番号で動かしています。すると、アプリケーション側で Dapr Sidecar のポート番号を指定する必要がないからです。

では Blazor Web App を起動します。問題なく動作するはずです。リロードしても常に同じ天気予報データが表示されるはずです。

では、Redis に保存されたデータを見てみましょう。Docker Desktop を使っている場合は左メニューから Containers を選び、 dapr_redis のリンクをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/697f8b5d-adf1-ccb2-861f-2a5a33a76396.png)

タブの Exec をクリックすると、コンテナの中からコマンドを叩くことができます。
redis-cli を使って、KEYと値を見てみると Web||forecast という KEY で ハッシュタイプの値が格納されていて、中を見てみると天気予報データが格納されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/46810446-df9d-a23b-57ce-9d699a399aae.png)

# Statestore を Azure Blob Storage に変更する

既定で用意されたものを使用するだけではなんだかしっくりきません。違う Statestore を使ってみましょう。Azure Blob Storageにします。Azurite というエミューレータを使うのでAzure契約がなくても大丈夫です。Azurite はコンテナで動かすのが簡単です。Docker を起動してから次のコマンドを実行します。

```bash
docker pull mcr.microsoft.com/azure-storage/azurite
```

Azurite を立ち上げます。Blob しか使用しないオプションを付けています。

```bash
docker run -p 10000:10000 mcr.microsoft.com/azure-storage/azurite azurite-blob --blobHost 0.0.0.0 --blobPort 10000
```

Azurite が起動したら [Azure Storage Explorer](https://azure.microsoft.com/ja-jp/products/storage/storage-explorer/) などを使用してツールから Azurite にアクセスしておくと、後で確認しやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f6575bbc-035a-b8ec-4769-f87c5805ba84.png)

次に Azure Blob Storage 用の　yaml ファイルを次の場所に作成します。
<dl>
  <dt>Windows の場合の yaml ファイルパス</dt>
  <dd>%userprofile%\.dapr\components\azureblob.yaml</dd>
  <dt>Mac OSの場合の yaml ファイルパス</dt>
  <dd>$HOME/.dapr/components/azureblob.yaml</dd>
</dl>

```yaml:azureblob.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azureblob
spec:
  type: state.azure.blobstorage
  version: v2
  metadata:
    - name: accountName
      value: "devstoreaccount1"
    - name: accountKey
      value: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
    - name: containerName
      value: "statestore"
    - name: endpoint
      value: "http://127.0.0.1:10000"
```

Azurite のアカウント名とキーについてはこちらの記載をご確認ください。

[ローカルでの Azure Storage の開発に Azurite エミュレーターを使用する # 既知のストレージ アカウントとキー](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#well-known-storage-account-and-key)

また、endpoint のオプションを追加していますが、Azure の Blob アカウントを使う場合であればこのオプションは不要です。Azurite を使う場合は Blob へのパスが Azure Blob を使う時と全く異なるため、特別に指定しています。
解説についてはこちらの endpoint の項目をご確認ください。

[Azure Blob Storage # Spec metadata fields](https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-azure-blobstorage/#spec-metadata-fields)

ではコードを修正します。使用する Statestore 名を、事前に定義した azureblob に変更するだけです。

```diff_c_sharp:DaprStatesore/Program.cs
    protected override async Task OnInitializedAsync()
    {
-       forecasts = await DaprClient.GetStateAsync<WeatherForecast[]>("statestore", "forecast");
+       forecasts = await DaprClient.GetStateAsync<WeatherForecast[]>("azureblob", "forecast");

        if (forecasts == null)
        {
            var startDate = DateOnly.FromDateTime(DateTime.Now);
            var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
            forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = startDate.AddDays(index),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = summaries[Random.Shared.Next(summaries.Length)]
                }).ToArray();

-           await DaprClient.SaveStateAsync("statestore", "forecast", forecasts);
+           await DaprClient.SaveStateAsync("azureblob", "forecast", forecasts);
        };
```

azureblob という Statestore 名は構成ファイルの metadata.name に指定したものです。

```yaml:azureblob.yaml（一部抜粋)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azureblob
```

では Dapr Sidecar プロセスを起動してください。オプションは先ほどと同じです。

```bash
dapr run --app-id web --dapr-http-port 3500  --dapr-grpc-port 50001
```

Blazor Web アプリケーションを実行してみると、問題なく天気予報データが画面に表示されたかと思います。Blob にはどんなファイルが作成されたのかを Azure Storage Explorer で見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/9e87e44c-ff61-74f8-4c19-d65eb4a7fabc.png)

statestore というコンテナが自動的に作成されて、中に web||forecast というファイルが作成されています。ダブルクリックして開いてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/afc57a30-066b-7096-b530-2423ce48be6f.png)

forecast 配列のオブジェクトが Json 形式でシリアライズされて格納されています。状態を保存する先のリソースを簡単に切り替えることできました。

# .NET Aspire で State management を扱う

うっかりと Dapr Sidecar プロセスを起動し忘れる、既定のポート番号を忘れて dapr run が実行できない、なんてことがよくあります。.NET Aspire を使うとそんな煩わしさから解放されます。

.NET Aspire は Dapr 使ったシステム開発を行う際に、ローカル環境設定用のツールとして登場した [Tye](https://github.com/dotnet/tye) の後継にあたります。　[Tye](https://github.com/dotnet/tye) とは異なり、コードベースで設定可能なのでとても使いやすくなっています。（Tye は.NET Aspire の登場ともに開発が終了しました）

.NET Aspire についてはこちらをご覧ください。

- [.NET Aspire って何？ - 概要](https://qiita.com/takashiuesaka/items/f45e930ef296c5710acc)
- [.NET Aspire を使ってみる](https://qiita.com/takashiuesaka/items/8794f99b0bf3ce3c3106)
- [.NET Aspire を デプロイする](https://qiita.com/takashiuesaka/items/3da744d0511f29270299)
- [.NET Aspire で Prometheus, Jaeger, Grafana を使う](https://qiita.com/takashiuesaka/items/4811e62aa52e39ed834c)
- [Next.js + ASP.NET Core を .NET Aspire で構成する（with YARP）](https://qiita.com/takashiuesaka/items/e167852af299a7b00939)
- [.NET Aspire でデータベースを扱う - PostgreSQL編](https://qiita.com/takashiuesaka/items/137cb1450c3d9fbc8d1e)
- [.NET Aspire でデータベースを扱う - SQL Server編](https://qiita.com/takashiuesaka/items/6d5ec4d4994649c5dca5)
- [.NET Aspire のダッシュボードを単独で使う](https://qiita.com/takashiuesaka/items/338ae64ca2033e078f7f)

では .NET Aspire を使って、もっと楽に Dapr を扱ってみましょう。
.NET Aspire の AppHost プロジェクトを追加します。ソリューションを右クリック→追加→新しいプロジェクトを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/c0ab10b5-8a5d-96b8-607d-21d6e821b75e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/58726505-998e-6151-724a-ca98a08b05a2.png)

追加すると、2つプロジェクトが存在するようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1117ba4b-8cfd-7ef5-adf7-998ff9b3eb68.png)

今まではアプリケーションを起動すると Blazor Web App が起動していましたが、今追加した AppHost が起動するように設定を変更してください。AppHost プロジェクトを右クリック→スタートアッププロジェクトに設定、を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2ba4bf13-328f-6611-fe34-71a798a9c087.png)

次に、AppHost から Blazor Web App プロジェクトをプロジェクト参照します。一番簡単なやり方はソリューションエクスプローラ上で Blazor Web App のプロジェクトを AppHost プロジェクトにドラッグ＆ドロップする方法です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8f002fa3-cb25-a454-e0d4-7d33df97aa07.png)

では、Dapr 対応をしてきましょう。まず AppHost プロジェクトに .NET Aspire の Dapr 対応用ライブラリをインストールします。

AppHost プロジェクトを右クリックし、追加→.NET Aspire パッケージを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/157ced4a-9b04-c38a-bcf2-965e8e0d035e.png)

すると、検索条件に「owner:Aspire tags:hosting」が入力された状態で Nuget パッケージマネージャーを開きます。この検索条件の後半、tags:hosting を削除して、代わりに Dapr と入力します。すると、 Aspire.Hosting.Dapr というライブラリがありますので、それをインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/766d3144-d6f8-e59f-9692-e29a377b9595.png)

AppHost の Program.cs に Dapr 対応の実装を追加します。

```diff_c_sharp:AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

+ var azureblobStateStore = builder.AddDaprStateStore("MyStore");

+ var web = builder.AddProject<Projects.DaprStatestore>("web")
+     .WithDaprSidecar();

builder.Build().Run();
```

AddProjectメソッドの GenericType に Projects.DaprStatestore を渡していますが、これは Blazor Web App のプロジェクトですので、作成したプロジェクト名に合わせて変更してください。

準備ができました。Dapr CLI によるサイドカーを実行中の場合は停止してから実行します。.NET Aspire のダッシュボードが立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/b5dfd600-f8f1-6ada-5bed-83bc20495711.png)

Resources に2行表示があります。上が dapr run を実行中であることを表しています。下は Blazor Web App を実行中であることを表しています。このように、.NET Aspire を使用することで依存関係をコードで表し、実行することが可能になるのでとても便利です。

Blazor Web App 行の Endpoints の URL を起動すると、今まで通り画面が表示されて、天気予報データも問題なく表示されます。表示されてない場合は Azurite を停止している可能性があります。再度コマンドを実行して起動してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/96041827-ff42-5913-7180-61a4e4a9c720.png)

ちなみに、ダッシュボードの　　dapr run 実行の引数を見てみると、ポート番号にデフォルト以外の数字が使用されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1df83fa9-f4c1-3197-befe-2ec7bbc96828.png)

ランダムなポート番号を使用することで、複数の Dapr Sidecar プロセスを起動しても問題が発生しないようになっています。そして、そのランダムなポート番号を使用するように、自動的に Blazor Web App に連携されているので、環境のトラブルに煩わされることなく開発できます。

C# で構築するシステムで Dapr を採用する場合、.NET Aspire の使用を強くお勧めします。

## さらに Azurite も .NET Aspire で起動する

.NET Aspire は Azurite の起動もサポートしています。AppHost プロジェクトに　**Aspire.Hosting.Azure.Storage ライブラリをインストール**し、Program.cs に次の実装を追加します。

```diff_c_sharp:Program.cs
var builder = DistributedApplication.CreateBuilder(args);

+ var azurite = builder.AddAzureStorage("azurite")
+     .RunAsEmulator(res =>
+     {
+         res.WithImageTag("latest");
+         res.UseBlobPort(10000);
+     } );

var azureblobStateStore = builder.AddDaprStateStore("MyStore");

var web = builder.AddProject<Projects.DaprStatestore>("web")
    .WithDaprSidecar();

builder.Build().Run();
```

Azure Blob を使用する Statestore の yaml で、エンドポイントのポート番号に 10000 を指定しているので、同じ 10000 を指定する必要があるため、このような実装になります。これをしないとランダムなポート番号で.NET Aspire は Azurite を立ち上げるからです。

手動で実行していた Azurite を停止してから実行すると、ダッシュボードに　Azurite が表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8c0fd692-0900-4863-b70c-6d2a67f6b004.png)

Weather にアクセスすると、問題なくデータを取得できることがわかるでしょう。これで、事前に Azurite を立ち上げる必要もなくなり、ただ実行するだけで環境が整うようになりました。

# まとめ

Dapr を使うと State managemet が実に簡単になります。１件ずつ取得するのではなくBulk でデータを取得することもできますので、とても便利です。

また、.NET Aspire を使うことで、Dapr Sidecar の立ち上げやエミュレーターなど環境周りの煩わしさから解放されます。是非お試しください。