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:
IActionResult
je nach erwartetem Typ.IActionResult
(in welchem Fall sind sie dann überhaupt vorhanden?)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 .HttpNoContentOutputFormatter
, die 404
für GET-Anforderungen zurückgibt.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?
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.
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.
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.
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 Thing
null
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;
}
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.
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 .