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

# やりたいこと
- .NET 8 で構築した ASP.NET Core でホストされた Blazor WebAssembly アプリからビジネスロジックを分離したい
- 現在 UseCase プロジェクトにデータ取得（を想定した生成）ロジックが書かれているので、変更が容易になるように永続化層を追加する

# 前回までの記事

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

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

https://qiita.com/higege-amdeker/items/621092057893b82845bb

# 現在のアーキテクチャー

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

## 問題点

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

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

# 手順

## データアクセス用インターフェイスを追加

1. `AppCore.Domain.WeatherForecastModels` に `IWeatherForecastRepository` インターフェイスを追加する

```csharp:IWeatherForecastRepository.cs
namespace HigeDaruma.DemoNet8BlazorWasm.AppCore.Domain.WeatherModels;

public interface IWeatherForecastRepository
{
    public Task<WeatherForecast> ReadAsync(DateOnly date);
}
```

:::note
このインターフェイスを使って UseCase からデータにアクセスします。
:::

## Infra.Data プロジェクトの実装

データの永続化を担う Infra.Data 層を追加します。

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

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

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

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

### WeatherForecastRepository を実装

ランダムな WeatherForecast を取得する Repository を実装します。

1. `WeatherForecastRepositories` に `RandomWeatherForecastRepository` クラスを追加

```csharp:RandomWeatherForecastRepository.cs
using HigeDaruma.DemoNet8BlazorWasm.AppCore.Domain.WeatherModels;

namespace HigeDaruma.DemoNet8BlazorWasm.Infra.Data.WeatherForecastRepositories;

public class RandomWeatherForecastRepository : IWeatherForecastRepository
{
    public async Task<WeatherForecast> ReadAsync(DateOnly date)
    {
        int temperatureC = Random.Shared.Next(-20, 55);
        WeatherForecast weatherForecast = new(date, temperatureC);

        return await Task.FromResult(weatherForecast);
    }
}
```

:::note
現在 UseCase に実装されているロジックをそのまま移植します。
:::

### UseCase から Repository を使用してデータアクセス

1. `GetWeatherForecastUseCase` から `IWeatherForecastRepository` を使うように修正

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

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

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

        return await Task.FromResult(output);
    }
}
```

2\. DI の登録

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

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

### 動作確認

変わらず表示できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/96de74c9-4bce-c4a4-5a76-0b9af74c7de1.png)

### 別の Repository を追加してみる
Infra.Data 層 にデータの永続化ロジックを抜き出したことによって、付け替えが簡単にできます。データベースの参照先が変わっても大丈夫！（そんなこと滅多にないのでは・・・）
今回は、いつでも同じ値を返す Fake な Repository を追加してみます。

1. `WeatherForecastRepositories` に `FakeWeatherForecastRepository` クラスを追加

```diff_c_sharp:FakeWeatherForecastRepository.cs
using HigeDaruma.DemoNet8BlazorWasm.AppCore.Domain.WeatherModels;

namespace HigeDaruma.DemoNet8BlazorWasm.Infra.Data.WeatherForecastRepositories;

public class FakeWeatherForecastRepository : IWeatherForecastRepository
{
    public async Task<WeatherForecast> ReadAsync(DateOnly date)
    {
        int temperatureC = 25;
        WeatherForecast weatherForecast = new(date, temperatureC);

        return await Task.FromResult(weatherForecast);
    }
}
```

2\. DI の登録

先ほど登録した `RandomWeatherForecastRepository` を `FakeWeatherForecastRepository` に差し替えます。

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

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

### 動作確認
すべて同じデータが取得できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1107938/6510bf58-323e-91f9-0887-0891d9b15a94.png)

# まとめ

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

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

満足のいくアーキテクチャーが構築できました。
これでぐっすり眠れます。