Tutorial: Erstellen einer minimalen API mit ASP.NET Core

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der ASP.NET Core 8.0-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 Preview, 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 Create a new project

  • 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

    Additional information

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

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

Das folgende Dialogfeld wird angezeigt:

Security warning dialog

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

Fügen Sie der App mithilfe des POST-Endpunkts Daten hinzu.

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

    Endpoints Explorer context menu highlighting Generate Request menu item.

    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 JS-ON-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 file window with run link highlighted.

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

    .http file window with response from the POST request.

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": false
      }
    ]
    
  • 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/{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": false
    }
    

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 To-do-Element, das über den ID-Wert 1 verfügt, und legen Sie als Namen "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
    
    {
      "id": 1,
      "name": "feed fish",
      "isComplete": false
    }
    

    Der vorangehende Code fügt einen Content-Type-Header und einen JS-ON-Anforderungstext hinzu.

  • Wählen Sie den Link Anforderung senden aus. Er befindet sich oberhalb der GET-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. In den Produktions-Apps sind die Daten, die eingegeben und mithilfe einer Teilmenge des Modells zurückgegeben werden, in der Regel eingeschränkt. 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
  • Auslassen einiger Eigenschaften, um die Nutzlast 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);
}

Aktualisieren Sie den Code in Program.cs, 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 Create a new project

  • 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

    Additional information

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

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

Das folgende Dialogfeld wird angezeigt:

Security warning dialog

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

Installieren von Postman zum Testen der App

Dieses Tutorial verwendet Postman zum Testen der API.

  • Installieren Sie Postman.
  • Starten Sie die Web-App.
  • Starten Sie Postman.
  • Wählen Sie Arbeitsbereiche>Arbeitsbereich erstellen und anschließend Weiter aus.
  • Nennen Sie den Arbeitsbereich TodoApi, und wählen Sie Erstellen aus.
  • Wählen Sie das Zahnradsymbol >Einstellungen (Registerkarte Allgemein ) aus, und deaktivieren Sie SSL-Zertifikatüberprüfung.

    Warnung

    Reaktivieren Sie die Überprüfung des SSL-Zertifikats, nachdem Sie die Beispiel-App getestet haben.

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.

Fügen Sie der App mithilfe des POST-Endpunkts Daten hinzu:

  • Erstellen Sie in Postman eine neue HTTP-Anforderung, indem Sie Neu>HTTP auswählen.

  • Legen Sie als HTTP-Methode POST fest.

  • Legen Sie den URI auf https://localhost:<port>/todoitems fest. Beispiel: https://localhost:5001/todoitems

  • Wählen Sie die Registerkarte Body (Text) aus.

  • Wählen Sie raw (Unformatiert) aus.

  • Legen Sie den Typ auf JSON fest.

  • Geben Sie für die Aufgabe den Anforderungstext im JSON-Format ein:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Wählen Sie Send (Senden) aus.

    Postman with Post request details

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 Postman aufrufen. Die folgenden Schritte gelten für Postman.

  • Erstellen Sie eine neue HTTP-Anforderung.
  • Legen Sie die HTTP-Methode auf GET fest.
  • Legen Sie den Anforderungs-URI auf https://localhost:<port>/todoitems fest. Beispiel: https://localhost:5001/todoitems.
  • Wählen Sie Send (Senden) aus.

Durch einen Aufruf von GET /todoitems wird eine Antwort ähnlich der folgenden erzeugt:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": false
  }
]
  • Legen Sie den Anforderungs-URI auf https://localhost:<port>/todoitems/1 fest. Beispiel: https://localhost:5001/todoitems/1.
  • Wählen Sie Send (Senden) aus.
  • Die Antwort ähnelt dem folgenden Code:
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": false
    }
    

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 To-do-Element, das über den ID-Wert 1 verfügt, und legen Sie als Namen "feed fish" fest:

{
  "id": 1,
  "name": "feed fish",
  "isComplete": false
}

Ü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();
});

So löschen Sie mit Postman eine Aufgabe

  • Legen Sie die Methode auf DELETE fest.
  • Legen Sie den URI des zu löschenden Objekts fest, z. B. https://localhost:5001/todoitems/1.
  • Wählen Sie Send (Senden) aus.

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. In den Produktions-Apps sind die Daten, die eingegeben und mithilfe einer Teilmenge des Modells zurückgegeben werden, in der Regel eingeschränkt. 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 anzeigen sollen
  • Auslassen einiger Eigenschaften, um die Nutzlast 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);
}

Aktualisieren Sie den Code in Program.cs, um dieses DTO-Modell zu verwenden:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

Vergewissern Sie sich, dass Sie für das Geheimnisfeld POST und GET ausführen können.

Problembehandlung mit dem fertiggestellten Beispiel

Wenn Sie auf ein Problem stoßen, das Sie nicht lösen können, vergleichen Sie Ihren den Code mit dem vollständigen Projekt. Anzeigen oder Herunterladen des fertiggestellten Projekts (Downloadanleitung).

Nächste Schritte

Konfigurieren von JSON-Serialisierungsoptionen

Informationen zum Konfigurieren der JSON-Serialisierung in Ihren Minimal-API-Apps finden Sie unter Konfigurieren von JSON-Serialisierungsoptionen.

Behandeln von Fehlern und Ausnahmen

Die Seite mit Ausnahmen für Entwickler ist in der Entwicklungsumgebung für Minimal-API-Apps standardmäßig aktiviert. Informationen zum Behandeln von Fehlern und Ausnahmen finden Sie unter Behandeln von Fehlern in ASP.NET Core-APIs.

Testen von Minimal-API-Apps

Ein Beispiel für das Testen einer Minimal-API-App finden Sie in diesem GitHub-Beispiel.

Verwenden von OpenAPI (Swagger)

Informationen zur Verwendung von OpenAPI mit Minimal-API-Apps finden Sie unter OpenAPI-Unterstützung in Minimal-APIs.

Veröffentlichen in Azure

Weitere Informationen zur Bereitstellung in Azure finden Sie unter Schnellstart: Bereitstellen einer ASP.NET-Web-App.

Weitere Informationen

Weitere Informationen zu Minimal-API-Apps finden Sie in der Schnellreferenz 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 Tutorial zum Erstellen eines API-Projekts basierend auf Controllern, das weitere Features umfasst, finden Sie unter Erstellen einer Web-API. Einen Vergleich finden Sie unter Unterschiede zwischen Minimal-APIs und APIs mit Controllern in diesem Dokument.

Übersicht

In diesem Tutorial wird die folgende API erstellt:

API Beschreibung Anforderungstext Antworttext
GET / Browsertest, „Hello World“ Keine Hello World!
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

VS22 installer workloads

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 APINach Vorlagen suchenden Suchbegriff ein.
    • Wählen Sie die ASP.NET Core-Web-API-Vorlage aus, und klicken Sie auf Weiter. Visual Studio Create a new project
  • Geben Sie dem Projekt den Namen TodoApi, und klicken Sie auf Weiter.

  • Im Dialogfeld Zusätzliche Informationen:

    • Wählen Sie .NET 6.0 (Langfristiger Support) aus.
    • Deaktivieren Sie Controller verwenden (zur Verwendung von Minimal-APIs deaktivieren)
    • Klicken Sie auf Erstellen

Additional information

Untersuchen des Codes

Die Datei Program.cs enthält den folgenden Code:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
       new WeatherForecast
       (
           DateTime.Now.AddDays(index),
           Random.Shared.Next(-20, 55),
           summaries[Random.Shared.Next(summaries.Length)]
       ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Die Projektvorlage erstellt eine WeatherForecast-API mit Unterstützung für Swagger. Swagger wird verwendet, um nützliche Dokumentations- und Hilfeseiten für APIs zu generieren.

Der folgende hervorgehobene Code fügt Unterstützung für Swagger hinzu:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

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

Das folgende Dialogfeld wird angezeigt:

Security warning dialog

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.

Die Swagger-Seite /swagger/index.html wird angezeigt. Wählen Sie GET > Try it out> Executeaus. Die Seite zeigt Folgendes an:

  • Der Curl-Befehl, zum Testen der WeatherForecast-API.
  • Die URL zum Testen der WeatherForecast-API.
  • Der Antwortcode, der Text und die Header.
  • Ein Dropdown-Listenfeld mit Medientypen und dem Beispielwert und -schema.

Kopieren Sie die Anforderungs-URL, und fügen Sie sie in den Browser ein: https://localhost:<port>/WeatherForecast. Der zurückgegebene JSON-Code sieht in etwa wie folgt aus:

[
  {
    "date": "2021-10-19T14:12:50.3079024-10:00",
    "temperatureC": 13,
    "summary": "Bracing",
    "temperatureF": 55
  },
  {
    "date": "2021-10-20T14:12:50.3080559-10:00",
    "temperatureC": -8,
    "summary": "Bracing",
    "temperatureF": 18
  },
  {
    "date": "2021-10-21T14:12:50.3080601-10:00",
    "temperatureC": 12,
    "summary": "Hot",
    "temperatureF": 53
  },
  {
    "date": "2021-10-22T14:12:50.3080603-10:00",
    "temperatureC": 10,
    "summary": "Sweltering",
    "temperatureF": 49
  },
  {
    "date": "2021-10-23T14:12:50.3080604-10:00",
    "temperatureC": 36,
    "summary": "Warm",
    "temperatureF": 96
  }
]

Aktualisieren des generierten Codes

In diesem Tutorial liegt der Fokus auf der Erstellung einer API, deshalb löschen wir den Swagger-Code und den WeatherForecast-Code. Ersetzen Sie den Inhalt der Datei Program.cs durch Folgendes:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Mit dem folgenden hervorgehobenen Code werden ein WebApplicationBuilder und eine WebApplication mit vorkonfigurierten Standardwerten erstellt:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

Der nachstehende Code erstellt einen HTTP GET-Endpunkt /, der Hello World! zurückgibt:

app.MapGet("/", () => "Hello World!");

app.Run(); führt die App aus.

Entfernen Sie die beiden "launchUrl": "swagger",-Zeilen aus der Properties/launchSettings.json-Datei. Wenn die launchUrl nicht angegeben ist, fordert der Webbrowser den Endpunkt / an.

Führen Sie die App aus. Hello World! wird angezeigt. Die aktualisierte Datei Program.cs enthält eine minimale, aber 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.
  • 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.

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();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Die Modell- und Datenbankkontextklassen

Die Beispiel-App enthält das folgende Modell:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Ein Modell ist eine Klasse, welche die in der App verwalteten Daten repräsentiert. Das Modell für diese App ist die Klasse Todo.

Die Beispiel-App enthält die folgende Datenbankkontextklasse:

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

Der Datenbankkontext ist die Hauptklasse, die die Entity Framework-Funktionalität für ein Datenmodell koordiniert. Diese Klasse wird durch Ableiten von der Microsoft.EntityFrameworkCore.DbContext-Klasse erstellt.

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.

Der folgende Code erstellt einen 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);
});

Installieren von Postman zum Testen der App

Dieses Tutorial verwendet Postman zum Testen der API.

  • Installieren Sie Postman.
  • Starten Sie die Web-App.
  • Starten Sie Postman.
  • Deaktivieren Sie SSL certificate verification (Verifizierung des SSL-Zertifikats).
    • Deaktivieren Sie auf der Registerkarte General (Allgemein) unter File>Settings (Datei > Einstellungen) SSL certificate verification (Verifizierung des SSL-Zertifikats).

      Warnung

      Aktivieren Sie die Verifizierung des SSL-Zertifikats wieder, nachdem Sie den Controller getestet haben.

Testen der Übertragung von Daten

Befolgen Sie diese Anweisungen, um Daten an die App zu übertragen:

  • Erstellen Sie eine neue HTTP-Anforderung.

  • Legen Sie als HTTP-Methode POST fest.

  • Legen Sie den URI auf https://localhost:<port>/todoitems fest. Beispiel: https://localhost:5001/todoitems

  • Wählen Sie die Registerkarte Body (Text) aus.

  • Wählen Sie raw (Unformatiert) aus.

  • Legen Sie den Typ auf JSON fest.

  • Geben Sie für die Aufgabe den Anforderungstext im JSON-Format ein:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Wählen Sie Send (Senden) aus. Postman with Post request details

Untersuchen der GET-Endpunkte

Die Beispiel-App implementiert mehrere GET-Endpunkte mithilfe von MapGet-Aufrufen:

API Beschreibung Anforderungstext Antworttext
GET / Browsertest, „Hello World“ Keine Hello World!
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 beiden Endpunkte in einem Browser oder über Postman aufrufen. Zum Beispiel:

  • GET https://localhost:5001/todoitems
  • GET https://localhost:5001/todoitems/1

Durch einen Aufruf von GET /todoitems wird eine Antwort ähnlich der folgenden erzeugt:

[
  {
    "id": 1,
    "name": "Item1",
    "isComplete": false
  }
]

Testen der GET-Endpunkte mit Postman

  • Erstellen Sie eine neue HTTP-Anforderung.
  • Legen Sie die HTTP-Methode auf GET fest.
  • Legen Sie den Anforderungs-URI auf https://localhost:<port>/todoitems fest. Beispiel: https://localhost:5001/todoitems.
  • Wählen Sie Send (Senden) aus.

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, senden Sie mit POST Daten an die App.

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 To-do-Element, das über den ID-Wert 1 verfügt, und legen Sie als Namen "feed fish" fest:

{
  "id": 1,
  "name": "feed fish",
  "isComplete": false
}

Untersuchen 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();
});

So löschen Sie mit Postman eine Aufgabe

  • Legen Sie die Methode auf DELETE fest.
  • Legen Sie den URI des zu löschenden Objekts fest, z. B. https://localhost:5001/todoitems/1.
  • Wählen Sie Send (Senden) aus.

Vermeiden von Overposting

Derzeit macht die Beispiel-App das gesamte Todo-Objekt verfügbar. In den Produktions-Apps sind die Daten, die eingegeben und mithilfe einer Teilmenge des Modells zurückgegeben werden, in der Regel eingeschränkt. 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 anzeigen sollen
  • Auslassen einiger Eigenschaften, um die Nutzlast 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 ein DTO-Modell:

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);
}

Aktualisieren Sie den Code, um TodoItemDTO 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 das geheime Feld weder veröffentlichen noch abrufen können.

Unterschiede zwischen Minimal-APIs und APIs mit Controllern

Verwenden von JsonOptions

Der folgende Code verwendet JsonOptions:

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapGet("/", () => new Todo { Name = "Walk dog", IsComplete = false });

app.Run();

class Todo
{
    // These are public fields instead of properties.
    public string? Name;
    public bool IsComplete;
}

Der folgende Code verwendet JsonSerializerOptions:

using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

app.MapGet("/", () => Results.Json(new Todo {
                      Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Im vorangehenden Code werden Webstandardwerte verwendet, mit denen Eigenschaftsnamen in das Camel Case-Format konvertiert werden.

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).

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