Tutorial: Erstellen einer minimalen API mit ASP.NET Core
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Von Rick Anderson und Tom Dykstra
Minimal-APIs sind so entworfen, dass HTTP-APIs mit minimalen Abhängigkeiten erstellt werden. Sie eignen sich ideal für Microservices und Apps, die nur ein Minimum an Dateien, Funktionen und Abhängigkeiten in ASP.NET Core enthalten sollen.
In diesem Tutorial lernen Sie die Grundlagen der Erstellung einer minimalen API mit ASP.NET Core kennen. Ein weiterer Ansatz zum Erstellen von APIs in ASP.NET Core ist die Verwendung von Controllern. Hilfe bei der Entscheidung zwischen minimalen APIs und controllerbasierten APIs finden Sie in der Übersicht über APIs. Ein Tutorial zum Erstellen eines API-Projekts basierend auf Controllern, das weitere Features umfasst, finden Sie unter Erstellen einer Web-API.
Übersicht
In diesem Tutorial wird die folgende API erstellt:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Abgeschlossene To-Do-Elemente | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
POST /todoitems |
Neues Element hinzufügen | To-do-Element | To-do-Element |
PUT /todoitems/{id} |
Vorhandenes Element aktualisieren | To-do-Element | Keine |
DELETE /todoitems/{id} |
Löschen eines Elements | Keine | Keine |
Voraussetzungen
Visual Studio 2022 Preview mit der Workload ASP.NET- und Webentwicklung
Erstellen eines API-Projekts
Starten Sie Visual Studio 2022, und wählen Sie Neues Projekt erstellen aus.
Im Dialogfeld Neues Projekt erstellen:
- Geben Sie im Suchfeld
Empty
Nach Vorlagen suchenden Suchbegriff ein. - Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.
- Geben Sie im Suchfeld
Geben Sie dem Projekt den Namen TodoApi, und klicken Sie auf Weiter.
Im Dialogfeld Zusätzliche Informationen:
- Wählen Sie .NET 9.0 (Vorschau) aus.
- Deaktivieren Sie Keine Anweisungen der obersten Ebene verwenden.
- Klicken Sie auf Erstellen
Untersuchen des Codes
Die Datei Program.cs
enthält den folgenden Code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Der vorangehende Code:
- Erstellt eine Instanz von WebApplicationBuilder und WebApplication mit vorkonfigurierten Standardwerten.
- Erstellt den HTTP GET-Endpunkt
/
, derHello World!
zurückgibt:
Ausführen der App
Drücken Sie STRG+F5, um die Ausführung ohne den Debugger zu starten.
In Visual Studio wird das folgende Dialogfeld angezeigt:
Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.
Das folgende Dialogfeld wird angezeigt:
Klicken Sie auf Ja, wenn Sie zustimmen möchten, dass das Entwicklungszertifikat vertrauenswürdig ist.
Informationen dazu, wie Sie dem Firefox-Browser vertrauen, finden Sie unter Firefox-Zertifikatfehler SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio startet den Kestrel-Webserver und öffnet ein Browserfenster.
Hello World!
wird im Browser angezeigt. Die Datei Program.cs
enthält eine Minimal-, aber dennoch vollständige App.
Schließen Sie das Browserfenster.
Hinzufügen von NuGet-Paketen
NuGet-Pakete müssen hinzugefügt werden, um die in diesem Tutorial verwendete Datenbank und Diagnose zu unterstützen.
- Klicken Sie im Menü Extras auf NuGet-Paket-Manager > NuGet-Pakete für Projektmappe verwalten.
- Wählen Sie die Registerkarte Durchsuchen aus.
- Wählen Sie Vorabversion einbeziehen aus.
- Geben Sie Microsoft.EntityFrameworkCore.InMemory in das Suchfeld ein, und wählen Sie
Microsoft.EntityFrameworkCore.InMemory
aus. - Aktivieren Sie das Kontrollkästchen Projekt im rechten Bereich, und klicken Sie dann auf Installieren.
- Folgen Sie den vorstehenden Anweisungen, um das
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
-Paket hinzuzufügen.
Die Modell- und Datenbankkontextklassen
- Erstellen Sie im Projektordner eine Datei mit dem Namen
Todo.cs
und dem folgenden Code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Der vorherige Code erstellt das Modell für diese App. Ein Modell ist eine Klasse, welche die in der App verwalteten Daten repräsentiert.
- Erstellen Sie eine Datei mit dem Namen
TodoDb.cs
und dem folgenden Code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Der vorherige Code definiert den Datenbankkontext, also die Hauptklasse, die die Entity Framework-Funktionen für ein Datenmodell koordiniert. Diese Klasse wird von der Microsoft.EntityFrameworkCore.DbContext-Klasse abgeleitet.
Hinzufügen des API-Codes
- Ersetzen Sie den Inhalt der Datei
Program.cs
durch den folgenden Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der folgende hervorgehobene Code fügt den Datenbankkontext zum Container für die Abhängigkeitsinjektion (Dependency Injection, DI) hinzu und ermöglicht die Anzeige von datenbankbezogenen Ausnahmen:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
Der DI-Container bietet Zugriff auf den Datenbankkontext und andere Dienste.
In diesem Tutorial werden Endpunkte Explorer- und HTTP-Dateien verwendet, um die API zu testen.
Testen der Übertragung von Daten
Der folgende Code in Program.cs
erstellt den HTTP POST-Endpunkt /todoitems
zum Hinzufügen von Daten zur In-Memory-Datenbank:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Führen Sie die App aus. Der Browser zeigt einen 404-Fehler an, da kein /
-Endpunkt mehr vorhanden ist.
Der POST-Endpunkt dient dazu, der App Daten hinzuzufügen.
Wählen Sie Ansicht>Weitere Fenster>Endpunkt-Explorer aus.
Klicken Sie mit der rechten Maustaste auf den POST-Endpunkt, und wählen Sie Anforderung generieren aus.
Im Projektordner wird eine neue Datei mit dem Namen
TodoApi.http
erstellt, deren Inhalt dem folgenden Beispiel ähnelt:@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###
- Die erste Zeile erstellt eine Variable, die für alle Endpunkte verwendet wird.
- In der nächsten Zeile wird eine POST-Anforderung definiert.
- Die Zeile mit dem dreifachen Hashtag (
###
) ist ein Anforderungstrennzeichen: Was danach kommt, gilt für eine andere Anforderung.
Die POST-Anforderung benötigt Header und einen Text. Um diese Teile der Anforderung zu definieren, fügen Sie die folgenden Zeilen unmittelbar nach der POST-Anforderungszeile hinzu:
Content-Type: application/json { "name":"walk dog", "isComplete":true }
Der vorangehende Code fügt einen Content-Type-Header und einen JSON-Anforderungstext hinzu. Die Datei „TodoApi.http“ sollte nun wie im folgenden Beispiel aussehen, jedoch mit Ihrer Portnummer:
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
Führen Sie die App aus.
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der
POST
-Anforderungszeile.Die POST-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Untersuchen der GET-Endpunkte
Die Beispiel-App implementiert mehrere GET-Endpunkte durch Aufrufen von MapGet
:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Alle abgeschlossenen To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Testen der GET-Endpunkte
Testen Sie die App, indem Sie die GET
-Endpunkte in einem Browser aufrufen oder indem Sie den Endpunkte-Explorer verwenden. Die folgenden Schritte gelten für den Endpunkte Explorer.
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den ersten GET-Endpunkt, und wählen Sie Anforderung generieren aus.
Der folgende Inhalt wird der Datei
TodoApi.http
hinzugefügt:Get {{TodoApi_HostAddress}}/todoitems ###
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der neuen
GET
-Anforderungszeile.Die GET-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Der Antworttext ist mit folgendem JSON vergleichbar:
[ { "id": 1, "name": "walk dog", "isComplete": true } ]
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den GET-Endpunkt von
/todoitems/{id}
, und wählen Sie Anforderung generieren aus. Der folgende Inhalt wird der DateiTodoApi.http
hinzugefügt:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
Ersetzen Sie
{id}
durch1
.Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der GET-Anforderungszeile.
Die GET-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Der Antworttext ist mit folgendem JSON vergleichbar:
{ "id": 1, "name": "walk dog", "isComplete": true }
Diese App verwendet eine In-Memory-Datenbank. Wenn die App neu gestartet wird, gibt die GET-Anforderung keinerlei Daten zurück. Wenn keine Daten zurückgegeben werden, übermitteln Sie Daten per POST an die App, und wiederholen Sie die GET-Anforderung.
Rückgabewerte
ASP.NET Core serialisiert automatisch das Objekt in JSON und schreibt den JSON-Code in den Text der Antwortnachricht. Der Antwortcode für diesen Rückgabetyp ist 200 OK, vorausgesetzt, es gibt keine Ausnahmefehler. Nicht behandelte Ausnahmen werden in 5xx-Fehler übersetzt.
Die Rückgabetypen können eine Vielzahl von HTTP-Statuscodes darstellen. Beispielsweise kann GET /todoitems/{id}
zwei verschiedene Statuswerte zurückgeben:
- Wenn kein Element mit der angeforderten ID übereinstimmt, gibt die Methode einen NotFound-Fehlercode Status 404 zurück.
- Andernfalls gibt die Methode 200 mit einem JSON-Antworttext zurück. Die Rückgabe von
item
löst eine HTTP 200-Antwort aus.
Untersuchen des PUT-Endpunkts
Die Beispiel-App implementiert einen einzelnen PUT-Endpunkt mithilfe von MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
Diese Methode ähnelt der MapPost
-Methode, verwendet jedoch HTTP PUT. Eine erfolgreiche Antwort gibt 204 (No Content) zurück. Gemäß der HTTP-Spezifikation erfordert eine PUT-Anforderung, dass der Client die gesamte aktualisierte Entität (nicht nur die Änderungen) sendet. Verwenden Sie HTTP PATCH, um Teilupdates zu unterstützen.
Testen des PUT-Endpunkts
In diesem Beispiel wird eine In-Memory-Datenbank verwendet, die jedes Mal initialisiert werden muss, wenn die App gestartet wird. Es muss ein Element in der Datenbank vorhanden sein, bevor Sie einen PUT-Aufruf durchführen. Rufen Sie vor einem PUT-Aufruf GET auf, um sicherzustellen, dass ein Element in der Datenbank vorhanden ist.
Aktualisieren Sie das Aufgabenelement mit dem Wert Id = 1
, und legen Sie seinen Namen auf "feed fish"
fest.
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den PUT-Endpunkt, und wählen Sie Anforderung generieren aus.
Der folgende Inhalt wird der Datei
TodoApi.http
hinzugefügt:Put {{TodoApi_HostAddress}}/todoitems/{id} ###
Ersetzen Sie in der PUT-Anforderungszeile
{id}
durch1
.Fügen Sie die folgenden Zeilen unmittelbar nach der PUT-Anforderungszeile hinzu:
Content-Type: application/json { "name": "feed fish", "isComplete": false }
Der vorangehende Code fügt einen Content-Type-Header und einen JSON-Anforderungstext hinzu.
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der PUT-Anforderungszeile.
Die PUT-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt. Der Antworttext ist leer, und der Statuscode ist 204.
Überprüfen und Testen des DELETE-Endpunkts
Die Beispiel-App implementiert einen einzelnen DELETE-Endpunkt mithilfe von MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den DELETE-Endpunkt, und wählen Sie Anforderung generieren aus.
Eine DELETE-Anforderung wird zu
TodoApi.http
hinzugefügt.Ersetzen Sie
{id}
in der DELETE-Anforderungszeile durch1
. Die DELETE-Anforderung sollte wie im folgenden Beispiel aussehen:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
Wählen Sie den Link Anforderung senden für die DELETE-Anforderung aus.
Die DELETE-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt. Der Antworttext ist leer, und der Statuscode ist 204.
Verwenden der MapGroup-API
Der Beispiel-App-Code wiederholt bei jeder Einrichtung eines Endpunkts das URL-Präfix todoitems
. APIs enthalten oft Gruppen von Endpunkten mit einem gemeinsamen URL-Präfix, und die MapGroup-Methode hilft bei der Organisation solcher Gruppen. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata.
Ersetzen Sie den Inhalt von Program.cs
durch den folgenden Code.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der vorstehende Code weist die folgenden Änderungen auf:
- Fügt
var todoItems = app.MapGroup("/todoitems");
hinzu, um die Gruppe mithilfe des URL-Präfixes/todoitems
einzurichten. - Ändert alle
app.Map<HttpVerb>
-Methoden intodoItems.Map<HttpVerb>
. - Entfernt das URL-Präfix
/todoitems
aus den Aufrufen derMap<HttpVerb>
-Methode.
Testet die Endpunkte, um zu prüfen, ob sie identisch funktionieren.
Verwenden der TypedResults-API
Die Rückgabe TypedResults anstelle von Results hat mehrere Vorteile, einschließlich Prüfbarkeit und automatische Rückgabe der Metadaten des Antworttyps für OpenAPI, mit denen der Endpunkt beschrieben wird. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
Die Map<HttpVerb>
-Methoden können Routinghandlermethoden aufrufen, anstatt Lambdafunktionen zu verwenden. Um ein Beispiel zu sehen, aktualisieren Sie Program.cs mit folgendem Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Der Map<HttpVerb>
-Code ruft jetzt Methoden anstelle von Lambdafunktionen auf:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
Diese Methoden geben Objekte zurück, die IResult implementieren und von TypedResults definiert werden:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Komponententests können diese Methoden aufrufen und testen, ob sie den richtigen Typ zurückgeben. Wenn die Methode z. B. GetAllTodos
ist:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Der Komponententestcode kann überprüfen, ob ein Objekt des Typs Ok<Todo[]> von der Handlermethode zurückgegeben wird. Beispiel:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Vermeiden von Overposting
Derzeit macht die Beispiel-App das gesamte Todo
-Objekt verfügbar. Produktions-Apps: In Produktionsanwendungen wird häufig nur eine Teilmenge des Modells verwendet, um die Daten einzuschränken, die eingegeben und zurückgegeben werden können. Hierfür gibt es mehrere Gründe, wobei die Sicherheit einer der Hauptgründe ist. Die Teilmenge eines Modells wird üblicherweise als Datenübertragungsobjekt (DTO, Data Transfer Object), Eingabemodell oder Anzeigemodell bezeichnet. In diesem Artikel wird das DTO verwendet.
Ein DTO kann für Folgendes verwendet werden:
- Vermeiden Sie Overposting.
- Ausblenden von Eigenschaften, die Clients nicht einsehen sollen
- Lassen Sie einige Eigenschaften weg, um die Nutzdatengröße zu verringern.
- Vereinfachen von Objektgraphen, die geschachtelte Objekte enthalten Vereinfachte Objektgraphen können für Clients zweckmäßiger sein.
Um den DTO-Ansatz zu veranschaulichen, aktualisieren Sie die Todo
-Klasse, sodass sie ein geheimes Feld einschließt:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
Das geheime Feld muss in dieser App ausgeblendet werden, eine administrative App kann es jedoch verfügbar machen.
Vergewissern Sie sich, dass Sie das geheime Feld veröffentlichen und abrufen können.
Erstellen Sie eine Datei mit dem Namen TodoItemDTO.cs
und dem folgenden Code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Ersetzen Sie den Inhalt der Datei Program.cs
durch folgenden Code, um dieses DTO-Modell zu verwenden:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Vergewissern Sie sich, dass Sie für das Geheimnisfeld POST und GET ausführen können.
Problembehandlung mit dem fertiggestellten Beispiel
Wenn Sie auf ein Problem stoßen, das Sie nicht lösen können, vergleichen Sie Ihren den Code mit dem vollständigen Projekt. Anzeigen oder Herunterladen des fertiggestellten Projekts (Downloadanleitung).
Nächste Schritte
- Konfigurieren von JSON-Serialisierungsoptionen
- Behandeln von Fehlern und Ausnahmen: Die Seite mit Ausnahmen für Entwickler*innen ist in der Entwicklungsumgebung für Minimal-API-Apps standardmäßig aktiviert. Informationen zum Behandeln von Fehlern und Ausnahmen finden Sie unter Behandeln von Fehlern in ASP.NET Core-APIs.
- Ein Beispiel für das Testen einer Minimal-API-App finden Sie in diesem GitHub-Beispiel.
- OpenAPI-Unterstützung in Minimal-APIs
- Schnellstart: Veröffentlichen in Azure
- Organisieren von Minimal-APIs für ASP.NET Core
Weitere Informationen
Weitere Informationen: Kurzreferenz zu Minimal-APIs
Minimal-APIs sind so entworfen, dass HTTP-APIs mit minimalen Abhängigkeiten erstellt werden. Sie eignen sich ideal für Microservices und Apps, die nur ein Minimum an Dateien, Funktionen und Abhängigkeiten in ASP.NET Core enthalten sollen.
In diesem Tutorial lernen Sie die Grundlagen der Erstellung einer minimalen API mit ASP.NET Core kennen. Ein weiterer Ansatz zum Erstellen von APIs in ASP.NET Core ist die Verwendung von Controllern. Hilfe bei der Entscheidung zwischen minimalen APIs und controllerbasierten APIs finden Sie in der Übersicht über APIs. Ein Tutorial zum Erstellen eines API-Projekts basierend auf Controllern, das weitere Features umfasst, finden Sie unter Erstellen einer Web-API.
Übersicht
In diesem Tutorial wird die folgende API erstellt:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Abgeschlossene To-Do-Elemente | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
POST /todoitems |
Neues Element hinzufügen | To-do-Element | To-do-Element |
PUT /todoitems/{id} |
Vorhandenes Element aktualisieren | To-do-Element | Keine |
DELETE /todoitems/{id} |
Löschen eines Elements | Keine | Keine |
Voraussetzungen
Visual Studio 2022 mit der Workload ASP.NET und Webentwicklung
Erstellen eines API-Projekts
Starten Sie Visual Studio 2022, und wählen Sie Neues Projekt erstellen aus.
Im Dialogfeld Neues Projekt erstellen:
- Geben Sie im Suchfeld
Empty
Nach Vorlagen suchenden Suchbegriff ein. - Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.
- Geben Sie im Suchfeld
Geben Sie dem Projekt den Namen TodoApi, und klicken Sie auf Weiter.
Im Dialogfeld Zusätzliche Informationen:
- Wählen Sie .NET 7.0 aus.
- Deaktivieren Sie Keine Anweisungen der obersten Ebene verwenden.
- Klicken Sie auf Erstellen
Untersuchen des Codes
Die Datei Program.cs
enthält den folgenden Code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Der vorangehende Code:
- Erstellt eine Instanz von WebApplicationBuilder und WebApplication mit vorkonfigurierten Standardwerten.
- Erstellt den HTTP GET-Endpunkt
/
, derHello World!
zurückgibt:
Ausführen der App
Drücken Sie STRG+F5, um die Ausführung ohne den Debugger zu starten.
In Visual Studio wird das folgende Dialogfeld angezeigt:
Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.
Das folgende Dialogfeld wird angezeigt:
Klicken Sie auf Ja, wenn Sie zustimmen möchten, dass das Entwicklungszertifikat vertrauenswürdig ist.
Informationen dazu, wie Sie dem Firefox-Browser vertrauen, finden Sie unter Firefox-Zertifikatfehler SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio startet den Kestrel-Webserver und öffnet ein Browserfenster.
Hello World!
wird im Browser angezeigt. Die Datei Program.cs
enthält eine Minimal-, aber dennoch vollständige App.
Hinzufügen von NuGet-Paketen
NuGet-Pakete müssen hinzugefügt werden, um die in diesem Tutorial verwendete Datenbank und Diagnose zu unterstützen.
- Klicken Sie im Menü Extras auf NuGet-Paket-Manager > NuGet-Pakete für Projektmappe verwalten.
- Wählen Sie die Registerkarte Durchsuchen aus.
- Geben Sie Microsoft.EntityFrameworkCore.InMemory in das Suchfeld ein, und wählen Sie
Microsoft.EntityFrameworkCore.InMemory
aus. - Aktivieren Sie im rechten Bereich das Kontrollkästchen Projekt.
- Wählen Sie in der Dropdownliste Version die neueste verfügbare Version 7 (z. B.
7.0.17
) und dann Installieren aus. - Folgen Sie den obigen Anweisungen, um das Paket
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
mit der neuesten verfügbaren Version 7 hinzuzufügen.
Die Modell- und Datenbankkontextklassen
Erstellen Sie im Projektordner eine Datei mit dem Namen Todo.cs
und dem folgenden Code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Der vorherige Code erstellt das Modell für diese App. Ein Modell ist eine Klasse, welche die in der App verwalteten Daten repräsentiert.
Erstellen Sie eine Datei mit dem Namen TodoDb.cs
und dem folgenden Code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Der vorherige Code definiert den Datenbankkontext, also die Hauptklasse, die die Entity Framework-Funktionen für ein Datenmodell koordiniert. Diese Klasse wird von der Microsoft.EntityFrameworkCore.DbContext-Klasse abgeleitet.
Hinzufügen des API-Codes
Ersetzen Sie den Inhalt der Datei Program.cs
durch den folgenden Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der folgende hervorgehobene Code fügt den Datenbankkontext zum Container für die Abhängigkeitsinjektion (Dependency Injection, DI) hinzu und ermöglicht die Anzeige von datenbankbezogenen Ausnahmen:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
Der DI-Container bietet Zugriff auf den Datenbankkontext und andere Dienste.
Erstellen der API-Testbenutzeroberfläche mit Swagger
Es sind viele Web-API-Testtools verfügbar, aus denen Sie auswählen können, und Sie können die einführenden API-Testschritte dieses Tutorials mit Ihrem bevorzugten Tool ausführen.
In diesem Tutorial wird das .NET-Paket NSwag.AspNetCore verwendet, das Swagger-Tools zum Generieren einer Testbenutzeroberfläche integriert, die der OpenAPI-Spezifikation entspricht:
- NSwag: eine .NET-Bibliothek, die Swagger direkt in ASP.NET Core-Anwendungen integriert und Middleware sowie eine Konfiguration bereitstellt
- Swagger: verschiedene Open-Source-Tools wie OpenAPIGenerator und SwaggerUI, die API-Testseiten generieren, die der OpenAPI-Spezifikation entsprechen
- OpenAPI-Spezifikation: ein Dokument, das die Funktionen der API basierend auf den XML- und Attributanmerkungen innerhalb der Controller und Modelle beschreibt
Weitere Informationen zur Verwendung von OpenAPI und NSwag mit ASP.NET finden Sie in der Web-API-Dokumentation von ASP.NET Core mit Swagger/OpenAPI.
Installieren von Swagger-Tools
Führen Sie den folgenden Befehl aus:
dotnet add package NSwag.AspNetCore
Der obige Befehl fügt das Paket NSwag.AspNetCore hinzu, das Tools zum Generieren von Swagger-Dokumenten und der Benutzeroberfläche enthält.
Konfigurieren von Swagger-Middleware
Fügen Sie den folgenden hervorgehobenen Code hinzu, bevor
app
in Zeilevar app = builder.Build();
definiert wird.using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
Im vorherigen Code:
builder.Services.AddEndpointsApiExplorer();
: aktiviert den API-Explorer, bei dem es sich um einen Dienst handelt, der Metadaten zur HTTP-API bereitstellt. Der API-Explorer wird von Swagger verwendet, um das Swagger-Dokument zu generieren.builder.Services.AddOpenApiDocument(config => {...});
: fügt den Anwendungsdiensten den OpenAPI-Dokumentgenerator von Swagger hinzu und konfiguriert ihn, um weitere Informationen zur API bereitzustellen, z. B. den Titel und die Version. Informationen zur Bereitstellung robusterer API-Details finden Sie unter Erste Schritte mit NSwag und ASP.NET Core.Fügen Sie auf der nächsten Zeile den folgenden hervorgehobenen Code hinzu, nachdem
app
in der Zeilevar app = builder.Build();
definiert wurde.var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }
Der obige Code aktiviert die Swagger Middleware für die Bereitstellung des generierten JSON-Dokuments und der Swagger-Benutzeroberfläche. Swagger wird nur in einer Entwicklungsumgebung aktiviert. Das Aktivieren von Swagger in einer Produktionsumgebung könnte potenziell vertrauliche Details zur Struktur und Implementierung der API offenlegen.
Testen der Übertragung von Daten
Der folgende Code in Program.cs
erstellt den HTTP POST-Endpunkt /todoitems
zum Hinzufügen von Daten zur In-Memory-Datenbank:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Führen Sie die App aus. Der Browser zeigt einen 404-Fehler an, da kein /
-Endpunkt mehr vorhanden ist.
Der POST-Endpunkt dient dazu, der App Daten hinzuzufügen.
Wenn die App noch ausgeführt wird, navigieren Sie im Browser zu
https://localhost:<port>/swagger
, um die API-Testseite anzuzeigen, die von Swagger generiert wurde.Wählen Sie auf der API-Testseite von Swagger die Option Post /todoitems>Try it out aus.
Beachten Sie, dass das Feld Anforderungstext ein generiertes Beispielformat enthält, das die Parameter für die API widerspiegelt.
Geben Sie im Anforderungstext JSON-Code für ein Aufgabenelement ohne Angabe der optionalen
id
ein:{ "name":"walk dog", "isComplete":true }
Wählen Sie Execute(Ausführen).
Swagger stellt unter der Schaltfläche Execute einen Bereich Responses bereit.
Beachten Sie einige nützliche Details:
- cURL: Swagger stellt einen cURL-Beispielbefehl in Unix/Linux-Syntax bereit, der an der Befehlszeile mit jeder Bash-Shell ausgeführt werden kann, die Unix/Linux-Syntax verwendet, einschließlich der Git-Bash von Git für Windows.
- Anforderungs-URL: eine vereinfachte Darstellung der HTTP-Anforderung, die durch den JavaScript-Code der Swagger-Benutzeroberfläche für den API-Aufruf erstellt wurde. Tatsächliche Anforderungen können Details wie Header und Abfrageparameter sowie einen Anforderungstext enthalten.
- Serverantwort: enthält den Antworttext und die Header. Der Antworttext zeigt, dass
id
auf1
festgelegt wurde. - Antwort-Code: Es wurde ein Statuscode 201
HTTP
zurückgegeben, der anzeigt, dass die Anforderung erfolgreich bearbeitet wurde und zur Erstellung einer neuen Ressource führte.
Untersuchen der GET-Endpunkte
Die Beispiel-App implementiert mehrere GET-Endpunkte durch Aufrufen von MapGet
:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Alle abgeschlossenen To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Testen der GET-Endpunkte
Testen Sie die App, indem Sie die Endpunkte in einem Browser oder über Swagger aufrufen.
Wählen Sie in Swagger GET /todoitems>Try it out>Execute aus.
Rufen Sie alternativ GET /todoitems in einem Browser auf, indem Sie den URI
http://localhost:<port>/todoitems
eingeben. Beispiel:http://localhost:5001/todoitems
Durch einen Aufruf von GET /todoitems
wird eine Antwort ähnlich der folgenden erzeugt:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
Rufen Sie GET /todoitems/{id} in Swagger auf, um Daten zu einer bestimmten ID zurückzugeben:
- Wählen Sie GET /todoitems>Try it out aus.
- Legen Sie das Feld id auf
1
fest, und wählen Sie Execute aus.
Rufen Sie alternativ GET /todoitems in einem Browser auf, indem Sie den URI
https://localhost:<port>/todoitems/1
eingeben. Beispiel:https://localhost:5001/todoitems/1
Die Antwort ähnelt dem folgenden Code:
{ "id": 1, "name": "walk dog", "isComplete": true }
Diese App verwendet eine In-Memory-Datenbank. Wenn die App neu gestartet wird, gibt die GET-Anforderung keinerlei Daten zurück. Wenn keine Daten zurückgegeben werden, übermitteln Sie Daten per POST an die App, und wiederholen Sie die GET-Anforderung.
Rückgabewerte
ASP.NET Core serialisiert automatisch das Objekt in JSON und schreibt den JSON-Code in den Text der Antwortnachricht. Der Antwortcode für diesen Rückgabetyp ist 200 OK, vorausgesetzt, es gibt keine Ausnahmefehler. Nicht behandelte Ausnahmen werden in 5xx-Fehler übersetzt.
Die Rückgabetypen können eine Vielzahl von HTTP-Statuscodes darstellen. Beispielsweise kann GET /todoitems/{id}
zwei verschiedene Statuswerte zurückgeben:
- Wenn kein Element mit der angeforderten ID übereinstimmt, gibt die Methode einen NotFound-Fehlercode Status 404 zurück.
- Andernfalls gibt die Methode 200 mit einem JSON-Antworttext zurück. Die Rückgabe von
item
löst eine HTTP 200-Antwort aus.
Untersuchen des PUT-Endpunkts
Die Beispiel-App implementiert einen einzelnen PUT-Endpunkt mithilfe von MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
Diese Methode ähnelt der MapPost
-Methode, verwendet jedoch HTTP PUT. Eine erfolgreiche Antwort gibt 204 (No Content) zurück. Gemäß der HTTP-Spezifikation erfordert eine PUT-Anforderung, dass der Client die gesamte aktualisierte Entität (nicht nur die Änderungen) sendet. Verwenden Sie HTTP PATCH, um Teilupdates zu unterstützen.
Testen des PUT-Endpunkts
In diesem Beispiel wird eine In-Memory-Datenbank verwendet, die jedes Mal initialisiert werden muss, wenn die App gestartet wird. Es muss ein Element in der Datenbank vorhanden sein, bevor Sie einen PUT-Aufruf durchführen. Rufen Sie vor einem PUT-Aufruf GET auf, um sicherzustellen, dass ein Element in der Datenbank vorhanden ist.
Aktualisieren Sie das Aufgabenelement mit dem Wert Id = 1
, und legen Sie seinen Namen auf "feed fish"
fest.
Verwenden Sie Swagger, um eine PUT-Anforderung zu senden:
Wählen Sie PUT /todoitems/{id}>Try it out aus.
Legen Sie das Feld id auf
1
fest.Legen Sie den Anforderungstext auf folgenden JSON-Code fest:
{ "name": "feed fish", "isComplete": false }
Wählen Sie Execute(Ausführen).
Überprüfen und Testen des DELETE-Endpunkts
Die Beispiel-App implementiert einen einzelnen DELETE-Endpunkt mithilfe von MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Verwenden Sie Swagger, um eine DELETE-Anforderung zu senden:
Wählen Sie DELETE /todoitems/{id}>Try it out aus.
Legen Sie das Feld ID auf
1
fest, und wählen Sie Execute aus.Die DELETE-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Responses angezeigt. Der Antworttext ist leer, und der Statuscode der Serverantwort lautet 204.
Verwenden der MapGroup-API
Der Beispiel-App-Code wiederholt bei jeder Einrichtung eines Endpunkts das URL-Präfix todoitems
. APIs enthalten oft Gruppen von Endpunkten mit einem gemeinsamen URL-Präfix, und die MapGroup-Methode hilft bei der Organisation solcher Gruppen. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata.
Ersetzen Sie den Inhalt von Program.cs
durch den folgenden Code.
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
config.DocumentName = "TodoAPI";
config.Title = "TodoAPI v1";
config.Version = "v1";
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi(config =>
{
config.DocumentTitle = "TodoAPI";
config.Path = "/swagger";
config.DocumentPath = "/swagger/{documentName}/swagger.json";
config.DocExpansion = "list";
});
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der vorstehende Code weist die folgenden Änderungen auf:
- Fügt
var todoItems = app.MapGroup("/todoitems");
hinzu, um die Gruppe mithilfe des URL-Präfixes/todoitems
einzurichten. - Ändert alle
app.Map<HttpVerb>
-Methoden intodoItems.Map<HttpVerb>
. - Entfernt das URL-Präfix
/todoitems
aus den Aufrufen derMap<HttpVerb>
-Methode.
Testet die Endpunkte, um zu prüfen, ob sie identisch funktionieren.
Verwenden der TypedResults-API
Die Rückgabe TypedResults anstelle von Results hat mehrere Vorteile, einschließlich Prüfbarkeit und automatische Rückgabe der Metadaten des Antworttyps für OpenAPI, mit denen der Endpunkt beschrieben wird. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
Die Map<HttpVerb>
-Methoden können Routinghandlermethoden aufrufen, anstatt Lambdafunktionen zu verwenden. Um ein Beispiel zu sehen, aktualisieren Sie Program.cs mit folgendem Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Der Map<HttpVerb>
-Code ruft jetzt Methoden anstelle von Lambdafunktionen auf:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
Diese Methoden geben Objekte zurück, die IResult implementieren und von TypedResults definiert werden:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Komponententests können diese Methoden aufrufen und testen, ob sie den richtigen Typ zurückgeben. Wenn die Methode z. B. GetAllTodos
ist:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Der Komponententestcode kann überprüfen, ob ein Objekt des Typs Ok<Todo[]> von der Handlermethode zurückgegeben wird. Beispiel:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Vermeiden von Overposting
Derzeit macht die Beispiel-App das gesamte Todo
-Objekt verfügbar. Produktions-Apps: In Produktionsanwendungen wird häufig nur eine Teilmenge des Modells verwendet, um die Daten einzuschränken, die eingegeben und zurückgegeben werden können. Hierfür gibt es mehrere Gründe, wobei die Sicherheit einer der Hauptgründe ist. Die Teilmenge eines Modells wird üblicherweise als Datenübertragungsobjekt (DTO, Data Transfer Object), Eingabemodell oder Anzeigemodell bezeichnet. In diesem Artikel wird das DTO verwendet.
Ein DTO kann für Folgendes verwendet werden:
- Vermeiden Sie Overposting.
- Ausblenden von Eigenschaften, die Clients nicht einsehen sollen
- Lassen Sie einige Eigenschaften weg, um die Nutzdatengröße zu verringern.
- Vereinfachen von Objektgraphen, die geschachtelte Objekte enthalten Vereinfachte Objektgraphen können für Clients zweckmäßiger sein.
Um den DTO-Ansatz zu veranschaulichen, aktualisieren Sie die Todo
-Klasse, sodass sie ein geheimes Feld einschließt:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
Das geheime Feld muss in dieser App ausgeblendet werden, eine administrative App kann es jedoch verfügbar machen.
Vergewissern Sie sich, dass Sie das geheime Feld veröffentlichen und abrufen können.
Erstellen Sie eine Datei mit dem Namen TodoItemDTO.cs
und dem folgenden Code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Ersetzen Sie den Inhalt der Datei Program.cs
durch folgenden Code, um dieses DTO-Modell zu verwenden:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Vergewissern Sie sich, dass Sie für das Geheimnisfeld POST und GET ausführen können.
Problembehandlung mit dem fertiggestellten Beispiel
Wenn Sie auf ein Problem stoßen, das Sie nicht lösen können, vergleichen Sie Ihren den Code mit dem vollständigen Projekt. Anzeigen oder Herunterladen des fertiggestellten Projekts (Downloadanleitung).
Nächste Schritte
- Konfigurieren von JSON-Serialisierungsoptionen
- Behandeln von Fehlern und Ausnahmen: Die Seite mit Ausnahmen für Entwickler*innen ist in der Entwicklungsumgebung für Minimal-API-Apps standardmäßig aktiviert. Informationen zum Behandeln von Fehlern und Ausnahmen finden Sie unter Behandeln von Fehlern in ASP.NET Core-APIs.
- Ein Beispiel für das Testen einer Minimal-API-App finden Sie in diesem GitHub-Beispiel.
- OpenAPI-Unterstützung in Minimal-APIs
- Schnellstart: Veröffentlichen in Azure
- Organisieren von Minimal-APIs für ASP.NET Core
Weitere Informationen
Weitere Informationen: Kurzreferenz zu Minimal-APIs
Minimal-APIs sind so entworfen, dass HTTP-APIs mit minimalen Abhängigkeiten erstellt werden. Sie eignen sich ideal für Microservices und Apps, die nur ein Minimum an Dateien, Funktionen und Abhängigkeiten in ASP.NET Core enthalten sollen.
In diesem Tutorial lernen Sie die Grundlagen der Erstellung einer minimalen API mit ASP.NET Core kennen. Ein weiterer Ansatz zum Erstellen von APIs in ASP.NET Core ist die Verwendung von Controllern. Hilfe bei der Entscheidung zwischen minimalen APIs und controllerbasierten APIs finden Sie in der Übersicht über APIs. Ein Tutorial zum Erstellen eines API-Projekts basierend auf Controllern, das weitere Features umfasst, finden Sie unter Erstellen einer Web-API.
Übersicht
In diesem Tutorial wird die folgende API erstellt:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Abgeschlossene To-Do-Elemente | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
POST /todoitems |
Neues Element hinzufügen | To-do-Element | To-do-Element |
PUT /todoitems/{id} |
Vorhandenes Element aktualisieren | To-do-Element | Keine |
DELETE /todoitems/{id} |
Löschen eines Elements | Keine | Keine |
Voraussetzungen
- Visual Studio 2022 mit der Workload ASP.NET und Webentwicklung
- .NET 6.0 SDK
Erstellen eines API-Projekts
Starten Sie Visual Studio 2022, und wählen Sie Neues Projekt erstellen aus.
Im Dialogfeld Neues Projekt erstellen:
- Geben Sie im Suchfeld
Empty
Nach Vorlagen suchenden Suchbegriff ein. - Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.
- Geben Sie im Suchfeld
Geben Sie dem Projekt den Namen TodoApi, und klicken Sie auf Weiter.
Im Dialogfeld Zusätzliche Informationen:
- Auswählen von .NET 6.0
- Deaktivieren Sie Keine Anweisungen der obersten Ebene verwenden.
- Klicken Sie auf Erstellen
Untersuchen des Codes
Die Datei Program.cs
enthält den folgenden Code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Der vorangehende Code:
- Erstellt eine Instanz von WebApplicationBuilder und WebApplication mit vorkonfigurierten Standardwerten.
- Erstellt den HTTP GET-Endpunkt
/
, derHello World!
zurückgibt:
Ausführen der App
Drücken Sie STRG+F5, um die Ausführung ohne den Debugger zu starten.
In Visual Studio wird das folgende Dialogfeld angezeigt:
Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.
Das folgende Dialogfeld wird angezeigt:
Klicken Sie auf Ja, wenn Sie zustimmen möchten, dass das Entwicklungszertifikat vertrauenswürdig ist.
Informationen dazu, wie Sie dem Firefox-Browser vertrauen, finden Sie unter Firefox-Zertifikatfehler SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio startet den Kestrel-Webserver und öffnet ein Browserfenster.
Hello World!
wird im Browser angezeigt. Die Datei Program.cs
enthält eine Minimal-, aber dennoch vollständige App.
Hinzufügen von NuGet-Paketen
NuGet-Pakete müssen hinzugefügt werden, um die in diesem Tutorial verwendete Datenbank und Diagnose zu unterstützen.
- Klicken Sie im Menü Extras auf NuGet-Paket-Manager > NuGet-Pakete für Projektmappe verwalten.
- Wählen Sie die Registerkarte Durchsuchen aus.
- Geben Sie Microsoft.EntityFrameworkCore.InMemory in das Suchfeld ein, und wählen Sie
Microsoft.EntityFrameworkCore.InMemory
aus. - Aktivieren Sie im rechten Bereich das Kontrollkästchen Projekt.
- Wählen Sie in der Dropdownliste Version die neueste verfügbare Version 7 (z. B.
6.0.28
) und dann Installieren aus. - Folgen Sie den obigen Anweisungen, um das Paket
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
mit der neuesten verfügbaren Version 7 hinzuzufügen.
Die Modell- und Datenbankkontextklassen
Erstellen Sie im Projektordner eine Datei mit dem Namen Todo.cs
und dem folgenden Code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Der vorherige Code erstellt das Modell für diese App. Ein Modell ist eine Klasse, welche die in der App verwalteten Daten repräsentiert.
Erstellen Sie eine Datei mit dem Namen TodoDb.cs
und dem folgenden Code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Der vorherige Code definiert den Datenbankkontext, also die Hauptklasse, die die Entity Framework-Funktionen für ein Datenmodell koordiniert. Diese Klasse wird von der Microsoft.EntityFrameworkCore.DbContext-Klasse abgeleitet.
Hinzufügen des API-Codes
Ersetzen Sie den Inhalt der Datei Program.cs
durch den folgenden Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der folgende hervorgehobene Code fügt den Datenbankkontext zum Container für die Abhängigkeitsinjektion (Dependency Injection, DI) hinzu und ermöglicht die Anzeige von datenbankbezogenen Ausnahmen:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
Der DI-Container bietet Zugriff auf den Datenbankkontext und andere Dienste.
Erstellen der API-Testbenutzeroberfläche mit Swagger
Es sind viele Web-API-Testtools verfügbar, aus denen Sie auswählen können, und Sie können die einführenden API-Testschritte dieses Tutorials mit Ihrem bevorzugten Tool ausführen.
In diesem Tutorial wird das .NET-Paket NSwag.AspNetCore verwendet, das Swagger-Tools zum Generieren einer Testbenutzeroberfläche integriert, die der OpenAPI-Spezifikation entspricht:
- NSwag: eine .NET-Bibliothek, die Swagger direkt in ASP.NET Core-Anwendungen integriert und Middleware sowie eine Konfiguration bereitstellt
- Swagger: verschiedene Open-Source-Tools wie OpenAPIGenerator und SwaggerUI, die API-Testseiten generieren, die der OpenAPI-Spezifikation entsprechen
- OpenAPI-Spezifikation: ein Dokument, das die Funktionen der API basierend auf den XML- und Attributanmerkungen innerhalb der Controller und Modelle beschreibt
Weitere Informationen zur Verwendung von OpenAPI und NSwag mit ASP.NET finden Sie in der Web-API-Dokumentation von ASP.NET Core mit Swagger/OpenAPI.
Installieren von Swagger-Tools
Führen Sie den folgenden Befehl aus:
dotnet add package NSwag.AspNetCore
Der obige Befehl fügt das Paket NSwag.AspNetCore hinzu, das Tools zum Generieren von Swagger-Dokumenten und der Benutzeroberfläche enthält.
Konfigurieren von Swagger-Middleware
Fügen Sie oben in der Datei „Program.cs“ die folgenden
using
-Anweisungen hinzu:using NSwag.AspNetCore;
Fügen Sie den folgenden hervorgehobenen Code hinzu, bevor
app
in Zeilevar app = builder.Build();
definiert wird.using NSwag.AspNetCore; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
Im vorherigen Code:
builder.Services.AddEndpointsApiExplorer();
: aktiviert den API-Explorer, bei dem es sich um einen Dienst handelt, der Metadaten zur HTTP-API bereitstellt. Der API-Explorer wird von Swagger verwendet, um das Swagger-Dokument zu generieren.builder.Services.AddOpenApiDocument(config => {...});
: fügt den Anwendungsdiensten den OpenAPI-Dokumentgenerator von Swagger hinzu und konfiguriert ihn, um weitere Informationen zur API bereitzustellen, z. B. den Titel und die Version. Informationen zur Bereitstellung robusterer API-Details finden Sie unter Erste Schritte mit NSwag und ASP.NET Core.Fügen Sie auf der nächsten Zeile den folgenden hervorgehobenen Code hinzu, nachdem
app
in der Zeilevar app = builder.Build();
definiert wurde.var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }
Der obige Code aktiviert die Swagger Middleware für die Bereitstellung des generierten JSON-Dokuments und der Swagger-Benutzeroberfläche. Swagger wird nur in einer Entwicklungsumgebung aktiviert. Das Aktivieren von Swagger in einer Produktionsumgebung könnte potenziell vertrauliche Details zur Struktur und Implementierung der API offenlegen.
Testen der Übertragung von Daten
Der folgende Code in Program.cs
erstellt den HTTP POST-Endpunkt /todoitems
zum Hinzufügen von Daten zur In-Memory-Datenbank:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Führen Sie die App aus. Der Browser zeigt einen 404-Fehler an, da kein /
-Endpunkt mehr vorhanden ist.
Der POST-Endpunkt dient dazu, der App Daten hinzuzufügen.
Wenn die App noch ausgeführt wird, navigieren Sie im Browser zu
https://localhost:<port>/swagger
, um die API-Testseite anzuzeigen, die von Swagger generiert wurde.Wählen Sie auf der API-Testseite von Swagger die Option Post /todoitems>Try it out aus.
Beachten Sie, dass das Feld Anforderungstext ein generiertes Beispielformat enthält, das die Parameter für die API widerspiegelt.
Geben Sie im Anforderungstext JSON-Code für ein Aufgabenelement ohne Angabe der optionalen
id
ein:{ "name":"walk dog", "isComplete":true }
Wählen Sie Execute(Ausführen).
Swagger stellt unter der Schaltfläche Execute einen Bereich Responses bereit.
Beachten Sie einige nützliche Details:
- cURL: Swagger stellt einen cURL-Beispielbefehl in Unix/Linux-Syntax bereit, der an der Befehlszeile mit jeder Bash-Shell ausgeführt werden kann, die Unix/Linux-Syntax verwendet, einschließlich der Git-Bash von Git für Windows.
- Anforderungs-URL: eine vereinfachte Darstellung der HTTP-Anforderung, die durch den JavaScript-Code der Swagger-Benutzeroberfläche für den API-Aufruf erstellt wurde. Tatsächliche Anforderungen können Details wie Header und Abfrageparameter sowie einen Anforderungstext enthalten.
- Serverantwort: enthält den Antworttext und die Header. Der Antworttext zeigt, dass
id
auf1
festgelegt wurde. - Antwort-Code: Es wurde ein Statuscode 201
HTTP
zurückgegeben, der anzeigt, dass die Anforderung erfolgreich bearbeitet wurde und zur Erstellung einer neuen Ressource führte.
Untersuchen der GET-Endpunkte
Die Beispiel-App implementiert mehrere GET-Endpunkte durch Aufrufen von MapGet
:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Alle abgeschlossenen To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Testen der GET-Endpunkte
Testen Sie die App, indem Sie die Endpunkte in einem Browser oder über Swagger aufrufen.
Wählen Sie in Swagger GET /todoitems>Try it out>Execute aus.
Rufen Sie alternativ GET /todoitems in einem Browser auf, indem Sie den URI
http://localhost:<port>/todoitems
eingeben. Beispiel:http://localhost:5001/todoitems
Durch einen Aufruf von GET /todoitems
wird eine Antwort ähnlich der folgenden erzeugt:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
Rufen Sie GET /todoitems/{id} in Swagger auf, um Daten zu einer bestimmten ID zurückzugeben:
- Wählen Sie GET /todoitems>Try it out aus.
- Legen Sie das Feld id auf
1
fest, und wählen Sie Execute aus.
Rufen Sie alternativ GET /todoitems in einem Browser auf, indem Sie den URI
https://localhost:<port>/todoitems/1
eingeben. Beispiel:https://localhost:5001/todoitems/1
Die Antwort ähnelt dem folgenden Code:
{ "id": 1, "name": "walk dog", "isComplete": true }
Diese App verwendet eine In-Memory-Datenbank. Wenn die App neu gestartet wird, gibt die GET-Anforderung keinerlei Daten zurück. Wenn keine Daten zurückgegeben werden, übermitteln Sie Daten per POST an die App, und wiederholen Sie die GET-Anforderung.
Rückgabewerte
ASP.NET Core serialisiert automatisch das Objekt in JSON und schreibt den JSON-Code in den Text der Antwortnachricht. Der Antwortcode für diesen Rückgabetyp ist 200 OK, vorausgesetzt, es gibt keine Ausnahmefehler. Nicht behandelte Ausnahmen werden in 5xx-Fehler übersetzt.
Die Rückgabetypen können eine Vielzahl von HTTP-Statuscodes darstellen. Beispielsweise kann GET /todoitems/{id}
zwei verschiedene Statuswerte zurückgeben:
- Wenn kein Element mit der angeforderten ID übereinstimmt, gibt die Methode einen NotFound-Fehlercode Status 404 zurück.
- Andernfalls gibt die Methode 200 mit einem JSON-Antworttext zurück. Die Rückgabe von
item
löst eine HTTP 200-Antwort aus.
Untersuchen des PUT-Endpunkts
Die Beispiel-App implementiert einen einzelnen PUT-Endpunkt mithilfe von MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
Diese Methode ähnelt der MapPost
-Methode, verwendet jedoch HTTP PUT. Eine erfolgreiche Antwort gibt 204 (No Content) zurück. Gemäß der HTTP-Spezifikation erfordert eine PUT-Anforderung, dass der Client die gesamte aktualisierte Entität (nicht nur die Änderungen) sendet. Verwenden Sie HTTP PATCH, um Teilupdates zu unterstützen.
Testen des PUT-Endpunkts
In diesem Beispiel wird eine In-Memory-Datenbank verwendet, die jedes Mal initialisiert werden muss, wenn die App gestartet wird. Es muss ein Element in der Datenbank vorhanden sein, bevor Sie einen PUT-Aufruf durchführen. Rufen Sie vor einem PUT-Aufruf GET auf, um sicherzustellen, dass ein Element in der Datenbank vorhanden ist.
Aktualisieren Sie das Aufgabenelement mit dem Wert Id = 1
, und legen Sie seinen Namen auf "feed fish"
fest.
Verwenden Sie Swagger, um eine PUT-Anforderung zu senden:
Wählen Sie PUT /todoitems/{id}>Try it out aus.
Legen Sie das Feld id auf
1
fest.Legen Sie den Anforderungstext auf folgenden JSON-Code fest:
{ "name": "feed fish", "isComplete": false }
Wählen Sie Execute(Ausführen).
Überprüfen und Testen des DELETE-Endpunkts
Die Beispiel-App implementiert einen einzelnen DELETE-Endpunkt mithilfe von MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Verwenden Sie Swagger, um eine DELETE-Anforderung zu senden:
Wählen Sie DELETE /todoitems/{id}>Try it out aus.
Legen Sie das Feld ID auf
1
fest, und wählen Sie Execute aus.Die DELETE-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Responses angezeigt. Der Antworttext ist leer, und der Statuscode der Serverantwort lautet 204.
Vermeiden von Overposting
Derzeit macht die Beispiel-App das gesamte Todo
-Objekt verfügbar. Produktions-Apps: In Produktionsanwendungen wird häufig nur eine Teilmenge des Modells verwendet, um die Daten einzuschränken, die eingegeben und zurückgegeben werden können. Hierfür gibt es mehrere Gründe, wobei die Sicherheit einer der Hauptgründe ist. Die Teilmenge eines Modells wird üblicherweise als Datenübertragungsobjekt (DTO, Data Transfer Object), Eingabemodell oder Anzeigemodell bezeichnet. In diesem Artikel wird das DTO verwendet.
Ein DTO kann für Folgendes verwendet werden:
- Vermeiden Sie Overposting.
- Ausblenden von Eigenschaften, die Clients nicht einsehen sollen
- Lassen Sie einige Eigenschaften weg, um die Nutzdatengröße zu verringern.
- Vereinfachen von Objektgraphen, die geschachtelte Objekte enthalten Vereinfachte Objektgraphen können für Clients zweckmäßiger sein.
Um den DTO-Ansatz zu veranschaulichen, aktualisieren Sie die Todo
-Klasse, sodass sie ein geheimes Feld einschließt:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
Das geheime Feld muss in dieser App ausgeblendet werden, eine administrative App kann es jedoch verfügbar machen.
Vergewissern Sie sich, dass Sie das geheime Feld veröffentlichen und abrufen können.
Erstellen Sie eine Datei mit dem Namen TodoItemDTO.cs
und dem folgenden Code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Ersetzen Sie den Inhalt der Datei Program.cs
durch folgenden Code, um dieses DTO-Modell zu verwenden:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Vergewissern Sie sich, dass Sie für das Geheimnisfeld POST und GET ausführen können.
Testen der Minimal-API
Ein Beispiel für das Testen einer Minimal-API-App finden Sie in diesem GitHub-Beispiel.
Veröffentlichen in Azure
Weitere Informationen zur Bereistellung in Azure finden Sie unter Schnellstart: Bereitstellen einer ASP.NET-Web-App.
Zusätzliche Ressourcen
Minimal-APIs sind so entworfen, dass HTTP-APIs mit minimalen Abhängigkeiten erstellt werden. Sie eignen sich ideal für Microservices und Apps, die nur ein Minimum an Dateien, Funktionen und Abhängigkeiten in ASP.NET Core enthalten sollen.
In diesem Tutorial lernen Sie die Grundlagen der Erstellung einer minimalen API mit ASP.NET Core kennen. Ein weiterer Ansatz zum Erstellen von APIs in ASP.NET Core ist die Verwendung von Controllern. Hilfe bei der Entscheidung zwischen minimalen APIs und controllerbasierten APIs finden Sie in der Übersicht über APIs. Ein Tutorial zum Erstellen eines API-Projekts basierend auf Controllern, das weitere Features umfasst, finden Sie unter Erstellen einer Web-API.
Übersicht
In diesem Tutorial wird die folgende API erstellt:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Abgeschlossene To-Do-Elemente | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
POST /todoitems |
Neues Element hinzufügen | To-do-Element | To-do-Element |
PUT /todoitems/{id} |
Vorhandenes Element aktualisieren | To-do-Element | Keine |
DELETE /todoitems/{id} |
Löschen eines Elements | Keine | Keine |
Voraussetzungen
Visual Studio 2022 mit der Workload ASP.NET und Webentwicklung
Erstellen eines API-Projekts
Starten Sie Visual Studio 2022, und wählen Sie Neues Projekt erstellen aus.
Im Dialogfeld Neues Projekt erstellen:
- Geben Sie im Suchfeld
Empty
Nach Vorlagen suchenden Suchbegriff ein. - Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.
- Geben Sie im Suchfeld
Geben Sie dem Projekt den Namen TodoApi, und klicken Sie auf Weiter.
Im Dialogfeld Zusätzliche Informationen:
- Wählen Sie .NET 8.0 (Langfristiger Support) aus
- Deaktivieren Sie Keine Anweisungen der obersten Ebene verwenden.
- Klicken Sie auf Erstellen
Untersuchen des Codes
Die Datei Program.cs
enthält den folgenden Code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Der vorangehende Code:
- Erstellt eine Instanz von WebApplicationBuilder und WebApplication mit vorkonfigurierten Standardwerten.
- Erstellt den HTTP GET-Endpunkt
/
, derHello World!
zurückgibt:
Ausführen der App
Drücken Sie STRG+F5, um die Ausführung ohne den Debugger zu starten.
In Visual Studio wird das folgende Dialogfeld angezeigt:
Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.
Das folgende Dialogfeld wird angezeigt:
Klicken Sie auf Ja, wenn Sie zustimmen möchten, dass das Entwicklungszertifikat vertrauenswürdig ist.
Informationen dazu, wie Sie dem Firefox-Browser vertrauen, finden Sie unter Firefox-Zertifikatfehler SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio startet den Kestrel-Webserver und öffnet ein Browserfenster.
Hello World!
wird im Browser angezeigt. Die Datei Program.cs
enthält eine Minimal-, aber dennoch vollständige App.
Schließen Sie das Browserfenster.
Hinzufügen von NuGet-Paketen
NuGet-Pakete müssen hinzugefügt werden, um die in diesem Tutorial verwendete Datenbank und Diagnose zu unterstützen.
- Klicken Sie im Menü Extras auf NuGet-Paket-Manager > NuGet-Pakete für Projektmappe verwalten.
- Wählen Sie die Registerkarte Durchsuchen aus.
- Geben Sie Microsoft.EntityFrameworkCore.InMemory in das Suchfeld ein, und wählen Sie
Microsoft.EntityFrameworkCore.InMemory
aus. - Aktivieren Sie das Kontrollkästchen Projekt im rechten Bereich, und klicken Sie dann auf Installieren.
- Folgen Sie den vorstehenden Anweisungen, um das
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
-Paket hinzuzufügen.
Die Modell- und Datenbankkontextklassen
- Erstellen Sie im Projektordner eine Datei mit dem Namen
Todo.cs
und dem folgenden Code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Der vorherige Code erstellt das Modell für diese App. Ein Modell ist eine Klasse, welche die in der App verwalteten Daten repräsentiert.
- Erstellen Sie eine Datei mit dem Namen
TodoDb.cs
und dem folgenden Code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Der vorherige Code definiert den Datenbankkontext, also die Hauptklasse, die die Entity Framework-Funktionen für ein Datenmodell koordiniert. Diese Klasse wird von der Microsoft.EntityFrameworkCore.DbContext-Klasse abgeleitet.
Hinzufügen des API-Codes
- Ersetzen Sie den Inhalt der Datei
Program.cs
durch den folgenden Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der folgende hervorgehobene Code fügt den Datenbankkontext zum Container für die Abhängigkeitsinjektion (Dependency Injection, DI) hinzu und ermöglicht die Anzeige von datenbankbezogenen Ausnahmen:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
Der DI-Container bietet Zugriff auf den Datenbankkontext und andere Dienste.
In diesem Tutorial werden Endpunkte Explorer- und HTTP-Dateien verwendet, um die API zu testen.
Testen der Übertragung von Daten
Der folgende Code in Program.cs
erstellt den HTTP POST-Endpunkt /todoitems
zum Hinzufügen von Daten zur In-Memory-Datenbank:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Führen Sie die App aus. Der Browser zeigt einen 404-Fehler an, da kein /
-Endpunkt mehr vorhanden ist.
Der POST-Endpunkt dient dazu, der App Daten hinzuzufügen.
Wählen Sie Ansicht>Weitere Fenster>Endpunkt-Explorer aus.
Klicken Sie mit der rechten Maustaste auf den POST-Endpunkt, und wählen Sie Anforderung generieren aus.
Im Projektordner wird eine neue Datei mit dem Namen
TodoApi.http
erstellt, deren Inhalt dem folgenden Beispiel ähnelt:@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###
- Die erste Zeile erstellt eine Variable, die für alle Endpunkte verwendet wird.
- In der nächsten Zeile wird eine POST-Anforderung definiert.
- Die Zeile mit dem dreifachen Hashtag (
###
) ist ein Anforderungstrennzeichen: Was danach kommt, gilt für eine andere Anforderung.
Die POST-Anforderung benötigt Header und einen Text. Um diese Teile der Anforderung zu definieren, fügen Sie die folgenden Zeilen unmittelbar nach der POST-Anforderungszeile hinzu:
Content-Type: application/json { "name":"walk dog", "isComplete":true }
Der vorangehende Code fügt einen Content-Type-Header und einen JSON-Anforderungstext hinzu. Die Datei „TodoApi.http“ sollte nun wie im folgenden Beispiel aussehen, jedoch mit Ihrer Portnummer:
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
Führen Sie die App aus.
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der
POST
-Anforderungszeile.Die POST-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Untersuchen der GET-Endpunkte
Die Beispiel-App implementiert mehrere GET-Endpunkte durch Aufrufen von MapGet
:
API | Beschreibung | Anforderungstext | Antworttext |
---|---|---|---|
GET /todoitems |
Alle To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/complete |
Alle abgeschlossenen To-do-Elemente abrufen | Keine | Array von To-do-Elementen |
GET /todoitems/{id} |
Ein Element nach ID abrufen | Keine | To-do-Element |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Testen der GET-Endpunkte
Testen Sie die App, indem Sie die GET
-Endpunkte in einem Browser aufrufen oder indem Sie den Endpunkte-Explorer verwenden. Die folgenden Schritte gelten für den Endpunkte Explorer.
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den ersten GET-Endpunkt, und wählen Sie Anforderung generieren aus.
Der folgende Inhalt wird der Datei
TodoApi.http
hinzugefügt:Get {{TodoApi_HostAddress}}/todoitems ###
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der neuen
GET
-Anforderungszeile.Die GET-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Der Antworttext ist mit folgendem JSON vergleichbar:
[ { "id": 1, "name": "walk dog", "isComplete": true } ]
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den GET-Endpunkt von
/todoitems/{id}
, und wählen Sie Anforderung generieren aus. Der folgende Inhalt wird der DateiTodoApi.http
hinzugefügt:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
Ersetzen Sie
{id}
durch1
.Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der GET-Anforderungszeile.
Die GET-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.
Der Antworttext ist mit folgendem JSON vergleichbar:
{ "id": 1, "name": "walk dog", "isComplete": true }
Diese App verwendet eine In-Memory-Datenbank. Wenn die App neu gestartet wird, gibt die GET-Anforderung keinerlei Daten zurück. Wenn keine Daten zurückgegeben werden, übermitteln Sie Daten per POST an die App, und wiederholen Sie die GET-Anforderung.
Rückgabewerte
ASP.NET Core serialisiert automatisch das Objekt in JSON und schreibt den JSON-Code in den Text der Antwortnachricht. Der Antwortcode für diesen Rückgabetyp ist 200 OK, vorausgesetzt, es gibt keine Ausnahmefehler. Nicht behandelte Ausnahmen werden in 5xx-Fehler übersetzt.
Die Rückgabetypen können eine Vielzahl von HTTP-Statuscodes darstellen. Beispielsweise kann GET /todoitems/{id}
zwei verschiedene Statuswerte zurückgeben:
- Wenn kein Element mit der angeforderten ID übereinstimmt, gibt die Methode einen NotFound-Fehlercode Status 404 zurück.
- Andernfalls gibt die Methode 200 mit einem JSON-Antworttext zurück. Die Rückgabe von
item
löst eine HTTP 200-Antwort aus.
Untersuchen des PUT-Endpunkts
Die Beispiel-App implementiert einen einzelnen PUT-Endpunkt mithilfe von MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
Diese Methode ähnelt der MapPost
-Methode, verwendet jedoch HTTP PUT. Eine erfolgreiche Antwort gibt 204 (No Content) zurück. Gemäß der HTTP-Spezifikation erfordert eine PUT-Anforderung, dass der Client die gesamte aktualisierte Entität (nicht nur die Änderungen) sendet. Verwenden Sie HTTP PATCH, um Teilupdates zu unterstützen.
Testen des PUT-Endpunkts
In diesem Beispiel wird eine In-Memory-Datenbank verwendet, die jedes Mal initialisiert werden muss, wenn die App gestartet wird. Es muss ein Element in der Datenbank vorhanden sein, bevor Sie einen PUT-Aufruf durchführen. Rufen Sie vor einem PUT-Aufruf GET auf, um sicherzustellen, dass ein Element in der Datenbank vorhanden ist.
Aktualisieren Sie das Aufgabenelement mit dem Wert Id = 1
, und legen Sie seinen Namen auf "feed fish"
fest.
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den PUT-Endpunkt, und wählen Sie Anforderung generieren aus.
Der folgende Inhalt wird der Datei
TodoApi.http
hinzugefügt:Put {{TodoApi_HostAddress}}/todoitems/{id} ###
Ersetzen Sie in der PUT-Anforderungszeile
{id}
durch1
.Fügen Sie die folgenden Zeilen unmittelbar nach der PUT-Anforderungszeile hinzu:
Content-Type: application/json { "name": "feed fish", "isComplete": false }
Der vorangehende Code fügt einen Content-Type-Header und einen JSON-Anforderungstext hinzu.
Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der PUT-Anforderungszeile.
Die PUT-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt. Der Antworttext ist leer, und der Statuscode ist 204.
Überprüfen und Testen des DELETE-Endpunkts
Die Beispiel-App implementiert einen einzelnen DELETE-Endpunkt mithilfe von MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Klicken Sie im Endpunkte-Explorer mit der rechten Maustaste auf den DELETE-Endpunkt, und wählen Sie Anforderung generieren aus.
Eine DELETE-Anforderung wird zu
TodoApi.http
hinzugefügt.Ersetzen Sie
{id}
in der DELETE-Anforderungszeile durch1
. Die DELETE-Anforderung sollte wie im folgenden Beispiel aussehen:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
Wählen Sie den Link Anforderung senden für die DELETE-Anforderung aus.
Die DELETE-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt. Der Antworttext ist leer, und der Statuscode ist 204.
Verwenden der MapGroup-API
Der Beispiel-App-Code wiederholt bei jeder Einrichtung eines Endpunkts das URL-Präfix todoitems
. APIs enthalten oft Gruppen von Endpunkten mit einem gemeinsamen URL-Präfix, und die MapGroup-Methode hilft bei der Organisation solcher Gruppen. Sie reduziert sich wiederholenden Code und ermöglicht die benutzerdefinierte Anpassung ganzer Gruppen von Endpunkten mit einem einzigen Aufruf von Methoden wie RequireAuthorization und WithMetadata.
Ersetzen Sie den Inhalt von Program.cs
durch den folgenden Code.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
Der vorstehende Code weist die folgenden Änderungen auf:
- Fügt
var todoItems = app.MapGroup("/todoitems");
hinzu, um die Gruppe mithilfe des URL-Präfixes/todoitems
einzurichten. - Ändert alle
app.Map<HttpVerb>
-Methoden intodoItems.Map<HttpVerb>
. - Entfernt das URL-Präfix
/todoitems
aus den Aufrufen derMap<HttpVerb>
-Methode.
Testet die Endpunkte, um zu prüfen, ob sie identisch funktionieren.
Verwenden der TypedResults-API
Die Rückgabe TypedResults anstelle von Results hat mehrere Vorteile, einschließlich Prüfbarkeit und automatische Rückgabe der Metadaten des Antworttyps für OpenAPI, mit denen der Endpunkt beschrieben wird. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
Die Map<HttpVerb>
-Methoden können Routinghandlermethoden aufrufen, anstatt Lambdafunktionen zu verwenden. Um ein Beispiel zu sehen, aktualisieren Sie Program.cs mit folgendem Code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Der Map<HttpVerb>
-Code ruft jetzt Methoden anstelle von Lambdafunktionen auf:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
Diese Methoden geben Objekte zurück, die IResult implementieren und von TypedResults definiert werden:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Komponententests können diese Methoden aufrufen und testen, ob sie den richtigen Typ zurückgeben. Wenn die Methode z. B. GetAllTodos
ist:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Der Komponententestcode kann überprüfen, ob ein Objekt des Typs Ok<Todo[]> von der Handlermethode zurückgegeben wird. Beispiel:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Vermeiden von Overposting
Derzeit macht die Beispiel-App das gesamte Todo
-Objekt verfügbar. Produktions-Apps: In Produktionsanwendungen wird häufig nur eine Teilmenge des Modells verwendet, um die Daten einzuschränken, die eingegeben und zurückgegeben werden können. Hierfür gibt es mehrere Gründe, wobei die Sicherheit einer der Hauptgründe ist. Die Teilmenge eines Modells wird üblicherweise als Datenübertragungsobjekt (DTO, Data Transfer Object), Eingabemodell oder Anzeigemodell bezeichnet. In diesem Artikel wird das DTO verwendet.
Ein DTO kann für Folgendes verwendet werden:
- Vermeiden Sie Overposting.
- Ausblenden von Eigenschaften, die Clients nicht einsehen sollen
- Lassen Sie einige Eigenschaften weg, um die Nutzdatengröße zu verringern.
- Vereinfachen von Objektgraphen, die geschachtelte Objekte enthalten Vereinfachte Objektgraphen können für Clients zweckmäßiger sein.
Um den DTO-Ansatz zu veranschaulichen, aktualisieren Sie die Todo
-Klasse, sodass sie ein geheimes Feld einschließt:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
Das geheime Feld muss in dieser App ausgeblendet werden, eine administrative App kann es jedoch verfügbar machen.
Vergewissern Sie sich, dass Sie das geheime Feld veröffentlichen und abrufen können.
Erstellen Sie eine Datei mit dem Namen TodoItemDTO.cs
und dem folgenden Code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Ersetzen Sie den Inhalt der Datei Program.cs
durch folgenden Code, um dieses DTO-Modell zu verwenden:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Vergewissern Sie sich, dass Sie für das Geheimnisfeld POST und GET ausführen können.
Problembehandlung mit dem fertiggestellten Beispiel
Wenn Sie auf ein Problem stoßen, das Sie nicht lösen können, vergleichen Sie Ihren den Code mit dem vollständigen Projekt. Anzeigen oder Herunterladen des fertiggestellten Projekts (Downloadanleitung).
Nächste Schritte
- Konfigurieren von JSON-Serialisierungsoptionen
- Behandeln von Fehlern und Ausnahmen: Die Seite mit Ausnahmen für Entwickler*innen ist in der Entwicklungsumgebung für Minimal-API-Apps standardmäßig aktiviert. Informationen zum Behandeln von Fehlern und Ausnahmen finden Sie unter Behandeln von Fehlern in ASP.NET Core-APIs.
- Ein Beispiel für das Testen einer Minimal-API-App finden Sie in diesem GitHub-Beispiel.
- OpenAPI-Unterstützung in Minimal-APIs
- Schnellstart: Veröffentlichen in Azure
- Organisieren von Minimal-APIs für ASP.NET Core
Weitere Informationen
Weitere Informationen: Kurzreferenz zu Minimal-APIs