やりたいこと
- .NET 8 で構築した ASP.NET Core でホストされた Blazor WebAssembly アプリからビジネスロジックを分離したい
- UI プロジェクトと Domain プロジェクトを作ったので、橋渡しをするレイヤーを追加する
前回までの記事
現在のアーキテクチャー
- SolutionName
- SolutionName.AppCore.Domain
- ビジネスロジック
- SolutionName.UI.Client
- クライアントアプリ
- SolutionName.UI.Server
- API
- SolutionName.UI.Share
- クライアントアプリとAPIの間でやり取りするオブジェクト等
- SolutionName.AppCore.Domain
問題点
ビジネスロジックに UI が直接依存してしまっている
Domain に置いたビジネスロジックは他の UI やコンポーネントからも使いたい。そんなときに UI から Domain を直接参照していると UI 毎に複雑な変換ロジックが書かれてしまうかもしれません。十分に小さいシステムであれば問題にはなりませんが、今回は UI と Domain の間に UseCase 層を追加してみます。
環境
- Windows 11 23H2
- Visual Studio Community 2022 Version 17.9.6
手順
AppCore.UseCase プロジェクトの実装
プロジェクトを追加
- クラスライブラリ プロジェクトを追加
- 名前は SolutionName.AppCore.Domain とする
- .NET 8 を選択
- 不要な Class1.cs を削除
-
WeatherForecastUseCases
フォルダーを追加
出力用 DTO を追加
-
WeatherForecastUsecases
の下にWeatherForecastOutput
クラスを追加
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;
}
UseCase 層から UI 層へ渡される DTO(Data Transfer Object) です。
UseCase のインターフェイスを追加
-
WeatherForecastUsecases
の下にIGetWeatherForecastUseCase
インターフェイスを追加
namespace SolutionName.AppCore.UseCase.WeatherForecastUsecases;
internal interface IGetWeatherForecastUseCase
{
public Task<WeatherForecastOutput> ExecuteAsync(DateOnly date);
}
WeatherForecast を取得する UseCase です。
プロジェクト参照の追加
UseCase から Domain を使うためのプロジェクト参照を追加します。
- プロジェクトの「依存関係」を右クリック、「プロジェクト参照の追加」をクリック
- AppCore.Domain プロジェクトをチェック、「OK」をクリック
UseCase を実装
-
WeatherForecastUsecases
の下にGetWeatherForecastUseCase
クラスを追加
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 プロジェクトから参照する
プロジェクト参照の追加
- プロジェクトの「依存関係」を右クリック、「プロジェクト参照の追加」をクリック
- AppCore.UseCase プロジェクトをチェック
- AppCore.Domain プロジェクトをチェック解除、「OK」をクリック
以後 UI プロジェクトは直接 Domain プロジェクトを参照しません。
AppCore.UseCase の外から使用するクラスを public に変更
-internal sealed class WeatherForecastOutput(
+public sealed class WeatherForecastOutput(
-internal interface IGetWeatherForecastUseCase
+public interface IGetWeatherForecastUseCase
-internal sealed class GetWeatherForecastUseCase : IGetWeatherForecastUseCase
+public sealed class GetWeatherForecastUseCase : IGetWeatherForecastUseCase
またもや「初めから public にしとけや」って声が聞こえた気がしますが、必要になったら public にする、が基本方針です。
UI.Server から IGetWeatherForecastUseCase を使用する
- WeatherForecastController に IGetWeatherForecastUseCase を注入する
-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 を使うことができます。
UseCase へのプロジェクト参照により推移的に Domain へのプロジェクト参照が残ってしまうことに注意しましょう。
2. UseCase を使用する
WeatherForecast のインスタンスを生成していた箇所で IGetWeatherForecastUseCase を使います。入力する日付はそのまま(DateTime.Now
から5日間)の仕様にしておきます。
[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 を注入するように登録します。
+// DI
+builder.Services.AddScoped<IGetWeatherForecastUseCase, GetWeatherForecastUseCase>();
var app = builder.Build();
using Microsoft.Extensions.DependencyInjection;
は global usings に追加されています。
動作確認
まとめ
変更後のアーキテクチャー
- SolutionName
- SolutionName.AppCore.Domain
- ビジネスロジック
- SolutionName.AppCore.UseCase
- アプリケーション固有のビジネスロジック
- SolutionName.UI.Client
- クライアントアプリ
- SolutionName.UI.Server
- API
- SolutionName.UI.Share
- クライアントアプリとAPIの間でやり取りするオブジェクト等
- SolutionName.AppCore.Domain
問題点
- WeatherForecast の生成を UseCase で行っている
- 今後データベースや外部の API を使ってデータを取得しようとした場合に UseCase を変更しなければならない
- データソースの切り替えが困難
次回は Infra.Data 層を追加して、永続化ロジックを選択できるように実装します。