Tutorial: Erstellen einer minimalen API mit ASP.NET Core

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-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.

Die aktuelle Version finden Sie in der .NET 9-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

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 EmptyNach Vorlagen suchenden Suchbegriff ein.
    • Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.

    Visual Studio-Seite „Neues Projekt erstellen“

  • 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

    Zusätzliche Informationen

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:

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:

Dieses Projekt ist für die Verwendung von SSL konfiguriert. Um SSL-Warnungen im Browser zu vermeiden, können Sie dem selbstsignierten Zertifikat vertrauen, das IIS Express generiert hat. Möchten Sie dem SSL-Zertifikat von IIS Express vertrauen?

Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.

Das folgende Dialogfeld wird angezeigt:

Dialogfeld „Sicherheitswarnung“

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.

    Kontextmenü des Endpunkte-Explorers, in dem das Menüelement „Anforderung generieren“ hervorgehoben wird.

    Im Projektordner wird eine neue Datei mit dem Namen TodoApi.httperstellt, 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.

    HTTP-Dateifenster mit hervorgehobenem Link „Execute“

    Die POST-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.

    .http-Dateifenster mit Antwort von der POST-Anforderung.

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 Datei TodoApi.http hinzugefügt:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersetzen Sie {id} durch 1.

  • 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} durch 1.

  • 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 durch 1. 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 in todoItems.Map<HttpVerb>.
  • Entfernt das URL-Präfix /todoitems aus den Aufrufen der Map<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

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

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 EmptyNach Vorlagen suchenden Suchbegriff ein.
    • Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.

    Visual Studio-Seite „Neues Projekt erstellen“

  • 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

    Zusätzliche Informationen

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:

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:

Dieses Projekt ist für die Verwendung von SSL konfiguriert. Um SSL-Warnungen im Browser zu vermeiden, können Sie dem selbstsignierten Zertifikat vertrauen, das IIS Express generiert hat. Möchten Sie dem SSL-Zertifikat von IIS Express vertrauen?

Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.

Das folgende Dialogfeld wird angezeigt:

Dialogfeld „Sicherheitswarnung“

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 Zeile var 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 Zeile var 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.

    Von Swagger generierte API-Testseite

  • 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 mit POST

Swagger stellt unter der Schaltfläche Execute einen Bereich Responses bereit.

Swagger mit POST-Antwort

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 auf 1 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 in todoItems.Map<HttpVerb>.
  • Entfernt das URL-Präfix /todoitems aus den Aufrufen der Map<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

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

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 EmptyNach Vorlagen suchenden Suchbegriff ein.
    • Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.

    Visual Studio-Seite „Neues Projekt erstellen“

  • 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:

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:

Dieses Projekt ist für die Verwendung von SSL konfiguriert. Um SSL-Warnungen im Browser zu vermeiden, können Sie dem selbstsignierten Zertifikat vertrauen, das IIS Express generiert hat. Möchten Sie dem SSL-Zertifikat von IIS Express vertrauen?

Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.

Das folgende Dialogfeld wird angezeigt:

Dialogfeld „Sicherheitswarnung“

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 Zeile var 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 Zeile var 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.

    Von Swagger generierte API-Testseite

  • 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 mit POST-Daten

Swagger stellt unter der Schaltfläche Execute einen Bereich Responses bereit.

Swagger mit POST-Antwortbereich

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 auf 1 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

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 EmptyNach Vorlagen suchenden Suchbegriff ein.
    • Wählen Sie die Vorlage ASP.NET Core leer und dann Weiter aus.

    Visual Studio-Seite „Neues Projekt erstellen“

  • 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

    Zusätzliche Informationen

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:

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:

Dieses Projekt ist für die Verwendung von SSL konfiguriert. Um SSL-Warnungen im Browser zu vermeiden, können Sie dem selbstsignierten Zertifikat vertrauen, das IIS Express generiert hat. Möchten Sie dem SSL-Zertifikat von IIS Express vertrauen?

Wählen Sie Ja aus, wenn Sie dem IIS Express-SLL-Zertifikat vertrauen möchten.

Das folgende Dialogfeld wird angezeigt:

Dialogfeld „Sicherheitswarnung“

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.

    Kontextmenü des Endpunkte-Explorers, in dem das Menüelement „Anforderung generieren“ hervorgehoben wird.

    Im Projektordner wird eine neue Datei mit dem Namen TodoApi.httperstellt, 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.

    HTTP-Dateifenster mit hervorgehobenem Link „Execute“

    Die POST-Anforderung wird an die App gesendet, und die Antwort wird im Bereich Antwort angezeigt.

    .http-Dateifenster mit Antwort von der POST-Anforderung.

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 Datei TodoApi.http hinzugefügt:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersetzen Sie {id} durch 1.

  • 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} durch 1.

  • 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 durch 1. 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 in todoItems.Map<HttpVerb>.
  • Entfernt das URL-Präfix /todoitems aus den Aufrufen der Map<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

Weitere Informationen

Weitere Informationen: Kurzreferenz zu Minimal-APIs