1. ホーム
  2. c#

[解決済み] 明示的に型付けされた ASP.NET Core API コントローラから 404 を返す (IActionResult ではない)

2023-02-23 02:47:59

質問

ASP.NET Core APIコントローラは、通常、次のような明示的な型を返します(新しいプロジェクトを作成するとデフォルトでそのようになります)。

[Route("api/[controller]")]
public class ThingsController : Controller
{
    // GET api/things
    [HttpGet]
    public async Task<IEnumerable<Thing>> GetAsync()
    {
        //...
    }

    // GET api/things/5
    [HttpGet("{id}")]
    public async Task<Thing> GetAsync(int id)
    {
        Thing thingFromDB = await GetThingFromDBAsync();
        if(thingFromDB == null)
            return null; // This returns HTTP 204

        // Process thingFromDB, blah blah blah
        return thing;
    }

    // POST api/things
    [HttpPost]
    public void Post([FromBody]Thing thing)
    {
        //..
    }

    //... and so on...
}

問題なのは return null; - を返しますが、これは HTTP 204 : 成功、コンテンツなし。

これは、多くのクライアントサイドのJavascriptコンポーネントによって成功とみなされるため、次のようなコードがあります。

const response = await fetch('.../api/things/5', {method: 'GET' ...});
if(response.ok)
    return await response.json(); // Error, no content!

ネットでの検索(例えば この質問 この回答 ) は、役に立つ return NotFound(); の拡張メソッドがありますが、これらはすべて IActionResult を返すので、私の Task<Thing> の戻り値とは互換性がありません。そのデザインパターンは次のようなものです。

// GET api/things/5
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id)
{
    var thingFromDB = await GetThingFromDBAsync();
    if (thingFromDB == null)
        return NotFound();

    // Process thingFromDB, blah blah blah
    return Ok(thing);
}

これは動作しますが、これを使用するためには、返り値として GetAsync に変更する必要があります。 Task<IActionResult> - に変更しなければならず、明示的な型付けが失われ、コントローラのすべての戻り値の型を変更しなければならないか(つまり、明示的な型付けを全く使用しない)、あるアクションと他のアクションが明示的な型を扱うようになるかのどちらかになります。さらに、ユニットテストはシリアライゼーションについて仮定し、明示的に IActionResult の内容を明示的にデシリアライズする必要があります。

これを回避する方法はたくさんありますが、簡単に設計できるような混乱した寄せ集めであるように見えますので、本当の疑問は ASP.NET Core の設計者が意図する正しい方法は何でしょうか?

可能な選択肢は以下のようなものだと思われます。

  1. 奇妙な (テストが面倒な) 明示的な型と IActionResult が混在しています。
  2. 明示的な型については、Core MVCではサポートされていませんので、忘れてください。 常に を使用します。 IActionResult (を使用します(この場合、なぜそれらが全く存在しないのでしょうか?)
  3. の実装を書きます。 HttpResponseException のような実装を書き、それを ArgumentOutOfRangeException (のように使うことができます。 この答え を参照)。しかし、その場合、プログラムの流れに例外を使う必要があり、これは一般的に悪い考えであり、また MVC Coreチームによって非推奨とされた .
  4. の実装を書きます。 HttpNoContentOutputFormatter を返す実装を書く。 404 を返します。
  5. Core MVCがどのように動作することになっているのか、私が見逃している他の何か?
  6. または 204 が正しくて 404 は失敗した GET リクエストのために間違っていますか?

これらはすべて、何かを失ったり、MVC Core の設計とは相反する不必要な複雑さを追加したりする妥協とリファクタリングを伴います。どの妥協点が正しいのか、そしてそれはなぜなのでしょうか。

どのように解決するのですか?

これは が ASP.NET Core 2.1 で扱われています。 と共に ActionResult<T> :

public ActionResult<Thing> Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        return NotFound();

    return thing;
}

あるいは、さらに

public ActionResult<Thing> Get(int id) =>
    GetThingFromDB() ?? NotFound();

実装したらもっと詳しくこの回答を更新します。

オリジナルの回答

ASP.NET Web API 5には HttpResponseException (で指摘されたように)。 ハッカーマン によって指摘されました) が、Core から削除され、それを処理するミドルウェアも存在しません。

この変更は.NET Coreによるものだと思います。ASP.NETは箱から出したら何でもやろうとしますが、ASP.NET Coreは特に指示したことだけを行います(これが、より迅速でポータブルである理由の大きな部分です)。

これを行う既存のライブラリを見つけることができないので、自分で書きました。まず、チェックするためのカスタム例外が必要です。

public class StatusCodeException : Exception
{
    public StatusCodeException(HttpStatusCode statusCode)
    {
        StatusCode = statusCode;
    }

    public HttpStatusCode StatusCode { get; set; }
}

次に RequestDelegate ハンドラが必要です。このハンドラは新しい例外をチェックし、それを HTTP レスポンスステータスコードに変換します。

public class StatusCodeExceptionHandler
{
    private readonly RequestDelegate request;

    public StatusCodeExceptionHandler(RequestDelegate pipeline)
    {
        this.request = pipeline;
    }

    public Task Invoke(HttpContext context) => this.InvokeAsync(context); // Stops VS from nagging about async method without ...Async suffix.

    async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await this.request(context);
        }
        catch (StatusCodeException exception)
        {
            context.Response.StatusCode = (int)exception.StatusCode;
            context.Response.Headers.Clear();
        }
    }
}

次に、このミドルウェアを Startup.Configure :

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app)
    {
        ...
        app.UseMiddleware<StatusCodeExceptionHandler>();

最後に、アクションは HTTP ステータスコード例外を投げることができます。 IActionResult :

public Thing Get(int id) {
    Thing thing = GetThingFromDB();

    if (thing == null)
        throw new StatusCodeException(HttpStatusCode.NotFound);

    return thing;
}

これにより、戻り値の明示的な型が維持され、成功した空の結果 ( return null; を投げるようなものだと考えています)。 ArgumentOutOfRangeException ).

これは問題に対する解決策ではありますが、私の疑問に対する本当の答えにはなっていません。Web APIの設計者は、明示的な型が使われることを想定してサポートを構築し、特定の処理を追加して return null; に対して特定の処理を追加し、200ではなく204を生成するようにし、そして404に対処する方法を追加しなかったのでしょうか?それほど基本的なことを追加するのは大変な作業のように思えます。