やりたいこと
- .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の間でやり取りするオブジェクト等
- SolutionName.UI.Client
このままでは Server プロジェクトにビジネスロジックがどんどん書かれて行ってしまいそうなので、分離していきます。
今回は第一弾として AppCore.Domain クラスライブラリを追加して UI プロジェクトから参照します。
UI プロジェクトから Domain を直接参照なんてとんでもない、と思った御仁、安心してください。段階的にレイヤーを追加していきます。
AppCore.Domain プロジェクトの実装
プロジェクトの追加
-
クラスライブラリ プロジェクトを追加
- 名前は
SolutionName.AppCore.Domain
とする - .NET 8 を選択
- 名前は
- 不要な Class1.cs を削除
-
WeatherModels
フォルダーを作成
私は不可算名詞に悩まなくていいように ~Models という名前空間にしちゃいます。
Summary の判定をドメインに実装する
以下の判定ロジックは UI 層ではなくて Domain 層に置きたい。
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();
}
-
WeatherSummary
列挙型をWeatherModels
名前空間に定義
internal enum WeatherSummary
{
Freezing,
Bracing,
Chilly,
Cool,
Mild,
Warm,
Balmy,
Hot,
Sweltering,
Scorching,
}
実装開始時はいつも internal で宣言します。必要になったとき、 public にするのです。
2. WeatherForecast
クラスを WeatherModels
名前空間に定義
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
で一括変換しましょう。
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 プロジェクトから参照する
プロジェクト参照の追加
- プロジェクトの「依存関係」を右クリック、「プロジェクト参照の追加」をクリック
- AppCore.Domain プロジェクトをチェック、「OK」
AppCore.Domain の外から使用するクラスを public に変更
- internal enum WeatherSummary
+ public enum WeatherSummary
- internal sealed class WeatherForecast
+ public sealed class WeatherForecast
初めから public にしとけや、っていう人もいるかもしれませんが、必要になったら public にする、が基本方針です。
WeatherForecast クラスを利用する
UI.Server のランダム生成部分で WeatherForecast
クラスを使うように変更します。
- 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 にします。ついでにプライマリコンストラクターを使った完全コンストラクターパターンに書き換えます。
-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;
+}
インスタンス生成部の変更
[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();
}
動作確認
まとめ
変更されたアーキテクチャー
- SolutionName
-
SolutionName.AppCore.Domain
- ビジネスロジック
- SolutionName.UI.Client
- クライアントアプリ
- SolutionName.UI.Server
- API
- SolutionName.UI.Share
- クライアントアプリとAPIの間でやり取りするオブジェクト等
-
SolutionName.AppCore.Domain
課題
UI 層が Domain 層に直接依存しています。アプリケーションの規模によっては UseCase(Application) 層を用意したいところです。
次回は UseCase 層を追加して、 DI(依存性注入)を実装していこうと思います。