Dela via


Självstudie: Skapa ett minimalt API med ASP.NET Core

Note

Det här är inte den senaste versionen av den här artikeln. För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Warning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i supportpolicyn för .NET och .NET Core. För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Important

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

För den nuvarande utgåvan, se .NET 9-versionen av den här artikeln .

Av Rick Anderson och Tom Dykstra

Minimala API:er har skapats för att skapa HTTP-API:er med minimala beroenden. De är idealiska för mikrotjänster och appar som bara vill inkludera de minsta filerna, funktionerna och beroendena i ASP.NET Core.

I den här självstudien lär du dig grunderna i att skapa ett minimalt API med ASP.NET Core. En annan metod för att skapa API:er i ASP.NET Core är att använda styrenheter. Hjälp med att välja mellan minimala API:er och kontrollantbaserade API:er finns i ÖVERSIKT över API:er. En självstudiekurs om hur du skapar ett API-projekt baserat på kontrollanter som innehåller fler funktioner finns i Skapa ett webb-API.

Overview

I den här handledningen skapas följande API:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
POST /todoitems Lägga till ett nytt objekt Att göra-objekt Att göra-objekt
PUT /todoitems/{id} Uppdatera ett befintligt objekt Att göra-objekt None
DELETE /todoitems/{id}     Ta bort ett objekt None None

Prerequisites

Skapa ett API-projekt

  • Starta Visual Studio 2022 och välj Skapa ett nytt projekt.

  • I dialogrutan Skapa ett nytt projekt :

    • Ange Empty i sökrutan Sök efter mallar .
    • Välj mallen ASP.NET Core Empty och välj Nästa.

    Visual Studio Skapa ett nytt projekt

  • Ge projektet namnet TodoApi och välj Nästa.

  • I dialogrutan Ytterligare information :

    • Välj .NET 9.0
    • Avmarkera Använd inte toppnivåinstruktioner
    • Välj Skapa

    Ytterligare information

Granska koden

Filen Program.cs innehåller följande kod:

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

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

app.Run();

Föregående kod:

Kör appen

Tryck på Ctrl+F5 för att köra utan felsökningsprogrammet.

Visual Studio visar följande dialogruta:

Det här projektet är konfigurerat för att använda SSL. För att undvika SSL-varningar i webbläsaren kan du välja att lita på det självsignerade certifikat som IIS Express har genererat. Vill du lita på IIS Express SSL-certifikatet?

Välj Ja om du litar på IIS Express SSL-certifikatet.

Följande dialogruta visas:

Dialogrutan Säkerhetsvarning

Välj Ja om du samtycker till att lita på utvecklingscertifikatet.

För information om hur du kan lita på Firefox-webbläsaren, se Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certifikatfel.

Visual Studio startar Kestrel webbservern och öppnar ett webbläsarfönster.

Hello World! visas i webbläsaren. Filen Program.cs innehåller en minimal men fullständig app.

Stäng webbläsarfönstret.

Lägga till NuGet-paket

NuGet-paketen måste läggas till för att stödja databasen och diagnostiken som används i den här handledningen.

  • På menyn Verktyg väljer du NuGet Package Manager > Hantera NuGet-paket för lösning.
  • Välj fliken Bläddra.
  • Välj Inkludera förhandsversion.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Projekt i den högra rutan och välj sedan Installera.
  • Följ anvisningarna ovan för att lägga till Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore-paketet.

Modell- och databaskontextklasserna

  • I projektmappen skapar du en fil med namnet Todo.cs med följande kod:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Föregående kod skapar modellen för den här appen. En modell är en klass som representerar data som appen hanterar.

  • Skapa en fil med namnet TodoDb.cs med följande kod:
using Microsoft.EntityFrameworkCore;

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

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

Föregående kod definierar databaskontexten, som är huvudklassen som samordnar Entity Framework-funktioner för en datamodell. Den här klassen härleds från klassen Microsoft.EntityFrameworkCore.DbContext.

Lägg till API-koden

  • Ersätt innehållet i Program.cs-filen med följande kod:
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();

Följande markerade kod lägger till databaskontexten i di-containern (dependency injection) och aktiverar visning av databasrelaterade undantag:

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

DI-containern ger åtkomst till databaskontexten och andra tjänster.

I den här guiden används Endpoints Explorer och .http-filer för att testa API.

Testdata för publicering

Följande kod i Program.cs skapar en HTTP POST-slutpunkt /todoitems som lägger till data i den minnesinterna databasen:

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Kör appen. Webbläsaren visar ett 404-fel eftersom det inte längre finns någon / slutpunkt.

POST-slutpunkten används för att lägga till data i appen.

  • Välj Visa>Andra fönster>Endpoints Explorer.

  • Högerklicka på POST-slutpunkten och välj Generera begäran.

    Kontextmenyn i Endpoints Explorer som markerar alternativet Generera begäran.

    En ny fil skapas i projektmappen med namnet TodoApi.http, med innehåll som liknar följande exempel:

    @TodoApi_HostAddress = https://localhost:7031
    
    POST {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • Den första raden skapar en variabel som används för alla slutpunkter.
    • Nästa rad definierar en POST-begäran.
    • Trippelhashtag-raden (###) är en begäranavgränsare: vad som följer efter är för en annan begäran.
  • POST-begäran behöver rubriker och en brödtext. Om du vill definiera dessa delar av begäran lägger du till följande rader omedelbart efter POST-begäranderaden:

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    Föregående kod lägger till ett content-type-huvud och en JSON-begärandetext. Filen TodoApi.http bör nu se ut som i följande exempel, men med portnumret:

    @TodoApi_HostAddress = https://localhost:7057
    
    POST {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Kör appen.

  • Välj länken Skicka begäran som ligger ovanför begäranderaden POST .

    .http-filfönstret med kör-länken markerad.

    POST-begäran skickas till appen och svaret visas i fönstret Svar .

    .http-filfönstret med svar från POST-begäran.

Granska GET-slutpunkterna

Exempelappen implementerar flera GET-slutpunkter genom att anropa MapGet:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
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());

Testa GET-slutpunkterna

Testa appen genom att anropa GET slutpunkterna från en webbläsare eller med hjälp av Endpoints Explorer. Följande steg är för Endpoints Explorer.

  • I Endpoints Explorer högerklickar du på den första GET-slutpunkten och väljer Generera begäran.

    Följande innehåll läggs till i filen TodoApi.http:

    GET {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Välj länken Skicka begäran som ligger ovanför den nya GET begäranderaden.

    GET-begäran skickas till appen och svaret visas i fönstret Svar .

  • Svarstexten liknar följande JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • I Endpoints Explorer högerklickar du på /todoitems/{id}GET-slutpunkten och väljer Generera begäran. Följande innehåll läggs till i filen TodoApi.http:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersätt {id} med 1.

  • Välj länken Skicka begäran som ligger ovanför den nya GET-begäranderaden.

    GET-begäran skickas till appen och svaret visas i fönstret Svar .

  • Svarstexten liknar följande JSON:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Den här appen använder en minnesintern databas. Om appen startas om returnerar GET-begäran inte några data. Om inga data returneras skickar du POST-data till appen och provar GET-begäran igen.

Returnera värden

ASP.NET Core serialiserar automatiskt objektet till JSON och skriver JSON i brödtexten i svarsmeddelandet. Svarskoden för den här returtypen är 200 OK, förutsatt att det inte finns några ohanterade undantag. Ohanterade undantag översätts till 5xx-fel.

Returtyperna kan representera ett brett utbud av HTTP-statuskoder. Till exempel kan GET /todoitems/{id} returnera två olika statusvärden:

  • Om inget objekt matchar det begärda ID:t returnerar metoden en 404-statusfelkodNotFound .
  • Annars returnerar metoden 200 med en JSON-svarstext. Om du returnerar item resulterar det i ett HTTP 200-svar.

Granska PUT-slutpunkten

Exempelappen implementerar en enda PUT-slutpunkt med hjälp av 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();
});

Den här metoden liknar metoden MapPost, förutom att den använder HTTP PUT. Ett lyckat svar returnerar 204 (inget innehåll). Enligt HTTP-specifikationen kräver en PUT-begäran att klienten skickar hela den uppdaterade entiteten, inte bara ändringarna. Om du vill stödja partiella uppdateringar använder du HTTP PATCH.

Testa PUT-slutpunkten

Det här exemplet använder en minnesintern databas som måste initieras varje gång appen startas. Det måste finnas ett objekt i databasen innan du gör ett PUT-anrop. Anropa GET för att se till att det finns ett objekt i databasen innan du gör ett PUT-anrop.

Uppdatera det to-do objekt som har Id = 1 och ange dess namn till "feed fish".

  • I Endpoints Explorer högerklickar du på PUT-slutpunkten och väljer Generera begäran.

    Följande innehåll läggs till i filen TodoApi.http:

    PUT {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersätt {id} med 1på PUT-begäranderaden.

  • Lägg till följande rader omedelbart efter PUT-begäranderaden:

    Content-Type: application/json
    
    {
      "id": 1,
      "name": "feed fish",
      "isComplete": false
    }
    

    Föregående kod lägger till ett content-type-huvud och en JSON-begärandetext.

  • Välj länken Skicka begäran som ligger ovanför den nya PUT-begäranderaden.

    PUT-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden är 204.

Granska och testa DELETE-slutpunkten

Exempelappen implementerar en enskild DELETE-slutpunkt med 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();
});
  • I Endpoints Explorer högerklickar du på DELETE-slutpunkten och väljer Generera begäran.

    En DELETE-begäran läggs till i TodoApi.http.

  • Byt ut {id} i raden för DELETE-begäran mot 1. DELETE-begäran bör se ut som i följande exempel:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Välj länken Skicka begäran för DELETE-begäran.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden är 204.

Använda MapGroup-API:et

Exempelappkoden upprepar todoitems URL-prefixet varje gång den konfigurerar en slutpunkt. API:er har ofta grupper av slutpunkter med ett vanligt URL-prefix, och metoden MapGroup är tillgänglig för att organisera sådana grupper. Det minskar repetitiv kod och gör det möjligt att anpassa hela grupper av slutpunkter med ett enda anrop till metoder som RequireAuthorization och WithMetadata.

Ersätt innehållet i Program.cs med följande kod:

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

Föregående kod har följande ändringar:

  • Lägger till var todoItems = app.MapGroup("/todoitems"); för att konfigurera gruppen med hjälp av URL-prefixet /todoitems.
  • Ändrar alla app.Map<HttpVerb> metoder till todoItems.Map<HttpVerb>.
  • Tar bort URL-prefixet /todoitems från Map<HttpVerb>-metodanrop.

Testa slutpunkterna för att kontrollera att de fungerar på samma sätt.

Använda Api:et TypedResults

Att returnera TypedResults i stället för Results har flera fördelar, inklusive testbarhet och att automatiskt returnera svarstypmetadata för OpenAPI för att beskriva slutpunkten. Mer information finns i TypedResults vs Results.

De Map<HttpVerb> metoderna kan anropa routningshanterarmetoder i stället för att använda lambdas. Om du vill se ett exempel uppdaterar du Program.cs med följande kod:

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

Den Map<HttpVerb> koden anropar nu metoder i stället för lambdas:

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

Dessa metoder returnerar objekt som implementerar IResult och definieras av TypedResults:

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

Enhetstester kan anropa dessa metoder och testa att de returnerar rätt typ. Om metoden till exempel är GetAllTodos:

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

Enhetstestkoden kan kontrollera att ett objekt av typen Ok<Todo[]> returneras från hanteringsmetoden. Till exempel:

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

Förhindra överpublicering

För närvarande exponerar exempelappen hela Todo objektet. I produktionsprogram används ofta en delmängd av modellen för att begränsa de data som kan matas in och returneras. Det finns flera orsaker till detta och säkerheten är viktig. Delmängden av en modell kallas vanligtvis för ett dataöverföringsobjekt (DTO), indatamodell eller vymodell. DTO används i den här artikeln.

En DTO kan användas för att:

  • Förhindra överpublicering.
  • Dölj egenskaper som klienter inte ska visa.
  • Utelämna vissa egenskaper för att minska nyttolaststorleken.
  • Platta ut objektdiagram som innehåller kapslade objekt. Utplattade objektdiagram kan vara enklare för klienter.

Om du vill demonstrera DTO-metoden uppdaterar du klassen Todo så att den innehåller ett hemligt fält:

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

Det hemliga fältet måste vara dolt från den här appen, men en administrativ app kan välja att exponera det.

Kontrollera att du kan skicka och hämta det hemliga fältet.

Skapa en fil med namnet TodoItemDTO.cs med följande kod:

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

Ersätt innehållet i Program.cs-filen med följande kod för att använda den här DTO-modellen:

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

Kontrollera att du kan publicera och hämta alla fält utom det hemliga fältet.

Felsökning med det slutförda exemplet

Om du stöter på ett problem som du inte kan lösa kan du jämföra koden med det slutförda projektet. Visa eller ladda ned slutfört projekt (ladda ned).

Nästa steg

Learn more

Se Snabbreferens för minimala API:er

Minimala API:er har skapats för att skapa HTTP-API:er med minimala beroenden. De är idealiska för mikrotjänster och appar som bara vill inkludera de minsta filerna, funktionerna och beroendena i ASP.NET Core.

I den här självstudien lär du dig grunderna i att skapa ett minimalt API med ASP.NET Core. En annan metod för att skapa API:er i ASP.NET Core är att använda styrenheter. Hjälp med att välja mellan minimala API:er och kontrollantbaserade API:er finns i ÖVERSIKT över API:er. En självstudiekurs om hur du skapar ett API-projekt baserat på kontrollanter som innehåller fler funktioner finns i Skapa ett webb-API.

Overview

I den här handledningen skapas följande API:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
POST /todoitems Lägga till ett nytt objekt Att göra-objekt Att göra-objekt
PUT /todoitems/{id} Uppdatera ett befintligt objekt Att göra-objekt None
DELETE /todoitems/{id}     Ta bort ett objekt None None

Prerequisites

Skapa ett API-projekt

  • Starta Visual Studio 2022 och välj Skapa ett nytt projekt.

  • I dialogrutan Skapa ett nytt projekt :

    • Ange Empty i sökrutan Sök efter mallar .
    • Välj mallen ASP.NET Core Empty och välj Nästa.

    Visual Studio Skapa ett nytt projekt

  • Ge projektet namnet TodoApi och välj Nästa.

  • I dialogrutan Ytterligare information :

    • Välj .NET 7.0
    • Avmarkera Använd inte toppnivåinstruktioner
    • Välj Skapa

    Ytterligare information

Granska koden

Filen Program.cs innehåller följande kod:

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

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

app.Run();

Föregående kod:

Kör appen

Tryck på Ctrl+F5 för att köra utan felsökningsprogrammet.

Visual Studio visar följande dialogruta:

Det här projektet är konfigurerat för att använda SSL. För att undvika SSL-varningar i webbläsaren kan du välja att lita på det självsignerade certifikat som IIS Express har genererat. Vill du lita på IIS Express SSL-certifikatet?

Välj Ja om du litar på IIS Express SSL-certifikatet.

Följande dialogruta visas:

Dialogrutan Säkerhetsvarning

Välj Ja om du samtycker till att lita på utvecklingscertifikatet.

För information om hur du kan lita på Firefox-webbläsaren, se Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certifikatfel.

Visual Studio startar Kestrel webbservern och öppnar ett webbläsarfönster.

Hello World! visas i webbläsaren. Filen Program.cs innehåller en minimal men fullständig app.

Lägga till NuGet-paket

NuGet-paketen måste läggas till för att stödja databasen och diagnostiken som används i den här handledningen.

  • På menyn Verktyg väljer du NuGet Package Manager > Hantera NuGet-paket för lösning.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Projekt i den högra rutan.
  • I listrutan Version väljer du den senaste version 7 som är tillgänglig, till exempel 7.0.17och väljer sedan Installera.
  • Följ anvisningarna ovan för att lägga till Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore-paketet med den senaste versionen 7 tillgänglig.

Modell- och databaskontextklasserna

I projektmappen skapar du en fil med namnet Todo.cs med följande kod:

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

Föregående kod skapar modellen för den här appen. En modell är en klass som representerar data som appen hanterar.

Skapa en fil med namnet TodoDb.cs med följande kod:

using Microsoft.EntityFrameworkCore;

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

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

Föregående kod definierar databaskontexten, som är huvudklassen som samordnar Entity Framework-funktioner för en datamodell. Den här klassen härleds från klassen Microsoft.EntityFrameworkCore.DbContext.

Lägg till API-koden

Ersätt innehållet i Program.cs-filen med följande kod:

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

Följande markerade kod lägger till databaskontexten i di-containern (dependency injection) och aktiverar visning av databasrelaterade undantag:

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

DI-containern ger åtkomst till databaskontexten och andra tjänster.

Skapa API-testgränssnitt med Swagger

Det finns många tillgängliga verktyg för webb-API-testning att välja mellan, och du kan följa den här självstudiekursens inledande API-teststeg med ditt eget önskade verktyg.

I den här självstudien används .NET-paketet NSwag.AspNetCore, som integrerar Swagger-verktyg för att generera ett testgränssnitt som följer OpenAPI-specifikationen:

  • NSwag: Ett .NET-bibliotek som integrerar Swagger direkt i ASP.NET Core-program, vilket ger mellanprogram och konfiguration.
  • Swagger: En uppsättning verktyg med öppen källkod som OpenAPIGenerator och SwaggerUI som genererar API-testsidor som följer OpenAPI-specifikationen.
  • OpenAPI-specifikation: Ett dokument som beskriver funktionerna i API:et, baserat på XML- och attributanteckningarna i kontrollanterna och modellerna.

Mer information om hur du använder OpenAPI och NSwag med ASP.NET finns i ASP.NET Core web API-dokumentation med Swagger/OpenAPI.

Installera Swagger-verktyg

  • Kör följande kommando:

    dotnet add package NSwag.AspNetCore
    

Föregående kommando lägger till paketet NSwag.AspNetCore, som innehåller verktyg för att generera Swagger-dokument och användargränssnitt.

Konfigurera Swagger-mellanprogram

  • Lägg till följande markerade kod innan app definieras i rad var app = builder.Build();

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

I föregående kod:

  • builder.Services.AddEndpointsApiExplorer();: Aktiverar API Explorer, som är en tjänst som tillhandahåller metadata om HTTP-API:et. API Explorer används av Swagger för att generera Swagger-dokumentet.

  • builder.Services.AddOpenApiDocument(config => {...});: Lägger till Swagger OpenAPI-dokumentgeneratorn i programtjänsterna och konfigurerar den för att ge mer information om API:et, till exempel dess titel och version. Information om hur du tillhandahåller mer robust API-information finns i Kom igång med NSwag och ASP.NET Core

  • Lägg till följande markerade kod på nästa rad när app har definierats i rad var app = builder.Build();

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

    Den tidigare koden aktiverar Swagger-mellanprogrammet för att hantera det genererade JSON-dokumentet och Swagger-användargränssnittet. Swagger är endast aktiverat i en utvecklingsmiljö. Om du aktiverar Swagger i en produktionsmiljö kan potentiellt känslig information om API:ets struktur och implementering exponeras.

Testdata för publicering

Följande kod i Program.cs skapar en HTTP POST-slutpunkt /todoitems som lägger till data i den minnesinterna databasen:

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Kör appen. Webbläsaren visar ett 404-fel eftersom det inte längre finns någon / slutpunkt.

POST-slutpunkten används för att lägga till data i appen.

  • När appen fortfarande körs går du till https://localhost:<port>/swagger i webbläsaren för att visa den API-testsida som genererats av Swagger.

    Swagger-genererad API-testsida

  • På testsidan för Swagger API väljer du Publicera /todoitems>Prova.

  • Observera att fältet Begärandetext innehåller ett genererat exempelformat som återspeglar parametrarna för API:et.

  • I begärandetexten anger du JSON för ett to-do objekt, utan att ange den valfria id:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Välj Kör.

    Swagger med Post

Swagger innehåller ett svarsfönster under knappen Kör .

Swagger med POST-svar

Observera några av de användbara detaljerna:

  • cURL: Swagger innehåller ett exempel på ett cURL-kommando i Unix/Linux-syntaxen, som kan köras på kommandoraden med alla bash-gränssnitt som använder Unix/Linux-syntax, inklusive Git Bash från Git för Windows.
  • Begärande-URL: En förenklad representation av HTTP-begäran som görs av Swagger UI:s JavaScript-kod för API-anropet. Faktiska begäranden kan innehålla information som rubriker och frågeparametrar och en begärandetext.
  • Serversvar: Innehåller svarstexten och rubrikerna. Svarstexten visar att id har angetts till 1.
  • Svarskod: En statuskod för 201 HTTP returnerades, vilket indikerar att begäran har bearbetats och resulterat i skapandet av en ny resurs.

Granska GET-slutpunkterna

Exempelappen implementerar flera GET-slutpunkter genom att anropa MapGet:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
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());

Testa GET-slutpunkterna

Testa appen genom att anropa slutpunkterna från en webbläsare eller Swagger.

  • I Swagger väljer du GET /todoitems>Prova>Kör.

  • Du kan också anropa GET /todoitems från en webbläsare genom att ange URI http://localhost:<port>/todoitems:n . Till exempel http://localhost:5001/todoitems

Anropet till GET /todoitems genererar ett svar som liknar följande:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Anropa GET /todoitems/{id} i Swagger för att returnera data från ett specifikt ID:

    • Välj GET /todoitems>Prova.
    • Ange id-fältet till 1 och välj Kör.
  • Du kan också anropa GET /todoitems från en webbläsare genom att ange URI https://localhost:<port>/todoitems/1:n . Till exempel https://localhost:5001/todoitems/1

  • Svaret liknar följande:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Den här appen använder en minnesintern databas. Om appen startas om returnerar GET-begäran inte några data. Om inga data returneras skickar du POST-data till appen och provar GET-begäran igen.

Returnera värden

ASP.NET Core serialiserar automatiskt objektet till JSON och skriver JSON i brödtexten i svarsmeddelandet. Svarskoden för den här returtypen är 200 OK, förutsatt att det inte finns några ohanterade undantag. Ohanterade undantag översätts till 5xx-fel.

Returtyperna kan representera ett brett utbud av HTTP-statuskoder. Till exempel kan GET /todoitems/{id} returnera två olika statusvärden:

  • Om inget objekt matchar det begärda ID:t returnerar metoden en 404-statusfelkodNotFound .
  • Annars returnerar metoden 200 med en JSON-svarstext. Om du returnerar item resulterar det i ett HTTP 200-svar.

Granska PUT-slutpunkten

Exempelappen implementerar en enda PUT-slutpunkt med hjälp av 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();
});

Den här metoden liknar metoden MapPost, förutom att den använder HTTP PUT. Ett lyckat svar returnerar 204 (inget innehåll). Enligt HTTP-specifikationen kräver en PUT-begäran att klienten skickar hela den uppdaterade entiteten, inte bara ändringarna. Om du vill stödja partiella uppdateringar använder du HTTP PATCH.

Testa PUT-slutpunkten

Det här exemplet använder en minnesintern databas som måste initieras varje gång appen startas. Det måste finnas ett objekt i databasen innan du gör ett PUT-anrop. Anropa GET för att se till att det finns ett objekt i databasen innan du gör ett PUT-anrop.

Uppdatera det to-do objekt som har Id = 1 och ange dess namn till "feed fish".

Använd Swagger för att skicka en PUT-begäran:

  • Välj Lägg till /todoitems/{id}>Prova.

  • Ange ID-fältet till 1.

  • Ställ in förfrågningskroppen som följande JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Välj Kör.

Granska och testa DELETE-slutpunkten

Exempelappen implementerar en enskild DELETE-slutpunkt med 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();
});

Använd Swagger för att skicka en DELETE-begäran:

  • Välj DELETE /todoitems/{id}>Prova.

  • Ange ID-fältet till 1 och välj Kör.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden för serversvar är 204.

Använda MapGroup-API:et

Exempelappkoden upprepar todoitems URL-prefixet varje gång den konfigurerar en slutpunkt. API:er har ofta grupper av slutpunkter med ett vanligt URL-prefix, och metoden MapGroup är tillgänglig för att organisera sådana grupper. Det minskar repetitiv kod och gör det möjligt att anpassa hela grupper av slutpunkter med ett enda anrop till metoder som RequireAuthorization och WithMetadata.

Ersätt innehållet i Program.cs med följande kod:

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

Föregående kod har följande ändringar:

  • Lägger till var todoItems = app.MapGroup("/todoitems"); för att konfigurera gruppen med hjälp av URL-prefixet /todoitems.
  • Ändrar alla app.Map<HttpVerb> metoder till todoItems.Map<HttpVerb>.
  • Tar bort URL-prefixet /todoitems från Map<HttpVerb>-metodanrop.

Testa slutpunkterna för att kontrollera att de fungerar på samma sätt.

Använda Api:et TypedResults

Att returnera TypedResults i stället för Results har flera fördelar, inklusive testbarhet och att automatiskt returnera svarstypmetadata för OpenAPI för att beskriva slutpunkten. Mer information finns i TypedResults vs Results.

De Map<HttpVerb> metoderna kan anropa routningshanterarmetoder i stället för att använda lambdas. Om du vill se ett exempel uppdaterar du Program.cs med följande kod:

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

Den Map<HttpVerb> koden anropar nu metoder i stället för lambdas:

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

Dessa metoder returnerar objekt som implementerar IResult och definieras av TypedResults:

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

Enhetstester kan anropa dessa metoder och testa att de returnerar rätt typ. Om metoden till exempel är GetAllTodos:

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

Enhetstestkoden kan kontrollera att ett objekt av typen Ok<Todo[]> returneras från hanteringsmetoden. Till exempel:

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

Förhindra överpublicering

För närvarande exponerar exempelappen hela Todo objektet. Produktionsappar I produktionsprogram används ofta en delmängd av modellen för att begränsa de data som kan matas in och returneras. Det finns flera orsaker till detta och säkerheten är viktig. Delmängden av en modell kallas vanligtvis för ett dataöverföringsobjekt (DTO), indatamodell eller vymodell. DTO används i den här artikeln.

En DTO kan användas för att:

  • Förhindra överpublicering.
  • Dölj egenskaper som klienter inte ska visa.
  • Utelämna vissa egenskaper för att minska nyttolaststorleken.
  • Platta ut objektdiagram som innehåller kapslade objekt. Utplattade objektdiagram kan vara enklare för klienter.

Om du vill demonstrera DTO-metoden uppdaterar du klassen Todo så att den innehåller ett hemligt fält:

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

Det hemliga fältet måste vara dolt från den här appen, men en administrativ app kan välja att exponera det.

Kontrollera att du kan skicka och hämta det hemliga fältet.

Skapa en fil med namnet TodoItemDTO.cs med följande kod:

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

Ersätt innehållet i Program.cs-filen med följande kod för att använda den här DTO-modellen:

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

Kontrollera att du kan publicera och hämta alla fält utom det hemliga fältet.

Felsökning med det slutförda exemplet

Om du stöter på ett problem som du inte kan lösa kan du jämföra koden med det slutförda projektet. Visa eller ladda ned slutfört projekt (ladda ned).

Nästa steg

Learn more

Se Snabbreferens för minimala API:er

Minimala API:er har skapats för att skapa HTTP-API:er med minimala beroenden. De är idealiska för mikrotjänster och appar som bara vill inkludera de minsta filerna, funktionerna och beroendena i ASP.NET Core.

I den här självstudien lär du dig grunderna i att skapa ett minimalt API med ASP.NET Core. En annan metod för att skapa API:er i ASP.NET Core är att använda styrenheter. Hjälp med att välja mellan minimala API:er och kontrollantbaserade API:er finns i ÖVERSIKT över API:er. En självstudiekurs om hur du skapar ett API-projekt baserat på kontrollanter som innehåller fler funktioner finns i Skapa ett webb-API.

Overview

I den här handledningen skapas följande API:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
POST /todoitems Lägga till ett nytt objekt Att göra-objekt Att göra-objekt
PUT /todoitems/{id} Uppdatera ett befintligt objekt Att göra-objekt None
DELETE /todoitems/{id}     Ta bort ett objekt None None

Prerequisites

Skapa ett API-projekt

  • Starta Visual Studio 2022 och välj Skapa ett nytt projekt.

  • I dialogrutan Skapa ett nytt projekt :

    • Ange Empty i sökrutan Sök efter mallar .
    • Välj mallen ASP.NET Core Empty och välj Nästa.

    Visual Studio Skapa ett nytt projekt

  • Ge projektet namnet TodoApi och välj Nästa.

  • I dialogrutan Ytterligare information :

    • Välj .NET 6.0
    • Avmarkera Använd inte toppnivåinstruktioner
    • Välj Skapa

Granska koden

Filen Program.cs innehåller följande kod:

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

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

app.Run();

Föregående kod:

Kör appen

Tryck på Ctrl+F5 för att köra utan felsökningsprogrammet.

Visual Studio visar följande dialogruta:

Det här projektet är konfigurerat för att använda SSL. För att undvika SSL-varningar i webbläsaren kan du välja att lita på det självsignerade certifikat som IIS Express har genererat. Vill du lita på IIS Express SSL-certifikatet?

Välj Ja om du litar på IIS Express SSL-certifikatet.

Följande dialogruta visas:

Dialogrutan Säkerhetsvarning

Välj Ja om du samtycker till att lita på utvecklingscertifikatet.

För information om hur du kan lita på Firefox-webbläsaren, se Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certifikatfel.

Visual Studio startar Kestrel webbservern och öppnar ett webbläsarfönster.

Hello World! visas i webbläsaren. Filen Program.cs innehåller en minimal men fullständig app.

Lägga till NuGet-paket

NuGet-paketen måste läggas till för att stödja databasen och diagnostiken som används i den här handledningen.

  • På menyn Verktyg väljer du NuGet Package Manager > Hantera NuGet-paket för lösning.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Projekt i den högra rutan.
  • I listrutan Version väljer du den senaste version 7 som är tillgänglig, till exempel 6.0.28och väljer sedan Installera.
  • Följ anvisningarna ovan för att lägga till Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore-paketet med den senaste versionen 7 tillgänglig.

Modell- och databaskontextklasserna

I projektmappen skapar du en fil med namnet Todo.cs med följande kod:

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

Föregående kod skapar modellen för den här appen. En modell är en klass som representerar data som appen hanterar.

Skapa en fil med namnet TodoDb.cs med följande kod:

using Microsoft.EntityFrameworkCore;

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

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

Föregående kod definierar databaskontexten, som är huvudklassen som samordnar Entity Framework-funktioner för en datamodell. Den här klassen härleds från klassen Microsoft.EntityFrameworkCore.DbContext.

Lägg till API-koden

Ersätt innehållet i Program.cs-filen med följande kod:

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

Följande markerade kod lägger till databaskontexten i di-containern (dependency injection) och aktiverar visning av databasrelaterade undantag:

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

DI-containern ger åtkomst till databaskontexten och andra tjänster.

Skapa API-testgränssnitt med Swagger

Det finns många tillgängliga verktyg för webb-API-testning att välja mellan, och du kan följa den här självstudiekursens inledande API-teststeg med ditt eget önskade verktyg.

I den här självstudien används .NET-paketet NSwag.AspNetCore, som integrerar Swagger-verktyg för att generera ett testgränssnitt som följer OpenAPI-specifikationen:

  • NSwag: Ett .NET-bibliotek som integrerar Swagger direkt i ASP.NET Core-program, vilket ger mellanprogram och konfiguration.
  • Swagger: En uppsättning verktyg med öppen källkod som OpenAPIGenerator och SwaggerUI som genererar API-testsidor som följer OpenAPI-specifikationen.
  • OpenAPI-specifikation: Ett dokument som beskriver funktionerna i API:et, baserat på XML- och attributanteckningarna i kontrollanterna och modellerna.

Mer information om hur du använder OpenAPI och NSwag med ASP.NET finns i ASP.NET Core web API-dokumentation med Swagger/OpenAPI.

Installera Swagger-verktyg

  • Kör följande kommando:

    dotnet add package NSwag.AspNetCore
    

Föregående kommando lägger till paketet NSwag.AspNetCore, som innehåller verktyg för att generera Swagger-dokument och användargränssnitt.

Konfigurera Swagger-mellanprogram

  • Lägg till följande using-instruktioner överst i Program.cs:

    using NSwag.AspNetCore;
    
  • Lägg till följande markerade kod innan app definieras i rad var app = builder.Build();

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

I föregående kod:

  • builder.Services.AddEndpointsApiExplorer();: Aktiverar API Explorer, som är en tjänst som tillhandahåller metadata om HTTP-API:et. API Explorer används av Swagger för att generera Swagger-dokumentet.

  • builder.Services.AddOpenApiDocument(config => {...});: Lägger till Swagger OpenAPI-dokumentgeneratorn i programtjänsterna och konfigurerar den för att ge mer information om API:et, till exempel dess titel och version. Information om hur du tillhandahåller mer robust API-information finns i Kom igång med NSwag och ASP.NET Core

  • Lägg till följande markerade kod på nästa rad när app har definierats i rad var app = builder.Build();

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

    Den tidigare koden aktiverar Swagger-mellanprogrammet för att hantera det genererade JSON-dokumentet och Swagger-användargränssnittet. Swagger är endast aktiverat i en utvecklingsmiljö. Om du aktiverar Swagger i en produktionsmiljö kan potentiellt känslig information om API:ets struktur och implementering exponeras.

Testdata för publicering

Följande kod i Program.cs skapar en HTTP POST-slutpunkt /todoitems som lägger till data i den minnesinterna databasen:

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Kör appen. Webbläsaren visar ett 404-fel eftersom det inte längre finns någon / slutpunkt.

POST-slutpunkten används för att lägga till data i appen.

  • När appen fortfarande körs går du till https://localhost:<port>/swagger i webbläsaren för att visa den API-testsida som genererats av Swagger.

    Swagger-genererad API-testsida

  • På testsidan för Swagger API väljer du Publicera /todoitems>Prova.

  • Observera att fältet Begärandetext innehåller ett genererat exempelformat som återspeglar parametrarna för API:et.

  • I begärandetexten anger du JSON för ett to-do objekt, utan att ange den valfria id:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Välj Kör.

    Swagger med postdata

Swagger innehåller ett svarsfönster under knappen Kör .

Swagger med fönstret Post resonse

Observera några av de användbara detaljerna:

  • cURL: Swagger innehåller ett exempel på ett cURL-kommando i Unix/Linux-syntaxen, som kan köras på kommandoraden med alla bash-gränssnitt som använder Unix/Linux-syntax, inklusive Git Bash från Git för Windows.
  • Begärande-URL: En förenklad representation av HTTP-begäran som görs av Swagger UI:s JavaScript-kod för API-anropet. Faktiska begäranden kan innehålla information som rubriker och frågeparametrar och en begärandetext.
  • Serversvar: Innehåller svarstexten och rubrikerna. Svarstexten visar att id har angetts till 1.
  • Svarskod: En statuskod för 201 HTTP returnerades, vilket indikerar att begäran har bearbetats och resulterat i skapandet av en ny resurs.

Granska GET-slutpunkterna

Exempelappen implementerar flera GET-slutpunkter genom att anropa MapGet:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
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());

Testa GET-slutpunkterna

Testa appen genom att anropa slutpunkterna från en webbläsare eller Swagger.

  • I Swagger väljer du GET /todoitems>Prova>Kör.

  • Du kan också anropa GET /todoitems från en webbläsare genom att ange URI http://localhost:<port>/todoitems:n . Till exempel http://localhost:5001/todoitems

Anropet till GET /todoitems genererar ett svar som liknar följande:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Anropa GET /todoitems/{id} i Swagger för att returnera data från ett specifikt ID:

    • Välj GET /todoitems>Prova.
    • Ange id-fältet till 1 och välj Kör.
  • Du kan också anropa GET /todoitems från en webbläsare genom att ange URI https://localhost:<port>/todoitems/1:n . Till exempel, till exempel https://localhost:5001/todoitems/1

  • Svaret liknar följande:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Den här appen använder en minnesintern databas. Om appen startas om returnerar GET-begäran inte några data. Om inga data returneras skickar du POST-data till appen och provar GET-begäran igen.

Returnera värden

ASP.NET Core serialiserar automatiskt objektet till JSON och skriver JSON i brödtexten i svarsmeddelandet. Svarskoden för den här returtypen är 200 OK, förutsatt att det inte finns några ohanterade undantag. Ohanterade undantag översätts till 5xx-fel.

Returtyperna kan representera ett brett utbud av HTTP-statuskoder. Till exempel kan GET /todoitems/{id} returnera två olika statusvärden:

  • Om inget objekt matchar det begärda ID:t returnerar metoden en 404-statusfelkodNotFound .
  • Annars returnerar metoden 200 med en JSON-svarstext. Om du returnerar item resulterar det i ett HTTP 200-svar.

Granska PUT-slutpunkten

Exempelappen implementerar en enda PUT-slutpunkt med hjälp av 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();
});

Den här metoden liknar metoden MapPost, förutom att den använder HTTP PUT. Ett lyckat svar returnerar 204 (inget innehåll). Enligt HTTP-specifikationen kräver en PUT-begäran att klienten skickar hela den uppdaterade entiteten, inte bara ändringarna. Om du vill stödja partiella uppdateringar använder du HTTP PATCH.

Testa PUT-slutpunkten

Det här exemplet använder en minnesintern databas som måste initieras varje gång appen startas. Det måste finnas ett objekt i databasen innan du gör ett PUT-anrop. Anropa GET för att se till att det finns ett objekt i databasen innan du gör ett PUT-anrop.

Uppdatera det to-do objekt som har Id = 1 och ange dess namn till "feed fish".

Använd Swagger för att skicka en PUT-begäran:

  • Välj Lägg till /todoitems/{id}>Prova.

  • Ange ID-fältet till 1.

  • Ställ in förfrågningskroppen som följande JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Välj Kör.

Granska och testa DELETE-slutpunkten

Exempelappen implementerar en enskild DELETE-slutpunkt med 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();
});

Använd Swagger för att skicka en DELETE-begäran:

  • Välj DELETE /todoitems/{id}>Prova.

  • Ange ID-fältet till 1 och välj Kör.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden för serversvar är 204.

Förhindra överpublicering

För närvarande exponerar exempelappen hela Todo objektet. Produktionsappar I produktionsprogram används ofta en delmängd av modellen för att begränsa de data som kan matas in och returneras. Det finns flera orsaker till detta och säkerheten är viktig. Delmängden av en modell kallas vanligtvis för ett dataöverföringsobjekt (DTO), indatamodell eller vymodell. DTO används i den här artikeln.

En DTO kan användas för att:

  • Förhindra överpublicering.
  • Dölj egenskaper som klienter inte ska visa.
  • Utelämna vissa egenskaper för att minska nyttolaststorleken.
  • Platta ut objektdiagram som innehåller kapslade objekt. Utplattade objektdiagram kan vara enklare för klienter.

Om du vill demonstrera DTO-metoden uppdaterar du klassen Todo så att den innehåller ett hemligt fält:

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

Det hemliga fältet måste vara dolt från den här appen, men en administrativ app kan välja att exponera det.

Kontrollera att du kan skicka och hämta det hemliga fältet.

Skapa en fil med namnet TodoItemDTO.cs med följande kod:

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

Ersätt innehållet i Program.cs-filen med följande kod för att använda den här DTO-modellen:

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

Kontrollera att du kan publicera och hämta alla fält utom det hemliga fältet.

Testa minimalt API

Ett exempel på hur du testar en minimal API-app finns i det här GitHub-exemplet.

Publicera till Azure

Information om hur du distribuerar till Azure finns i Snabbstart: Distribuera en ASP.NET webbapp.

Ytterligare resurser

Minimala API:er har skapats för att skapa HTTP-API:er med minimala beroenden. De är idealiska för mikrotjänster och appar som bara vill inkludera de minsta filerna, funktionerna och beroendena i ASP.NET Core.

I den här självstudien lär du dig grunderna i att skapa ett minimalt API med ASP.NET Core. En annan metod för att skapa API:er i ASP.NET Core är att använda styrenheter. Hjälp med att välja mellan minimala API:er och kontrollantbaserade API:er finns i ÖVERSIKT över API:er. En självstudiekurs om hur du skapar ett API-projekt baserat på kontrollanter som innehåller fler funktioner finns i Skapa ett webb-API.

Overview

I den här handledningen skapas följande API:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
POST /todoitems Lägga till ett nytt objekt Att göra-objekt Att göra-objekt
PUT /todoitems/{id} Uppdatera ett befintligt objekt Att göra-objekt None
DELETE /todoitems/{id}     Ta bort ett objekt None None

Prerequisites

Skapa ett API-projekt

  • Starta Visual Studio 2022 och välj Skapa ett nytt projekt.

  • I dialogrutan Skapa ett nytt projekt :

    • Ange Empty i sökrutan Sök efter mallar .
    • Välj mallen ASP.NET Core Empty och välj Nästa.

    Visual Studio Skapa ett nytt projekt

  • Ge projektet namnet TodoApi och välj Nästa.

  • I dialogrutan Ytterligare information :

    • Välj .NET 8.0 (långsiktig support)
    • Avmarkera Använd inte toppnivåinstruktioner
    • Välj Skapa

    Ytterligare information

Granska koden

Filen Program.cs innehåller följande kod:

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

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

app.Run();

Föregående kod:

Kör appen

Tryck på Ctrl+F5 för att köra utan felsökningsprogrammet.

Visual Studio visar följande dialogruta:

Det här projektet är konfigurerat för att använda SSL. För att undvika SSL-varningar i webbläsaren kan du välja att lita på det självsignerade certifikat som IIS Express har genererat. Vill du lita på IIS Express SSL-certifikatet?

Välj Ja om du litar på IIS Express SSL-certifikatet.

Följande dialogruta visas:

Dialogrutan Säkerhetsvarning

Välj Ja om du samtycker till att lita på utvecklingscertifikatet.

För information om hur du kan lita på Firefox-webbläsaren, se Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certifikatfel.

Visual Studio startar Kestrel webbservern och öppnar ett webbläsarfönster.

Hello World! visas i webbläsaren. Filen Program.cs innehåller en minimal men fullständig app.

Stäng webbläsarfönstret.

Lägga till NuGet-paket

NuGet-paketen måste läggas till för att stödja databasen och diagnostiken som används i den här handledningen.

  • På menyn Verktyg väljer du NuGet Package Manager > Hantera NuGet-paket för lösning.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Projekt i den högra rutan och välj sedan Installera.
  • Följ anvisningarna ovan för att lägga till Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore-paketet.

Modell- och databaskontextklasserna

  • I projektmappen skapar du en fil med namnet Todo.cs med följande kod:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Föregående kod skapar modellen för den här appen. En modell är en klass som representerar data som appen hanterar.

  • Skapa en fil med namnet TodoDb.cs med följande kod:
using Microsoft.EntityFrameworkCore;

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

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

Föregående kod definierar databaskontexten, som är huvudklassen som samordnar Entity Framework-funktioner för en datamodell. Den här klassen härleds från klassen Microsoft.EntityFrameworkCore.DbContext.

Lägg till API-koden

  • Ersätt innehållet i Program.cs-filen med följande kod:
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();

Följande markerade kod lägger till databaskontexten i di-containern (dependency injection) och aktiverar visning av databasrelaterade undantag:

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

DI-containern ger åtkomst till databaskontexten och andra tjänster.

I den här guiden används Endpoints Explorer och .http-filer för att testa API.

Testdata för publicering

Följande kod i Program.cs skapar en HTTP POST-slutpunkt /todoitems som lägger till data i den minnesinterna databasen:

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

Kör appen. Webbläsaren visar ett 404-fel eftersom det inte längre finns någon / slutpunkt.

POST-slutpunkten används för att lägga till data i appen.

  • Välj Visa>Andra fönster>Endpoints Explorer.

  • Högerklicka på POST-slutpunkten och välj Generera begäran.

    Kontextmenyn i Endpoints Explorer som markerar alternativet Generera begäran.

    En ny fil skapas i projektmappen med namnet TodoApi.http, med innehåll som liknar följande exempel:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • Den första raden skapar en variabel som används för alla slutpunkter.
    • Nästa rad definierar en POST-begäran.
    • Trippelhashtag-raden (###) är en begäranavgränsare: vad som följer efter är för en annan begäran.
  • POST-begäran behöver rubriker och en brödtext. Om du vill definiera dessa delar av begäran lägger du till följande rader omedelbart efter POST-begäranderaden:

    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    

    Föregående kod lägger till ett content-type-huvud och en JSON-begärandetext. Filen TodoApi.http bör nu se ut som i följande exempel, men med portnumret:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Kör appen.

  • Välj länken Skicka begäran som ligger ovanför begäranderaden POST .

    .http-filfönstret med kör-länken markerad.

    POST-begäran skickas till appen och svaret visas i fönstret Svar .

    .http-filfönstret med svar från POST-begäran.

Granska GET-slutpunkterna

Exempelappen implementerar flera GET-slutpunkter genom att anropa MapGet:

API Description begäranens innehåll Svarskropp
GET /todoitems Hämta alla to-do objekt None Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt None Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID None Att göra-objekt
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());

Testa GET-slutpunkterna

Testa appen genom att anropa GET slutpunkterna från en webbläsare eller med hjälp av Endpoints Explorer. Följande steg är för Endpoints Explorer.

  • I Endpoints Explorer högerklickar du på den första GET-slutpunkten och väljer Generera begäran.

    Följande innehåll läggs till i filen TodoApi.http:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Välj länken Skicka begäran som ligger ovanför den nya GET begäranderaden.

    GET-begäran skickas till appen och svaret visas i fönstret Svar .

  • Svarstexten liknar följande JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • I Endpoints Explorer högerklickar du på /todoitems/{id}GET-slutpunkten och väljer Generera begäran. Följande innehåll läggs till i filen TodoApi.http:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersätt {id} med 1.

  • Välj länken Skicka begäran som ligger ovanför den nya GET-begäranderaden.

    GET-begäran skickas till appen och svaret visas i fönstret Svar .

  • Svarstexten liknar följande JSON:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": true
    }
    

Den här appen använder en minnesintern databas. Om appen startas om returnerar GET-begäran inte några data. Om inga data returneras skickar du POST-data till appen och provar GET-begäran igen.

Returnera värden

ASP.NET Core serialiserar automatiskt objektet till JSON och skriver JSON i brödtexten i svarsmeddelandet. Svarskoden för den här returtypen är 200 OK, förutsatt att det inte finns några ohanterade undantag. Ohanterade undantag översätts till 5xx-fel.

Returtyperna kan representera ett brett utbud av HTTP-statuskoder. Till exempel kan GET /todoitems/{id} returnera två olika statusvärden:

  • Om inget objekt matchar det begärda ID:t returnerar metoden en 404-statusfelkodNotFound .
  • Annars returnerar metoden 200 med en JSON-svarstext. Om du returnerar item resulterar det i ett HTTP 200-svar.

Granska PUT-slutpunkten

Exempelappen implementerar en enda PUT-slutpunkt med hjälp av 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();
});

Den här metoden liknar metoden MapPost, förutom att den använder HTTP PUT. Ett lyckat svar returnerar 204 (inget innehåll). Enligt HTTP-specifikationen kräver en PUT-begäran att klienten skickar hela den uppdaterade entiteten, inte bara ändringarna. Om du vill stödja partiella uppdateringar använder du HTTP PATCH.

Testa PUT-slutpunkten

Det här exemplet använder en minnesintern databas som måste initieras varje gång appen startas. Det måste finnas ett objekt i databasen innan du gör ett PUT-anrop. Anropa GET för att se till att det finns ett objekt i databasen innan du gör ett PUT-anrop.

Uppdatera det to-do objekt som har Id = 1 och ange dess namn till "feed fish".

  • I Endpoints Explorer högerklickar du på PUT-slutpunkten och väljer Generera begäran.

    Följande innehåll läggs till i filen TodoApi.http:

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Ersätt {id} med 1på PUT-begäranderaden.

  • Lägg till följande rader omedelbart efter PUT-begäranderaden:

    Content-Type: application/json
    
    {
      "name": "feed fish",
      "isComplete": false
    }
    

    Föregående kod lägger till ett content-type-huvud och en JSON-begärandetext.

  • Välj länken Skicka begäran som ligger ovanför den nya PUT-begäranderaden.

    PUT-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden är 204.

Granska och testa DELETE-slutpunkten

Exempelappen implementerar en enskild DELETE-slutpunkt med 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();
});
  • I Endpoints Explorer högerklickar du på DELETE-slutpunkten och väljer Generera begäran.

    En DELETE-begäran läggs till i TodoApi.http.

  • Byt ut {id} i raden för DELETE-begäran mot 1. DELETE-begäran bör se ut som i följande exempel:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Välj länken Skicka begäran för DELETE-begäran.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar . Svarstexten är tom och statuskoden är 204.

Använda MapGroup-API:et

Exempelappkoden upprepar todoitems URL-prefixet varje gång den konfigurerar en slutpunkt. API:er har ofta grupper av slutpunkter med ett vanligt URL-prefix, och metoden MapGroup är tillgänglig för att organisera sådana grupper. Det minskar repetitiv kod och gör det möjligt att anpassa hela grupper av slutpunkter med ett enda anrop till metoder som RequireAuthorization och WithMetadata.

Ersätt innehållet i Program.cs med följande kod:

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

Föregående kod har följande ändringar:

  • Lägger till var todoItems = app.MapGroup("/todoitems"); för att konfigurera gruppen med hjälp av URL-prefixet /todoitems.
  • Ändrar alla app.Map<HttpVerb> metoder till todoItems.Map<HttpVerb>.
  • Tar bort URL-prefixet /todoitems från Map<HttpVerb>-metodanrop.

Testa slutpunkterna för att kontrollera att de fungerar på samma sätt.

Använda Api:et TypedResults

Att returnera TypedResults i stället för Results har flera fördelar, inklusive testbarhet och att automatiskt returnera svarstypmetadata för OpenAPI för att beskriva slutpunkten. Mer information finns i TypedResults vs Results.

De Map<HttpVerb> metoderna kan anropa routningshanterarmetoder i stället för att använda lambdas. Om du vill se ett exempel uppdaterar du Program.cs med följande kod:

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

Den Map<HttpVerb> koden anropar nu metoder i stället för lambdas:

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

Dessa metoder returnerar objekt som implementerar IResult och definieras av TypedResults:

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

Enhetstester kan anropa dessa metoder och testa att de returnerar rätt typ. Om metoden till exempel är GetAllTodos:

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

Enhetstestkoden kan kontrollera att ett objekt av typen Ok<Todo[]> returneras från hanteringsmetoden. Till exempel:

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

Förhindra överpublicering

För närvarande exponerar exempelappen hela Todo objektet. Produktionsappar I produktionsprogram används ofta en delmängd av modellen för att begränsa de data som kan matas in och returneras. Det finns flera orsaker till detta och säkerheten är viktig. Delmängden av en modell kallas vanligtvis för ett dataöverföringsobjekt (DTO), indatamodell eller vymodell. DTO används i den här artikeln.

En DTO kan användas för att:

  • Förhindra överpublicering.
  • Dölj egenskaper som klienter inte ska visa.
  • Utelämna vissa egenskaper för att minska nyttolaststorleken.
  • Platta ut objektdiagram som innehåller kapslade objekt. Utplattade objektdiagram kan vara enklare för klienter.

Om du vill demonstrera DTO-metoden uppdaterar du klassen Todo så att den innehåller ett hemligt fält:

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

Det hemliga fältet måste vara dolt från den här appen, men en administrativ app kan välja att exponera det.

Kontrollera att du kan skicka och hämta det hemliga fältet.

Skapa en fil med namnet TodoItemDTO.cs med följande kod:

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

Ersätt innehållet i Program.cs-filen med följande kod för att använda den här DTO-modellen:

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

Kontrollera att du kan publicera och hämta alla fält utom det hemliga fältet.

Felsökning med det slutförda exemplet

Om du stöter på ett problem som du inte kan lösa kan du jämföra koden med det slutförda projektet. Visa eller ladda ned slutfört projekt (ladda ned).

Nästa steg

Learn more

Se Snabbreferens för minimala API:er