wake-up-neo.com

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

ASP.NET Core-API-Controller geben normalerweise explizite Typen zurück (und tun 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 gibt einen HTTP 204: Erfolg, keinen Inhalt zurück.

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

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

Eine Online-Suche (z. B. diese Frage und diese Antwort ) zeigt hilfreiche return NotFound();-Erweiterungsmethoden für den Controller an, aber alle diese geben IActionResult zurück, was nicht mit meinem Rückgabetyp Task<Thing> kompatibel ist . Das Designmuster sieht so 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 GetAsync in Task<IActionResult> geändert werden - die explizite Typisierung geht verloren, und entweder müssen alle Rückgabetypen auf dem Controller geändert werden (dh es wird keine explizite Typisierung verwendet) oder es wird ein Mix, wo sich einige Aktionen mit expliziten Typen befassen, andere dagegen. Außerdem müssen Unit-Tests nun Annahmen über die Serialisierung treffen und den Inhalt der IActionResult explizit deserialisieren, wo sie vorher einen konkreten Typ hatten.

Es gibt viele Möglichkeiten, dies zu umgehen, aber es scheint ein verwirrender Mischmasch zu sein, der leicht entworfen werden könnte. Die eigentliche Frage ist 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 ungewöhnliche (unordentlich zu testende) Mischung expliziter Typen und IActionResult je nach erwartetem Typ.
  2. Vergessen Sie explizite Typen, die von Core MVC nicht wirklich unterstützt werden. Always use IActionResult (in welchem ​​Fall sind sie dann überhaupt vorhanden?)
  3. Schreiben Sie eine Implementierung von HttpResponseException und verwenden Sie sie wie ArgumentOutOfRangeException (siehe diese Antwort für eine Implementierung). Dies erfordert jedoch die Verwendung von Ausnahmen für den Programmablauf. Dies ist im Allgemeinen eine schlechte Idee und auch vom MVC-Core-Team verworfen .
  4. Schreiben Sie eine Implementierung von HttpNoContentOutputFormatter, die 404 für GET-Anforderungen zurückgibt.
  5. Was fehlt mir noch, wie Core MVC funktionieren soll?
  6. Oder gibt es einen Grund, warum 204 für eine fehlgeschlagene GET-Anforderung korrekt und 404 falsch ist?

Dies alles beinhaltet Kompromisse und Refactoring, die etwas verlieren oder etwas hinzufügen, was unnötig kompliziert erscheint, was mit dem Design von MVC Core nicht übereinstimmt. Welcher Kompromiss ist der richtige und warum?

35
Keith

Dies ist in ASP.NET Core 2.1 mit ActionResult<T> angesprochen:

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 ausführlicher aktualisieren, sobald ich sie implementiert habe.

Ursprüngliche Antwort

In ASP.NET Web API 5 gab es eine HttpResponseException (wie von Hackerman angegeben), aber es wurde aus Core entfernt und es gibt keine Middleware, die damit umgehen kann.

Ich denke, dass diese Änderung auf .NET Core zurückzuführen ist. ASP.NET versucht, ASP.NET Core nur so zu machen, wie Sie es speziell 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 prüfen:

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

    public HttpStatusCode StatusCode { get; set; }
}

Dann benötigen wir einen RequestDelegate-Handler, der nach der neuen Ausnahme sucht und diese 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-Statuscodeausnahme auslösen, während sie weiterhin einen expliziten Typ zurückgeben, der leicht vom Unit-Test ohne Konvertierung aus IActionResult geprüft werden kann:

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 die einfache Unterscheidung zwischen erfolgreichen leeren Ergebnissen (return null;) und einem Fehler, da etwas nicht gefunden werden kann (ich denke, es ist wie das Abgeben einer ArgumentOutOfRangeException).

Obwohl dies eine Lösung für das Problem ist, beantwortet es meine Frage immer noch nicht wirklich. Die Konstrukteure des Web-API-Builds unterstützen explizite Typen mit der Erwartung, dass sie verwendet werden würden, und fügten eine spezifische Behandlung für return null; hinzu, sodass ein 204 erzeugt wird anstatt einer 200, und fügte dann keine Möglichkeit hinzu, mit 404 umzugehen? Es scheint eine Menge Arbeit zu sein, etwas so grundlegendes hinzuzufügen.

36
Keith

Sie können tatsächlich IActionResult oder Task<IActionResult> anstelle von Thing oder Task<Thing> oder sogar Task<IEnumerable<Thing>> verwenden. Wenn Sie über eine API verfügen, 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)
    {
    }
}

Update

Es scheint, als sei die Sorge, dass explizit in der Rückgabe einer API etwas hilfreich ist, während es möglich ist, explizit zu sein, es ist jedoch nicht sehr nützlich. Wenn Sie Unit-Tests schreiben, die die Request/Response-Pipeline ausüben, werden Sie in der Regel die reine Rendite überprüfen (was höchstwahrscheinlich JSON wäre, dh eine Zeichenfolge in C #. . Sie können die zurückgegebene Zeichenfolge einfach nehmen und für Vergleiche mit Assert in das stark typisierte Äquivalent zurückwandeln. 

Dies scheint der einzige Nachteil bei der Verwendung von IActionResult oder Task<IActionResult> zu sein. Wenn Sie wirklich explizit sein wollen und trotzdem den Statuscode setzen 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, d. H .; Verwenden der zurückgegebenen Wrapper der IActionResult-Methode in der Controller-Klasse. Sie könnten einige benutzerdefinierte Middleware schreiben, um damit umzugehen, wie Sie es möchten.

Abschließend möchte ich darauf hinweisen, dass ein Statuscode von 204 tatsächlich korrekt ist, wenn ein API-Aufruf null gemäß W3 zurückgibt. Warum in aller Welt möchten Sie ein 404?

204

Der Server hat die Anforderung erfüllt, muss jedoch kein .__ zurückgeben. Entity-Body und möchte aktualisierte Metainformationen zurückgeben. Das Antwort KANN neue oder aktualisierte Metainformationen in Form von .__ enthalten. Entity-Header, die, falls vorhanden, dem .__ zugeordnet werden sollten. angeforderte Variante.

Wenn der Client ein Benutzeragent ist, SOLLTE er NICHT seine Dokumentansicht ändern von dem, was die Anfrage gesendet hat. Diese Antwort lautet In erster Linie soll die Eingabe von Aktionen ohne .__ möglich sein. Änderung der aktiven Dokumentansicht des Benutzeragenten, obwohl Jegliche neue oder aktualisierte Metainformation sollte auf das Dokument angewendet werden derzeit in der aktiven Ansicht des Benutzeragenten.

Die Antwort darf NICHT einen Nachrichtentext enthalten und ist daher immer wird durch die erste leere Zeile nach den Kopfzeilenfeldern beendet.

Ich denke, der erste Satz des zweiten Absatzes sagt es am besten: "Wenn der Client ein Benutzeragent ist, SOLLTE er NICHT seine Dokumentansicht von derjenigen ändern, die dazu geführt hat, dass die Anfrage gesendet wurde". Dies ist bei einer API der Fall. Im Vergleich zu einem 404:

Der Server hat nichts gefunden, das mit dem Request-URI übereinstimmt. Nein es wird angezeigt, ob die Bedingung vorübergehend ist oder permanent. Der Statuscode 410 (Gone) SOLLTE verwendet werden, wenn der Server weiß durch einen intern konfigurierbaren Mechanismus, dass ein alter Ressource ist dauerhaft nicht verfügbar und hat keine Weiterleitungsadresse . Dieser Statuscode wird häufig verwendet, wenn der Server nicht .__ wünscht. verraten Sie genau, warum die Anfrage abgelehnt wurde oder wenn kein anderer Antwort ist anwendbar.

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

14
David Pine

Um so etwas zu erreichen (trotzdem denke ich, dass der beste Ansatz IActionResult sein sollte), können Sie folgen, wo Sie throw eine HttpResponseException können, wenn Ihre Thingnull ist:

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

Was Sie mit dem Zurückgeben von "Explicit Types" vom Controller tun, wird nicht mit Ihrer Anforderung kooperieren, um explizit mit Ihrem eigenen Antwortcode umzugehen. Die einfachste Lösung ist die Verwendung von IActionResult (wie von anderen vorgeschlagen). Sie können Ihren Rückgabetyp jedoch auch explizit mit dem [Produces]-Filter steuern.

IActionResult verwenden.

Um die Statusergebnisse unter Kontrolle zu bekommen, müssen Sie eine IActionResult zurückgeben, in der Sie dann den StatusCodeResult-Typ nutzen können. Jetzt haben Sie jedoch das Problem, ein bestimmtes Format erzwingen zu wollen ...

Das folgende Material stammt aus dem Microsoft-Dokument: Formatieren von Antwortdaten - Ein bestimmtes Format formatieren

Erzwingen eines bestimmten Formats

Wenn Sie die Antwortformate für ein bestimmtes .__ einschränken möchten. Aktion, die Sie können, können Sie den [Produces]-Filter anwenden. Das [Produces] filter gibt die Antwortformate für einen bestimmten .__ an. Aktion (oder Controller). Wie die meisten Filter kann dies bei .__ angewendet werden. die Aktion, den Controller oder den globalen Bereich.

Alles zusammenstellen

Hier ist ein Beispiel für die Kontrolle über die Variable StatusCodeResult zusä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 der Filter [Produces] Sie dem entsprechenden Formatter/Serializer/Explicit Type zuordnen muss. Sie könnten diese Antwort für weitere Ideen oder diese für eine genauere Kontrolle Ihrer ASP.NET Core-Web-API implementieren .

0
Svek