.NET 8 で ASP.NET Core でホストされた Blazor WebAssembly アプリにレイヤードアーキテクチャーを実装する①

# やりたいこと
- .NET 8 で構築した ASP.NET Core でホストされた Blazor WebAssembly アプリからビジネスロジックを分離したい
- とりあえず UI プロジェクトとは別のクラスライブラリのロジックを定義する

## 前回の記事

https://qiita.com/higege-amdeker/items/5a22dbc7cc61df9bbb6c

# 環境
- Windows 11 23H2
- Visual Studio Community 2022 Version 17.9.5

# 手順

## 現在のアーキテクチャー
- SolutionName
    - SolutionName.UI.Client
        - クライアントアプリ
    - SolutionName.UI.Server
        - API
    - SolutionName.UI.Share
        - クライアントアプリとAPIの間でやり取りするオブジェクト等

このままでは Server プロジェクトにビジネスロジックがどんどん書かれて行ってしまいそうなので、分離していきます。
今回は第一弾として AppCore.Domain クラスライブラリを追加して UI プロジェクトから参照します。

:::note
UI プロジェクトから Domain を直接参照なんてとんでもない、と思った御仁、安心してください。段階的にレイヤーを追加していきます。
:::

## AppCore.Domain プロジェクトの実装

### プロジェクトの追加
1. **クラスライブラリ** プロジェクトを追加
    1. 名前は `SolutionName.AppCore.Domain` とする
    1. .NET 8 を選択
1. 不要な Class1.cs を削除
1. `WeatherModels` フォルダーを作成

:::note
私は不可算名詞に悩まなくていいように ~Models という名前空間にしちゃいます。
:::

### Summary の判定をドメインに実装する
以下の判定ロジックは UI 層ではなくて Domain 層に置きたい。
```csharp:WeatherForecastController.cs
    private static readonly string[] Summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
```
1. `WeatherSummary` 列挙型を `WeatherModels` 名前空間に定義
```csharp:WeatherSummary.cs
internal enum WeatherSummary
{
    Freezing,
    Bracing,
    Chilly,
    Cool,
    Mild,
    Warm,
    Balmy,
    Hot,
    Sweltering,
    Scorching,
}
```

:::note
実装開始時はいつも internal で宣言します。必要になったとき、 public にするのです。
:::

2\. `WeatherForecast` クラスを `WeatherModels` 名前空間に定義
```csharp:WeatherForecast.cs
internal sealed class WeatherForecast
{
    public int TemperatureC { get; }
    public int TemperatureF { get; }
    public WeatherSummary Summary { get; }

    public WeatherForecast(int temperatureC)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(temperatureC, -273);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(temperatureC, 1032);
        TemperatureC = temperatureC;

        TemperatureF = CelsiusToFahrenheit(temperatureC);
        Summary = CelsiusToSummary(temperatureC);
    }

    private static int CelsiusToFahrenheit(int temperatureC)
    {
        return 32 + (int)(temperatureC / 0.5556);
    }

    private static WeatherSummary CelsiusToSummary(int temperatureC)
    {
        return temperatureC switch
        {
            <= -18 => WeatherSummary.Freezing,
            > -18 and <= -10 => WeatherSummary.Bracing,
            > -10 and <= 0 => WeatherSummary.Chilly,
            > 0 and <= 10 => WeatherSummary.Cool,
            > 10 and <= 20 => WeatherSummary.Mild,
            > 20 and <= 25 => WeatherSummary.Warm,
            > 25 and <= 30 => WeatherSummary.Balmy,
            > 30 and <= 35 => WeatherSummary.Hot,
            > 35 and <= 40 => WeatherSummary.Sweltering,
            _ => WeatherSummary.Scorching
        };
    }
}
```

Domain 層 AppCore.Domain に気温から Summary の変換ロジックを記述できました。

## UI プロジェクトの下準備
### ぶつかる名前を変えておく
AppCore.Domain に `WeatherForecast` クラスを追加しましたが、UI.Share で作成したクラスと名前がぶつかっています。UI.Share の方のクラスは表示用のクラスなのでそうわかるように名前を変更しましょう。 自分の中でまだしっくり来ていませんが、MVC パターンでいうところの ViewModel なので `WeatherForecastViewModel` としておきます。`Ctrl + R R` で一括変換しましょう。

```csharp:WeatherForecastViewModel.cs
public class WeatherForecastViewModel
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public string? Summary { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

:::note
表示用のモデルに `TemperatureF` の計算ロジックがあるのが気になりますね。後で抹殺しましょう。
:::

## UI プロジェクトから参照する

### プロジェクト参照の追加

1. プロジェクトの「依存関係」を右クリック、「プロジェクト参照の追加」をクリック
1. AppCore.Domain プロジェクトをチェック、「OK」

### AppCore.Domain の外から使用するクラスを public に変更

```diff_c_sharp:WeatherSummary.cs
- internal enum WeatherSummary
+ public enum WeatherSummary
```

```diff_c_sharp:WeatherForecast.cs
- internal sealed class WeatherForecast
+ public sealed class WeatherForecast
```

:::note
初めから public にしとけや、っていう人もいるかもしれませんが、必要になったら public にする、が基本方針です。
:::

### WeatherForecast クラスを利用する

UI.Server のランダム生成部分で `WeatherForecast` クラスを使うように変更します。

```diff_c_sharp:WeatherForecastController.cs
-   private static readonly string[] Summaries =
-   [
-       "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
-   ];

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecastViewModel> Get()
    {
-       return Enumerable.Range(1, 5).Select(index => new WeatherForecastViewModel
-       {
-           Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
-           TemperatureC = Random.Shared.Next(-20, 55),
-           Summary = Summaries[Random.Shared.Next(Summaries.Length)]
-       })
-       .ToArray();
+       return Enumerable.Range(1, 5).Select(index =>
+       {
+           DateOnly date = DateOnly.FromDateTime(DateTime.Now.AddDays(index));
+           int temperatureC = Random.Shared.Next(-20, 55);
+           WeatherForecast weatherForecast = new(date, temperatureC);
+
+           return new WeatherForecastViewModel
+           {
+               Date = weatherForecast.Date,
+               TemperatureC = weatherForecast.TemperatureC,
+               Summary = weatherForecast.Summary.ToString(),
+           };
+       })
+       .ToArray();
    }
```

## TemperatureF も AppCore.Domain で計算

### ViewModel の変更
計算ロジックを排して完全な DTO にします。ついでにプライマリコンストラクターを使った完全コンストラクターパターンに書き換えます。

```diff_c_sharp:WeatherForecastViewModewl.cs
-public class WeatherForecastViewModel
-{
-    public DateOnly Date { get; set; }
-
-    public int TemperatureC { get; set; }
-
-    public string? Summary { get; set; }
-
-   public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-}
+public class WeatherForecastViewModel(
+    DateOnly date,
+    int temperatureC,
+    string summary,
+    int temperatureF)
+{
+    public DateOnly Date { get; } = date;
+
+    public int TemperatureC { get; } = temperatureC;
+
+    public string? Summary { get; } = summary;
+
+    public int TemperatureF { get; } = temperatureF;
+}
```

### インスタンス生成部の変更

```diff_c_sharp:WeatherForecastController.cs
    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecastViewModel> Get()
    {
        return Enumerable.Range(1, 5).Select(index =>
        {
            DateOnly date = DateOnly.FromDateTime(DateTime.Now.AddDays(index));
            int temperatureC = Random.Shared.Next(-20, 55);
            WeatherForecast weatherForecast = new(date, temperatureC);

-            return new WeatherForecastViewModel
-            {
+            return new WeatherForecastViewModel(
                weatherForecast.Date,
                weatherForecast.TemperatureC,
                weatherForecast.Summary.ToString(),
+               weatherForecast.TemperatureF);
-            };
        })
        .ToArray();
    }
```

### 動作確認
動きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/6bf19858-0c15-a847-7f1a-3a5b7c53b09a.png)

# まとめ

## 変更されたアーキテクチャー
- SolutionName
    - **SolutionName.AppCore.Domain**
        - ビジネスロジック
    - SolutionName.UI.Client
        - クライアントアプリ
    - SolutionName.UI.Server
        - API
    - SolutionName.UI.Share
        - クライアントアプリとAPIの間でやり取りするオブジェクト等
## 課題
UI 層が Domain 層に直接依存しています。アプリケーションの規模によっては UseCase(Application) 層を用意したいところです。
次回は UseCase 層を追加して、 DI（依存性注入）を実装していこうと思います。