ASP.NET Web APIのコントローラーの戻り値型の使い分け


結論

以下の理由で、エラーレスポンスを返すときはIHttpActionResult型を使うのが良さそうです。

  1. 可読性がよい
  2. デバッグしやすい
  3. テストコードにstubが要らない

エラーレスポンスを返さないときは任意の型を使うのが良いです。

前提

ASP.NET Web APIのコントローラーの戻り値の型

次の4パターンがあります。

  1. 任意の型
  2. HttpResponseMessage
  3. void
  4. IHttpActionResult

IHttpActionResult は ASP.NET Web API 2 から増えました。

このうち

  • HttpResponseMessageは、特に便利ではない
  • voidは204レスポンス固定

ので、今回の議論からは外します。

エラーレスポンスのないAPI

GET /api/pruductsのようなエラーレスポンスを返さないAPIを実装します。

任意の型

public IEnumerable<Product> Get()
{
    return products;
}

メソッドシグネチャで戻り値の型が明示されていて良さげです。

IHttpActionResult

public IHttpActionResult Get()
{
    return Ok(products);
}
  • 戻り値型が固定のIHttpActionResult
  • Ok(product)

が、ちょっと格好わるいです。

エラーレスポンスのないAPIは任意の型を使うのが良さそうです。

エラーレスポンスのあるAPI

GET /api/pruducts/:idのようなエラーレスポンスを返すAPIを実装します。

任意の型

可読性

200 OK

public Product Get(int id)
{
    return products.FirstOrDefault(p => p.Id == id);
}

ここまではエラーレスポンスのないAPIと一緒です。
404などのエラーレスポンスを返す時はどうするのでしょうか?Product型以外はreturnできません。

Product型をHttpResponseMessageのサブクラスにしてレスポンスコードを持たせるのは、面倒です。

404 Not Found

public Product Get(int id)
{
    var product = products.FirstOrDefault(p => p.Id == id);

    if (product == null)
    {
        throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
    }

    return product;
}

HttpResponseExceptionをthrowします。
例外であれば、メソッドの戻り値と違う型をthrowできます。

よくない点

ここで二つの疑問があります。

  1. 例外を作るソースコードnew HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound))がやたら長い
  2. 制御フローに例外を使う

1番目はヘルパーメソッドを書けば解決できそうです。

2番目は例外的状態にだけ例外を使用する - Strategic Choiceによると

パフォーマンスが悪くなる。
可読性が低くなる。
関係ない例外を消費して、他のバグを見逃す危険がある。

の点があげられています。
パフォーマンスはチューニングの問題なので、ここでは考えません。
可読性が悪くてバグを見逃す可能性が高い点を考えると
こうすれば・・・

public Product Get(int id)
{
    var product = products.FirstOrDefault(p => p.Id == id);

    if (product == null)
    {
        _404();
    }

    return product;
}

private void _404()
{
    throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}

可読性はクリアできそうです。

デバッグ

ところで、デバッグしてみましょう。

ハンドルしていない例外を投げると、デバッガはそこで毎回止まります。
不便です。

テスト

controllerのテストを書いてみます。

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace HelloWorldAPI.Controllers.Tests
{
    [TestClass()]
    public class ProductsControllerTests
    {
        [TestMethod()]
        public void GetTest()
        {
            new ProductsController().Get(2);
        }
    }
}

動かしてみると・・・

例外が発生します。

よく見るとArgumentNullExceptionです。Requestプロパティの値がnullなので、起きる例外です。

Requestプロパティに何かしらのstubを設定する必要があります。

IHttpActionResult

可読性

404 Not Found

public IHttpActionResult Get(int id)
{
    var product = products.FirstOrDefault(p => p.Id == id);

    if (product == null)
    {
        return NotFound();
    }

    return Ok(product);
}

最初から読みやすいです。

ヘルパーメソッド

  • Ok
  • NotFound

はApiControllerに定義されたヘルパーメソッドです。

ApiController メソッド (System.Web.Http)に他のメソッド定義があります。

デバッグ

もちろん、デバッガで意図せずに止まりません。

テスト

先ほどのテストコードがそのまま動きます。

まとめ

  1. GET /api/pruductsのようなエラーレスポンスを返さないAPIは任意の型を使うのが簡単
  2. GET /api/pruducts/:idのようなエラーレスポンスを返すAPIはIHttpActionResultを使うのが便利

参考