Rückgabe eines 404 von einem explizit typisierten ASP.NET Core API-Controller (nicht IActionResult)


73

ASP.NET Core API-Controller geben normalerweise explizite Typen zurück (und dies standardmäßig, wenn Sie ein neues Projekt erstellen).

[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...
}

Das Problem ist, dass return null;- es ein HTTP zurückgibt 204: Erfolg, kein Inhalt.

Dies wird dann von vielen clientseitigen Javascript-Komponenten als Erfolg angesehen, daher gibt es Code wie:

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

Eine Online-Suche (wie diese Frage und diese Antwort ) weist auf hilfreiche return NotFound();Erweiterungsmethoden für den Controller hin, aber alle diese Rückgaben IActionResultsind nicht mit meinem Task<Thing>Rückgabetyp kompatibel . Das Designmuster sieht folgendermaßen aus:

// 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);
}

Das funktioniert, aber um es zu verwenden, muss der Rückgabetyp von GetAsyncgeändert werden Task<IActionResult>- die explizite Typisierung geht verloren, und entweder müssen alle Rückgabetypen auf dem Controller geändert werden (dh es wird überhaupt keine explizite Typisierung verwendet), oder es wird eine Mischung geben, bei der Einige Aktionen befassen sich mit expliziten Typen, während andere. Außerdem müssen Unit-Tests jetzt Annahmen über die Serialisierung treffen und den Inhalt des Where explizit deserialisieren, IActionResultbevor sie einen konkreten Typ hatten.

Es gibt viele Möglichkeiten, dies zu umgehen, aber es scheint ein verwirrender Mischmasch zu sein, der leicht herausgearbeitet werden kann. Die eigentliche Frage lautet also: Was ist der richtige Weg, den die ASP.NET Core-Designer beabsichtigen?

Es scheint, dass die möglichen Optionen sind:

  1. Haben Sie eine seltsame (chaotisch zu testende) Mischung aus expliziten Typen und IActionResultabhängig vom erwarteten Typ.
  2. Vergessen Sie explizite Typen, sind sie nicht wirklich von Core MVC unterstützt, immer Gebrauch IActionResult(in diesem Fall , warum sind sie überhaupt vorstellen?)
  3. Schreiben Sie eine Implementierung HttpResponseExceptionund verwenden Sie es wie ArgumentOutOfRangeException(siehe diese Antwort für eine Implementierung). Dies erfordert jedoch die Verwendung von Ausnahmen für den Programmablauf, was im Allgemeinen eine schlechte Idee ist und auch vom MVC Core-Team abgelehnt wird .
  4. Schreiben Sie eine Implementierung HttpNoContentOutputFormatterdieser Rückgabe 404für GET-Anforderungen.
  5. Fehlt mir noch etwas, wie Core MVC funktionieren soll?
  6. Oder gibt es einen Grund, warum eine fehlgeschlagene GET-Anforderung 204richtig und 404falsch ist?

All dies beinhaltet Kompromisse und Refactoring, die etwas verlieren oder eine scheinbar unnötige Komplexität hinzufügen, die im Widerspruch zum Design von MVC Core steht. Welcher Kompromiss ist der richtige und warum?


1
@ Hackerman Hallo, hast du die Frage gelesen? Ich bin mir dessen besonders bewusst StatusCode(500)und es funktioniert nur mit Aktionen, die zurückkehren IActionResult, auf die ich dann näher eingehen werde.
Keith

1
@ Hackerman nein, das ist es nicht. Das funktioniert nur mit IActionResult. Ich frage nach Aktionen mit expliziten Typen . Ich erkundige mich weiter nach der Verwendung von IActionResultim ersten Aufzählungspunkt, aber ich frage nicht, wie StatusCode(404)ich anrufen soll - ich weiß es bereits und zitiere es in der Frage.
Keith

1
Für Ihr Szenario könnte die Lösung so etwas wie return new HttpResponseMessage(HttpStatusCode.NotFound);... auch so lauten : docs.microsoft.com/en-us/aspnet/core/mvc/models/formattingFor non-trivial actions with multiple return types or options (for example, different HTTP status codes based on the result of operations performed), prefer IActionResult as the return type.
Hackerman

1
@ Hackerman Sie haben dafür gestimmt, meine Frage als Betrug einer Frage zu schließen, die ich gefunden, gelesen und durchgearbeitet hatte, bevor ich diese Frage gestellt habe, und eine, die ich in der Frage als nicht die gesuchte Antwort angesprochen habe. Offensichtlich bin ich in die Defensive gegangen - ich möchte eine Antwort auf meine Frage, nicht im Kreis zurückweisen. Ihr letzter Kommentar ist tatsächlich nützlich und beginnt zu sprechen, worüber ich eigentlich frage - Sie sollten ihn zu einer vollständigen Antwort ausarbeiten.
Keith

1
Ok, ich habe weitere Informationen zu diesem Thema ... , um so etwas zu erreichen (nach wie vor denke ich , dass der beste Ansatz verwenden sollten IActionResult), können Sie diesem Beispiel folgen , public Item Get(int id) { var item = _repo.FindById(id); if (item == null) throw new HttpResponseException(HttpStatusCode.NotFound); return item; } wo Sie eine zurückkehren können , HttpResponseExceptionwenn thingist null...
Hackerman

Antworten:


72

Dies wird in ASP.NET Core 2.1 behoben mit ActionResult<T>:

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

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

    return thing;
}

Oder auch:

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

Ich werde diese Antwort detaillierter aktualisieren, sobald ich sie implementiert habe.

Ursprüngliche Antwort

In ASP.NET Web API 5 gab es eine HttpResponseException(wie von Hackerman hervorgehoben ), die jedoch aus Core entfernt wurde und für deren Verarbeitung keine Middleware vorhanden ist.

Ich denke, diese Änderung ist auf .NET Core zurückzuführen - wo ASP.NET versucht, alles sofort zu erledigen, macht ASP.NET Core nur das, was Sie ausdrücklich sagen (was ein großer Teil dessen ist, warum es so viel schneller und portabler ist ).

Ich kann keine vorhandene Bibliothek finden, die dies tut, also habe ich sie selbst geschrieben. Zuerst benötigen wir eine benutzerdefinierte Ausnahme, um Folgendes zu überprüfen:

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

    public HttpStatusCode StatusCode { get; set; }
}

Dann brauchen wir einen RequestDelegateHandler, der nach der neuen Ausnahme sucht und sie in den HTTP-Antwortstatuscode konvertiert:

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();
        }
    }
}

Dann registrieren wir diese Middleware in unserem Startup.Configure:

public class Startup
{
    ...

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

Schließlich können Aktionen die HTTP-Statuscode-Ausnahme auslösen und dennoch einen expliziten Typ zurückgeben, der ohne Konvertierung von IActionResult:

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

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

    return thing;
}

Dies behält die expliziten Typen für die Rückgabewerte bei und ermöglicht eine einfache Unterscheidung zwischen erfolgreichen leeren Ergebnissen ( return null;) und einem Fehler, da etwas nicht gefunden werden kann (ich denke, es ist wie das Auslösen eines ArgumentOutOfRangeException).

Obwohl dies eine Lösung für das Problem ist, beantwortet es meine Frage immer noch nicht wirklich - die Designer der Web-API erstellen Unterstützung für explizite Typen mit der Erwartung, dass sie verwendet werden, und fügten eine spezifische Behandlung hinzu return null;, damit eher ein 204 erzeugt wird als eine 200, und dann keine Möglichkeit hinzugefügt, mit 404 umzugehen? Es scheint eine Menge Arbeit zu sein, etwas so Grundlegendes hinzuzufügen.


Ich denke, wenn die Routenressource gültig ist, aber nichts zurückgibt, sollte die richtige Antwort 204 (kein Inhalt) sein. Wenn die Route nicht existiert, sollte eine 404-Antwort (nicht gefunden) zurückgegeben werden. macht Sinn, oder?
Hackerman

1
@ Hackerman Ich vermute, dass dies eine Option sein könnte, aber viele clientseitige APIs verallgemeinern (1xx drauf ... 2xx ok, 3xx hier, 4xx du hast es falsch gemacht, 5xx wir haben es falsch gemacht) - 2xx impliziert, dass alles ist OK, wenn der Benutzer tatsächlich eine Ressource angefordert hat, die nicht vorhanden ist (wie im Beispiel in meiner Frage betrachtet die Fetch-API 204 als OK, um fortzufahren). Ich nehme an, 204 könnte richtigen Ressourcenpfad, falsche Ressourcen-ID bedeuten, aber das war nicht mein Verständnis. Gibt es ein Zitat dazu als das gewünschte Muster?
Keith

1
Schauen Sie sich diesen Artikel an racksburg.com/choosing-an-http-status-code ... Ich denke, dass das Flussdiagramm für die Statuscodes 2xx / 3xx es sehr gut erklärt ... Sie können sich auch die anderen ansehen :) \
Hackerman

@ Hackerman Das deutet immer noch darauf hin, dass 404 in diesem Zusammenhang korrekt ist.
Keith

1
@ Hackerman Da bin ich mir wirklich nicht sicher - es scheint, als würde man versuchen, Informationen über die API in der API zu vermitteln. Wenn wir das tun, sollten wir dann nicht auch 405 (statt 404) für gültige Controller ohne die angeforderte Aktion implementieren und so weiter? In REST wird die Ressource zurückgegeben, nicht die API selbst. Ich denke, deshalb ist die Konvention, dass Namen Plural sind (und nicht Singular wie in einer DB) - api/things/5ist die Ressource, die erwartet, eine einzige Sache zu sein .
Keith

15

Sie können tatsächlich IActionResultoder Task<IActionResult>anstelle von Thingoder Task<Thing>oder sogar verwenden Task<IEnumerable<Thing>>. Wenn Sie eine API haben, die JSON zurückgibt , können Sie einfach Folgendes tun:

[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)
    {
    }
}

Aktualisieren

Es scheint , als ob die Sorge ist , dass sein persönliches Vertrauen in der Rückkehr einer API irgendwie hilfreich ist, während es möglich ist , wird explizit es in der Tat nicht sehr nützlich ist. Wenn Sie Komponententests schreiben, die die Anforderungs- / Antwort-Pipeline ausführen, überprüfen Sie normalerweise die unformatierte Rückgabe (die höchstwahrscheinlich JSON ist , dh eine Zeichenfolge in C # ). Sie können einfach die zurückgegebene Zeichenfolge nehmen und sie für Vergleiche mit wieder in das stark typisierte Äquivalent konvertieren Assert.

Dies scheint das einzige Manko bei der Verwendung von IActionResultoder zu sein Task<IActionResult>. Wenn Sie wirklich, wirklich explizit sein und dennoch den Statuscode festlegen möchten, gibt es mehrere Möglichkeiten, dies zu tun - aber es ist verpönt, da das Framework bereits einen eingebauten Mechanismus dafür hat, dh; Verwenden der IActionResultWrapper für die Rückgabemethode in der ControllerKlasse. Sie können jedoch eine benutzerdefinierte Middleware schreiben , um dies zu handhaben, wie Sie möchten.

Abschließend möchte ich darauf hinweisen, dass ein Statuscode von 204 tatsächlich korrekt ist , wenn ein API-Aufruf nullgemäß W3 zurückgegeben wird. Warum um alles in der Welt möchten Sie einen 404 ?

204

Der Server hat die Anforderung erfüllt, muss jedoch keinen Entitätskörper zurückgeben und möchte möglicherweise aktualisierte Metainformationen zurückgeben. Die Antwort kann neue oder aktualisierte Metainformationen in Form von Entity-Headern enthalten, die, falls vorhanden, der angeforderten Variante zugeordnet werden sollten.

Wenn der Client ein Benutzeragent ist, DARF er seine Dokumentansicht NICHT von der Ansicht ändern, die das Senden der Anforderung verursacht hat. Diese Antwort soll in erster Linie die Eingabe von Aktionen ermöglichen, ohne die aktive Dokumentansicht des Benutzeragenten zu ändern, obwohl alle neuen oder aktualisierten Metainformationen auf das Dokument angewendet werden sollten, das sich derzeit in der aktiven Ansicht des Benutzeragenten befindet.

Die Antwort 204 darf keinen Nachrichtentext enthalten und wird daher immer durch die erste leere Zeile nach den Headerfeldern beendet.

Ich denke, der erste Satz des zweiten Absatzes sagt es am besten: "Wenn der Client ein Benutzeragent ist, sollte er seine Dokumentansicht NICHT von der ändern, die das Senden der Anfrage verursacht hat." Dies ist bei einer API der Fall. Im Vergleich zu einem 404 :

Der Server hat nichts gefunden, das mit dem Request-URI übereinstimmt. Es wird kein Hinweis darauf gegeben, ob der Zustand vorübergehend oder dauerhaft ist. Der 410 (Gone) -Statuscode sollte verwendet werden, wenn der Server über einen intern konfigurierbaren Mechanismus weiß, dass eine alte Ressource dauerhaft nicht verfügbar ist und keine Weiterleitungsadresse hat. Dieser Statuscode wird häufig verwendet, wenn der Server nicht genau angeben möchte, warum die Anforderung abgelehnt wurde, oder wenn keine andere Antwort zutreffend ist.

Der Hauptunterschied besteht darin, dass einer eher für eine API und der andere für die Dokumentansicht gilt, d. H. die angezeigte Seite.


Hallo David, Prost. Mir ist klar, dass ich zur Rückkehr wechseln kann IActionResult- aber wenn dies die Antwort ist, warum unterstützt der ASP.NET Core API Controller überhaupt implizite Typkonvertierungen, wenn IActionResult wirklich erforderlich ist?
Keith

1
Prost David, aber es ist nicht - dieser Kommentar ist ein direktes Zitat aus meiner Frage. Auch von meiner Frage suche ich keine Rückgabe NotFound();als Antwort - diese funktionieren nur mit IActionResult..., die explizite Rückgabetypen sinnlos zu machen scheinen. Das ist es, was ich wirklich frage, nicht "Wie verwende ich NotFound()", sondern "Wie soll ich ? " Rückgabe 404 mit expliziter Eingabe "- Ihre Antwort scheint" keine explizite Eingabe verwenden "zu sein, aber wenn dies der Fall ist, fehlen die kritischen Details, warum explizite Eingabe die Standardeinstellung ist (oder überhaupt unterstützt wird)
Keith

1
Dies sollte die richtige Antwort sein ... und in Bezug auf die Statuscodes gibt es verschiedene Implementierungen (einige verwenden einen Code, andere einen anderen), je nachdem, was Sie erreichen möchten und wie ... wenn Sie in 404einigen Fällen einen verwenden möchten und solange deine api gut dokumentiert ist, sollte es kein problem sein.
Hackerman

9
Dies sollte nicht die richtige Antwort sein. Wenn Sie / get / {id} möchten und kein Element dieser ID auf dem Server vorhanden ist, ist ein 404 - nicht gefunden die richtige Antwort darauf. Und was die explizite Typisierung betrifft - die Typinformationen der Methode werden tatsächlich in Tools wie swagger verwendet, die sich auf die Controller-Deklaration verlassen, um die richtige Swagger-Datei generieren zu können. In IActionResult fehlen in diesem Fall viele Informationen.
Sebastian PR Gingter

1
Gut, dass dies nicht als die richtige Antwort akzeptiert wurde. Danke für den Kommentar. :)
David Pine

3

Um so etwas zu erreichen (immer noch, ich glaube , dass der beste Ansatz verwenden sollten IActionResult), können Sie folgen, wo Sie throwein , HttpResponseExceptionwenn Ihr Thingheißt null:

// 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;
}

Prost (+1) - Ich denke, dies ist ein Schritt in die richtige Richtung, aber (beim Testen) HttpResponseExceptionund die Middleware dafür scheinen nicht in .NET Core 1.1 enthalten zu sein. Die nächste Frage lautet: Soll ich meine eigene Middleware rollen oder gibt es ein vorhandenes (idealerweise MS) Paket, das dies bereits tut?
Keith

Es sieht so aus, als wäre dies der Weg, dies in ASP.NET Web API 5 zu tun, aber es wurde in Core gelöscht, was angesichts des manuellen Ansatzes von Core sinnvoll ist. In den meisten Fällen, in denen Core ein Standard-ASP-Verhalten eingestellt hat, gibt es eine neue optionale Middleware, die Sie hinzufügen können Startup.Configure, aber hier scheint es keine zu geben. Stattdessen scheint es nicht allzu schwierig zu sein, einen von Grund auf neu zu schreiben.
Keith

Ok, ich habe eine Antwort darauf ausgearbeitet, die funktioniert, aber die ursprüngliche Frage immer noch nicht beantwortet: stackoverflow.com/a/41484262/905
Keith

0

Was Sie mit der Rückgabe von "Explicit Types" vom Controller tun, wird nicht mit Ihrer Anforderung zusammenarbeiten, explizit mit Ihrem eigenen Antwortcode umzugehen . Die einfachste Lösung ist zu gehen IActionResult(wie andere vorgeschlagen haben); Sie können Ihren Rückgabetyp jedoch auch explizit mithilfe des [Produces]Filters steuern .

Verwenden von IActionResult.

Um die Kontrolle über die Statusergebnisse zu erlangen, müssen Sie a zurückgeben IActionResult, wo Sie dann den StatusCodeResultTyp nutzen können. Jetzt haben Sie jedoch das Problem, ein bestimmtes Format erzwingen zu wollen ...

Die folgenden Informationen stammen aus dem Microsoft-Dokument: Formatieren von Antwortdaten - Erzwingen eines bestimmten Formats

Erzwingen eines bestimmten Formats

Wenn Sie die Antwortformate für eine bestimmte Aktion einschränken möchten, können Sie den [Produces]Filter anwenden . Der [Produces]Filter gibt die Antwortformate für eine bestimmte Aktion (oder einen bestimmten Controller) an. Wie die meisten Filter kann dies auf die Aktion, den Controller oder den globalen Bereich angewendet werden.

Alles zusammenfügen

Hier ist ein Beispiel für die Kontrolle über die StatusCodeResultzusätzlich zur Kontrolle über den "expliziten Rückgabetyp".

// 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);
}

Ich bin kein großer Befürworter dieses Entwurfsmusters, aber ich habe diese Bedenken in einige zusätzliche Antworten auf die Fragen anderer Leute aufgenommen. Sie müssen beachten, dass Sie für den [Produces]Filter den entsprechenden Formatierer / Serializer / Expliziten Typ zuordnen müssen. In dieser Antwort finden Sie weitere Ideen oder in dieser Antwort eine detailliertere Steuerung Ihrer ASP.NET Core-Web-API .


Cheers @Svek, das ist interessant, aber ein bisschen tangential. Ich bin (in dieser Frage sowieso) nicht wirklich besorgt über die Formatierung IActionResult, mit der es möglich ist , sondern vielmehr über die Notwendigkeit, sie überhaupt zu verwenden.
Keith
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.