Einen Get-By-ID-Endpunkt in .NET Aspire auf Linux hinzufügen
[[academy-video-youtube({"vid": "hMhA5sLHUpY", "start_time": "0", "title": "Adding a Get By ID Endpoint in .NET Aspire on Linux", "creator": "Tim Corey", "length": "15m 20s"})]]
Die Rückgabe jedes Datensatzes aus einer Datenbanktabelle ist nützlich für Listenseiten, aber die meisten API-Konsumenten müssen auch einen einzelnen Datensatz anhand seines Identifikators abrufen können. Dieser zweite Endpunkt führt Entscheidungen ein, die die "alle abrufen"-Route nicht erforderte: Welcher Rückgabetyp ist sinnvoll für ein einzelnes Objekt im Gegensatz zu einer Sammlung, was passiert, wenn die ID keinem Datensatz entspricht, und wie man diesen Fehler dem Anrufer mit dem korrekten HTTP-Statuscode mitteilt.
In seinem Video "Adding a Get By ID Endpoint in .NET Aspire on Linux" setzt Tim Corey die Tiny Ticket-API fort, indem er ein GET /api/tickets/{id}-Endpunkt hinzufügt. Was als Copy-Paste der bestehenden Route beginnt, entwickelt sich zu einem Live-Durchgang zur Behandlung von Randfällen: Zurückgabe eines einzelnen Objekts statt eines Arrays, Überprüfung auf null-Ergebnisse und Verwendung von TypedResults, um entweder ein 200 OK mit dem Ticket oder ein 404 Not Found zurückzugeben, wenn die ID nicht existiert. Wenn Sie die C# auf Linux-Serie verfolgen oder minimale APIs erstellen, die ordnungsgemäße Statuscode-Antworten benötigen, deckt diese Episode den vollständigen Denkprozess ab.
Kopieren der "alle abrufen" Route als Ausgangspunkt
[0:35 - 1:45] Tim öffnet den Program.cs des API-Dienstes und dupliziert den vorhandenen GET /api/tickets-Endpunkt. Die neue Route benötigt einen Pfadparameter für die Ticket-ID, daher ändert sich das URL-Muster, um {id:int} einzuschließen, und der Handler erhält einen int id-Parameter. Die gespeicherte Prozedurreferenz wechselt von spTickets_GetAll zu spTickets_Get, die einen ID-Parameter erwartet.
app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});
app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});
Ein erwähnenswerter Namenskonvention: der Parameter der gespeicherten Prozedur ist in Kleinbuchstaben id, was genau dem C#-Parametername entspricht. Das bedeutet, dass Dapper das anonyme Objekt new { id } direkt ohne Festlegung eines Eigenschaftsnamen zuordnen kann. Wenn der SQL-Parameter eine andere Großschreibung verwendete, müsste dem anonymen Objekt eine explizite Eigenschaftszuordnung wie new { Id = id } zugewiesen werden.
Rückgabe eines einzelnen Objekts statt einer Liste
[2:39 - 4:16] Der erste Test über Swagger zeigt ein Problem: Die Übergabe der ID 2 führt zu einem 200 mit dem richtigen Ticket, aber der Antwortkörper ist in einem JSON-Array eingeschlossen. Wenn ein Anrufer eine einzelne Ressource per ID anfordert, erwartet er ein einzelnes Objekt, nicht eine Sammlung mit einem Element.
Das Anhängen von .FirstOrDefault() an das Abfrageergebnis behebt das Umschließen. FirstOrDefault gibt das erste Element zurück, wenn die Liste Elemente hat, oder null, wenn die Liste leer ist. Das löst das Array-Problem, wirft jedoch die Frage auf: Was sollte die API zurückgeben, wenn die ID mit keinem Datensatz übereinstimmt?
var output = tickets.FirstOrDefault();
var output = tickets.FirstOrDefault();
Die einzeilige Änderung erzeugt die korrekte Antwortstruktur für vorhandene Datensätze. Das Testen mit ID 4, die nicht in der Datenbank existiert, zeigt jedoch eine tiefere Lücke. Die Antwort kommt als null mit einem 200-Statuscode zurück. Das ist technisch gesehen gültiges HTTP, aber es führt den Anrufer in die Irre: Ein 200 bedeutet, dass die Anfrage erfolgreich war und die Ressource gefunden wurde, obwohl tatsächlich nichts übereinstimmt.
Umgang mit 'Nicht gefunden' mit TypedResults
[4:41 - 11:44] In diesem Abschnitt arbeitet Tim in Echtzeit an Designentscheidungen, was es wertvoll macht, es anzuschauen, anstatt nur den endgültigen Code zu lesen. Sein Gedankengang durchläuft mehrere Iterationen:
Zuerst erwägt er die Verwendung von .First() anstelle von .FirstOrDefault(), das eine Ausnahme auslöst, wenn die Liste leer ist. Das führt zu einem 500-Fehler, was schlimmer ist als ein null 200, weil 500 einen Serverfehler impliziert, nicht eine fehlende Ressource.
Dann geht er einen Schritt zurück und baut eine Null-Prüfung ein. Er speichert das Ergebnis in einer Variablen, prüft, ob es null ist, und gibt in jedem Fall eine andere Antwort. Die Herausforderung besteht darin, dass ein minimaler API-Handler seinen Rückgabetypp explizit deklarieren muss, wenn er mehr als eine Art von Antwort zurückgeben kann.
Die Lösung ist TypedResults, das Ihnen ermöglicht, die möglichen Antworttypen in der Methodensignatur anzugeben:
app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});
app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});
Dieser Rückgabetyp, Task<Results<Ok<TicketModel>, NotFound>>, sagt dem Framework (und Swagger), dass dieser Endpunkt entweder eine 200 mit einem TicketModel-Body oder eine 404 ohne Body produziert. Gefärbtes Klammern-Matching in VS Code hilft bei der Navigation durch die verschachtelten spitzen Klammern, die sich schnell bei generischen Ergebnistypen stapeln.
Ein subtiler Fehler tritt während des Testens auf: die erste Version ruft TypedResults.NotFound() auf, beendet ihn aber nicht mit return. Der Endpunkt kompiliert, weil NotFound() ein gültiger Ausdruck ist, aber ohne das return-Schlüsselwort fällt die Ausführung auf den Ok-Pfad zurück. Tim bemerkt dies, als Swagger immer noch eine 200 für eine fehlende ID anzeigt, fügt das return hinzu und die 404 erscheint korrekt beim nächsten Lauf.
Testen beider Pfade in Swagger
[11:44 - 14:09] Mit dem endgültigen Code führt Tim beide Szenarien in der Swagger UI durch. Die Übergabe der ID 3 ergibt ein 200 mit dem Ticket-Objekt. Die Übergabe der ID 4 ergibt ein 404 mit einem leeren Antwortkörper.
Er weist auch auf ein Detail in der Swagger-Oberfläche hin, das Erstbenutzer verwirren kann: Der Abschnitt "Responses" unter dem Ausführen-Button zeigt die möglichen Antwortcodes (200 und 404), nicht das tatsächliche Ergebnis. Die tatsächliche Serverantwort erscheint in einem separaten Bereich darüber. Die Verwechslung der beiden Bereiche ist eine häufige Ursache für "Warum erhalte ich ein 200?"-Verwirrung.
Die TypedResults-Vorgehensweise verbessert auch automatisch die Swagger-Dokumentation. Da der Rückgabetyp sowohl Ok<TicketModel> als auch NotFound deklariert, zeigt Swagger beide als mögliche Ergebnisse mit ihren entsprechenden Schemata an. Anrufer, die die API-Dokumentation lesen, wissen, dass sie einen 404-Fall behandeln müssen, ohne dass der Entwickler separate OpenAPI-Anmerkungen schreiben muss.
Abschluss: Randfälle vor Funktionen
[14:09 - 15:09] Was als einfaches Copy-Paste des Get-All-Endpunkts begann, entwickelte sich zu einer tieferen Übung im API-Design. Die endgültige Version behandelt den Happy-Path (Datensatz gefunden), den erwarteten Fehler (Datensatz nicht gefunden) und eine defensive Null-Prüfung für unerwartete Szenarien (Abfrage, die null zurückgibt). Tims Ansatz, diese Fälle live durchzuarbeiten anstatt polierten Code zu präsentieren, zeigt die Art von iterativem Denken, die Produktionsendpunkte erfordern.
Abschluss
[15:09 - 15:20] Das Hinzufügen eines get-by-ID-Endpunkts zu einer minimalen API erfordert drei Entscheidungen zusätzlich zur Routendefinition: Verwenden Sie FirstOrDefault, um die Sammlung zu entpacken, überprüfen Sie auf null, um "nicht gefunden" von "gefunden" zu unterscheiden, und deklarieren Sie TypedResults im Rückgabetyp, damit das Framework den richtigen HTTP-Statuscode zurückgibt. Das Results<Ok<t>, NotFound>-Muster ist wiederverwendbar über jeden Endpunkt hinweg, der Erfolg oder Abwesenheit kommunizieren muss.
Seriennavigation: Dieser Artikel ist Teil der C# auf Linux-Serie, die die Tiny Ticket-App aufbaut. Vorher: Hinzufügen von Swagger UI. Nächster Schritt: Hinzufügen eines POST Insert Endpunkts.
Beispiel-Tipp: Wenn Ihr minimaler API-Handler mehrere mögliche Statuscodes zurückgibt, deklarieren Sie diese immer im Results<>-Generikum. Das generiert automatisch genaue Swagger-Dokumentation und zwingt den Compiler zu überprüfen, dass jeder Codepfad einen gültigen Ergebnistyp zurückgibt.
Sehen Sie sich das vollständige Video auf seinem YouTube Kanal an und gewinnen Sie weitere Einblicke in den Aufbau robuster API-Endpunkte in der C# auf Linux-Serie.
