web-dev-qa-db-ja.com

明示的に型指定されたASP.NET Core APIコントローラー(IActionResultではない)から404を返す

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!

オンライン検索( this questionthis answer など)は、コントローラーの便利な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. 明示的な型は忘れてください。CoreMVCでは実際にはサポートされていません。alwaysIActionResultを使用します(この場合、なぜ存在するのですか?)
  3. HttpResponseExceptionの実装を記述し、ArgumentOutOfRangeExceptionのように使用します(実装については この回答 を参照)。ただし、プログラムフローに例外を使用する必要があります。これは一般に悪い考えであり、また MVCコアチームによって非推奨 です。
  4. GET要求に対して404を返すHttpNoContentOutputFormatterの実装を記述します。
  5. Core MVCがどのように機能するかについて私が見逃していることはありますか?
  6. または、GET要求の失敗に対して204が正しく、404が間違っている理由はありますか?

これらはすべて、妥協とリファクタリングを伴い、MVC Coreの設計と矛盾して、何かを失ったり、不必要に思われるものを追加したりします。どの妥協が正しいものであり、なぜですか?

46
Keith

これは ASP.NET Core 2.1で対応 with 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にはHttpResponseExceptionHackerman で指摘されているように)がありましたが、Coreから削除されており、それを処理するミドルウェアはありません。

この変更は.NET Coreによるものだと思います-ASP.NETはすぐにすべてを実行しようとしますが、ASP.NET Coreはユーザーが具体的に指定したことのみを実行します(これが非常に高速でポータブルな理由の大部分です) )。

これを行う既存のライブラリが見つからないため、自分で作成しました。最初に確認するカスタム例外が必要です:

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

    public HttpStatusCode StatusCode { get; set; }
}

次に、新しい例外をチェックし、それをHTTP応答ステータスコードに変換するRequestDelegateハンドラーが必要です。

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を処理する方法を追加しませんでしたか?とても基本的なものを追加するのは大変な作業のようです。

44
Keith

実際には、IActionResultまたはTask<IActionResult>またはTask<Thing>の代わりにThingまたはTask<IEnumerable<Thing>>を使用できます。 JSONを返すAPIがある場合は、次のことを行うだけです。

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

    // 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); // This will be JSON by default
    }

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

更新

懸念は、APIの戻り値にexplicitであることは何らかの形で役立つことであるようですが、explicit実際、あまり役に立ちません。要求/応答パイプラインを実行する単体テストを作成している場合、通常は生の戻り値(JSONである可能性が高いことを確認します。つまり、C#)。返された文字列を単純に取得し、Assertを使用した比較のために、厳密に型指定された同等の文字列に戻すことができます。

これは、IActionResultまたはTask<IActionResult>を使用する場合の唯一の欠点のようです。本当に明示的になりたいのにステータスコードを設定したい場合は、いくつかの方法がありますが、フレームワークには既にこのためのメカニズムが組み込まれているため、それは嫌われています。 IActionResultクラスのメソッドラッパーを返すControllerを使用します。ただし、これを処理するために カスタムミドルウェア を記述することもできます。

最後に、APIコールがW3のステータスコード204に従ってnullを返す場合、 は実際には正確です。一体なぜ404が欲しいのでしょうか?

204

サーバーは要求を満たしましたが、エンティティ本体を返す必要はなく、更新されたメタ情報を返したい場合があります。応答には、エンティティヘッダーの形式で新規または更新されたメタ情報が含まれる場合があります。これは、存在する場合、要求されたバリアントに関連付けられる必要があります。

クライアントがユーザーエージェントである場合、リクエストの送信を引き起こしたドキュメントビューからドキュメントビューを変更すべきではありません。この応答は、ユーザーエージェントのアクティブなドキュメントビューを変更せずにアクションの入力を許可することを主な目的としていますが、新規または更新されたメタ情報は、ユーザーエージェントのアクティブビューに現在あるドキュメントに適用される必要があります。

204応答にはメッセージ本文を含めてはならない(MUST NOT)ため、ヘッダーフィールドの後の最初の空行で常に終了します。

2番目の段落の最初の文は、「クライアントがユーザーエージェントである場合、要求の送信を引き起こしたドキュメントビューからドキュメントビューを変更すべきではない」と最もよく言っていると思います。これはAPIの場合です。 404と比較して:

サーバーは、Request-URIに一致するものを見つけられませんでした。状態が一時的なものか永続的なものかは示されていません。 410(Gone)ステータスコードは、サーバーが内部で構成可能なメカニズムを通じて、古いリソースが永続的に利用できず、転送アドレスがないことを知っている場合に使用する必要があります。このステータスコードは、サーバーが要求が拒否された正確な理由を明らかにしたくない場合、または他の応答が適用できない場合に一般的に使用されます。

主な違いは、1つがAPIに、もう1つがドキュメントビューに適用されることです。表示されるページ。

14
David Pine

そのようなことを達成するために(まだ、IActionResultを使用するのが最善のアプローチだと思います)、あなたはthrowHttpResponseExceptionがあなたのThingnullです:

// GET api/things/5
[HttpGet("{id}")]
public async Task<Thing> GetAsync(int id)
{
    Thing thingFromDB = await GetThingFromDBAsync();
    if(thingFromDB == null){
        throw new HttpResponseException(HttpStatusCode.NotFound); // This returns HTTP 404
    }
    // Process thingFromDB, blah blah blah
    return thing;
}
3
Hackerman

コントローラーから "Explicit Types"を返すことで何をしているのかは、独自の要件応答コード。最も簡単な解決策は、IActionResultを使用することです(他の人が提案しているように)。ただし、[Produces]フィルターを使用して戻り値の型を明示的に制御することもできます。

IActionResultを使用します。

ステータスの結果を制御する方法は、IActionResult型を利用できる場所であるStatusCodeResultを返す必要があることです。しかし、今では特定の形式を強制したいという問題があります...

以下は、Microsoftドキュメントからのものです。 応答データのフォーマット-特定のフォーマットの強制

特定の形式を強制する

可能な特定のアクションの応答形式を制限する場合は、[Produces]フィルターを適用できます。 [Produces]フィルターは、特定のアクション(またはコントローラー)の応答形式を指定します。ほとんどの Filters と同様に、これはアクション、コントローラー、またはグローバルスコープで適用できます。

すべてを一緒に入れて

「明示的な戻り値の型」の制御に加えて、StatusCodeResultの制御の例を次に示します。

// GET: api/authors/search?namelike=foo
[Produces("application/json")]
[HttpGet("Search")]
public IActionResult Search(string namelike)
{
    var result = _authorRepository.GetByNameSubstring(namelike);
    if (!result.Any())
    {
        return NotFound(namelike);
    }
    return Ok(result);
}

私はこのデザインパターンの大後援者ではありませんが、他の人の質問に対するいくつかの追加の回答にそれらの懸念を入れています。 [Produces]フィルターは、適切なフォーマッター/シリアライザー/明示的なタイプにマップする必要があることに注意する必要があります。 この回答 をご覧になるか、またはこれを ASP.NET Core Web APIのよりきめ細かい制御を実装する でご覧ください。

0
Svek