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 IActionResult
sind 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 GetAsync
geä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, IActionResult
bevor 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:
- Haben Sie eine seltsame (chaotisch zu testende) Mischung aus expliziten Typen und
IActionResult
abhängig vom erwarteten Typ. - Vergessen Sie explizite Typen, sind sie nicht wirklich von Core MVC unterstützt, immer Gebrauch
IActionResult
(in diesem Fall , warum sind sie überhaupt vorstellen?) - Schreiben Sie eine Implementierung
HttpResponseException
und verwenden Sie es wieArgumentOutOfRangeException
(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 . - Schreiben Sie eine Implementierung
HttpNoContentOutputFormatter
dieser Rückgabe404
für GET-Anforderungen. - Fehlt mir noch etwas, wie Core MVC funktionieren soll?
- Oder gibt es einen Grund, warum eine fehlgeschlagene GET-Anforderung
204
richtig und404
falsch 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?
StatusCode(500)
und es funktioniert nur mit Aktionen, die zurückkehrenIActionResult
, auf die ich dann näher eingehen werde.IActionResult
. Ich frage nach Aktionen mit expliziten Typen . Ich erkundige mich weiter nach der Verwendung vonIActionResult
im ersten Aufzählungspunkt, aber ich frage nicht, wieStatusCode(404)
ich anrufen soll - ich weiß es bereits und zitiere es in der Frage.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.
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 ,HttpResponseException
wennthing
istnull
...Antworten:
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
RequestDelegate
Handler, 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 einesArgumentOutOfRangeException
).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.quelle
api/things/5
ist die Ressource, die erwartet, eine einzige Sache zu sein .Sie können tatsächlich
IActionResult
oderTask<IActionResult>
anstelle vonThing
oderTask<Thing>
oder sogar verwendenTask<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
IActionResult
oder zu seinTask<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 derIActionResult
Wrapper für die Rückgabemethode in derController
Klasse. 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
null
gemäß W3 zurückgegeben wird. Warum um alles in der Welt möchten Sie einen 404 ?204
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 Hauptunterschied besteht darin, dass einer eher für eine API und der andere für die Dokumentansicht gilt, d. H. die angezeigte Seite.
quelle
IActionResult
- aber wenn dies die Antwort ist, warum unterstützt der ASP.NET Core API Controller überhaupt implizite Typkonvertierungen, wenn IActionResult wirklich erforderlich ist?NotFound();
als Antwort - diese funktionieren nur mitIActionResult
..., die explizite Rückgabetypen sinnlos zu machen scheinen. Das ist es, was ich wirklich frage, nicht "Wie verwende ichNotFound()
", 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)404
einigen Fällen einen verwenden möchten und solange deine api gut dokumentiert ist, sollte es kein problem sein.Um so etwas zu erreichen (immer noch, ich glaube , dass der beste Ansatz verwenden sollten
IActionResult
), können Sie folgen, wo Siethrow
ein ,HttpResponseException
wenn IhrThing
heißtnull
:// 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; }
quelle
HttpResponseException
und 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?Startup.Configure
, aber hier scheint es keine zu geben. Stattdessen scheint es nicht allzu schwierig zu sein, einen von Grund auf neu zu schreiben.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 denStatusCodeResult
Typ 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
Alles zusammenfügen
Hier ist ein Beispiel für die Kontrolle über die
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 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 .quelle
IActionResult
, mit der es möglich ist , sondern vielmehr über die Notwendigkeit, sie überhaupt zu verwenden.