1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

やりたいこと

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

前回の記事

環境

  • 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 プロジェクトから参照します。

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

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

プロジェクトの追加

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

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

Summary の判定をドメインに実装する

以下の判定ロジックは UI 層ではなくて Domain 層に置きたい。

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 名前空間に定義
WeatherSummary.cs
internal enum WeatherSummary
{
    Freezing,
    Bracing,
    Chilly,
    Cool,
    Mild,
    Warm,
    Balmy,
    Hot,
    Sweltering,
    Scorching,
}

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

2. WeatherForecast クラスを WeatherModels 名前空間に定義

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 で一括変換しましょう。

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);
}

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

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

プロジェクト参照の追加

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

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

WeatherSummary.cs
- internal enum WeatherSummary
+ public enum WeatherSummary
WeatherForecast.cs
- internal sealed class WeatherForecast
+ public sealed class WeatherForecast

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

WeatherForecast クラスを利用する

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

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 にします。ついでにプライマリコンストラクターを使った完全コンストラクターパターンに書き換えます。

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;
+}

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

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

まとめ

変更されたアーキテクチャー

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

課題

UI 層が Domain 層に直接依存しています。アプリケーションの規模によっては UseCase(Application) 層を用意したいところです。
次回は UseCase 層を追加して、 DI(依存性注入)を実装していこうと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?