【詳説】C#でREST APIサーバを作り、Azureへデプロイする実装例



# はじめに
この記事では、C#を用いたREST APIサーバ構築方法を具体的な手順で解説します。間違いがあれば指摘をお願いいたします。  

また、ASP.NET Coreを利用しますが、ASP.NET Coreの解説というわけではないので、独自実装が多いです。

## 作成するAPIサーバの機能
下記機能を持つAPIサーバを作ります。これらの機能はほとんどのサーバでも必要だと思うので、このサーバを拡張・修正して利用してください。
### ユーザ登録機能
ユーザを登録し、サインインできるようにします。ユーザが入力する情報は、ユーザ名、サインインID、パスワードとします。  
ユーザが登録されると、自動的に登録日時が保存されるものとします。

#### ルール
ユーザ名: 1文字以上50文字以内  
サインインId: 10文字以上50文字以内。他のユーザIDとはユニークでなければならない  
パスワード: 10文字以上

### サインイン機能
サインインID、パスワードを利用したサインイン機能を実装します。サインイン完了後はJWTを発行し、クライアントへ返します。 

### トークン認証機能
サインイン時に返却したJWTを利用した認証を行い、認証されていない利用者が利用できないAPIを実装します。

### トークンリフレッシュ機能
記事が思ったよりも長くなってしまったので概要だけ示します。

## 対象読者・必要なソフトウェア
### 対象読者
- Gitのリポジトリ作成やプル/プッシュ操作に慣れている
- Dockerを触ったことがある人
- C#でREST APIサーバを作りたい人

### この記事でわかること
- C#を利用したREST APIサーバ実装方法
- Dockerを利用した.NET開発環境の構築方法
  - コンテナ内でデバッグする方法
  - `dotnet watch`コマンドを用いてソースコードの変更を即時反映する方法
- パスワード管理などセキュリティの基礎知識
- xUnitを利用したテストの実装方法
  - モックの利用方法・注意点
  - パラメータ化されたテストの実装
- GitHub Actionsを利用したCIの構築方法
  - プルリクエスト時、`main`ブランチへのプッシュ時に自動テストを実行します
- Azure上でサーバを動作させる方法
- GitHub Actionsを利用し、Azure Container Appsへ継続的デプロイする方法
  - `production`ブランチへプッシュされた際に自動デプロイします

設計思想についても説明しますが、この辺は主観が目立つため、参考程度にとどめてください。

### エディタ
筆者はWindows環境で`Visual Studio Code`または`Visual Studio 2022`を利用しています。
本記事では他OSでも開発できるように`Visual Studio Code`を利用して説明します。

### 必要なもの
- .NET SDK
- Webブラウザ
- Docker
- Git

本記事では.NET 8を利用しますが、恐らく`.NET 9`でも動作します。.NETはメジャーバージョンが上がっても破壊的変更は優しいです。

### 各サービスやソフトウェアのつながり
![環境図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/97a666da-fde5-91d6-7d71-1834e5bf5668.png)


開発環境や使うもののつながりを図にまとめました。Visual Studio Codeを軸とした開発環境です。
- REST API サーバやデータベースサーバのローカル実行環境はDocker上に構築します
- ソースコード等ファイルはホストOS上に用意し、Dockerのボリュームを利用して同期します
- CI/CDの構築はGitHub Actionsを利用します
- 本番環境はAzureを利用します

# サンプルプログラム
サーバのサンプルプログラムをGitHubに置いているので、必要に応じてご参照ください。この記事では説明を一部省略している箇所があります。
[サンプル](https://github.com/AutumnSky1010/BasicRESTAPIServer)

# データベースの環境構築
空のローカルリポジトリを作成した段階から開始します。

## SQL Serverの開発環境を準備
### Visual Studio Codeに拡張機能をインストールする
2つの拡張機能をインストールします。  
![data_workspace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/4a57c98a-3e79-7e84-04a1-556d2facd108.png)
![dbproject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/78c5fe52-fbb6-17a7-f42d-7bcab89dc73a.png)

```:拡張機能の情報
名前: Data Workspace
ID: ms-mssql.data-workspace-vscode
説明: Additional common functionality for database projects
バージョン: 0.5.0
パブリッシャー: Microsoft

名前: SQL Database Projects
ID: ms-mssql.sql-database-projects-vscode
説明: Enables users to develop and publish database schemas for MSSQL Databases
バージョン: 1.4.3
パブリッシャー: Microsoft
```
[Data Workspaceのリンク](https://marketplace.visualstudio.com/items?itemName=ms-mssql.data-workspace-vscode)  
[SQL Database Projectsのリンク](https://marketplace.visualstudio.com/items?itemName=ms-mssql.sql-database-projects-vscode)  
### プロジェクトファイルを作成する
1. リポジトリルートに`DB`ディレクトリを作ります
2. 画面左側のDatabase Projectsをクリックします  
![create_dbp_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/2eaeb879-218f-0336-489d-68688380dc42.png)
3. Create newをクリックし、Azure SQL Databaseを選択します  
![create_dbp_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/742d06a4-d3b6-b9a2-f0b7-3571ee11140d.png)
4. プロジェクト名を入力します
5. プロジェクトファイルの場所を指定します。`リポジトリルート>DB`ディレクトリを指定してください
6. SDK スタイルのプロジェクトファイルを使うかを聞かれるので、`yes`を選択します

作成すると、この画面になります(プロジェクト名をbasic_dbとしました)。  
![create_dbp_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/61706c8f-33ea-aed5-cab8-06e7fae2f812.png) 
この画面で`basic_db`を右クリックし、テーブル定義のファイルなどを追加・記述していく開発フローとなります。  
また、エクスプローラーの画面を見ると、このような結果となっています。  
![create_dbp_result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/b387c319-b626-f896-1309-a729ee485caa.png)

## Docker上でSQL Serverを動かす
Azure SQL DatabaseはSQL Serverとの互換性が高いので、ローカル環境ではSQL ServerをDocker上に構築します。  
SQL ServerのDockerイメージは`mcr.microsoft.com/mssql/server:2022-latest`のようにMicrosoft公式のイメージがあるため、これを利用します。  
[dockerhubへのリンク](https://hub.docker.com/r/microsoft/mssql-server)
### Dockerfileを書く
直接上記イメージを利用してもいいですが、今回はコンテナの初回起動時のみ`.sqlproj`をビルドし、SQL Serverへパブリッシュする仕組みを構築します。 
`Dockerfile`をプロジェクト名ディレクトリに作成します(`dockerfile`、`DockerFile`等ファイル名の誤字に注意)。  
![create_db_dockerfile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/92f2c875-606a-0e36-7bae-5ea7b011ba7f.png)

```dockerfile:Dockerfile
FROM mcr.microsoft.com/mssql/server:2022-latest

# ROOT権限にする
USER root

# dotnetをインストールし、sqlprojをビルド可能にする
RUN apt-get update && apt-get install -y dotnet-sdk-8.0
RUN dotnet tool install -g microsoft.sqlpackage

# sqlpackageへのパスを通す
ENV PATH="${PATH}:/root/.dotnet/tools"

# 実行権限を付与
RUN chmod +x /root/.dotnet/tools/sqlpackage

# init.shをコピー
COPY init.sh .

# 初期化スクリプト
ENTRYPOINT ["/bin/bash", "init.sh"]
```

マルチステージビルド等を活用すればイメージのサイズを抑えられそうですが、このイメージは開発環境でしか使わないので、この構成で進めます。

#### 解説
まず、.NET 8.0をインストールし、sqlpackageツールをインストールします。
最後にパスや権限を通し、後述する初期化スクリプトから利用できるようにします。

### 初期化スクリプトを記述する
続いて、初期化用のシェルスクリプトを記述します。  
`init.sh`を`Dockerfile`と同じ階層に作成します。  
![create_db_init.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/62a3cb46-0e23-e8bc-c51b-973148ec59f0.png)

大まかな処理の流れを示します。
1. 環境変数が登録されているかの確認を行います
2. SQL Serverを起動します
3. SQL Serverの起動を制限時間付きで待ちます
4. 起動完了後、`.sqlproj`をビルド・パブリッシュします

```bash:init.sh
#!/bin/bash

# エラーが発生したら即座に終了する
set -e

# 環境変数のチェック
required_vars=("MSSQL_SA_PASSWORD" "PROJECT_PATH" "DACPAC_PATH")
for var in "${required_vars[@]}"; do
    if [ -z "${!var}" ]; then
        echo "エラー: ${var} が設定されていません。"
        exit 1
    fi
done

wait_mssql() {
    echo "DBSを起動します(最低10秒待機します)。"
    sleep 10

    timelimit=30
    for i in $(seq 1 "${timelimit}"); do
        if version=$(/opt/mssql-tools18/bin/sqlcmd -S localhost,1433 -U sa -P "${MSSQL_SA_PASSWORD}" -Q "SELECT @@VERSION" -C 2>/dev/null); then
            echo "起動完了しました。バージョン情報: ${version}"
            return 0
        else
            echo "起動待機中... (${i}/${timelimit})"
            sleep 1
        fi
    done

    echo "タイムアウト: DBSの起動に失敗しました。"
    return 1
}

execute_ddl() {
    echo "sqlプロジェクトをビルドします。"
    if ! dotnet build "${PROJECT_PATH}"; then
        echo "ビルドに失敗しました。"
        return 1
    fi

    echo "プロジェクトビルド内容をmssqlにパブリッシュします。"
    if ! sqlpackage /Action:Publish \
        /SourceFile:"${DACPAC_PATH}" \
        /TargetServerName:"localhost,1433" \
        /TargetUser:"sa" \
        /TargetPassword:"${MSSQL_SA_PASSWORD}" \
        /TargetDatabaseName:"${DB_NAME}" \
        /TargetTrustServerCertificate:"True"; then
        echo "パブリッシュに失敗しました。"
        return 1
    fi

    echo "DDLの実行が完了しました。"
    return 0
}

main() {
    if ! wait_mssql; then
        exit 1
    fi

    initialized="/init"
    if [ ! -e "${initialized}" ]; then
        if execute_ddl; then
            touch "${initialized}"
            echo "初期化が完了しました。"
        else
            echo "初期化に失敗しました。"
            exit 1
        fi
    else
        echo "既に初期化済みです。"
    fi
}

main &
exec /opt/mssql/bin/sqlservr --accept-eula
```
#### 解説
main関数をバックグラウンドで実行しながら、SQL Serverを起動しているのがポイントです。  
DockerのコンテナはPIDが1のプロセスが終了すると終了してしまうので、main関数はバックグラウンドで動かす必要があります。  
```bash:SQL Server実行の注意点
main &
exec /opt/mssql/bin/sqlservr --accept-eula
```
また、初回起動時の判定方法は、初回起動時に空ファイルを生成し、そのファイルが存在するかをチェックする方法にしています。
```bash:初回起動か判定する
initialized="/init"
if [ ! -e "${initialized}" ]; then
    if execute_ddl; then
        touch "${initialized}"
        echo "初期化が完了しました。"
    else
        echo "初期化に失敗しました。"
        exit 1
    fi
else
    echo "既に初期化済みです。"
fi
```

環境変数については後述する`docker-compose.yml`に記述します。

### docker-compose.ymlを記述する
リポジトリルートに`docker-compose.yml`を作成します。  
![create_db_dc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/afc5a202-1b28-b035-b241-e39838555f58.png)

```yml:docker-compose.yml
services:
  db:
    build:
      context: ./DB
    # 自動で起動する
    restart: always
    ports:
      - "1433:1433"
    volumes:
      - ./DB/{SQLプロジェクト名}:/src
    environment:
      - MSSQL_SA_PASSWORD={管理者アカウントのパスワード}
      - DB_NAME={DBの名前}
      - PROJECT_PATH=/src/{SQLプロジェクト名}.sqlproj
      - DACPAC_PATH=/src/bin/debug/{SQLプロジェクト名}.dacpac
```
`docker-compose`はどの場所の`Dockerfile`をビルドするかや、環境変数の設定など細かい設定をしているだけです。  
`volumes`ではデータベースプロジェクトソースを即反映できるように指定しています。`Dockefile`でコピーするという方法もありますが、
この方法だとソースを変更するたびにイメージをビルドする必要が出てくるため、コンテナ内のディレクトリにマウントする方法を取りました。  
`init.sh`も頻繁に変更する予定があればボリュームを使う方法の方が良いです。  

ここまで出来たら、リポジトリルートで`docker compose up -d db`のようにコンテナを起動できるようになります。  

### なぜDockerfileを自分で書くのか運用を踏まえて解説
コンテナ起動する度に手動で`.sqlproj`をビルド・パブリッシュするのは面倒だからです。
この方法なら、コンテナ起動だけでデータベースを利用可能です。
実際の運用方法をケースごとに説明します。

#### DBを起動したい
```bash
cd <docker-compose.ymlがあるディレクトリ>
docker compose up -d db
```

#### DBを止めたい
```bash
cd <docker-compose.ymlがあるディレクトリ>
docker compose stop db
```

#### DBをリセットしたい
コンテナを削除してから再起動することでDBのリセットができます。
```bash
cd <docker-compose.ymlがあるディレクトリ>
docker compose rm -sfv db
docker compose up -d db
```

この3操作だけでローカル上のDBを管理できるので、シンプルな運用ができると思います。

## テーブル設計
開発環境が整ったので、ユーザ情報を管理するためのテーブルを設計します。ER図を示します。
![db_er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/7f99d601-f838-bf3b-b4ab-cfdca7e7aa4d.png)
  
ユーザ情報とログイン等認証に関する情報を分けて設計しています。このように分ける理由は、認証方法は頻繁に変わるからです。  
今回はパスワードとログインIDによる認証ですが、2要素認証や、ソーシャルログイン、IdPを利用した認証など様々な方法があります。これらへの対応を容易に行えるよう切り分けています。
ユーザIDはプログラム側から生成したいため、`uniqueidentifier`型にしました。C#からは`Guid`構造体として利用できます。  
もし`user_authentications`のIDもプログラム側から生成したい場合は、同様に`uniqueidentifier`型を利用できます。

### ここでの命名ルール
- テーブル名は複数形
- 主キーは`id`で統一
- 外部キーは`外部テーブル名_id`

この辺は個人の好みなので自由に決めていいです。

## T-SQLを記述する
### スキーマを定義
SQL ServerはMySQL等と異なりスキーマを持つため、スキーマの定義から行います。
まず、`Database Projects`タブに移動し、プロジェクト名を右クリック->`Add Item...`をクリックします。  
![create_schema.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/7ad529f4-144b-181c-ed6b-99ef4e01d10a.png)
  
Scriptをクリックし、ファイル名の入力を求められたら`schemata`と入力します(自由に決めてOK)。  
ファイルが作成されたら、そこにスキーマを定義します(スキーマ名は自由に決めてOK)。

```sql:schemata.sql
CREATE SCHEMA [app];
GO
```

この流れでT-SQLを記述します。

### テーブルを定義
まず、以下のようなファイル構成としました。  
![tables.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/7f403480-6ed4-5e8a-e36f-fdec3fefdfd0.png)
  
テーブルごとにファイルを作り、`tables`ディレクトリ配下に保存する構成です。

次に、ER図の通りに`CREATE TABLE`します。

#### users
```sql:users.sql
CREATE TABLE [app].[users]
(
    [id] uniqueidentifier PRIMARY KEY NOT NULL,
    [name] nvarchar(50) NOT NULL,
    [registered_at] datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```

#### user_authentications
```sql:user_authentications.sql
CREATE TABLE [app].[user_authentications]
(
    [id] int PRIMARY KEY IDENTITY(1,1) NOT NULL,
    [user_id] uniqueidentifier NOT NULL,
    [sign_in_id] nvarchar(100) UNIQUE NOT NULL,
    [password] nvarchar(128) NOT NULL,
    [refresh_token] nvarchar(128) DEFAULT NULL,
    [refresh_token_expiration] datetime2 DEFAULT NULL,
    CONSTRAINT [fk_user_authentications_users] FOREIGN KEY([user_id]) 
        REFERENCES [app].[users]([id]) ON DELETE CASCADE
);
```

## データベースを初期化・確認
### データベースを初期化
テーブルの定義ができたので、実行します。先述した通り、`docker compose up -d db`することで自動実行されます。  
もし既に実行している場合は`docker compose rm -sfv db`をしてから実行してください。  
成功すると、次のようなログが流れます(`docker compose logs -f db`でログを確認できます)。
![create_table_log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/ed8e04e6-4a7a-1df3-f115-3f1109e7ed12.png)
 

### DBに接続して確認する
`SQL Server`タブに移動し、接続の追加をクリックします。クリックするとSQL Serverへ接続するための画面が出てくるので、画像の通りに入力します。  
![create_connection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/31c49d74-9d27-3a2c-848a-cce695f363fa.png)
  
接続に成功すると、テーブル等を見ることができるようになるので、正しく作成できているかを確認します。  
![created_connection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/5f136138-3aed-5f3e-741d-42ab61671512.png)

以上でデータベースの準備が完了しました。

# REST APIサーバ環境構築・開発
データベースの準備が整ったので、サーバの開発を開始します。
## 環境構築
### プロジェクトの準備
#### ソリューション・プロジェクト作成
リポジトリルートへ移動し、次のスクリプトを実行します。
```bash:ソリューション、プロジェクト作成スクリプト
# サーバ開発用ディレクトリを作成
mkdir ./API
cd ./API
# ソリューションを作成
dotnet new sln -n API
# サーバプロジェクト用ディレクトリを作成
mkdir ./Server
# プロジェクトを作成し、ソリューションにプロジェクトを追加
dotnet new webapi -f "net8.0" -lang "C#" -o "./Server"
dotnet sln "API.sln" add "./Server/Server.csproj"
cd ..
```
動作内容はコメントアウトの通りです。実行すると、次の画像の通りとなります。  
![create_api_proj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/db30fd50-1431-bf95-4338-0e3dadcaf0de.png)  

#### プロジェクトにライブラリをインストールする
`Microsoft.Data.SqlClient`と`Dapper`、`Microsoft.AspNetCore.Authentication.JwtBearer`をインストールします。
```bash:ライブラリをインストール
dotnet add ./API/Server/Server.csproj package Microsoft.Data.SqlClient --version 5.2.2
dotnet add ./API/Server/Server.csproj package Dapper --version 2.1.35
dotnet add ./API/Server/Server.csproj package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.6
```

#### 不要なファイル・実装を削除

次に、ソリューションエクスプローラーでソリューションを開きます。  
![open_sln.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/f38582b3-f928-ec1c-af25-fed9895e88cd.png)
  
`WeatherForecastController.cs`、`Server.http`、`WeatherForecast.cs`は削除します。
また、`Properties/launchSettings.json`内を書き換えます。
```json:launchSettings.json
{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://0.0.0.0:8080",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
  }
}
```
ホストマシン上からアクセスするために`applicationUrl`のIPアドレスを`0.0.0.0`にしています。

さらに、`appsettings.json`から`"AllowedHosts": "*"`の記述を削除します。
```json:appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
```

#### ビルドが通るか確認

リポジトリルートで`dotnet build ./API/API.sln`を実行すると、コンパイルに成功します。プロジェクトの準備はここで一区切りつけます。

### Dockerfileを準備
API用の`Dockerfile`はデプロイ時も利用するため、マルチステージビルドでイメージサイズを削減します。
APIディレクトリ(ソリューションファイルと同じ階層)に`Dockerfile`を作成し、次の通りに記述します。  
[.NET SDKのイメージ](https://hub.docker.com/r/microsoft/dotnet-sdk)
[ASP.NET Core Runtimeのイメージ](https://hub.docker.com/r/microsoft/dotnet-aspnet)

```dockerfile:Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /api

# すべてコピーする
COPY . ./

# リストアする
RUN dotnet restore
# パブリッシュする
RUN dotnet publish -c Release -o out

# ランタイムイメージを利用し、実行環境を準備する。
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /api
COPY --from=build-env /api/out .
# http通信を行い、ポートを設定
ENV ASPNETCORE_URLS="http://+:8080;"
CMD ["dotnet", "Server.dll"]
```

#### 解説
dotnetのビルド、実行環境の設定という2層構造のビルドとなっています。最終ステージではasp.netのランタイムイメージを利用するため、イメージサイズを削減できます。  

### サーバ起動用スクリプトを記述
APIディレクトリ(ソリューションファイルと同じ階層)に`api_run_dev.sh`を作成し、次の通りに記述します。
```bash:api_run_dev.sh
#!/bin/bash

# デバッガを使用する場合はサーバを起動しない。
if [ "${USE_DEBUGGER}" = true ]; then
    echo リモートエクスプローラからコンテナに接続し、デバッグしてください。
    # bashに入って終了しないようにする
    bash
else
    dotnet watch run --project /api_src/Server/Server.csproj --launch-profile http
fi
```



### docker-compose.ymlに追記する
dbを作成した際に作成した`docker-compose.yml`へ追記します。
```yml:docker-compose.yml
  api:
    build:
      context: ./API
      # dotnet watchはdotnet/sdk:8.0に含まれるので、ビルド環境ステージのイメージを利用する
      target: build-env
    tty: true
    volumes:
      - ./API:/api_src/
      # objは名前付きボリュームにする。こうすることでコンテナ内でdotnet restoreする際にこっちに影響を与えなくなる。
      - ignores:/api_src/Server/obj/
      # devcontainer用に、VSCodeの拡張機能を保持するようにする。
      # https://zenn.dev/greendrop/articles/8bf88aad068f7d#docker-volume%E3%81%A7%E6%B0%B8%E7%B6%9A%E5%8C%96%E3%81%97%E3%80%81%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%82%92%E4%BF%9D%E6%8C%81
      - VSCode-extentions-api:/root/.vscode-server
    ports:
      - "8080:8080"
    depends_on:
      - "db"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - DOTNET_WATCH_RESTART_ON_RUDE_EDIT=true
      - USE_DEBUGGER=true
    command: bash /api_src/api_run_dev.sh
volumes:
  ignores:
  VSCode-extentions-api:
```

いくつかポイントがあるので解説します。
#### buildでtargetを指定する理由
`dotnet watch`を利用したいからです。`dotnet watch`を利用するには`.NET SDK`が必要なので、`build-env`を利用します。

#### ttyの設定
`api_run_dev.sh`でデバッガを利用する場合に`bash`を実行していますが、ttyを`true`に設定することでコンテナの終了を防ぐことができます。

#### ボリューム
ソースコードの変更がコンテナ内へ即時反映させるために`api_src`ソースをディレクトリにマウントしています。
しかしこれだけでは問題があり、コンテナ内でビルドすると、objディレクトリ内が書き換わってしまいます。それを防ぐために名前付きボリュームへ隔離します。  

:::note info
objディレクトリが書き換わるとファイル内のパスがDockerコンテナ内のパスに置き換わるため、インストールした外部ライブラリの入力補完が効かなくなります。
:::

#### 環境変数
実行環境を開発環境とし、ホットリロードできない変更がある場合は自動で再起動するようにします。

### Docker上でサーバを起動できるか確認する
リポジトリルートで`docker compose up -d api`を実行すると、コンテナが起動します。
ログを`docker compose logs -f api`で表示し、起動に成功している場合は次のようなログとなります。
```
basicrestapiserver-api-1  | dotnet watch ⌚ Polling file watcher is enabled
basicrestapiserver-api-1  | dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
basicrestapiserver-api-1  |   💡 Press "Ctrl + R" to restart.
basicrestapiserver-api-1  | dotnet watch 🔧 Building...
basicrestapiserver-api-1  |   Determining projects to restore...
basicrestapiserver-api-1  |   All projects are up-to-date for restore.
basicrestapiserver-api-1  |   Server -> /api_src/Server/bin/Debug/net8.0/Server.dll
basicrestapiserver-api-1  | dotnet watch 🚀 Started
basicrestapiserver-api-1  | info: Microsoft.Hosting.Lifetime[14]
basicrestapiserver-api-1  |       Now listening on: http://[::]:8080
basicrestapiserver-api-1  | info: Microsoft.Hosting.Lifetime[0]
basicrestapiserver-api-1  |       Application started. Press Ctrl+C to shut down.
basicrestapiserver-api-1  | info: Microsoft.Hosting.Lifetime[0]
basicrestapiserver-api-1  |       Hosting environment: Development
basicrestapiserver-api-1  | info: Microsoft.Hosting.Lifetime[0]
basicrestapiserver-api-1  |       Content root path: /api_src/Server
basicrestapiserver-api-1  | warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
basicrestapiserver-api-1  |       Failed to determine the https port for redirect.
```

正しく起動できていれば`http://localhost:8080/swagger/index.html`へアクセスすると`swagger`の画面が表示されます。  
![swagger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/548bd4fa-ce21-0d02-c2a3-86a5e0ce8674.png)
  
APIの動作確認もここで行うので、お気に入りやブックマークですぐにアクセスできるようにすると便利です。

### Visual Studio Codeからデバッグする方法
普段の開発では、ホットリロードだけで十分開発できますが、デバッガを利用したいときもあるのでその準備をします。まだデバッグ環境については模索中なので、もっと便利な方法があれば教えてほしいです。

#### Dev Containers拡張機能をインストールする
コンテナ内で作業をできるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/f53c16d7-bcd9-7868-0c88-c2397bcb0e80.png)
```:拡張機能の情報
名前: Dev Containers
ID: ms-vscode-remote.remote-containers
バージョン: 0.388.0
パブリッシャー: Microsoft
```
[拡張機能のリンク](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)

#### リモートエクスプローラーからコンテナへアタッチする
1. `docker-compose.yml`の環境変数`USE_DEBUGGER`を`true`に変更し、コンテナを再起動します

2. リモートエクスプローラーからapiのコンテナを新しいウインドウでアタッチします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/38127311-1f99-8153-5f5d-0fa420f04aa2.png)

3. `api_src`ディレクトリを開きます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/411d6f89-f514-b60f-7fb9-6be401e25c53.png)

#### コンテナ内に拡張機能をインストールする
![cs_dev_kit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/d2bc0711-6af4-bc8f-0bd8-8ae9e3272d6b.png)
  
```:拡張機能の情報
名前: C# Dev Kit
ID: ms-dotnettools.csdevkit
説明: Official C# extension from Microsoft
バージョン: 1.13.9
パブリッシャー: Microsoft
```
[拡張機能のリンク](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit)

コンテナ外でも利用したい場合はコンテナ外でもインストールしてください。

#### デバッグ用の設定
まず、コンテナにアタッチしたVisual Studio Codeで、`api_src`ディレクトリ配下に`.vscode/launch.json`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/804c0111-ab20-006a-80e1-a82458f0ecd8.png)

```json:launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "サーバをデバッグ",
            "type": "dotnet",
            "request": "launch",
            "projectPath": "${workspaceFolder}/Server/Server.csproj",
        }
    ]
}
```

次に、ホットリロードを有効にします。試験的機能なので、デフォルトでは無効になっています。
`/root/.vscode-server/data/Machine/settings.json`を開き、次の設定を追加します。
```json:settings.json
{
    "csharp.experimental.debug.hotReload": true
}
```

#### サーバを起動
実行とデバッグタブに移動し、再生ボタンをクリックするだけで起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/ffab0e6b-2246-6bff-12b7-b46383bc2854.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/dc258e8c-3652-bb23-986b-bb9479768419.png)

これでブレークポイントを設定し、デバッグできるようになります。
以上で開発環境の構築が完了しました。

# セキュリティについて

## SQLの文字列でフォーマット文字列を使う際は注意
INSERT文などでパラメータを与える際はプレースホルダを利用します。直接文字列を埋め込む場合はSQLインジェクション攻撃の注意が必要です。
```cs:SQLインジェクションの発生例
var userInput = "1;SELECT * FROM 認証情報"
var query = $"SELECT id FROM 認証情報 WHERE id = {userInput}"
```

この例では`SELECT * FROM 認証情報`も実行されてしまいます。

## パスワードはハッシュ化してからDBへ
パスワード流出時の被害を軽減するための対策法です。攻撃者は入手したパスワードを逆ハッシュ化する必要が生じるため、生のパスワードが流出するまでの時間を稼ぐ事ができます。ここでは、ハッシュ化する際にできる工夫について説明します。

### 複数回ハッシュ化する(ストレッチング)
パスワードのハッシュ化にかかる計算コストを大きくすることで、総当たり攻撃を困難にする目的があります。したがって、ストレッチング回数はコンピュータの性能が向上するほど多くした方が良いことになります。また、予め計算してあるハッシュ値とパスワードの対応表を元にパスワードを推測するレインボーテーブル攻撃の対策にもなります。

:::note info
今回作るサーバでは約30万回ハッシュ化していますが、ASP.NETでの規定では10万回ハッシュ化しているようです。

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount?view=aspnetcore-8.0
:::

### パスワードに文字列を付加してからハッシュ化する
パスワードに文字列を付加してからハッシュ化することでレインボーテーブル攻撃の対策を強化できます。
付加する文字列は推測が困難なものにします。この文字列のことを**ソルト**といいます。今回の実装では2種類のソルトを利用します。

#### ユーザ固有の情報を利用したソルト
ユーザごとに生成する文字列を利用したソルトです。今回はユーザ登録日時をソルトとして利用しますが、ユーザ登録時にソルト用のランダムな文字列を生成し、データベースへ保存する方法もあります。

#### データベース外に定義したソルト
データベースにソルトを保存すると、データベースが流出した場合ソルトも流出してしまいます。そこで、環境変数などデータベース外に別のソルトを用意し、これも付加して使うようにします。このようなソルトを**ペッパー**または**シークレットソルト**と言います。パスワードが流出する場合データベース内にあるソルトも流出している可能性が高いため、この対策はした方が良いと考えています。

#### フローの例
![refresh_token_flow.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/69117d71-22b7-d6e3-19ed-88b23ea8d6e7.png)

### ハッシュ関数の選択
ハッシュ関数にも種類があり、パスワードハッシュ化に特化したハッシュ関数があります。
- PBKDF2(Password-Based Key Derivation Function 2)
- bcrypt
- Argon2

実装例ではPBKDF2を利用したパスワードハッシュ化方法を紹介します。

## パスワードの情報はクライアントに渡さない
パスワード認証では、サインイン時にユーザが入力したパスワードをハッシュ化し、データベース内のパスワードと比較することで実現できます。パスワードをクライアントへ渡す必要はありません。

## アクセストークンの有効期限を短く設定する
JWTはデータベースに保存する必要なくユーザを識別、認証できるメリットがあります。しかし、サーバ側で無効化できないデメリットもあります。このデメリットを軽減するため有効期限を短くします。

### 有効期限を短くするとログインする頻度が多く不便になってしまう
トークンが切れる度にログインを求めると利便性の低下につながります。この問題を解決する方法には、**リフレッシュトークン**の発行があります。リフレッシュトークンとは**アクセストークンを再発行するためのトークン**です。リフレッシュトークンはアクセストークンより長い有効期限を持つ事がポイントです。アクセストークンより送信頻度が少ないため、アクセストークンの有効期限を長くする場合より、リスクが小さいです。

![refresh.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/ddf76a3c-dfff-22c0-9e64-e781293d37f7.png)
上図の方法ではデータベースを利用したリフレッシュトークン管理が必要になりますが、**リフレッシュトークンの検証頻度はアクセストークンの検証頻度より少ない**ので、アクセストークンをデータベース管理するよりデータベースへアクセスする頻度は少なく済みます。

:::note info
この記事で実装するAPIサーバは上図の流れでリフレッシュトークンを実装できるようにテーブル設計をしています。今回の記事では実装方法を示しませんが、図の流れ通りに実装すれば実現できます。
:::


# 設計
MVCパターンとオニオンアーキテクチャを拡張した設計にしています。依存関係の図を示します。
![server_design.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/2204889e-ba62-4004-f416-2b91608082fd.png)

## Server(名前空間のルート)
ここはサーバのエントリポイント、環境変数ヘアクセスする処理などサーバ全体の設定を扱う役割を持ちます。特にサーバのエントリポイントではサービスの登録などサーバの機能をセットアップする役割を持ちます。

## Models
ユーザ、サインインID、パスワードなどのモデルやビジネスロジックを実装します。今回はサインインIDの文字数など、ルールが存在します。これらのルールもモデル内に実装します。

## UseCases
ユースケース毎にメソッドを実装し、各ユースケースのメインルーチンの役割を持ちます。リポジトリや認証処理はインターフェイスを介して利用します。

## Databases
ここではモデルの永続化を行う処理を実装します。これらの処理は**Repository**と命名しています。ユースケースでトランザクションを張ることが多いですが、ユースケースのテストをしたい都合でトランザクションもここで張ります。

### Databases.DBClients
クエリを実行し、実際にデータベースへ問い合わせする役割を持ちます。リポジトリからのみ呼び出されます。
![リポジトリ関連 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/4277df81-9a02-2b08-38bd-2df8a48b2d72.png)

### メリット・デメリット
複雑な集計処理がないことを前提としている設計なので、メリット・デメリットがあります。

**メリット**
- 単純なクエリで実装できる
- 再利用が容易
- テーブルを参照している箇所の特定が容易

**デメリット**
- 問い合わせ回数が増加する
- トランザクションを張る回数が増え、デッドロックなどのリスクが高まる
- Repositoryがモデル詰め替え場所のような実装になる

実装例では偶然テーブル単位になっていますが、集約単位で実装します。


## Controllers
コントローラを実装します。ハンドラメソッドはユースケース単位で実装します。
処理内容は、
1. リクエストパラメータをユースケース呼び出し用に詰め替える
2. ユースケースを呼び出す
3. 結果を返す

程度のものとなるため、薄いレイヤとなります。「外部との窓口を作る役割」だけを持たせます。

## Authentications
パスワードのハッシュ化やアクセストークンを生成する処理を実装します。認証に関わるセキュリティ処理をする役割で取り扱い注意のため、独立した名前空間を与えています。

# 実装例
ソースコードのフォルダ構成は名前空間と同じ構成にしています。詳細はサンプルプログラムを参照してください。

## モデルを実装
必要なモデルを実装します。  

### UserName
```cs
using System.Diagnostics.CodeAnalysis;

namespace Server.Models.Users;
public record UserName
{
    private UserName(string value)
    {
        Value = value;
    }

    public static readonly UserName Empty = new("");

    public string Value { get; }

    /// <summary>
    /// ユーザ名の作成を試みる
    /// </summary>
    /// <param name="value">ユーザ名の文字列</param>
    /// <param name="userName">作成したユーザ名</param>
    /// <returns>成功: true 失敗: false</returns>
    public static bool TryCreate(string value, [NotNullWhen(true)] out UserName? userName)
    {
        // ルール：空値を許可しない かつ 50文字以内
        if (!string.IsNullOrWhiteSpace(value) && value.Length <= 50)
        {
            userName = new(value);
            return true;
        }

        userName = null;
        return false;
    }
}
```

値オブジェクトの実装にしました。C#で開発する場合は`record`を使うと自動でプロパティによる等価比較を行うため非常に便利です。
また、インスタンスの生成時は`TryCreate`メソッドを呼び出す必要があるように実装しています。  
このように実装することで、ユーザ名のルール「1文字以上50文字以内」を満たすオブジェクトのみ存在するようになります。

#### NotNullWhen属性とは
メソッドの戻り値によって`null`ではないことを保証するための属性です。今回の場合は、`userName`が`nullable`ですが、`true`が戻る場合は`null`ではないことが保証されるようになります。

### User
```cs
namespace Server.Models.Users;

public readonly record struct UserId(Guid Value)
{
    public static readonly UserId Empty = new(Guid.Empty);

    public static UserId CreateNew() => new(Guid.NewGuid());
}

public class User
{
    private User(UserId id, UserName name, DateTime registeredAt)
    {
        Id = id;
        Name = name;
        RegisteredAt = registeredAt;
    }

    public static readonly User Unknown = new(UserId.Empty, UserName.Empty, DateTime.MinValue);

    public UserId Id { get; }

    public UserName Name { get; }

    public DateTime RegisteredAt { get; }

    /// <summary>
    /// ユーザを作成する
    /// </summary>
    /// <param name="id">ユーザID</param>
    /// <param name="userName">ユーザ名</param>
    /// <param name="registeredAt">登録日時</param>
    /// <returns>作成したユーザ</returns>
    public static User Create(UserId id, UserName userName, DateTime registeredAt)
    {
        return new(id, userName, registeredAt);
    }

    public override bool Equals(object? obj)
    {
        if (obj is User user)
        {
            return user.Id == Id;
        }

        return false;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}
```

ユーザはエンティティなので、IDで等価比較するように実装します。

#### Guidの重複について
重複しない前提で実装しています。重複のチェックをしてもいいのですが、コストに対して得られるメリットが少ないです。  
**理由**  
- DB側で主キー制約をかけているため、重複してもデータの不整合は起こらない
- Guidが重複する確率は限りなく低い(.NET 8、9ではバージョン4 UUIDを生成している)  
2^128通りのIDが存在するうえに、暗号学的に安全な疑似乱数生成器を使用している

[.NET 8 Guid 構造体(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/dotnet/api/system.guid?view=net-8.0)  
[.NET 9 Guid 構造体(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/dotnet/api/system.guid?view=net-9.0)  

[.NET 8 Guid.NewGuid メソッド(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/dotnet/api/system.guid.newguid?view=net-8.0)  
[.NET 9 Guid.NewGuid メソッド(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/dotnet/api/system.guid.newguid?view=net-9.0)  

### SignInId
```cs
using System.Diagnostics.CodeAnalysis;

namespace Server.Models.UserAuthentications;
public record SignInId
{
    private SignInId(string value)
    {
        Value = value;
    }

    public string Value { get; }

    /// <summary>
    /// サインインIDの作成を試みる。
    /// </summary>
    /// <param name="value">サインインIDの文字列</param>
    /// <param name="signInId">作成したサインインID</param>
    /// <returns>成功: true 失敗: false</returns>
    public static bool TryCreate(string value, [NotNullWhen(true)] out SignInId? signInId)
    {
        // ルール：サインインIDは10文字以上100文字以下とする。
        if (10 <= value.Length && value.Length <= 100)
        {
            signInId = new(value);
            return true;
        }

        signInId = null;
        return false;
    }
}
```
サインインIDもユーザ名と同様の実装です。

### RawPassword
```cs
using System.Diagnostics.CodeAnalysis;

namespace Server.Models.UserAuthentications;
public record RawPassword
{
    private RawPassword(string value)
    {
        Value = value;
    }

    public string Value { get; }

    /// <summary>
    /// 生パスワードの作成を試みる
    /// </summary>
    /// <param name="value">生パスワードの文字列</param>
    /// <param name="rawPassword">作成した生パスワード</param>
    /// <returns>成功: true 失敗: false</returns>
    public static bool TryCreate(string value, [NotNullWhen(true)] out RawPassword? rawPassword)
    {
        // ルール：パスワードは10文字以上とする。
        if (10 <= value.Length)
        {
            rawPassword = new(value);
            return true;
        }

        rawPassword = null;
        return false;
    }
}
```

ハッシュ化前のパスワードを`RawPassword`と命名し、実装しました。実装内容は`UserName`や`SignInId`と同様です。


### IHasher
```cs
using Server.Models.Users;

namespace Server.Models.UserAuthentications;

public interface IHasher
{
    /// <summary>
    /// ハッシュ化されたパスワードを生成する。
    /// </summary>
    /// <param name="user">パスワードの所有者</param>
    /// <param name="rawPassword">パスワード</param>
    /// <returns>ハッシュ化されたパスワード</returns>
    public string Generate(User user, RawPassword rawPassword);
}
```

パスワードをハッシュ化するインターフェイスを定義します。`Models`名前空間でハッシュ化処理を利用する目的でここに置いています。

### HashedPassword
```cs
using Server.Models.Users;

namespace Server.Models.UserAuthentications;

public record HashedPassword
{
    private HashedPassword(string value)
    {
        Value = value;
    }

    public string Value { get; }

    /// <summary>
    /// ハッシュ化したパスワードの生成をする。
    /// </summary>
    /// <param name="hasher">ハッシュ化用オブジェクト</param>
    /// <param name="rawPassword">生パスワード</param>
    /// <param name="user">パスワードの所有者</param>
    /// <returns>ハッシュ化したパスワード</returns>
    public static HashedPassword HashFromRawPassword(IHasher hasher, RawPassword rawPassword, User user)
    {
        // 生パスワードをハッシュ化する。
        return new(hasher.Generate(user, rawPassword));
    }
}
```
ハッシュ化したパスワードを`HashedPassword`と命名し、実装しました。
`HashedPassword`も`HashFromRawPassword`メソッド経由で生成しますが、ハッシュ化処理と生パスワード、パスワードの所有者を受け取ります。

### StoredPassword
このオブジェクトもハッシュ化済みパスワードを表現しますが、`HashedPassword`レコードと区別します。データベースなどから取得した文字列でオブジェクトを生成する必要があるからです。`HashedPassword`レコードに文字列から生成するメソッドを実装すると、不正なオブジェクトの生成を許すことになります。ほかのモデル以上にパスワードにはリスクがあるため、このように厳密な設計としています。

```cs
using Server.Models.Users;

namespace Server.Models.UserAuthentications;
public record StoredPassword
{
    private StoredPassword(string value)
    {
        Value = value;
    }

    public string Value { get; }

    public static readonly StoredPassword Empty = new("");

    /// <summary>
    /// 保存済みパスワード文字列から生成する
    /// </summary>
    /// <param name="value">保存済みパスワード</param>
    /// <returns>HashedPasswordオブジェクト</returns>
    public static StoredPassword CreateFromString(string value)
    {
        return new(value);
    }

    /// <summary>
    /// 入力された生パスワードが正しいかを検証する。
    /// </summary>
    /// <param name="hasher">ハッシュ化用オブジェクト</param>
    /// <param name="inputRawPassword">入力された生パスワード</param>
    /// <param name="inputUser">入力者</param>
    /// <returns>正しい: true、不正: false</returns>
    public bool Verify(IHasher hasher, RawPassword inputRawPassword, User inputUser)
    {
        var inputHashedPassword = HashedPassword.HashFromRawPassword(hasher, inputRawPassword, inputUser);

        return Value == inputHashedPassword.Value;
    }
}
```

生パスワードを検証するメソッドを実装します。

## ユースケースを実装
### IUserRepository
```cs
using Server.Models.UserAuthentications;
using Server.Models.Users;

namespace Server.UseCases.Users;

public interface IUserRepository
{
    /// <summary>
    /// ユーザ作成を試みる。
    /// </summary>
    /// <param name="newUser">新規ユーザ</param>
    /// <param name="signInId">サインインID</param>
    /// <param name="hashedPassword">ハッシュ化済みパスワード</param>
    /// <returns>成功時: true, 失敗時: false</returns>
    Task<bool> TryCreateUserAsync(User newUser, SignInId signInId, HashedPassword hashedPassword);

    /// <summary>
    /// ユーザIDからユーザを探す
    /// </summary>
    /// <param name="targetUserId"></param>
    /// <returns>(成功したか、見つかったユーザ。失敗した場合はUnknownユーザ)</returns>
    Task<(bool ok, User foundUser)> TryFindUserByIdAsync(UserId targetUserId);
}
```

ユースケースからDBへ格納する処理を呼び出したいですが、ユースケースのテストを行う都合でインターフェイスを介します。

### ResultTypes
ユースケースの実行結果を簡単に返すために列挙型を定義します。
```cs
namespace Server.UseCases;

public enum ResultTypes
{
    /// <summary>
    /// ユースケースが成功した場合
    /// </summary>
    Success,
    /// <summary>
    /// ユースケースがユーザ入力値のバリデーション結果で失敗した場合
    /// </summary>
    ValidationError,
    /// <summary>
    /// ユースケースが内部エラーが原因で失敗した場合
    /// </summary>
    InternalError,
}

```

成功、ユーザ入力内容が原因のエラー、内部エラーに分類します。

### UserUseCase
```cs
using Server.Models.UserAuthentications;
using Server.Models.Users;
using Server.UseCases.UserAuthentications;

namespace Server.UseCases.Users;

public class UserUseCase(IUserRepository userRepository, IUserAuthenticationRepository authRepository, IHasher hasher)
{
    private readonly IUserRepository _userRepository = userRepository;

    private readonly IUserAuthenticationRepository _authRepository = authRepository;

    private readonly IHasher _hasher = hasher;

    public record RegisterValidationResult(bool UserNameOk, bool SignInIdOk, bool RawPasswordOk);
    /// <summary>
    /// ユーザ登録ユースケース
    /// </summary>
    /// <param name="userNameValue">ユーザ名</param>
    /// <param name="signInIdValue">サインインID</param>
    /// <param name="rawPasswordValue">生パスワード</param>
    /// <returns>(ユースケース実行結果、入力値バリデーション結果)</returns>
    public async Task<(ResultTypes resultTypes, RegisterValidationResult validationResult)> RegisterUserAsync(string userNameValue, string signInIdValue, string rawPasswordValue)
    {
        // 入力値のバリデーションを行う。
        if (!UserName.TryCreate(userNameValue, out var userName))
        {
            var validationResult = new RegisterValidationResult(
                false,
                SignInId.TryCreate(signInIdValue, out _),
                RawPassword.TryCreate(rawPasswordValue, out _)
            );
            return (ResultTypes.ValidationError, validationResult);
        }
        if (!SignInId.TryCreate(signInIdValue, out var signInId))
        {
            var validationResult = new RegisterValidationResult(
                true,
                SignInId.TryCreate(signInIdValue, out _),
                RawPassword.TryCreate(rawPasswordValue, out _)
            );
            return (ResultTypes.ValidationError, validationResult);
        }
        if (!RawPassword.TryCreate(rawPasswordValue, out var rawPassword))
        {
            var validationResult = new RegisterValidationResult(true, true, false);
            return (ResultTypes.ValidationError, validationResult);
        }

        // サインインIDが既に登録されているかをチェックする
        var (existsId, _, _) = await _authRepository.TryFindAuthenticationAsync(signInId);
        if (existsId)
        {
            var validationResult = new RegisterValidationResult(true, false, true);
            return (ResultTypes.ValidationError, validationResult);
        }

        // 以降の処理ではバリデーションに成功している。
        var validParamResult = new RegisterValidationResult(true, true, true);

        // ユーザを作成し、DBに格納する。
        var user = User.CreateNew(userName);
        var hashedPassword = HashedPassword.HashFromRawPassword(_hasher, rawPassword, user);
        var ok = await _userRepository.TryCreateUserAsync(user, signInId, hashedPassword);
        if (ok)
        {
            // 成功。
            return (ResultTypes.Success, validParamResult);
        }
        else
        {
            // DBに格納する処理で失敗した場合は内部エラーとして扱う。
            return (ResultTypes.InternalError, validParamResult);
        }
    }
}
```

ユーザ入力値のバリデーション後、詰め替えたモデルをリポジトリ経由でDBに格納しています。

### IAccessTokenGenerator
アクセストークンを生成する処理のインターフェイスを定義します。
```cs
using Server.Models.Users;

namespace Server.UseCases.UserAuthentications;

public interface IAccessTokenGenerator
{
    /// <summary>
    /// トークンを生成する。
    /// </summary>
    /// <param name="userId">ユーザID</param>
    /// <returns>トークン</returns>
    string Generate(UserId userId);
}
```

### IUserAuthenticationRepository
サインインIDからDBに保存されたユーザIDとハッシュ化済みパスワードを取得するメソッドを定義します。
`UserId userId, HashedPassword hashedPassword`の部分はレコードにしてそれを返した方がよさそうですが、
そこまで問題にならなそうなのでタプルで返しています。

```cs
using Server.Models.UserAuthentications;
using Server.Models.Users;

namespace Server.UseCases.UserAuthentications;
public interface IUserAuthenticationRepository
{
    /// <summary>
    /// サインインIDからユーザID、パスワードを探す
    /// </summary>
    /// <param name="signInId">サインインID</param>
    /// <returns>成功したか、見つけたユーザID、見つけたパスワード</returns>
    Task<(bool ok, UserId userId, StoredPassword storedPassword)> TryFindAuthenticationAsync(SignInId signInId);
}
```

### UserAuthenticationUseCase
```cs
using Server.Models.UserAuthentications;
using Server.UseCases.Users;

namespace Server.UseCases.UserAuthentications;

public class UserAuthenticationUseCase(IHasher hasher, IAccessTokenGenerator tokenGenerator, IUserAuthenticationRepository authRepository, IUserRepository userRepository)
{
    private readonly IUserAuthenticationRepository _authRepository = authRepository;

    private readonly IUserRepository _userRepository = userRepository;

    private readonly IHasher _hasher = hasher;

    private readonly IAccessTokenGenerator _tokenGenerator = tokenGenerator;

    /// <summary>
    /// サインインのユースケース
    /// </summary>
    /// <param name="signInIdString">サインインIDの文字列</param>
    /// <param name="rawPasswordString">ユーザが入力した生パスワード文字列</param>
    /// <returns>(ユースケース実行結果、アクセストークン文字列)</returns>
    public async Task<(ResultTypes result, string accessToken)> SignInAsync(string signInIdString, string rawPasswordString)
    {
        // ユーザ入力の文字列からサインインID、生パスワードオブジェクトを生成する
        if (!SignInId.TryCreate(signInIdString, out var signInId) || !RawPassword.TryCreate(rawPasswordString, out var rawPassword))
        {
            return (ResultTypes.ValidationError, "");
        }

        // サインインIDから登録されたユーザID、ハッシュ化済みパスワードを取得す
        // ここで失敗する場合はどちらかというとユーザ入力が原因であることが多いため、バリデーションエラーとする
        var (isFoundAuthentication, userId, storedPassword) = await _authRepository.TryFindAuthenticationAsync(signInId);
        if (!isFoundAuthentication)
        {
            return (ResultTypes.ValidationError, "");
        }

        // ユーザIDからユーザを取得する。ここで失敗した場合は確実にサーバ側エラー
        var (isFoundUser, user) = await _userRepository.TryFindUserByIdAsync(userId);
        if (!isFoundUser)
        {
            return (ResultTypes.InternalError, "");
        }

        // パスワードの検証を行い、成功した場合はアクセストークンを返却する
        if (storedPassword.Verify(_hasher, rawPassword, user))
        {
            var accessToken = _tokenGenerator.Generate(userId);
            return (ResultTypes.Success, accessToken);
        }

        return (ResultTypes.ValidationError, "");
    }
}
```

## データベースにアクセスする処理
### UserAuthenticationsClient
データベースにアクセスし、読み書きする機能を実装します。
`record`を使うと簡単にイミュータブルなモデルを定義できるので引数などで活用していきます。

```cs
using System.Data.Common;
using Dapper;
using Microsoft.Data.SqlClient;

namespace Server.Databases.DBClients;
public class UserAuthenticationsClient
{
    public record InsertParam(Guid UserId, string SignInId, string HashedPassword);
    /// <summary>
    /// ユーザ認証情報を挿入する
    /// </summary>
    /// <param name="connection">コネクション</param>
    /// <param name="tx">トランザクション</param>
    /// <param name="param">挿入用パラメタ</param>
    /// <returns>タスク</returns>
    public async Task InsertAsync(SqlConnection connection, DbTransaction tx, InsertParam param)
    {
        // 認証情報を挿入する
        var authenticationParam = new
        {
            param.UserId,
            param.SignInId,
            param.HashedPassword,
        };
        var authenticationQuery = $"""
                INSERT INTO [app].[user_authentications] VALUES (
                    @{nameof(authenticationParam.UserId)},
                    @{nameof(authenticationParam.SignInId)},
                    @{nameof(authenticationParam.HashedPassword)},
                    default,
                    default
                );
            """;
        await connection.ExecuteAsync(authenticationQuery, authenticationParam, tx);
    }

    public record UserAuthenticationRow(int Id, Guid UserId, string SignInId, string Password, string RefreshToken, DateTime RefreshTokenExpiration);
    /// <summary>
    /// サインインIDをキーとして読む
    /// </summary>
    /// <param name="connection">コネクション</param>
    /// <param name="signInId">キーとなるサインインID</param>
    /// <returns>見つかった認証情報</returns>
    public async Task<UserAuthenticationRow> ReadBySignInIdAsync(SqlConnection connection, string signInId)
    {
        var param = new
        {
            signInId
        };
        var query = $"""
                SELECT
                    id,
                    user_id,
                    sign_in_id,
                    password,
                    refresh_token,
                    refresh_token_expiration
                FROM
                    [app].[user_authentications] user_authentications
                WHERE
                    user_authentications.sign_in_id = @{nameof(param.signInId)}
            """;

        var row = await connection.QueryFirstAsync<UserAuthenticationRow>(query, param);

        return row;
    }
}
```

### UsersClient
ユーザ挿入処理も同様に実装します。

```cs
using System.Data.Common;
using Dapper;
using Microsoft.Data.SqlClient;

namespace Server.Databases.DBClients;
public class UsersClient
{
    public record UserRow(Guid Id, string Name, DateTime RegisteredAt);
    /// <summary>
    /// ユーザIDをキーとしてユーザを読む
    /// </summary>
    /// <param name="connection">コネクション</param>
    /// <param name="userId">キーとするユーザID</param>
    /// <returns>読んだユーザ</returns>
    public async Task<UserRow> ReadUserByIdAsync(SqlConnection connection, Guid userId)
    {
        var param = new
        {
            userId,
        };

        var query = $"""
                SELECT
                    id,
                    name,
                    registered_at
                FROM
                    [app].[users] users
                WHERE
                    users.id = @{nameof(param.userId)}
            """;

        var userRow = await connection.QueryFirstAsync<UserRow>(query, param);

        return userRow;
    }

    public record InsertUserParam(Guid UserId, string Name, DateTime RegisteredAt);
    /// <summary>
    /// ユーザ情報を挿入する
    /// </summary>
    /// <param name="connection">コネクション</param>
    /// <param name="tx">トランザクション</param>
    /// <param name="param">挿入用パラメタ</param>
    /// <returns>タスク</returns>
    public async Task InsertAsync(SqlConnection connection, DbTransaction tx, InsertUserParam param)
    {
        // ユーザデータの挿入を行う。
        var userParam = new
        {
            param.UserId,
            param.Name,
            param.RegisteredAt
        };
        var userQuery = $"""
                INSERT INTO [app].[users] VALUES (
                    @{nameof(userParam.UserId)},
                    @{nameof(userParam.Name)},
                    @{nameof(userParam.RegisteredAt)}
                );
            """;
        await connection.ExecuteAsync(userQuery, userParam, tx);
    }
}

```

### UserRepository
実装したDBClientを利用した処理を記述します。

```cs
using Microsoft.Data.SqlClient;
using Server.Databases.DBClients;
using Server.Models.UserAuthentications;
using Server.Models.Users;
using Server.UseCases.Users;

namespace Server.Databases;

public class UserRepository(ILogger<UserRepository> logger, string connectionString) : IUserRepository
{
    private readonly UsersClient _userClient = new();

    private readonly ILogger<UserRepository> _logger = logger;

    private readonly UserAuthenticationsClient _authenticationsClient = new();

    /// <summary>
    /// ユーザ作成を試みる。
    /// </summary>
    /// <param name="newUser">新規ユーザ</param>
    /// <param name="signInId">サインインID</param>
    /// <param name="hashedPassword">ハッシュ化済みパスワード</param>
    /// <returns>成功時: true, 失敗時: false</returns>
    public async Task<bool> TryCreateUserAsync(User newUser, SignInId signInId, HashedPassword hashedPassword)
    {
        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();

        using var tx = await connection.BeginTransactionAsync();
        try
        {
            // ユーザを挿入する。
            var insertUserParam = new UsersClient.InsertUserParam(newUser.Id.Value, newUser.Name.Value, newUser.RegisteredAt);
            await _userClient.InsertAsync(connection, tx, insertUserParam);

            // 認証用情報を挿入する。
            var insertAuthParam = new UserAuthenticationsClient.InsertParam(newUser.Id.Value, signInId.Value, hashedPassword.Value);
            await _authenticationsClient.InsertAsync(connection, tx, insertAuthParam);

            // コミットする
            await tx.CommitAsync();
            return true;
        }
        catch (Exception exception)
        {
            // 例外を出力し、ロールバックする。
            _logger.LogError(exception, "ユーザ作成時にエラーが発生しました。");
            await tx.RollbackAsync();
            return false;
        }
    }

    /// <summary>
    /// ユーザIDからユーザを探す
    /// </summary>
    /// <param name="targetUserId">見つける対象のユーザID</param>
    /// <returns>(成功したか、見つかったユーザ。失敗した場合はUnknownユーザ)</returns>
    public async Task<(bool ok, User foundUser)> TryFindUserByIdAsync(UserId targetUserId)
    {
        using var connection = new SqlConnection(connectionString);
        try
        {
            var userRow = await _userClient.ReadUserByIdAsync(connection, targetUserId.Value);
            _ = UserName.TryCreate(userRow.Name, out var userName);
            var user = User.Create(targetUserId, userName!, userRow.RegisteredAt);
            return (true, user);
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "ユーザ取得時にエラーが発生しました。");
            return (false, User.Unknown);
        }
    }
}

```

### UserAuthenticationRepository
こちらも同様に実装します。
```cs
using Microsoft.Data.SqlClient;
using Server.Databases.DBClients;
using Server.Models.UserAuthentications;
using Server.Models.Users;
using Server.UseCases.UserAuthentications;

namespace Server.Databases;

public class UserAuthenticationRepository(ILogger<UserAuthenticationRepository> logger, string connectionString) : IUserAuthenticationRepository
{
    private readonly UserAuthenticationsClient _authenticationsClient = new();

    private readonly ILogger<UserAuthenticationRepository> _logger = logger;

    /// <summary>
    /// サインインIDからユーザID、パスワードを探す
    /// </summary>
    /// <param name="signInId">サインインID</param>
    /// <returns>成功したか、見つけたユーザID、見つけたパスワード</returns>
    public async Task<(bool ok, UserId userId, StoredPassword storedPassword)> TryFindAuthenticationAsync(SignInId signInId)
    {
        using var connection = new SqlConnection(connectionString);

        try
        {
            var authRow = await _authenticationsClient.ReadBySignInIdAsync(connection, signInId.Value);
            var hashedPassword = StoredPassword.CreateFromString(authRow.Password);
            var userId = new UserId(authRow.UserId);
            return (true, userId, hashedPassword);
        }
        catch (Exception exception)
        {
            // 例外を出力する。
            _logger.LogError(exception, "ユーザID・パスワードの取得に失敗しました。");
            return (false, UserId.Empty, StoredPassword.Empty);
        }
    }
}
```

## パスワードハッシュ化処理を実装
### HasherByPBKDF2
.NET 8では特にライブラリをインストールせずに`PBKDF2`を利用できます。パスワードのハッシュ化に特化しているハッシュ関数なので、活用します。  
[Rfc2898DeriveBytes クラス(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.rfc2898derivebytes?view=net-8.0)
```cs
using Server.Models.UserAuthentications;
using Server.Models.Users;
using System.Security.Cryptography;
using System.Text;

namespace Server.Authentications;

public class HasherByPBKDF2(string pepper) : IHasher
{
    /// <summary>
    /// ペッパー
    /// </summary>
    private readonly string _pepper = pepper;
    /// <summary>
    /// ストレッチング回数
    /// </summary>
    private readonly static int _iterations = 310000;

    /// <summary>
    /// 生パスワードの作成を試みる
    /// </summary>
    /// <param name="value">生パスワードの文字列</param>
    /// <param name="rawPassword">作成した生パスワード</param>
    /// <returns>成功: true 失敗: false</returns>
    public string Generate(User user, RawPassword rawPassword)
    {
        // ユーザ登録日をソルトとする
        var saltBytes = Encoding.UTF8.GetBytes(user.RegisteredAt.ToString("yyyyMMddHHmmss"));
        var pepperBytes = Encoding.UTF8.GetBytes(_pepper);

        using var pbkdf2 = new Rfc2898DeriveBytes(
            Encoding.UTF8.GetBytes(rawPassword.Value),
            // ソルトとペッパーを結合して渡す
            [.. saltBytes, .. pepperBytes],
            _iterations,
            HashAlgorithmName.SHA512);

        // 8 * 64 = 512 ビットのハッシュ値を取得する
        var hash = pbkdf2.GetBytes(64);

        return Convert.ToBase64String(hash);
    }
}
```

31万回ストレッチングし、ソルト、ペッパーを利用して`SHA-512`ハッシュアルゴリズムでパスワードをハッシュ化します。  
`Rfc2898DeriveBytes`のコンストラクタ引数ではソルトのみ受け付けているので、`[.. saltBytes, .. pepperBytes]`でソルトとペッパーを結合して渡しています。
この構文は`C# 12`でコレクション式が実装されたタイミングで実装されましたが、このような引数を渡す場面で非常に便利です。  

## アクセストークン生成処理を実装
アクセストークンの生成処理を実装します。今回はJWT認証を利用するため、JWTを生成する処理となります。
### AccessTokenGenerator
```cs
using Microsoft.IdentityModel.Tokens;
using Server.Models.Users;
using Server.UseCases.UserAuthentications;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;

namespace Server.Authentications;
public class AccessTokenGenerator(string key, string issuer, string audience) : IAccessTokenGenerator
{
    private readonly string _key = key;

    private readonly string _issuer = issuer;

    private readonly string _audience = audience;

    /// <summary>
    /// トークンを生成する。
    /// </summary>
    /// <param name="userId">ユーザID</param>
    /// <returns>トークン</returns>
    public string Generate(UserId userId)
    {
        var secrityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_key));
        // 資格情報
        var credentials = new SigningCredentials(secrityKey, SecurityAlgorithms.HmacSha256);

        Claim[] claims = [
            // sub（subject）クレームは JWT の主語となる主体の識別子である. JWT に含まれるクレームは, 通常 subject について述べたものである.  
            // https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html#subDef
            new(JwtRegisteredClaimNames.Sub, userId.Value.ToString()),
            // jti (JWT ID) クレームは, JWT のための一意な識別子を提供する. その識別子の値は, 重複確率が無視できるほど十分低いことを保証できる方法で割り当てられなければならない
            // https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html#jtiDef
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        ];

        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            signingCredentials: credentials,
            expires: DateTime.UtcNow.AddHours(1)
        );
        var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
        return tokenString;
    }

    /// <summary>
    /// リクエストからユーザIDを解析する
    /// </summary>
    /// <param name="authHeader">認可ヘッダ文字列</param>
    /// <returns>ユーザID</returns>
    public static Guid ParseUserId(HttpRequest request)
    {
        var authHeader = request.Headers.Authorization.ToString();
        var handler = new JwtSecurityTokenHandler();
        // 認可ヘッダには「Bearer 」という接頭辞がついているため、取り除く
        authHeader = authHeader.Replace("Bearer ", "");
        var jwtSecurityToken = (JwtSecurityToken)handler.ReadToken(authHeader);
        // サブジェクトクレームにユーザIDがある
        var id = jwtSecurityToken.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.Sub).Value;
        // Guidとして解析し戻す
        return Guid.Parse(id);
    }

    /**
     * [参考]
     * https://qiita.com/te-k/items/600df0d0d812139de881
     * https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html
     */
}

```

`subject`クレームにはユーザID、`JWT ID`クレームには`Guid`を割り当てています。
JWTには改ざんを検知する仕組みが備わっているため、トークン認証後にユーザIDを取得する際はJWTをデコードしてユーザIDを取得します。

:::note alert
クレームには機密情報を含めないようにしてください。JWTのクレーム部分はBase64エンコードされているだけなので簡単にデコードできます。
:::



## 環境変数へアクセスするためのクラスを実装する(任意)
このプログラムでは環境変数を利用します。C#で環境変数を取得するには`Environment.GetEnvironmentVariable(string)`を利用しますが、これを直接利用すると少し不便な点があります。
1. 環境変数名は文字列で渡す必要がある  
→ 同じ環境変数を複数個所で参照している場合、変数名が変わった際の変更箇所が複数生じる
2. 取得できない場合はnullが返ってしまう  
→ 取得できない場合はサーバが動作しないはずなのでサーバを落としていい
3. どこで環境変数を取得しているかがわかりづらくなる  
→ 調べるために`Environment.GetEnvironmentVariable`で検索をかけたくない
4. 利用している環境変数を一目で確認できない  
→ docker-composeに書かれる環境変数はプログラムから参照しているものだけではない

これらの問題を解決するために、環境変数を取得するためのラッパクラスを実装します。

`Program.cs`と同じ階層に`ServerEnvironments.cs`を作成します。  
```cs
namespace Server;

public enum VariableTypes
{
    /// <summary>
    /// データベースの接続文字列
    /// </summary>
    DBConnectionString,
    /// <summary>
    /// ハッシュ化時に用いるペッパー
    /// </summary>
    HasherPepper,
    /// <summary>
    /// JWTで用いる発行者
    /// </summary>
    JWTIssuer,
    /// <summary>
    /// リフレッシュトークン生成に用いる秘密鍵
    /// </summary>
    JWTKeyForRefreshToken,
    /// <summary>
    /// アクセストークン生成に用いる秘密鍵
    /// </summary>
    JWTKeyForAccessToken,
    /// <summary>
    /// JWTの受信者情報(基本的にURI形式)
    /// </summary>
    JWTAudience,
}

public class ServerEnvironments
{
    /// <summary>
    /// 環境変数の値を取得する。失敗した場合は落ちる。
    /// </summary>
    /// <param name="type">取得可能な環境変数の種類</param>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException">typeが不正な値だった場合スローされる</exception>
    /// <exception cref="ArgumentException">環境変数が設定されていなかった場合スローされる</exception>
    public static string Get(VariableTypes type)
    {
        var variableName = type switch
        {
            VariableTypes.DBConnectionString => "DB_CONNECTION_STR",
            VariableTypes.HasherPepper => "PEPPER",
            VariableTypes.JWTIssuer => "JWT_ISSURER",
            VariableTypes.JWTKeyForRefreshToken => "JWT_KEY_REFRESH_TOKEN",
            VariableTypes.JWTKeyForAccessToken => "JWT_KEY_ACCESS_TOKEN",
            VariableTypes.JWTAudience => "JWT_AUDIENCE",
            _ => throw new ArgumentOutOfRangeException(nameof(type), $"環境変数名の設定を忘れています: {type}")
        };

        return Environment.GetEnvironmentVariable(variableName)
            ?? throw new ArgumentException($"環境変数 {variableName} が設定されていません。", nameof(type));
    }
}

```

今後必要になる環境変数はあらかじめ設定しました。このように実装することで、先述した課題は解決されます。
1. 環境変数名は文字列で渡す必要がある  
→ 列挙型を用いて参照できるようになる
2. 取得できない場合はnullが返ってしまう  
→ 例外で落ちるようになる
3. どこで環境変数を取得しているかがわかりづらくなる  
→ エディタ・IDEの機能である参照ボタンをクリックするだけで確認できるようになる  
![ref_button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/c305086a-a851-f2cb-5c8d-669e6a209cd9.png)

4. 実装したプログラムで利用している環境変数を一目で確認できない  
→ 列挙型のメンバを見ればよくなる

## Program.csを書き換える
```cs
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Server;
using Server.Authentications;
using Server.Databases;
using Server.Models.UserAuthentications;
using Server.UseCases.UserAuthentications;
using Server.UseCases.Users;

internal class Program
{
    public const string REGISTER_USER_RATE_LIMITER = "register";

    public const string TOKEN_RATE_LIMITER = "token";

    private static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.

        builder.Services.AddControllers(
            // デフォルトで認証が必要にする、
            // https://qiita.com/mkuwan/items/bd5ff882108998d76dca
            options => options.Filters.Add(
                new AuthorizeFilter(
                    new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
                        .RequireAuthenticatedUser()
                        .Build()
                )
            )
        );
        // ベアラートークン認証関連の登録
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = ServerEnvironments.Get(VariableTypes.JWTIssuer),
                    ValidAudience = ServerEnvironments.Get(VariableTypes.JWTAudience),
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(
                            ServerEnvironments.Get(VariableTypes.JWTKeyForAccessToken))),
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ClockSkew = TimeSpan.Zero
                };
            });
        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen(option =>
        {
            option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                In = ParameterLocation.Header,
                Description = "JWTトークンを入力してください。",
                Name = "Authorization",
                Type = SecuritySchemeType.Http,
                BearerFormat = "JWT",
                Scheme = "Bearer"
            });
            option.AddSecurityRequirement(new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecurityScheme {
                        Reference = new OpenApiReference {
                            Type=ReferenceType.SecurityScheme, Id="Bearer"
                        }
                    },
                    []
                }
            });
        });

        // カラム名のアンダースコアを無視する。こうすることでキャメルケースでもマップできる。
        Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;

        // Hasherを登録
        RegisterHasher(builder.Services);

        // トークン生成器を登録
        RegisterTokenGenerator(builder.Services);

        // リポジトリを登録
        RegisterRepositories(builder.Services);

        // ユースケースを登録
        RegisterUseCases(builder.Services);

        // レート制限を設定
        RegisterRateLimit(builder.Services);

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        // 認証
        app.UseAuthentication();
        // 認可
        app.UseAuthorization();

        app.MapControllers();

        // レート制限ミドルウェアを開始
        app.UseRateLimiter();

        app.Run();
    }

    /// <summary>
    /// レート制限をかけるための設定を行う
    /// </summary>
    /// <param name="services">サービス</param>
    private static void RegisterRateLimit(IServiceCollection services)
    {
        services.AddRateLimiter(options =>
        {
            options.AddFixedWindowLimiter(REGISTER_USER_RATE_LIMITER, options =>
            {
                // 時間枠内で受け取れるリクエスト数
                options.PermitLimit = 1;
                // リクエストを受け取る時間枠
                options.Window = TimeSpan.FromSeconds(60);
                // リクエストのキューの容量
                options.QueueLimit = 0;
            })
            .AddFixedWindowLimiter(TOKEN_RATE_LIMITER, options =>
            {
                options.PermitLimit = 3;
                options.Window = TimeSpan.FromSeconds(60);
                options.QueueLimit = 0;
            });
        });
    }

    /// <summary>
    /// ハッシュ化処理を登録
    /// </summary>
    /// <param name="services">DIコンテナ</param>
    private static void RegisterTokenGenerator(IServiceCollection services)
    {
        // トークン生成には機密情報を使うので、ここでのみ生成を行う。
        services
            .AddSingleton<IAccessTokenGenerator, AccessTokenGenerator>(services =>
            {
                var audience = ServerEnvironments.Get(VariableTypes.JWTAudience);
                var issuer = ServerEnvironments.Get(VariableTypes.JWTIssuer);
                var key = ServerEnvironments.Get(VariableTypes.JWTKeyForAccessToken);

                return new(key, issuer, audience);
            });
    }

    /// <summary>
    /// ハッシュ化処理を登録
    /// </summary>
    /// <param name="services">DIコンテナ</param>
    private static void RegisterHasher(IServiceCollection services)
    {
        services
            .AddSingleton<IHasher, HasherByPBKDF2>(services => new(ServerEnvironments.Get(VariableTypes.HasherPepper)));
    }

    /// <summary>
    /// リポジトリを登録する
    /// </summary>
    /// <param name="services">DIコンテナ</param>
    private static void RegisterRepositories(IServiceCollection services)
    {
        var connectionString = ServerEnvironments.Get(VariableTypes.DBConnectionString);
        services
            .AddSingleton<IUserRepository, UserRepository>(services => new(services.GetRequiredService<ILogger<UserRepository>>(), connectionString))
            .AddSingleton<IUserAuthenticationRepository, UserAuthenticationRepository>(
                    services => new(services.GetRequiredService<ILogger<UserAuthenticationRepository>>(), connectionString)
                );
    }

    /// <summary>
    /// DIコンテナにユースケースを登録する。
    /// </summary>
    /// <param name="services">DIコンテナ</param>
    private static void RegisterUseCases(IServiceCollection services)
    {
        services
            .AddSingleton(services =>
            {
                var userRepository = services.GetRequiredService<IUserRepository>();
                var userAuthRepository = services.GetRequiredService<IUserAuthenticationRepository>();
                var hasher = services.GetRequiredService<IHasher>();
                return new UserUseCase(userRepository, userAuthRepository, hasher);
            })
            .AddSingleton(services =>
            {
                var userRepository = services.GetRequiredService<IUserRepository>();
                var hasher = services.GetRequiredService<IHasher>();
                var tokenGenerator = services.GetRequiredService<IAccessTokenGenerator>();
                var userAuthRepository = services.GetRequiredService<IUserAuthenticationRepository>();
                return new UserAuthenticationUseCase(hasher, tokenGenerator, userAuthRepository, userRepository);
            });
    }
}
```
追加した個所が多いため、列挙します。
- DIコンテナへ各処理を登録
- swaggerで認証を利用できるようにする設定
- Dapperでカラム名のアンダースコアを無視する設定
- レート制限の設定
- コントローラ側で特に指定しなければ必ず認証が必要になるよう設定
環境変数へアクセスするクラスの実装をしていない場合は`ServerEnvironments.Get`を`Environment.GetEnvironmentVariable(string)!`に置き換えてください。

筆者は`Main`メソッドを書くスタイルに慣れているので直しましたが、この辺は好みの問題なので直さなくてもいいです。

#### レート制限設定部分の解説
```cs
    /// <summary>
    /// レート制限をかけるための設定を行う
    /// </summary>
    /// <param name="services">サービス</param>
    private static void RegisterRateLimit(IServiceCollection services)
    {
        services.AddRateLimiter(options =>
        {
            options.AddFixedWindowLimiter(REGISTER_USER_RATE_LIMITER, options =>
            {
                // 時間枠内で受け取れるリクエスト数
                options.PermitLimit = 1;
                // リクエストを受け取る時間枠
                options.Window = TimeSpan.FromSeconds(60);
                // リクエストのキューの容量
                options.QueueLimit = 0;
            })
            .AddFixedWindowLimiter(TOKEN_RATE_LIMITER, options =>
            {
                options.PermitLimit = 3;
                options.Window = TimeSpan.FromSeconds(60);
                options.QueueLimit = 0;
            });
        });
    }
```

ここの実装で設定しています。レート制限は複数作ることができ、文字列をキーとして利用できます。今回の実装ではコントローラの属性引数で利用するために定数として設定しています。`REGISTER_USER_RATE_LIMITER`のレート制限設定を利用する場合は、60秒の間に1回だけリクエストを受け付けるようにできます。`TOKEN_RATE_LIMITER`の設定を利用する場合は、60秒の間に3回リクエストを受け付けるようにできます。このように設定することで、ユーザ登録やサインインを短い間隔で繰り返し実行されることを防ぐことができます。特にサインインでは総当たり攻撃の妨害に利用できます。

## コントローラを実装
### UserController
```cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Server.Authentications;
using Server.UseCases;
using Server.UseCases.Users;

namespace Server.Controllers;

[ApiController]
[Route("users")]
public class UserController(ILogger<UserController> logger, UserUseCase userUseCase) : ControllerBase
{
    private readonly ILogger<UserController> _logger = logger;
    private readonly UserUseCase _userUseCase = userUseCase;

    public record RegisterParams(string Name, string SignInId, string Password);
    public record RegisterValidationResponse(bool UserNameOk, bool SignInIdOk, bool RawPasswordOk);
    [AllowAnonymous]
    [EnableRateLimiting(Program.REGISTER_USER_RATE_LIMITER)]
    [HttpPost("register")]
    public async Task<IActionResult> RegisterAsync(RegisterParams param)
    {
        // 登録ユースケースを実行
        var (resultTypes, validationResult) = await _userUseCase.RegisterUserAsync(param.Name, param.SignInId, param.Password);
        if (resultTypes is ResultTypes.Success)
        {
            return Ok();
        }

        if (resultTypes is ResultTypes.InternalError)
        {
            _logger.LogError("ユーザ登録時にサーバ内エラーが発生しました");
            return StatusCode(500, "Internal Server Error");
        }

        var validationResultResponse = new RegisterValidationResponse(validationResult.UserNameOk, validationResult.SignInIdOk, validationResult.RawPasswordOk);
        return BadRequest(validationResultResponse);
    }

    public record GetUserResponse(string Name);
    [HttpGet]
    public async Task<IActionResult> GetUserAsync()
    {
        var userId = AccessTokenGenerator.ParseUserId(Request);
        var (resultTypes, user) = await _userUseCase.GetUserAsync(userId);

        if (resultTypes is ResultTypes.Success)
        {
            return Ok(new GetUserResponse(user.Name.Value));
        }

        _logger.LogError("ユーザ取得時にサーバ内エラーが発生しました");
        return StatusCode(500, "Internal Server Error");
    }
}

```

コントローラではユースケースを実行・レスポンスの生成を行っています。コントローラはテストしないので、できる限り薄く実装します。また、認証されていない場合でもリクエストを受け付けるため、レート制限を設けています。

### AuthenticationController
サインイン用のコントローラを実装します。

```cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Server.UseCases;
using Server.UseCases.UserAuthentications;

namespace Server.Controllers;

[ApiController]
[Route("auth")]
public class AuthenticationController(ILogger<AuthenticationController> logger, UserAuthenticationUseCase authUseCase) : ControllerBase
{
    private readonly ILogger<AuthenticationController> _logger = logger;
    private readonly UserAuthenticationUseCase _authUseCase = authUseCase;

    public record SignInParams(string SignInId, string Password);
    public record SignInResponse(string AccessToken);
    [AllowAnonymous]
    [EnableRateLimiting(Program.TOKEN_RATE_LIMITER)]
    [HttpPost("sign_in")]
    public async Task<IActionResult> RegisterAsync(SignInParams param)
    {
        // サインインユースケースを実行
        var (resultTypes, accessToken) = await _authUseCase.SignInAsync(param.SignInId, param.Password);
        if (resultTypes is ResultTypes.Success)
        {
            return Ok(new SignInResponse(accessToken));
        }

        if (resultTypes is ResultTypes.InternalError)
        {
            _logger.LogError("サインイン時にサーバ内エラーが発生しました");
            return StatusCode(500, "Internal Server Error");
        }

        return BadRequest();
    }
}

```

サインインのハンドラもレート制限を付けます。

## docker-compose.ymlに環境変数を登録
```yml
    .
    .
    .
    depends_on:
      - "db"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - DOTNET_WATCH_RESTART_ON_RUDE_EDIT=true
      - PEPPER=pepper
      - DB_CONNECTION_STR=Data Source=db, 1433;Initial Catalog=basic_db;User ID=sa;Password=P@sswordP@ssword;TrustServerCertificate=true;
    command: dotnet watch run --project /api_src/Server/Server.csproj --launch-profile http    depends_on:
      - "db"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - DOTNET_WATCH_RESTART_ON_RUDE_EDIT=true
      - PEPPER=pepper
      - DB_CONNECTION_STR=Data Source=db, 1433;Initial Catalog=basic_db;User ID=sa;Password={管理者アカウントのパスワード};TrustServerCertificate=true;
      - JWT_AUDIENCE=クライアント側を登録
      - JWT_ISSURER=localhost:8080
      - JWT_KEY_ACCESS_TOKEN=The_encryption_algorithm_'HS256'_requires_a_key_size_of_at_least_'128'_bits.
    command: dotnet watch run --project /api_src/Server/Server.csproj --launch-profile http
    .
    .
    .
```
`environment`部分に環境変数を追記し、コンテナを再起動します。各環境変数の内容をまとめます。


## 動作確認
正しく起動できていれば`http://localhost:8080/swagger/index.html`へアクセスすると、次の画像の通り表示されます。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/04e842ce-6b13-3487-89cb-cae0665ee030.png)
  
`/users/register`をクリックすると、パラメータのスキーマ等が表示されます。  
`Try it out`をクリックすると、リクエストパラメータを編集し、実際にリクエストできます。
![swagger_flow1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/8a8f2865-0708-d34e-0c56-7e022ff66703.png)
  
![swagger_try_register.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/023ce784-668e-35aa-cea1-611552ae9e3a.png)  
1のところでパラメータを編集し、2をクリックすると、3の位置に結果が表示されます。

リクエストを連続で送信すると、次の画像の通り失敗します。
![rate_limit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/e5ccee0d-7995-b83b-1a4d-982f81c16cc8.png)

### DBを確認したい場合
```sql
SELECT
    * 
FROM [app].[users] users
    INNER JOIN [app].[user_authentications] user_authentications
        ON users.id = user_authentications.user_id
```

このようなクエリを適当なファイルに書き、右上の再生ボタンを押すとクエリを実行できます。  
![register_query.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/a47bc850-b744-d4ae-a162-f5d0ecabb754.png)
  
他には`A5:SQL Mk-2`等のSQLクライアントソフトウェアを利用したり、DB初期化スクリプトで利用した`sqlcmd`をコンテナ内で実行したりする方法がありますが、割愛します。

### 認証が必要なAPIの動作確認
ユーザ登録後、サインインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/7c5d75c3-833a-4405-748c-a576dcc2df21.png)
サインインのレスポンスにあるアクセストークンをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/57a4dcb3-a867-7baf-bf4e-c8dad0d16b92.png)
画面右上のAuthorizeボタンをクリックするとダイアログが表示されるので、そこにトークンを入力します。入力後、ダイアログ左下のAuthorizeボタンをクリックすることで、認証が必要なAPIを試すことができるようになります。


# xUnitを利用したテストの実装
## プロジェクトの準備
### ソリューション・プロジェクト作成
リポジトリルートへ移動し、次のスクリプトを実行します。
```bash
cd ./API
# サーバテストプロジェクト用ディレクトリを作成
mkdir ./ServerTests
# プロジェクトを作成し、ソリューションにプロジェクトを追加
dotnet new xunit -f "net8.0" -lang "C#" -o "./ServerTests"
dotnet sln "API.sln" add "./ServerTests/ServerTests.csproj"
cd ..
```

### ライブラリをインストール
モックを利用したテストをするので、`Moq`をインストールします。
```bash
dotnet add ./API/ServerTests/ServerTests.csproj package Moq --version 4.20.72
```

### プロジェクト参照を追加する
テストプロジェクトからサーバプロジェクトを参照できるようにします。

```bash
dotnet add ./API/ServerTests/ServerTests.csproj reference ./API/Server/Server.csproj
```

## テストコードを書く
すべてを示していません。完全なテストはサンプルプログラムを参照して下さい。

### モデルの生成メソッドをテストする
サインインIDの生成メソッドを境界値テストしています。
```cs
using Server.Models.UserAuthentications;

namespace ServerTests.Models.UserAuthentications;
public class TestSignInId
{
    [Theory(DisplayName = "サインインIDのバリデーションの正常系")]
    // 10文字
    [InlineData("0123456789")]
    // 100文字
    [InlineData("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789")]
    public void ValidateSignInId_Valid(string signInIdValue)
    {
        var ok = SignInId.TryCreate(signInIdValue, out var signInId);
        Assert.True(ok);
        Assert.Equal(signInIdValue, signInId?.Value);
    }

    [Theory(DisplayName = "サインインIDのバリデーションの異常系")]
    // 9文字
    [InlineData("012345678")]
    // 100文字 + 1
    [InlineData("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")]
    public void ValidateSignInId_InvalidFormat(string signInIdValue)
    {
        var ok = SignInId.TryCreate(signInIdValue, out var signInId);
        Assert.False(ok);
        Assert.Null(signInId);
    }
}
```

### モックを利用してユースケースをテストする
モックを利用してユーザ登録のユースケースをテストします。
```cs
using Moq;
using Server.Models.UserAuthentications;
using Server.Models.Users;
using Server.UseCases;
using Server.UseCases.UserAuthentications;
using Server.UseCases.Users;

namespace ServerTests.UseCases.Users;
public class TestUserUseCase
{
    [Theory(DisplayName = "ユーザを挿入できるかの正常系テスト")]
    [InlineData("user", "aiueo@test.co.jp", "          ")]
    public async Task RegisterUser_Valid(string userName, string signInId, string password)
    {
        // Arrange
        var hasher = new Mock<IHasher>();
        var tokenGenerator = new Mock<IAccessTokenGenerator>();
        var userRepository = new Mock<IUserRepository>();
        var authRepository = new Mock<IUserAuthenticationRepository>();
        var hashedPassword = "hashed";
        if (!RawPassword.TryCreate(password, out var rawPassword) ||
            !UserName.TryCreate(userName, out var expectedUserName) ||
            !SignInId.TryCreate(signInId, out var expectedSignInId))
        {
            Assert.Fail($"インラインデータが不正です。{userName},{signInId},{password}");
            return;
        }
        // authRepositoryが値を返さないように設定する
        authRepository
            .Setup(r => r.TryFindAuthenticationAsync(expectedSignInId))
            .ReturnsAsync((false, new(), StoredPassword.Empty));
        var userId = UserId.CreateNew();
        var registeredAt = DateTime.Now;
        var user = User.Create(userId, expectedUserName, registeredAt);
        // ハッシュ化したパスワードを定義
        hasher
            .Setup(h => h.Generate(user, rawPassword))
            .Returns(hashedPassword);
        var expectedHashedPassword = HashedPassword.HashFromRawPassword(hasher.Object, rawPassword, user);
        // リポジトリのユーザ作成メソッドは成功を返すように設定
        userRepository
            .Setup(r => r.TryCreateUserAsync(user, expectedSignInId, expectedHashedPassword))
            .ReturnsAsync(true);
        var useCase = new UserUseCase(userRepository.Object, authRepository.Object, hasher.Object);

        // Act
        var (resultTypes, validationResult) = await useCase.RegisterUserAsync(userId, userName, registeredAt, signInId, password);

        // Assert
        // パスワードハッシュ化メソッドを呼び出しているかをテストする。
        hasher.Verify(hasher => hasher.Generate(user, rawPassword));
        // リポジトリのユーザ作成メソッドが呼び出されている。
        userRepository.Verify(repo =>
            repo.TryCreateUserAsync(user, expectedSignInId, expectedHashedPassword),
            Times.Once());
        // ユースケースの戻り値で成功した結果が返っているか
        Assert.Equal(ResultTypes.Success, resultTypes);
        // バリデーション結果が正しいか
        Assert.Equal(new UserUseCase.RegisterValidationResult(true, true, true), validationResult);
    }
}
```

モックを利用してメソッドが呼び出されているかをテストしています。大まかには、
- パスワードハッシュ化メソッドを準備した生パスワードを渡して呼び出しているか
- ユーザ作成メソッドを期待したハッシュ化済みパスワードを渡して呼び出しているか
- 戻り値のテスト
をテストしています。

#### モックを利用したテストの注意点
このようなテストはホワイトボックステストになるため、**壊れやすい**テストになります。今回はユーザ登録やサインインを扱っているため、具体的な実装までテストしています。このように、モックを利用する場合はテストの目的をよく考える必要があります。

## CI(継続的インテグレーション)の構築
テストを実装できたので、`GitHub Actions`を利用してテストを自動化します。リポジトリルートに`.github/workflows`ディレクトリを作成し、その中に`CI_API.yml`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/5771b923-1694-5e7e-f043-08dc70f344b8.png)

### アクションの実装例
プルリクエストが作られた時またはメインブランチへプッシュされた際にテストが自動で実行されます。
```yml:CI_API.yml
name: Build and Test

on:
  pull_request:
  push: 
    branches: ["main"]

jobs:

  build:
    runs-on: ubuntu-latest  

    steps:
    - name: Checkout
      uses: actions/checkout@v3
      with:
        fetch-depth: 0

    - name: .NET 8 をインストール
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 8.0.x
    
    - name: リストアする
      run: dotnet restore API/Server/Server.csproj; dotnet restore API/ServerTests/ServerTests.csproj

    - name: API本体をビルドする
      run: dotnet build API/Server/Server.csproj --no-restore --warnaserror

    - name: テストプロジェクトをビルドする
      run: dotnet build API/ServerTests/ServerTests.csproj --no-restore

    - name: テストを実行
      run: dotnet test API/API.sln /p:Configuration=Release
```

実際にGitHubへプッシュするとこのように動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/eed20645-f491-512d-8223-7eac3fdd8d7c.png)

# Azureの準備
## 注意
この方法では年間で1万円(1日あたり25～26円)ほどかかります。Azureは初回登録時に無料クレジットを配布したり、学生向けサブスクリプションも利用できるため、上手く活用します。

## 利用するリソース
| リソース名 | 概要 |
|:-:|:-:|
| Azure Container Registry  |  ビルドしたDockerイメージを置く場所です。 |
| Azure Container Apps  | コンテナ環境でアプリケーションを動作させる場所です。APIサーバを動作させます。 |
| Azure Conainer Apps Environments | 証明書の設定やネットワークの設定などサーバが動作する環境を設定できます。 |
| SQL Server | データベースを動作する環境を設定できます。 |
| Azure SQL Database | データベースを動作させる場所です。 |

## リソースグループを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/1acdc746-3141-14a9-305c-130b593f8129.png)
すべてのサービスから`リソース グループ`を検索し、クリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/f86067d0-3ee0-693e-32f7-276a00a561da.png)
作成ボタンが左側に表示されるので、クリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/3a1fec14-3d75-37a4-60b8-3523312ae381.png)]
クリックするとサブスクリプション、リソースグループ名、リージョンの選択をする画面が出てくるので入力します。リージョンは東日本と西日本を選ぶことができます。
最後に「確認及び作成タブ」からリソースを作成します。

## Azure Container Registoryの準備
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/4530db14-b857-6235-0f24-cdac4443315d.png)
まず画像の通りにコンテナレジストリを作成します。プランは最も安いBasicにしています。パブリックアクセスを許可したくない場合はプレミアムプラン以上が必要です。

### GitHub Actionsからイメージをプッシュするための設定
Cloud Shellを起動し、次のスクリプトを実行します。
```bash
resourcesName="<リソースグループ名>"

# リソースグループIDを取得する
groupId=$(az group show \
  --name $resourcesName \
  --query id --output tsv)

# 認証にはサービスプリンシパルが必要なので、作成する
az ad sp create-for-rbac \
  --scope $groupId \
  --role Contributor \
  --sdk-auth
```

ここまで実行すると、JSON形式の文字列がターミナルに出力されます。
出力されたJSONをメモします。
```bash
registryId=$(az acr show \
  --name <レジストリ名> \
  --resource-group $resourcesName \
  --query id --output tsv)

# プッシュの権限を付与
az role assignment create \
  --assignee <クライアントID> \
  --scope $registryId \
  --role AcrPush
```

### 管理者アカウントの有効化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/228702f5-db0d-ba76-f735-520dc842c5c1.png)
作成したコンテナレジストリのアクセスキーページから管理者ユーザにチェックします。コンテナアプリの作成時に必要な設定です。

### GitHub側の設定
リポジトリに次のシークレット変数を登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/4778d707-2e23-7d96-feaf-06e902e7e808.png)

|変数名|値|
|---|---|
|`AZURE_CREDENTIALS`|メモしたJSON|
|`REGISTRY_USERNAME`|メモしたJSONから抜き出したクライアントID|
|`REGISTRY_PASSWORD`|メモしたJSONから抜き出したクライアントシークレット|
|`REGISTRY_LOGIN_SERVER`|<レジストリ名>.azurecr.io|

## Azure SQL Databaseの準備
### サーバーを作成
SQLデータベースの作成ページへ移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/7b3e9186-86dc-f038-f2ed-8de6faf4ca7c.png)
現時点ではサーバーが存在しないので、新規作成からサーバを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/9e7ddf9c-b878-f41b-cb4d-90ca4af5cc88.png)
画像の通りに入力します。今回はSQL認証を用いるため、管理者アカウントを作成します。

### データベースを作成
サーバーの作成が完了すると、データベース作成画面に戻ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/fc744376-034e-0c6c-cd4a-39e54b6d6025.png)
初期設定で進めます。

#### Microsoft Defenderを無効化(任意)
セキュリティタブでMicrosoft Defenderがデフォルトでオンになっているため、「後で」に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/8261d5fc-2b21-ae8f-6f88-02331b99b695.png)

#### 照合順序の変更(任意)
システムに合わせて照合順序を変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/b48a146b-bdd8-7b14-cd7d-a33a1e979f41.png)

[照合順序(learn.microsoft.com)](https://learn.microsoft.com/ja-jp/sql/relational-databases/collations/collation-and-unicode-support?view=sql-server-ver16)

```sql:APIサーバからログインするユーザを作成する
CREATE USER [ユーザ名] WITH PASSWORD = 'パスワード';
ALTER ROLE db_datareader ADD MEMBER [ユーザ名];
ALTER ROLE db_datawriter ADD MEMBER [ユーザ名];
```
## Azure Container Appsの準備
実際にAPIサーバを動作させる環境を作ります。
コンテナアプリを作成する画面に移動すると、次の画像のように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/2b548076-eb44-fcd2-0fee-a9561fbeef66.png)
現時点ではContainer Apps環境がないため、新規作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/5dd22d49-73ff-b405-0133-9392404ff406.png)
名前だけ入力して初期設定のまま作成します。
環境を作成すると、元の画面に戻るので、ここでコンテナアプリ名等を入力します。今回はACRからデプロイするので、展開元は`コンテナー イメージ`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/73916845-4dfe-448b-9ee7-389e6296f02e.png)

コンテナタブでは作成したコンテナレジストリを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/01c5b5f4-d0e3-cc83-56f0-e6a7ccf64389.png)

イングレスの設定をし、外部からリクエストを受け取れるようにします。ターゲットポートは`8080`にします。
ここまで設定出来たら作成をクリックします。作成してしばらくするとデプロイに失敗した通知が来ますが、これはコンテナレジストリにイメージをプッシュしていないことによるものなので無視します。
## REST APIサーバをAzureへ継続的デプロイする
APIサーバをGitHub Actionsを用いて自動デプロイできるようにします。`.github/workflows`配下に`CD_API.yml`を作成します。下記アクションでは、`production`ブランチにプッシュした場合、または手動実行した場合にデプロイされます。

```yml:CD_API.yml
name: Deploy to Azure

on:
  push:
    branches:
      - production
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: リポジトリをチェックアウトする。
        uses: actions/checkout@v4
        
      - name: Azure CLI経由でログインする。
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      
      - name: イメージをビルドしてAzure Container Registoryにプッシュする。
        uses: azure/docker-login@v1
        with:
          login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - run: |
          docker build ./API -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/<イメージ名>:${{ github.sha }}
          docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/<イメージ名>:${{ github.sha }}

      # https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/reference/azure-container-apps-v1?view=azure-pipelines
      - name: Azure Container Appsへデプロイする。
        uses: azure/container-apps-deploy-action@v1
        with:
          acrName: <コンテナレジストリ名>
          imageToDeploy: ${{ secrets.REGISTRY_LOGIN_SERVER }}/<イメージ名>:${{ github.sha }}
          resourceGroup: <リソースグループ名>
          containerAppName: '<コンテナアプリ名>'
          location: 'Japan East'
```
イメージ名は任意の名前ですが、筆者はコンテナレジストリ名と同じにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/a335f530-8774-8019-f1b3-b73ea45da28c.png)

デプロイ後、コンテナアプリが起動しますが、現状では動作しないのでコンテナアプリを停止します。


## データベースプロジェクトをAzure SQL Databaseへパブリッシュする
Visual Studio Codeでデータベースプロジェクトを右クリックし、Publishを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/a2a8f3e4-892d-6db9-67f9-41c6ac22b279.png)

既存のSQLサーバへパブリッシュします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/d1793da8-e3dc-0b6c-fd97-c589d5f1e146.png)

プロファイルを使わない方を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/b531c407-a075-d22e-52a9-ed54b9f29f5c.png)

接続プロファイルの作成を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/020c0107-d4c5-19e9-8c9f-90ae5ee0dfd2.png)

SQLデータベースのページからSQL認証の接続文字列を取得し、入力します。パスワードは置き換えてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/85051658-c6a1-79d7-83d2-b539d1ed81c5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/e84522fb-916f-48e1-4ff5-0250a5fd42cd.png)

接続に成功した後データベース名を選択します。今回はすでにデータベースを作成しているので、先ほど作成したSQLデータベース名を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/0dfc18a5-58da-e87b-0f83-926eff01b795.png)

Publishを選択するとデプロイが開始されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/05e9ff85-a475-5b8d-8ff4-a6d5354d7b0b.png)

デプロイが完了すると画像のようにトースト通知が届きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/cf44e12a-b469-93ba-a227-77641e2aa34f.png)
### APIからアクセスするためのユーザを作成する
#### ネットワークの設定変更
作成したサーバのページへ移動し、ネットワークの設定を画像の通り変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/60763314-05e0-5aeb-c685-afb83ab6f405.png)

#### ユーザ作成クエリを実行
作成した管理者アカウントでログインし、APIから利用するSQLユーザを作成します。Azure上からクエリを実行する場合はSQLデータベースのクエリエディタから実行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/76191fa8-ed47-3695-d68a-b12b72d570a5.png)

```sql
CREATE USER [ユーザ名] WITH PASSWORD = 'パスワード';
ALTER ROLE db_datareader ADD MEMBER [api];
ALTER ROLE db_datawriter ADD MEMBER [api];
```

## 環境変数を設定する
APIサーバの環境変数を登録します。`Azure Container Apps`にはシークレットを環境変数として参照できるようにする機能があるので、それを利用します。
### シークレットの登録
作成した`Container Apps`のページからシークレットページへ移動し、本番環境用の値を設定します。`docker-compose.yml`に記述した変数をここで登録します(変数の値は本番環境用です)。
| シークレット名 | 設定内容 |
|:-:|:-:|
| `s-db-connection-str`  | データベース接続文字列です。先ほど作成したAPI用ユーザにログインできる接続文字列を登録します。  |
| `s-jwt-audience`  | クライアント側は未定なので、適当な値を入力します。  |
| `s-jwt-issurer`  | イングレス画面から得たエンドポイントURLを入力します。  |
| `s-jwt-key-access`  | JWT生成時に利用する秘密鍵を入力します。  |
| `s-pepper`  | パスワードハッシュ化時に用いるペッパーを入力します。  |

![エンドポイントURL](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/2b2a19b2-5d46-1595-167b-177daf33cfa5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/60f7df80-a47f-612a-9d3e-fe6a24b2e80b.png)

### 環境変数にシークレットを登録する

コンテナー画面から、新しいリビジョンを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/e1966c0c-487b-94d4-7016-b669aa1c2a10.png)

コンテナーイメージを選択すると、画面右側で環境変数を設定できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208939/a25365d8-eda1-c970-1622-435a3ab37d91.png)

リビジョンを作成後、サーバを開始すると動作するようになります。

以上で最低限の構築が完了しました。

# おわりに
この記事では、C#を利用したREST APIサーバーの構築から、Azureへのデプロイまでの詳細な手順を解説しました。
C#はいまだに`Windows or Unity専用言語`のような偏見を持たれている気がしますが、実際はDocker上でも動作したり、Visual Studio Codeでも便利に開発できたりでWindows環境以外でも戦える言語ということがわかると思います。普段C#を書かない人でも興味を持った方は是非使ってみてください。きっと期待に応えてくれると思います。

## 謝辞
この記事を読んでいただき、ありがとうございます。
もし間違いや改善点が見つかった場合は、ぜひコメントをいただけると幸いです。