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

# やりたいこと
- .NET 8 で構築した ASP.NET Core でホストされた Blazor WebAssembly アプリからビジネスロジックを分離したい
- UI プロジェクトと Domain プロジェクトを作ったので、橋渡しをするレイヤーを追加する

# 前回までの記事

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

https://qiita.com/higege-amdeker/items/b99eeb37e9900e906a5b

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

## 問題点
**ビジネスロジックに UI が直接依存してしまっている**
Domain に置いたビジネスロジックは他の UI やコンポーネントからも使いたい。そんなときに UI から Domain を直接参照していると UI 毎に複雑な変換ロジックが書かれてしまうかもしれません。十分に小さいシステムであれば問題にはなりませんが、今回は UI と Domain の間に UseCase 層を追加してみます。

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

# 手順

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

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

### 出力用 DTO を追加
1. `WeatherForecastUsecases` の下に `WeatherForecastOutput` クラスを追加

```csharp:WeatherForecastOutput.cs
namespace SolutionName.AppCore.UseCase.WeatherForecastUsecases;

internal sealed class WeatherForecastOutput(
    DateOnly date,
    int temperatureC,
    int temperatureF,
    string summary)
{
    public DateOnly Date { get; } = date;
    public int TemperatureC { get; } = temperatureC;
    public int TemperatureF { get; } = temperatureF;
    public string Summary { get; } = summary;
}
```

:::note
UseCase 層から UI 層へ渡される DTO(Data Transfer Object) です。
:::

### UseCase のインターフェイスを追加
1. `WeatherForecastUsecases` の下に `IGetWeatherForecastUseCase` インターフェイスを追加

```csharp:IGetWeatherForecastUseCase.cs
namespace SolutionName.AppCore.UseCase.WeatherForecastUsecases;

internal interface IGetWeatherForecastUseCase
{
    public Task<WeatherForecastOutput> ExecuteAsync(DateOnly date);
}
```

:::note
WeatherForecast を取得する UseCase です。
:::

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

UseCase から Domain を使うためのプロジェクト参照を追加します。

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

### UseCase を実装
1. `WeatherForecastUsecases` の下に `GetWeatherForecastUseCase` クラスを追加

```csharp:GetWeatherForecastUseCase.cs
namespace SolutionName.AppCore.UseCase.WeatherForecastUsecases;

internal sealed class GetWeatherForecastUseCase : IGetWeatherForecastUseCase
{
    public async Task<WeatherForecastOutput> ExecuteAsync(DateOnly date)
    {
        // Domain のロジックを使用
        int temperatureC = Random.Shared.Next(-20, 55);
        WeatherForecast weatherForecast = new(date, temperatureC);

        WeatherForecastOutput output = new(
            weatherForecast.Date,
            weatherForecast.TemperatureC,
            weatherForecast.TemperatureF,
            weatherForecast.Summary.ToString());

        return await Task.FromResult(output);
    }
}
```
現在 UI.Server と同じようにランダムな気温が取得できるようにします。

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

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

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

:::note
以後 UI プロジェクトは直接 Domain プロジェクトを参照しません。
:::

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

```diff_c_sharp:WeatherForecastOutput.cs
-internal sealed class WeatherForecastOutput(
+public sealed class WeatherForecastOutput(
```

```diff_c_sharp:IGetWeatherForecastUseCase.cs
-internal interface IGetWeatherForecastUseCase
+public interface IGetWeatherForecastUseCase
```

```diff_c_sharp:GetWeatherForecastUseCase.cs
-internal sealed class GetWeatherForecastUseCase : IGetWeatherForecastUseCase
+public sealed class GetWeatherForecastUseCase : IGetWeatherForecastUseCase
```

:::note
またもや「初めから public にしとけや」って声が聞こえた気がしますが、必要になったら public にする、が基本方針です。
:::

### UI.Server から IGetWeatherForecastUseCase を使用する

1. WeatherForecastController に IGetWeatherForecastUseCase を注入する

```diff_C_sharp:WeatherForecastController.cs
-using HigeDaruma.DemoNet8BlazorWasm.AppCore.Domain.WeatherModels;
+using HigeDaruma.DemoNet8BlazorWasm.AppCore.UseCase.WeatherForecastUseCases;
using HigeDaruma.DemoNet8BlazorWasm.UI.Share.WeatherForecastModels;
using Microsoft.AspNetCore.Mvc;

namespace SolutionName.UI.Server.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController(
+   IGetWeatherForecastUseCase getWeatherForecastUseCase,
    ILogger<WeatherForecastController> logger) : ControllerBase
```

これで、WeatherForecastController 内で IGetWeatherForecastUseCase を使うことができます。

:::note warn
UseCase へのプロジェクト参照により推移的に Domain へのプロジェクト参照が残ってしまうことに注意しましょう。
:::

2\. UseCase を使用する

WeatherForecast のインスタンスを生成していた箇所で IGetWeatherForecastUseCase を使います。入力する日付はそのまま（`DateTime.Now` から5日間）の仕様にしておきます。

```diff_C_sharp:WeatherForecastController.cs
    [HttpGet(Name = "GetWeatherForecast")]
-   public IEnumerable<WeatherForecastViewModel> Get()
+   public async Task<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
-           {
-               Date = weatherForecast.Date,
-               TemperatureC = weatherForecast.TemperatureC,
-               Summary = weatherForecast.Summary.ToString(),
-           };
-       })
-       .ToArray();
+       List<WeatherForecastViewModel> viewModels = [];
+       for (int i = 0; i < 5; i++)
+       {
+           DateTime dateTime = DateTime.Now.AddDays(i);
+           DateOnly dateOnly = DateOnly.FromDateTime(dateTime);
+           WeatherForecastOutput output = await getWeatherForecastUseCase.ExecuteAsync(dateOnly);
+           WeatherForecastViewModel viewModel = new(
+               output.Date,
+               output.TemperatureC,
+               output.Summary,
+               output.TemperatureF);
+           viewModels.Add(viewModel);
+       }
+
+       return viewModels;
    }
```

3\. DI の登録

IGetWeatherForecastUseCase に GetWeatherForecastUseCase を注入するように登録します。

```diff_c_sharp:Program.cs
+// DI
+builder.Services.AddScoped<IGetWeatherForecastUseCase, GetWeatherForecastUseCase>();

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

:::note
`using Microsoft.Extensions.DependencyInjection;` は global usings に追加されています。
:::

### 動作確認
データを取得・表示できました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/b7cf9273-2b5e-7415-c7d4-c16f531fd299.png)

# まとめ

## 変更後のアーキテクチャー

- SolutionName
    - SolutionName.AppCore.Domain
        - ビジネスロジック
    - SolutionName.AppCore.UseCase
        - アプリケーション固有のビジネスロジック
    - SolutionName.UI.Client
        - クライアントアプリ
    - SolutionName.UI.Server
        - API
    - SolutionName.UI.Share
        - クライアントアプリとAPIの間でやり取りするオブジェクト等

## 問題点

- WeatherForecast の生成を UseCase で行っている
    - 今後データベースや外部の API を使ってデータを取得しようとした場合に UseCase を変更しなければならない
    - データソースの切り替えが困難

次回は Infra.Data 層を追加して、永続化ロジックを選択できるように実装します。