次の記事
簡易工程進捗管理システムの改良
前回、Web開発技術の学習を主目的とし、作業・工程の進捗状況をWebブラウザから登録・確認・更新できる簡易工程進捗管理システムを作成した。
前回開発したシステム
今回は改良版の開発を行う。
開発の流れ
フェーズ1 プロジェクト作成・DB設計・API構成整理
Controller → Service → Repository → EF Core という構成を作り、DTOやDIも整理する。
フェーズ2 認証・認可
ログイン、JWT、Admin / WorkerなどのRole制御を実装する。
フェーズ3 業務機能の実装
作業・工程・担当者などのCRUDを、整理したAPI構成で実装する。
フェーズ4 Docker化
Next.js / ASP.NET Core / PostgreSQLをDocker Composeで起動できるようにする。
フェーズ5 Next.js フロントエンド作成
フェーズ6 リアルタイム更新
SignalRを使い、別端末での状態変更を画面へ即時反映させる。
フェーズ7 テスト
xUnit、Integration Test、Playwrightを順番に追加する。
フェーズ8 デプロイ
最後に公開環境を整える。
全体構成
ProductionProgress.sln
│
├─ ProductionProgress.Api
│ └─ Controller / HTTP / DI設定
│
├─ ProductionProgress.Application
│ └─ Service / DTO / Interface
│
├─ ProductionProgress.Domain
│ └─ Entity / 業務ルール
│
└─ ProductionProgress.Infrastructure
└─ AppDbContext / Repository / EF Core / PostgreSQL
Visual Studioでソリューション作成
Visual Studio → 新しいプロジェクトの作成 → 空のソリューション
名前:ProductionProgress
場所:C:\task-management-system-v2\backend
APIプロジェクト作成
ソリューションを右クリック→追加→新しいプロジェクト→ASP.NET Core Web API
プロジェクト名:ProductionProgress.Api
Framework:.NET 10
Class Libraryを3つ作成
ソリューションを右クリック→追加→新しいプロジェクト→クラスライブラリ
※ .NetFrameworkは選ばない
プロジェクト名:
ProductionProgress.Application
ProductionProgress.Domain
ProductionProgress.Infrastructure
プロジェクト参照を設定
プロジェクトを右クリック→追加→プロジェクト参照
Api
├──→ Application
└──→ Infrastructure
Infrastructure
├──→ Application
└──→ Domain
Application
└──→ Domain
Domain
└──→ なし
Swagger UIを追加する
Apiを右クリック→NuGetパッケージの管理→Swashbuckle.AspNetCore
DomainにWorkTaskStatus enumを作る
Domain→Enumsフォルダ→WorkTaskState.cs
namespace ProductionProgress.Domain.Enums;
/// <summary>
/// 作業の進捗状態を表す。
///
/// DBには整数値として保存する。
/// 0 = 未着手
/// 1 = 作業中
/// 2 = 完了
/// </summary>
public enum WorkTaskStatus
{
/// <summary>
/// まだ作業を開始していない状態。
/// </summary>
NotStarted = 0,
/// <summary>
/// 現在作業中の状態。
/// </summary>
InProgress = 1,
/// <summary>
/// 作業が完了した状態。
/// </summary>
Completed = 2
}
DomainにWorkTaskを作る
Domain→Entitiesフォルダ→WorkTask.cs
using ProductionProgress.Domain.Enums;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ProductionProgress.Domain.Entities;
/// <summary>
/// 工程で管理する作業を表すエンティティ。
///
/// Table属性・Column属性を使用して、
/// PostgreSQLのテーブル名・列名との対応を定義する。
/// </summary>
[Table("work_tasks")]
public class WorkTask
{
/// <summary>
/// 作業を一意に識別するID。
/// PostgreSQLではid列に対応する。
/// </summary>
[Key]
[Column("id")]
public int Id { get; set; }
/// <summary>
/// 作業名。
/// 最大200文字、必須項目とする。
/// </summary>
[Required]
[MaxLength(200)]
[Column("task_name")]
public string TaskName { get; set; } = string.Empty;
/// <summary>
/// 作業の進捗状態。
///
/// C#ではWorkTaskStatusとして扱うが、
/// PostgreSQLには整数値として保存される。
/// </summary>
[Column("status")]
public WorkTaskStatus Status { get; set; }
/// <summary>
/// 作業予定日。
/// nullの場合は予定日未設定を表す。
/// </summary>
[Column("planned_date")]
public DateOnly? PlannedDate { get; set; }
/// <summary>
/// データ作成日時。
/// </summary>
[Column("created_at")]
public DateTime CreatedAt { get; set; }
/// <summary>
/// 最終更新日時。
/// </summary>
[Column("updated_at")]
public DateTime UpdatedAt { get; set; }
}
InfrastructureにEF Coreを導入
Infrastructureを右クリック→NuGetパッケージの管理
次をインストール
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
Npgsql.EntityFrameworkCore.PostgreSQL
AppDbContextを作成
Infrastructure→Dataフォルダ→AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using ProductionProgress.Domain.Entities;
namespace ProductionProgress.Infrastructure.Data;
/// <summary>
/// PostgreSQLとの接続を管理するDbContext。
///
/// Entity Framework Coreを使用して、
/// C#のEntityとPostgreSQLのテーブルを橋渡しする。
/// </summary>
public class AppDbContext : DbContext
{
/// <summary>
/// Program.csで設定したDbContextOptionsを受け取る。
/// 接続先DBやDBプロバイダーなどの情報が含まれる。
/// </summary>
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
/// <summary>
/// PostgreSQLのwork_tasksテーブルを操作するためのDbSet。
/// </summary>
public DbSet<WorkTask> WorkTasks => Set<WorkTask>();
}
RepositoryのInterfaceを作る
Application→Interfacesフォルダ→ITaskRepository.cs
using ProductionProgress.Domain.Entities;
namespace ProductionProgress.Application.Interfaces;
/// <summary>
/// タスクのデータ取得方法を定義するインターフェース。
///
/// Application層は、
/// 「PostgreSQLから取得する」
/// 「EF Coreを使用する」
/// といった具体的な実装を知らない。
///
/// そのため、InterfaceだけをApplication側に置く。
/// </summary>
public interface ITaskRepository
{
/// <summary>
/// 登録されているタスクをすべて取得する。
/// </summary>
Task<IReadOnlyList<WorkTask>> GetAllAsync();
}
Repository本体を作る
Infrastructure→Repositoriesフォルダ→TaskRepository.cs
using Microsoft.EntityFrameworkCore;
using ProductionProgress.Application.Interfaces;
using ProductionProgress.Domain.Entities;
using ProductionProgress.Infrastructure.Data;
namespace ProductionProgress.Infrastructure.Repositories;
/// <summary>
/// タスクデータをPostgreSQLから取得するRepository。
///
/// ITaskRepositoryの具体的な実装をInfrastructure層に置くことで、
/// Application層からDBアクセス処理を分離する。
/// </summary>
public class TaskRepository : ITaskRepository
{
private readonly AppDbContext _dbContext;
/// <summary>
/// DIによってAppDbContextを受け取る。
/// </summary>
public TaskRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
/// <summary>
/// タスク一覧をDBから取得する。
/// </summary>
public async Task<IReadOnlyList<WorkTask>> GetAllAsync()
{
// AsNoTrackingを使用すると、
// 取得したデータをEF Coreが変更監視しない。
//
// 一覧表示のような「読み取りだけ」の処理では、
// 不要な監視をしないことで処理を軽くできる。
return await _dbContext.WorkTasks
.AsNoTracking()
.OrderBy(x => x.Id)
.ToListAsync();
}
}
DTOを作る
Application→Dtosフォルダ→TaskDto.cs
namespace ProductionProgress.Application.Dtos;
/// <summary>
/// APIからタスク情報を返すためのDTO。
///
/// DBのEntityをそのまま外部に公開せず、
/// APIで必要なデータだけを返すために使用する。
/// </summary>
public class TaskDto
{
public int Id { get; set; }
public string TaskName { get; set; } = string.Empty;
public WorkTaskStatus Status { get; set; }
public DateOnly? PlannedDate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
Serviceを作る
Application→Servicesフォルダ→ITaskService.cs
using ProductionProgress.Application.Dtos;
namespace ProductionProgress.Application.Services;
/// <summary>
/// タスクに対して行える業務処理を定義する。
/// </summary>
public interface ITaskService
{
/// <summary>
/// タスク一覧を取得する。
/// </summary>
Task<IReadOnlyList<TaskDto>> GetTasksAsync();
}
TaskService.cs
using ProductionProgress.Application.Dtos;
using ProductionProgress.Application.Interfaces;
namespace ProductionProgress.Application.Services;
/// <summary>
/// タスクに関する業務処理を担当するService。
///
/// Controllerから直接Repositoryを呼ばず、
/// Serviceを経由することで、
/// 業務ロジックをControllerから分離する。
/// </summary>
public class TaskService : ITaskService
{
private readonly ITaskRepository _taskRepository;
/// <summary>
/// DIによってRepositoryを受け取る。
/// </summary>
public TaskService(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
/// <summary>
/// タスク一覧を取得する。
/// </summary>
public async Task<IReadOnlyList<TaskDto>> GetTasksAsync()
{
// DBからDomain Entityを取得する。
var tasks = await _taskRepository.GetAllAsync();
// EntityをそのままAPIへ返さず、
// API公開用のDTOへ変換する。
return tasks
.Select(task => new TaskDto
{
Id = task.Id,
TaskName = task.TaskName,
Status = task.Status,
PlannedDate = task.PlannedDate,
CreatedAt = task.CreatedAt,
UpdatedAt = task.UpdatedAt
})
.ToList();
}
}
Controllerを作る
Api→Controllersフォルダ→TasksController.cs
using Microsoft.AspNetCore.Mvc;
using ProductionProgress.Application.Dtos;
using ProductionProgress.Application.Services;
namespace ProductionProgress.Api.Controllers;
/// <summary>
/// タスク操作用のWeb API。
///
/// ControllerはHTTPリクエストを受け付ける入口だけを担当し、
/// DBアクセスや業務処理はServiceへ任せる。
/// </summary>
[ApiController]
[Route("api/tasks")]
public class TasksController : ControllerBase
{
private readonly ITaskService _taskService;
/// <summary>
/// DIによってTaskServiceを受け取る。
/// </summary>
public TasksController(ITaskService taskService)
{
_taskService = taskService;
}
/// <summary>
/// タスク一覧を取得する。
///
/// GET /api/tasks
/// </summary>
[HttpGet]
public async Task<ActionResult<IReadOnlyList<TaskDto>>> GetTasks()
{
var tasks = await _taskService.GetTasksAsync();
return Ok(tasks);
}
}
PostgreSQL接続を設定する
A5:SQL Mk-2(以下、A5M2)でデフォルトDBに接続し、SQLエディタで以下を実行してDBを作成する。
CREATE DATABASE production_progress_v2;
Apiを右クリック→ユーザー シークレットの管理
ここはGit管理されないので、GitHub公開用でも問題なし。
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=production_progress_v2;Username=postgres;Password=自分のパスワード"
}
}
Program.csを設定する
ApiのProgram.csを編集する。
using Microsoft.EntityFrameworkCore;
using ProductionProgress.Application.Interfaces;
using ProductionProgress.Application.Services;
using ProductionProgress.Infrastructure.Data;
using ProductionProgress.Infrastructure.Repositories;
var builder = WebApplication.CreateBuilder(args);
// ------------------------------------------------------------
// Controllerを使用できるようにする。
// ------------------------------------------------------------
builder.Services.AddControllers();
// ------------------------------------------------------------
// SwaggerでAPI情報を取得できるようにする。
// ------------------------------------------------------------
builder.Services.AddEndpointsApiExplorer();
// ------------------------------------------------------------
// Swaggerドキュメントを生成するサービスをDIへ登録する。
//
// 今回のエラーはこの登録がないため、
// ISwaggerProviderを取得できなかったことが原因。
// ------------------------------------------------------------
builder.Services.AddSwaggerGen();
// ------------------------------------------------------------
// PostgreSQLの接続文字列を取得する。
// ------------------------------------------------------------
var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException(
"ConnectionStrings:DefaultConnection が設定されていません。");
// ------------------------------------------------------------
// PostgreSQL用のDbContextをDIへ登録する。
// ------------------------------------------------------------
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
// ------------------------------------------------------------
// RepositoryをDIへ登録する。
// ------------------------------------------------------------
builder.Services.AddScoped<ITaskRepository, TaskRepository>();
// ------------------------------------------------------------
// ServiceをDIへ登録する。
// ------------------------------------------------------------
builder.Services.AddScoped<ITaskService, TaskService>();
var app = builder.Build();
// ------------------------------------------------------------
// 開発環境の場合のみSwaggerを有効にする。
// ------------------------------------------------------------
if (app.Environment.IsDevelopment())
{
// swagger.jsonを生成する。
app.UseSwagger();
// Swagger UIを表示する。
app.UseSwaggerUI();
}
// ------------------------------------------------------------
// HTTPアクセスをHTTPSへリダイレクトする。
// 開発中はSwaggerからHTTP APIを直接確認するため一旦無効化する。
// ------------------------------------------------------------
// app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Migrationを作成
まず、Microsoft.OpenApiの脆弱性警告を解消する。
Apiを右クリック→NuGet パッケージの管理→Microsoft.OpenApi
2.12.2をインストールする。
次に、Microsoft.EntityFrameworkCore.Relationalのバージョン競合を解消する。
Infrastructureを右クリック→NuGet パッケージの管理→
Microsoft.EntityFrameworkCore.Relational
10.0.12
をインストール。
次に、Microsoft.EntityFrameworkCore.Designを追加インストールする。
Apiを右クリック→NuGet パッケージの管理→Microsoft.EntityFrameworkCore.Design
10.0.12
をインストール。
その後、下記を実行する。
ツール→NuGet パッケージ マネージャー→パッケージ マネージャー コンソール
Add-Migration InitialCreate -Project ProductionProgress.Infrastructure -StartupProject ProductionProgress.Api
Update-Database -Project ProductionProgress.Infrastructure -StartupProject ProductionProgress.Api
動作確認
Apiを起動。
ブラウザで下記を実行。
http://localhost:xxxx/swagger
xxxx:ポート番号。
Apiを起動して出力される画面で、
Now listening on: http://localhost:xxxx
のxxxxがポート番号。
GET /api/Tasks→try it out→Execute
Code:200
Response body:[]
が返ってくればOK。
つまずいた点
Add-MigrationでNU1903が発生した
現象
Migration作成
Add-Migration InitialCreate -Project ProductionProgress.Infrastructure -StartupProject ProductionProgress.Api
で、警告が出た。
warning NU1903:
パッケージ 'Microsoft.OpenApi' 2.0.0 に
既知の高重大度の脆弱性があります。
原因
Microsoft.OpenApi 2.0.0 が使用されており、既知の脆弱性警告が発生していた。
対処
ProductionProgress.ApiのMicrosoft.OpenApiを更新した。
Microsoft.OpenApi
2.0.0
↓
2.12.2
Microsoft.EntityFrameworkCore.Relationalのバージョン競合
現象
ビルドすると、以下の警告が出た。
Microsoft.EntityFrameworkCore.Relational
Version=10.0.4.0
Microsoft.EntityFrameworkCore.Relational
Version=10.0.12.0
の間で競合が発生しました。
原因
Package Manager Consoleで以下を実行。
dotnet package list --include-transitive
すると、ProductionProgress.Apiでは以下の状態。
Microsoft.EntityFrameworkCore 10.0.12
Microsoft.EntityFrameworkCore.Relational 10.0.4
Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3
一方、ProductionProgress.Infrastructure は、
Microsoft.EntityFrameworkCore 10.0.12
Microsoft.EntityFrameworkCore.Relational 10.0.12
Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3
になっていた。
Microsoft.EntityFrameworkCore.Relationalは直接追加していなくても、Npgsqlなどの依存関係として自動的に入る。
対処
ProductionProgress.Infrastructureに明示的に、
Microsoft.EntityFrameworkCore.Relational
10.0.12
を追加した。
Add-MigrationでMicrosoft.EntityFrameworkCore.Designがないと言われた
現象
Migrationを作成すると、次のエラーが出。
Your startup project 'ProductionProgress.Api'
doesn't reference Microsoft.EntityFrameworkCore.Design.
原因
コマンドでは、
-StartupProject ProductionProgress.Api
を指定している。
EF Core ToolsはStartupProjectを実際に使用して、以下の情報を取得する。
Program.cs
DI設定
ConnectionString
DbContext設定
そのためStartupProject側にも Microsoft.EntityFrameworkCore.Designが必要。
対処
ProductionProgress.Api に、
Microsoft.EntityFrameworkCore.Design
10.0.12
を追加。
Swaggerを開くとISwaggerProviderエラー
Swaggerを開くと、以下のエラーが出た。
InvalidOperationException:
Unable to resolve service for type
'Swashbuckle.AspNetCore.Swagger.ISwaggerProvider'
while attempting to Invoke middleware
'Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware'.
原因
Program.cs に、
app.UseSwagger();
app.UseSwaggerUI();
は書いてたが、
builder.Services.AddSwaggerGen();
を登録していなかった。
UseSwagger()はSwaggerを使用する側だが、Swagger情報を生成するサービスがDIに登録されていないため、ISwaggerProviderを取得できなかった。
対処
以下を追加。
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();