.NET Core 2.0のリリースによってMicrosoftは、2016年に最初にリリースされた汎用目的でモジュール構造、プラットフォーム不問のオープンソースプラットフォームの次期メジャーバージョンを手にしました。.NET Coreは、現行の.NET Frameworkで利用可能なAPIの大部分を保持するように設計されています。当初はASP.NETソリューションの次世代版を実現するべく開発されたのですが、現在ではIoTやクラウド、次世代モバイルソリューションなど、まったく違うシナリオの原動力であり、基盤的な存在です。このシリーズでは.NET Coreのメリットを探るとともに、従来からの.NET開発者のみでなく、堅牢性、パフォーマンス、経済性を兼ね備えたソリューションを市場に投入する必要のあるすべてのソリューションに寄与しています。
本記事は“.NET Core”シリーズの一部です。新たな記事発行の通知はRSS経由で受信することができます。
インターネットは、私がプロとして開発を始めた20年前はもちろん、5年前と比較してもまったく違う場所になっています。今日ではWeb APIが最新のインターネットを結び、Webアプリケーションとモバイルアプリを支えています。そこで求められているのは、他の開発者が利用することのできる、堅牢なWeb APIを構築するためのスキルです。最新のWebアプリとモバイルアプリを支えるAPIには、トラフィックがパフォーマンスの限界に達してもサービスを継続できるような、安定性と信頼性が求められます。
今回の記事では、ヘキサゴナルアーキテクチャ(Hexagonal Architecture)とポートアダプタ(Ports and Adapters)パターンを使ったASP.NET Core 2.0 Web APIソリューションを、アーキテクチャの面から解説します。まず最初に、最新のWeb APIで有用な.NET CoreとASP.NET Coreの新機能を見ていきます。
この記事で例として紹介するソリューションとすべてのコードは、私のGitHubリポジトリChinookASPNETCoreAPIHexで公開しています。
Web APIにおける.NET CoreとASP.NET Core
ASP.NET Coreは、.NET 1.0から存在する従来型テクノロジを排除するためにMicrosoftが.NET Core上に構築した、まったく新しいWebフレームワークです。これに対してASP.NET 4.6では、WebFormsライブラリをすべて含むSystem.Webassemblyを依然として使用しており、その結果として、最新のASP.NET MVC 5ソリューションが引き続き提供されています。このようなレガシな依存関係を排除し、フレームワークをスクラッチから構築することによって、ASP.NET Core 2.0は、開発者に対して優れたパフォーマンスを提供すると同時に、クロスプラットフォーム運用のためのアーキテクチャを実現しています。ASP.NET Core 2.0開発したソリューションは、Windows上と同じように、Linux上でも動作するのです。
.NET CoreとASP.NET Coreのメリットに関する詳細は、このシリーズの他3つの記事で紹介しています。最初がMaarten Balliauw氏の“Performance isa .NET Core Feature”、次にChris Klug氏の“ASP.NET Core - シンプルの力”、そしてEric Boyd氏による“Azure and .NET Core Are Beautiful Together”です。
アーキテクチャ
優れたAPIの構築には優れたアーキテクチャが必要です。ここではASP.NET Coreの組込み機能からアーキテクチャ哲学、デザインパターンまで、APIの設計と開発をさまざまな面から見ていきたいと思います。このアーキテクチャは周到に計画され、さまざまな面での考慮がされています。では始めましょう。
依存性注入
ASP.NET Core Web APIソリューションのアーキテクチャを掘り下げる前に、.NET Coreでの開発を極めて快適にするメリットのひとつであると私が考えているもの - 依存性注入(Dependency Injection / DI)について論じたいと思います。DIならば.NETフレームワークやASP.NETソリューションにもある、と言うかもしれません。確かにそうなのですが、これまで使用されていたDIは、サードパーティの商用プロバイダや、あるいはオープンソースライブラリからのものでした。それらはよくできていますが、.NET開発者の大多数にとっては大きな学習曲線がありました。また、それぞれのDIライブラリには、処理の方法に独特なものがあったのです。今回の.NET Coreでは、最初からフレームワークにDIが組み込まれています。操作方法も極めて簡単で、特別なセットアップの必要もありません。
APIでDIを使用する理由としては、アーキテクチャ層の分離の実現、データ層のモックを可能にすること、API用に複数のデータソースを構築可能にすること、などが挙げられます。
.NET Core DIフレームワークを使用するには、プロジェクトでMicrosoft.AspNetCore.AllNuGetパッケージ(Microsoft.Extnesions.DependencyInjection.Abstractionspackageへの依存関係を含む)を参照する必要があります。このパッケージのSystem.IServiceProviderインターフェースを持つIServiceCollectionインターフェースにアクセスすることで、GetService<TService>を呼び出すことができます。IServiceCollectionインターフェースから必要なサービスを取得するためには、プロジェクトに必要なサービスを追加する必要があります。
.NET Core Dependency Injectを詳しく学ぶには、“MSDN: Introduction to Dependency Injection in ASP.NET Core”を調べるとよいでしょう。
次に、APIをこのように設計した理由について、その哲学を見ていきます。一般的に、アーキテクチャ設計は2つの側面、すなわち、高度な保守性の実現、実証済みのパターンとアーキテクチャのソリューションへの活用、という2つのアイデアに基づきます。
APIの保守性
エンジニアリングプロセスの保守性とは、欠陥の発見とその修正が容易であること、可動部分に手を入れることなく故障したコンポーネントのみを修理ないし交換可能であること、予期しない誤動作の防止、製品の供用年数の最大化、新たな要件への対応能力、将来的なメンテナンスが容易であること、環境変化への対応などにより、製品の保守が容易であること、などです。十分な計画の下で実施されたアーキテクチャなくしては、これらを実現することは極めて困難です。
保守性は長期的な問題であり、遠く離れた場所からAPIを見ることが必要になります。それを念頭に置いた上で、将来的なビジョンにつながる意思決定を行わなくてはなりません。当面の容易さを求めるような近道であってはならないのです。最初に難しい判断を下すことによって、プロジェクトの長寿命化とユーザの求めるメリットの提供が可能になります。
ソフトウェアアーキテクチャを保守性を高めるものは何でしょう? APIの保守性を評価するにはどうすればよいのでしょうか?
- 変更がシステムの他の部分に影響を与える場合、それを最小限にすることの可能なアーキテクチャか?
- APIはデバッグが容易で、セットアップが簡単でなければならない。また、確立したパターンを持ち、一般的な手法(ブラウザデバッグツールなど)を通じて行われるべきである。
- テストは可能な限り自動化され、明確で、複雑でないものであるべきだ。
インタフェースと実装
今回のAPIアーキテクチャで重要な部分は、C#インターフェースを使って実装の変更を可能にしている点です。C#を使って.NETコードを書いるのであれば、インターフェースを使った経験はお持ちでしょう。このソリューションでは、ドメイン層にコントラクトを構築して、API用に開発された任意のデータ層がデータリポジトリのコントラクトに準拠していることを保証する目的で、インターフェースを使用しています。さらに、APIプロジェクトのコントローラが別に設定されたコントラクトに準拠することで、ドメインプロジェクトのスーパバイザ(Supervisor)のAPIメソッドを処理する手段を獲得することも可能になります。インターフェースは.NET Coreでは非常に重要です。詳細はこちらで再確認してみてください。
ポート・アンド・アダプタパターン
このAPIソリューションでは、全体としてオブジェクトにひとつの責務を持たせたいと考えています。こうすることでオブジェクトがシンプルになり、バグ修正やコード拡張が容易になるからです。コードにこのような“コード臭(code smells)”があるならば、単一責務の原則に反している可能性があります。原則として私は、長さと複雑さからインターフェースコントラクトの実装をチェックしています。メソッドのライン数は制限しませんが、IDEで一度に表示できないようであれば長過ぎるかも知れません。さらに、メソッドの循環的複雑度(cyclomatic complexity)をチェックして、プロジェクトのメソッドや関数の複雑性を判断しています。
ポート・アンド・アダプタパターン(ヘキサゴナルアーキテクチャ)は、ビジネスロジックがデータアクセスやAPIフレームワークなど他の依存関係と強く結び付くという、このような問題を解決するための手法です。このパターンを用いることで、APIソリューションが明確なバウンダリと、単一責務を持った適切な名称のオブジェクトを持つことが可能になり、結果として開発容易性とメンテナンス性が向上します。
このパターンを視覚的に表現すると、六角形の外側にポート、中心近くにアダプタとビジネスロジックを置いた、タマネギのようになります。ポートはアーキテクチャの外部との接続点を表します。外部から使用されるAPIエンドポイントやEntity Framework Core 2.0が使用するデータベース接続がポートに、内部データリポジトリがアダプタに当たります。
次にアーキテクチャの論理セグメントと、デモコード例をいくつか見ていきましょう。
ドメイン層
API層とドメイン層を見る前に、インターフェースを通じてコントラクトを構築する方法と、ビジネスロジックを実装する方法について説明する必要があります。ドメイン層を見てみましょう。ドメイン層には次の機能があります。
- ソリューション全体で使用されるEntitiesオブジェクトを定義します。これらのモデルが、データ層のデータモデル(DataModel)を表します。
- ビューモデル(ViewModel)を定義します。ビューモデルは単一ないし複数のオブジェクトとして、HTTPリクエストとレスポンスのためにAPI層が使用します。
- データ層がデータアクセスロジックを実装するためのインターフェースを定義します。
- スーパバイザを実装します。スーパバイザはAPI層がコールするメソッドを含んでいます。それぞれのメソッドがAPI呼び出しに対応し、注入(inject)されたデータ層をビューモデルに変換して返します。
ドメインエンティティオブジェクトは、APIビジネスロジックが使用するデータの格納と取得に使用されるデータベースを表現しています。エンティティオブジェクトにはそれぞれ、今回のケースであればSQLテーブルで表現されるプロパティが含まれます。例えば、Albumエンティティは次のようになります。
public sealed class Album
{
public int AlbumId { get; set; }
public string Title { get; set; }
public int ArtistId { get; set; }
public ICollection<Track> Tracks { get; set; } = new HashSet<Track>();
public Artist Artist { get; set; }
}
SQLデータベースのAlbumテーブルには、AlbumId、Title、Artistidという3つの列があります。この3つのプロパティがAlbumエンティティの一部として、アーティストの名前、関連するトラックのコレクション、関連するアーティストを表します。APIアーキテクチャの他の層と同じように、このエンティティオブジェクトに対するビューモデルをプロジェクト内に構築します。
ビューモデルはエンティティの拡張で、APIのコンシューマに関する詳細な情報を提供します。Albumビューモデルを見てみましょう。Albumエンティティとよく似ていますが、プロパティが追加されています。このAPI設計では、各Albumが、APIから返されるペイロード内のアーティスト名を持つことにしました。こうすることでAPIコンシューマは、ペイロードにアーティストのビューモデルを返送されることなく、Albumの重要な情報を保持できるようになります(Albumの大規模なセットを返送する場合は特に重要です)。AlbumViewModelの例は次のようになります。
public class AlbumViewModel
{
public int AlbumId { get; set; }
public string Title { get; set; }
public int ArtistId { get; set; }
public string ArtistName { get; set; }
public ArtistViewModel Artist { get; set; }
public IList<TrackViewModel> Tracks { get; set; }
}
ドメイン層に配置される他の領域としては、層の中で定義された各エンティティのインターフェースを通じたコントラクトがあります。ここでも、定義されたインターフェースを示すためにAlbumエンティティを使用します。
public interface IAlbumRepository : IDisposable
{
Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken));
Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken));
Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken));
Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken));
Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken));
Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken));
}
上の例に示したように、インターフェースでは、Albumエンティティのデータアクセス手段を実装するために必要なメソッドを定義しています。それぞれのエンティティオブジェクトとインターフェースは、次のレイヤを明確に定義可能にするために、明確に定義され、単純化されています。
最後は、ドメインプロジェクトの中核となるスーパバイザクラスです。スーパバイザの目的は、エンティティとビューモデル間の変換を行うことと、APIエンドポイントやデータアクセスロジックと切り離されてビジネスロジックを実行することです。スーパバイザがこれを処理することで、変換処理とビジネスロジックも分離され、ビジネスロジックのユニットテストが可能になります。
アルバムを1枚購入して、そのAlbumオブジェクトをAPIエンドポイントに渡す場合のスーパバイザのメソッドを見れば、APIフロントエンドとスーパバイザに注入されたデータアクセスを接続するロジックが確認できます。しかしその場合でも、それぞれは分離されているのです。
public async Task<AlbumViewModel> GetAlbumByIdAsync(int id, CancellationToken ct = default(CancellationToken))
{
var albumViewModel = AlbumCoverter.Convert(await _albumRepository.GetByIdAsync(id, ct));
albumViewModel.Artist = await GetArtistByIdAsync(albumViewModel.ArtistId, ct);
albumViewModel.Tracks = await GetTrackByAlbumIdAsync(albumViewModel.AlbumId, ct);
albumViewModel.ArtistName = albumViewModel.Artist.Name;
return albumViewModel;
}
ドメインプロジェクトのコードとロジックの大部分を保持することによって、すべてのプロジェクトが単一責務の原則を順守できるようになります。
データ層
次に注目するAPIアーキテクチャのレイヤはデータ層です。今回のサンプルソリューションではEntity Framework Core 2.0を使用しているので、Entity Framework CoreのDBContextが定義されているだけでなく、SQLデータベースの各エンティティ用のデータモデルも生成されます。Albumエンティティのデータモデルを例にすれば、3つのプロパティの他に、アルバムに関連するトラックのリストを含むプロパティや、アーティストオブジェクトを含むプロパティもデータベースに格納されることになります。
複数のデータ層を実装することができますが、ドメイン層に記載されている要件に準拠する必要があります。すなわち、データ層の実装はそれぞれがビューモデルと、ドメイン層に詳説されているリポジトリインターフェースで動作しなければならないのです。今回のAPI用に開発したアーキテクチャでは、API層とデータ層のコネクションにリポジトリパターンを使用しています。このために、実装したリポジトリオブジェクトのそれぞれについて、前述の依存性注入(Dependency Injection)を使用しています。依存性注入がどのように用いられているか、API層のコードがどのようになっているのかを見ていきましょう。データ層で重要なのは、各エンティティリポジトリの実装において、ドメイン層で開発したインターフェースを使用していることです。IAlbumRepositoryインターフェースを実装したドメイン層のAlbumリポジトリを例として見てみましょう。それぞれのリポジトリにDBContextが挿入されて、Entity Framework Coreを使用したSQLデータベースへのアクセスが可能になります。
public class AlbumRepository : IAlbumRepository
{
private readonly ChinookContext _context;
public AlbumRepository(ChinookContext context)
{
_context = context;
}
private async Task<bool> AlbumExists(int id, CancellationToken ct = default(CancellationToken))
{
return await GetByIdAsync(id, ct) != null;
}
public void Dispose()
{
_context.Dispose();
}
public async Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken))
{
return await _context.Album.ToListAsync(ct);
}
public async Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken))
{
return await _context.Album.FindAsync(id);
}
public async Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken))
{
_context.Album.Add(newAlbum);
await _context.SaveChangesAsync(ct);
return newAlbum;
}
public async Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken))
{
if (!await AlbumExists(album.AlbumId, ct))
return false;
_context.Album.Update(album);
_context.Update(album);
await _context.SaveChangesAsync(ct);
return true;
}
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken))
{
if (!await AlbumExists(id, ct))
return false;
var toRemove = _context.Album.Find(id);
_context.Album.Remove(toRemove);
await _context.SaveChangesAsync(ct);
return true;
}
public async Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken))
{
return await _context.Album.Where(a => a.ArtistId == id).ToListAsync(ct);
}
}
データ層がすべてのデータアクセスをカプセル化することで、APIのテストストーリがより容易なものになります。ひとつはSQLデータベースストレージ用、もうひとつはクラウドNoSQLストレージモデル用、さらにソリューションのユニットテストのためのモックストレージ用、というように、複数のデータアクセスを実装することが可能です。
API層
最後に確認するレイヤは、今回のAPIコンシューマが対話する領域です。このレイヤにはコントローラなど、APIエンドポイントのロジックのためのコードが含まれています。今回のソリューションでは、APIプロジェクトは単一責務を持って、単にWebサーバの受信したHTTPリクエストを処理し、成功ないし失敗のHTTPレスポンスを返します。このプロジェクトには、最小限のビジネスロジックしかありません。また、ドメインないしデータプロジェクトで発生したエラーや例外を処理して、APIのコンシューマと効果的なコミュニケーションを行います。このコミュニケーションではHTTPレスポンスコードの他、HTTPレスポンスのボディ内で戻されるデータが使用されます。
ASP.NET Core 2.0 Web APIでは、ルーティングは属性ルーティング(Attribute Routing)を使用して処理されます。ASP.NET Coreの属性ルーティングについては、こちらに詳しく説明されています。今回のサンプルでは、コントローラにスーパバイザを割り当てるために依存性注入も使用しています。各コントローラのActionメソッドには、APIコールのロジックを処理するスーパバイザが対応しています。これらの概念を示すために、Albumコントローラの一部を紹介しましょう。
[Route("api/[controller]")]
public class AlbumController : Controller
{
private readonly IChinookSupervisor _chinookSupervisor;
public AlbumController(IChinookSupervisor chinookSupervisor)
{
_chinookSupervisor = chinookSupervisor;
}
[HttpGet]
[Produces(typeof(List<AlbumViewModel>))]
public async Task<IActionResult> Get(CancellationToken ct = default(CancellationToken))
{
try
{
return new ObjectResult(await _chinookSupervisor.GetAllAlbumAsync(ct));
}
catch (Exception ex)
{
return StatusCode(500, ex);
}
}
...
}
今回紹介したソリューションのWeb APIプロジェクトは、ごく単純で小規模なものです。将来的に違う形式のインタラクションに置き換え可能なように、ソリューションのコードをできる限り少なくしました。
結論
ここで示したように、優れたASP.NET Core 2.0 Web APIソリューションを設計し開発するには、各レイヤがテスト可能にするための分離型アーキテクチャを採用し、単一責務の原則に従うための洞察力が必要です。今回提供した情報によって、読者の皆さんが、自身の組織のニーズに沿った実用的なWeb APIを開発し、維持することが可能になればと思います。
著者について
Chris Woodruff(Woody)氏は、ミシガン州立大学の工学部でコンピュータサイエンスの学位を取得しています。氏は20年以上にわたってソフトウエアソリューションの設計と開発に従事しており、さまざまなプラットフォームとツールを使用した経験を持ちます。氏はコミュニティのリーダとして、GRDevNight、GRDevDay、West Michigan Day of .NET、CodeMash等のイベントを支援しています。さらに氏は、人気のあるGive Campイベントをウェスタンミシガン州に紹介することで、地元の非営利団体のサポートに技術専門者が自らの時間と開発経験を提供するために貢献しました。さらに講演者およびポッドキャスタとして、データベース設計やオープンソースなどのさまざまな話題を論じています。氏はVisual C#、データプラットフォーム、SQLのMicrosoft MVPでもあり、2010年には世界トップ20のMVPのひとりとして認められています。現在はJetBrainsのデベロッパアドボケートとして.NET、.NET Core、および北米でのJetBrains製品の普及に努めています。
.NET Core 2.0のリリースによってMicrosoftは、2016年に最初にリリースされた汎用目的でモジュール構造、プラットフォーム不問のオープンソースのプラットフォームの次期メジャーバージョンを手にしました。.NET Coreは、現行の.NET Frameworkで利用可能なAPIの大部分を保持するように設計されています。当初はASP.NETソリューションの次世代版を実現するべく開発されたのですが、現在ではIoTやクラウド、次世代モバイルソリューションなど、まったく違うシナリオの原動力であり、基盤的な存在です。このシリーズでは.NET Coreのメリットを探るとともに、従来からの.NET開発者のみでなく、堅牢性、パフォーマンス、経済性を兼ね備えたソリューションを市場に投入する必要のあるすべてのソリューションに寄与しています。
本記事は“.NET Core”シリーズの一部です。新たな記事発行の通知はRSS経由で受信することができます。