サーバーサイドレンダリングから Web API へ
これまでの解説では、Razor Pages を利用して HTML をブラウザに直接描画するサーバーサイドレンダリング型の ASP.NET Core アプリケーション構築について学びました。本章では、視点を変えて Web API の構築に焦点を当てます。これは、シングルページアプリケーション(SPA)やモバイルアプリのバックエンドとして機能するアプローチです。
Web API 開発においても、これまで習得した MVC デザインパターンの知識の多くを流用できます。ルーティング、モデルバインディング、バリデーションといった概念は共通しており、主な違いは「ビュー」の扱いにあります。HTML を返す代わりに、JSON や XML といったデータ形式で情報を返し、クライアント側がそれを用いて UI を制御または更新します。
ここでは、コントローラーとアクションの定義方法、クライアントアプリケーションが理解できる形式でのデータおよび HTTP ステータスコードの返し方について解説します。また、属性ルートを用いた URL 設計や、ASP.NET Core 2.1 以降で導入された [ApiController] 属性による convention の自動化、さらにコンテンツネゴシエーションを通じたレスポンスフォーマットの制御方法についても触れます。
Web API の役割と適用場面
従来の Web アプリケーションは、ユーザーに対して HTML を返すことで機能します。Razor Pages を使えば、サーバー側でテンプレートエンジンを用いて HTML を生成することが容易です。しかし、現代のアプリケーション開発では、図 9.1 に示すように多様なクライアントを想定する必要があります。
Angular、React、Vue などのフレームワークを用いたクライアント側 SPA が普及しています。これらのアプリケーションは、ブラウザ上で動作する JavaScript によって HTML を生成します。初期ロード時にサーバーから JavaScript ファイルを取得し、その後は HTTP 通信を通じてデータ(主に JSON)のみを取得し、クライアント側で描画を更新します。このデータ提供用のサーバー側エンドポイントを Web API と呼びます。
モバイルアプリも同様に、HTTP Web API を経由してサーバーと通信し、JSON などの汎用フォーマットでデータを受け取り UI を更新します。また、Web API は他のバックエンドサービス連携にも利用されます。例えば、メール送信機能を持つサービスを用意し、他アプリケーションから API を経由して利用させるようなケースです。
Web API を構築すべきかどうかは、アプリケーションの要件によります。クライアントフレームワークの利用予定がある場合や、モバイルアプリとの連携が必要な場合は Web API の導入を検討すべきです。一方で、ブラウザ外からのアクセスが必要なく、サーバーサイドで完結する UI で十分な場合は、Razor Pages のまま効率的に開発を続けられます。なお、後から Web API を追加することも容易であるため、最初はサーバーサイドレンダリングで始め、必要に応じて API を追加するアプローチも有効です。
Web API プロジェクトの作成と構造
ASP.NET Core では、Web API 用のプロジェクトテンプレートが用意されています。Visual Studio の新規プロジェクト作成画面、または CLI で dotnet new webapi コマンドを実行することで作成可能です。
生成されたプロジェクトの Startup.cs(または .NET 6 以降の Program.cs)を確認すると、Razor Pages プロジェクトとは異なる設定が見られます。リスト 9.1 は、Web API テンプレートにおける基本的な設定例です。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// API コントローラーに必要なサービス登録
services.AddControllers();
// Swagger/OpenAPI 文档生成サービスの追加
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApi", Version = "v1" });
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// 開発環境での Swagger UI 有効化
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyApi v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// API コントローラーのエンドポイントマップ
endpoints.MapControllers();
});
}
}
この設定により、アプリケーション内の API コントローラーが自動的に検出され、エンドポイントとして登録されます。また、開発時には Swagger UI を通じて API のテストや探索が可能になります。
コントローラーは、プロジェクト内の任意の場所に作成できますが、慣例として Controllers フォルダに配置されることが多いです。リスト 9.2 は、シンプルな Web API コントローラーの実装例です。
[ApiController]
public class ProductController : ControllerBase
{
// 商品データの模擬リスト
private readonly List<ProductItem> _inventory = new List<ProductItem>
{
new ProductItem { Id = 1, Name = "Notebook" },
new ProductItem { Id = 2, Name = "Pen" },
new ProductItem { Id = 3, Name = "Eraser" }
};
[HttpGet("products")]
public IEnumerable<ProductItem> GetAll()
{
// 商品リストを返却
return _inventory;
}
}
public class ProductItem
{
public int Id { get; set; }
public string Name { get; set; }
}
Web API コントローラーは、通常 ControllerBase クラスを継承し、[ApiController] 属性を付与します。この属性は、Web API 開発における一般的な convention を自動的に適用します。アクションメソッド GetAll は商品リストを返却しており、クライアントには JSON 形式でシリアライズされたデータが 200 OK ステータスと共に返されます。
URL とアクションの紐付けはルーティングによって行われます。[HttpGet("products")] 属性により、このメソッドが HTTP GET 請求かつ URL が /products の場合に実行されることが定義されています。
また、状況に応じてデータだけでなく HTTP ステータスコードを明示的に返すことも重要です。例えば、存在しない ID が指定された場合に 404 Not Found を返すような処理は、リスト 9.3 のように実装できます。
[HttpGet("products/{id}")]
public ActionResult<ProductItem> GetById(int id)
{
var item = _inventory.FirstOrDefault(x => x.Id == id);
if (item == null)
{
// 該当商品がない場合、404 を返す
return NotFound();
}
// 見つかった場合、データを返す(200 OK)
return item;
}
戻り値の型に ActionResult<T> を使用することで、データそのもの(T)と、NotFoundResult などのステータスコードを表す結果の両方を返すことが可能になります。これは OpenAPI 文档との統合性も高める利点があります。
MVC パターンの Web API への適用
ASP.NET Core 以前では、Web API 框架と MVC 框架は別物でしたが、ASP.NET Core では統一されています。Web API コントローラーも、Razor Pages や MVC コントローラーと同じ基盤フレームワーク上で動作します。
リクエスト処理の流れは以下の通りです。
- RoutingMiddleware により URL が適切なコントローラーとアクションにマッチングされます。
- モデルバインディングにより、リクエストデータがアクション引数に結合されます。
- バリデーションが行われ、エラーがあれば
ModelStateに記録されます。 - アクションメソッドが実行され、ビジネスロジックが処理されます。
- 戻り値が出力フォーマッターによって JSON や XML などにシリアライズされ、レスポンスとして返されます。
Razor Pages では最終的に HTML が生成されるのに対し、Web API ではデータモデルがシリアライズされる点が主な差異です。ビジネスロジックはアプリケーションモデル(サービス層)に委譲し、コントローラーは入出力の処理に専念させる設計が推奨されます。
属性ルートによる URL 設計
Web API では、属性ルートを使用して URL とアクションを紐付けます。Razor Pages の @page ディレクティブと同様の概念ですが、C# 属性として記述します。
[Route("api/[controller]")]
public class VehicleController : ControllerBase
{
// GET api/vehicle/start
[HttpGet("start")]
public IActionResult Ignition()
{
return Ok("Engine Started");
}
// GET api/vehicle/speed/100
[HttpGet("speed/{speed:int}")]
public IActionResult SetSpeed(int speed)
{
return Ok($"Speed set to {speed}");
}
}
コントローラーレベルで [Route("api/[controller]")] を定義し、アクションレベルで相対パスを指定することで、重複を防ぎつつ整理された URL 構造を維持できます。[controller] トークンは、クラス名から "Controller" を除いた部分に置換されます。
また、HTTP メソッドごとにアクションを分けることも可能です。同じ URL パスでも、GET と POST で異なる処理を割り当てることができます。
[ApiController]
[Route("api/events")]
public class ScheduleController : ControllerBase
{
// GET api/events - リスト取得
[HttpGet]
public IActionResult List() { /* ... */ }
// POST api/events - 新規作成
[HttpPost]
public IActionResult Create([FromBody] EventDto dto) { /* ... */ }
}
このように HTTP 動詞をルーティングの一部として扱うことは、RESTful な API 設計において一般的です。定義されていない動詞でアクセスされた場合、405 Method Not Allowed が返されます。
[ApiController] 属性による convention の自動化
[ApiController] 属性を付与することで、以下の便利な convention が自動的に適用されます。
- 属性ルートの必須化: 従来の convention ベースのルートは使用不可となります。
- 自動的な 400 応答: モデルバリデーションに失敗した場合、自動的に 400 Bad Request が返されます。手動で
ModelState.IsValidをチェックする必要がなくなります。 - モデルバインディングソースの推論: 複雑な型は自動的にリクエストボディからバインドされます(
[FromBody]属性の省略が可能)。 - ProblemDetails への変換: エラーレスポンスが標準化された
ProblemDetails形式で返されます。
これにより、ボイラープレートコードを大幅に削減できます。ただし、これらの動作をカスタマイズしたい場合は、ConfigureApiBehaviorOptions を使用して設定を変更可能です。
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// 自動的な 400 応答を無効化
options.SuppressModelStateInvalidFilter = true;
});
レスポンスフォーマットとコンテンツネゴシエーション
ASP.NET Core の Web API は、デフォルトで JSON 形式でレスポンスを返します。ASP.NET Core 3.0 以降では、高性能な System.Text.Json が採用されています。必要に応じて、Newtonsoft.Json に切り替えることも可能です。
クライアントが要求するフォーマットに応じてレスポンス形式を変更する仕組みを、コンテンツネゴシエーションと呼びます。クライアントは Accept ヘッダーで希望するメディアタイプを通知し、サーバーはそれに対応できるフォーマッターを選択します。
デフォルトでは JSON のみがサポートされていますが、XML などをサポートするには出力フォーマッターを追加します。
services.AddControllers()
.AddXmlSerializerFormatters();
これにより、Accept: application/xml ヘッダーを含むリクエストに対して XML 形式でレスポンスを返せるようになります。ただし、ブラウザからのリクエスト(Accept: */*)などは、デフォルトで JSON が優先される挙動に注意が必要です。この挙動を変更するには、以下のように設定します。
services.AddControllers(options =>
{
// ブラウザの Accept ヘッダーを尊重する
options.RespectBrowserAcceptHeader = true;
// 文字列型の text/plain フォーマッターを削除
options.OutputFormatters.RemoveType<StringOutputFormatter>();
});
これらの設定を適切に行うことで、多様なクライアントに対応可能な柔軟な Web API を構築できます。