はじめに
担当:インターン生🐻🐰
この記事は
- ASP.NET Core(Razor PagesやMVC)を学び始めた方
- C#を独学していて、これから実務に入る方
- 実務で初めてRazorPages(PageModel)・Service・Repository構成に触れた方
向けに書いています。
私たちは授業の一環でアクティブフュージョンズにインターンで来ている実習生です。
実習中の課題としてC#・ASP.NET Core Razor Pagesを使用して書籍管理システムを開発しました。
提示されたサンプルコードを見たとき、PageModel・Service・Repositoryと複数のクラスに処理が分かれていました。
そのため、「なぜここまで細かく分ける必要があるのだろう?」と疑問に感じていました。
しかし、開発を始めたばかりの頃は、お手本となるプロジェクトを参考に実装するだけで精一杯で、それぞれの役割を意識する余裕はありませんでした。
この記事では、私たちが実際に開発した書籍管理システムを例に、PageModel・Service・Repositoryの役割と、なぜ役割を分けるのかについて紹介します。
開発環境
書籍管理システムでは、以下の環境を使用しました。
- C#
- ASP.NET Core Razor Pages
- Entity Framework Core
- MariaDB
- AdminLTE
また、本システムはASP.NET Core Razor Pagesを採用しているため、ControllerではなくPageModel(.cshtml.cs)が画面処理を担当しています。
インターネット上にある記事では「Controller / Service / Repository」の構成が紹介されることが多いです。
本記事ではASP.NET Core Razor Pagesで使用するPageModelを例に説明します。
| MVC | Razor Pages |
|---|---|
| Controller | PageModel (.cshtml.cs) |
| View | .cshtml |
| Service | Service |
| Repository | Repository |
ControllerとPageModelは仕組みこそ異なりますが、「画面からのリクエストを受け取り、Serviceへ処理を依頼する」という役割は共通しています。
PageModel・Service・Repositoryの役割
実際のプロジェクトでは、機能ごとにService・Repositoryを分けています。
例えば、今回開発した書籍管理システムでは、次のように機能ごとにクラスを分割しています。
- BookService / BookRepository(書籍管理)
- BorrowService / BorrowRepository(貸出・返却)
- EmployeeService / EmployeeRepository(社員管理)
ここからは、BookService / BookRepositoryの中でも書籍登録機能を例に、それぞれの具体的な役割と処理の流れを紹介します。
書籍登録機能における全体の処理は、次のような流れで実行されます。
PageModel・Service・Repositoryの処理の流れ
① PageModel
PageModelは、利用者からのリクエストを受け取り、入力内容を確認したうえでServiceへ処理を依頼する役割を担当します。
PageModelの主な役割
- 利用者からのリクエストを受け取る
- 入力内容をチェック(検証)する
- 画面用のデータを、システム内部用のデータに変換する
- Serviceへ処理を依頼する
- 処理の完了後に、適切な画面へ遷移させる
public async Task<IActionResult> OnPostAsync()
{
// ①入力チェック
// 画面の入力内容(ISBNやタイトルなど)に漏れや不整合がないか検証
if (!ModelState.IsValid)
{
return Page();
}
// ②ViewModelからEntityへ変換
// 画面から受け取ったViewModelを、保存に適したEntity(Bookクラス)へ変換
var book = _mapper.Map<Book>(Book);
// ③Serviceへ登録処理を依頼
// データの準備ができたらBookServiceへバトンを渡し、登録処理を依頼
await _bookService.AddBookAsync(book);
// ④登録後は一覧画面へ戻る
// すべての処理が無事に完了したら、一覧画面(Index)へと画面遷移
return RedirectToPage("Index");
}
このように、PageModelは画面処理(入力チェックや画面遷移など)に専念し、具体的な登録処理はBookServiceへ依頼しています。
② BookService
BookServiceは、書籍に関する業務ロジック(システムが満たすべきルールや処理)を担当します。
BookServiceの主な役割
- 業務ロジックを実行する
- 必要に応じて他のServiceや外部機能と連携する
- Repositoryへ処理を依頼する
public async Task AddBookAsync(Book book)
{
// ①ISBNの整形
// 入力揺れを防ぐため、ハイフン(`-`)を取り除き、前後の不要な空白を削除
book.ISBN = book.ISBN.Replace("-", "").Trim();
// ②ISBNチェック
// 整形したISBNが、正しい形式のルールになっているか検証
if (!IsValidIsbn(book.ISBN))
{
throw new ArgumentException("ISBNが正しくありません。");
}
// ③Google Books APIからISBNをキーにして情報を取得
// 外部APIと連携し、インターネット上から不足している書籍詳細データを自動で取得
if (!string.IsNullOrWhiteSpace(book.ISBN))
{
var info = await _googleBooksService.GetBookInfoAsync(book.ISBN);
if (info != null)
{
// ④取得した情報を設定
// APIからデータが取得できた場合、説明文やページ数、カテゴリを自動でセット
book.Description = info.Description;
book.PageCount = info.PageCount;
book.Category = info.Category;
}
}
// ⑤Repositoryへ保存を依頼
// すべての業務ルールをクリアした安全なデータを、Repositoryへ渡してデータベース保存を依頼
await _bookRepository.AddAsync(book);
}
このように、BookServiceには「登録前に必要な処理」が集約されています。
そのため、画面処理はPageModel、書籍登録の手順はBookServiceというように役割を分けています。
また、Serviceは画面処理とデータベース処理の間に入り、
アプリケーション固有の業務ロジックをまとめる役割を担っています。
③ BookRepository
BookRepositoryは、データベースとの直接的なやり取り(データアクセス)のみを担当します。
BookRepositoryの主な役割
- 書籍データの登録・取得・更新・削除を行う
// 書籍登録の処理
public async Task AddAsync(Book book)
{
// ① データベースへの追加
// 渡された書籍データを、データベースへの追加対象として登録
await _dbContext.Books.AddAsync(book);
// ② 変更内容を確定して保存
// `SaveChangesAsync()` を実行し、変更内容をデータベースへ保存
await _dbContext.SaveChangesAsync();
}
※ 実際のシステムではジェネリックリポジトリを使用していますが、役割を分かりやすくするため一部簡略化しています。
データベースへの登録や取得などのデータアクセス処理をRepositoryへまとめることで、
Serviceはデータベースの実装を意識せず利用できます。
それぞれの役割をまとめると、次のような構成になります。
| クラス | 主な役割 |
|---|---|
| PageModel | 画面からのリクエストを受け取り、Serviceへ処理を依頼する |
| BookService | 業務ロジックを実行し、Repositoryへ処理を依頼する |
| BookRepository | Entity Framework Coreを利用してデータベースへアクセスする |
まとめ
画面表示や業務ルール、データベース操作をすべて1つのファイルに書いてしまうと、コードが長くなって見通しが悪くなってしまいます。
それを防ぐため、今回は「関心の分離」を意識し、以下の3つに役割を分けて実装しました。
- PageModel:リクエスト受付・入力チェック・画面遷移
- Service:業務ロジック・外部API連携
- Repository:データベースアクセス(データの保存・取得)
開発当初は、「ここまで細かく分ける必要があるの?」と疑問でした。
しかし、貸出機能やAPI連携を実装していく中で、その価値を強く実感しました。
役割ごとにファイルが分かれているため、不具合の原因特定や修正がスムーズに行えます。
また、クラスごとに影響範囲が限定されるおかげで、既存の処理を壊すことなく、安心して新しい機能を追加できました。
「とりあえず1つのファイルに書く」のではなく、「役割を意識してコードを分ける」設計にする。
この考え方を意識することで、コードの読みやすさも開発の進めやすさも大きく変わります。
この記事が、これからASP.NET CoreやRazor Pagesで開発を始める方の参考になれば幸いです。