Dela via


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

Obs

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

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i .NET och .NET Core Support Policy. Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Viktig

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.

Den aktuella versionen finns i den .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 översikten ö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.

Överblick

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

API Beskrivning Begärandetext Svarsinnehåll
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 Ingen
DELETE /todoitems/{id}     Ta bort ett objekt Ingen Ingen

Förutsättningar

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 dialogen Tilläggsinformation:

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

Säkerhetsvarning dialogruta

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 > Manage NuGet Packages for Solution.
  • Välj fliken Bläddra.
  • Välj Inkludera Prerelease.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Project i den högra panelen 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 beroendeinmatning (DI) container 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 handledningen används Endpoints Explorer och .http-filer för att testa API:t.

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

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

    Snabbmenyn Endpoints Explorer med markerat menyalternativ

    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 POST begäranderaden.

    .http-filfönster med startlä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 Beskrivning Begärandetext Svarstext
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 slutpunkter från en webbläsare eller med hjälp av Endpoints Explorer. Följande steg gäller för Endpoints Explorer.

  • I Endpoints Explorerhö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 Explorerhögerklickar du på slutpunkten /todoitems/{id}GET 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, POST data till appen och försök 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-statusNotFound felkod.
  • 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 ha stöd för 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 Explorerhögerklickar du på slutpunkten PUT 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 Explorerhögerklickar du på slutpunkten DELETE 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 verifiera 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 (hur du laddar ned).

Nästa steg

Lära sig mer

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

Överblick

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

API Beskrivning Begärandetext Svarskropp
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 Ingen
DELETE /todoitems/{id}     Ta bort ett objekt Ingen Ingen

Förutsättningar

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

dialogruta för säkerhetsvarning

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

Information om att lita på webbläsaren Firefox finns i 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-paket måste läggas till för att stödja databasen och diagnostiken som används i denna självstudie.

  • På menyn Verktyg väljer du NuGet Package Manager > Manage NuGet Packages for Solution.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Project 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 beroendeinmatning (DI) container 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 Komma 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.

Testpubliceringsdata

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 Utför.

    Swagger med Post

Swagger innehåller ett svar fönster nedanför knappen Kör.

Swagger med postsvar

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 tilldelats 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 Beskrivning Begärandetext Svarskropp
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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. 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.
    • Ställ in 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. 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 POST data till appen och prova 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-statusNotFound felkod.
  • 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 ha stöd för 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 Placera /todoitems/{id}>Prova.

  • Ställ in id-fältet på 1.

  • Ange begäranens brödtext till 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 fältet ID till 1 och välj Kör.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar. Svarstexten är tom och Server-svaret statuskod ä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 verifiera 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 publicera 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 (hur du laddar ned).

Nästa steg

Lära sig mer

Se snabbreferensen 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 översikten ö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.

Överblick

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

API Beskrivning Begäransinnehåll Svarstext
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 Ingen
DELETE /todoitems/{id}     Ta bort ett objekt Ingen Ingen

Förutsättningar

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

Information om hur du litar på Webbläsaren Firefox finns i 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-paket måste läggas till för att stödja den databas och diagnostik som används i denna handledning.

  • På menyn Verktyg väljer du NuGet Package Manager > Manage NuGet Packages for Solution.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Project 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 beroendeinmatning (DI) container 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 Komma 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 svar fönster nedanför 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 ställts in på 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 Beskrivning Begärandeinnehåll Svarsdel
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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. 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.
    • Ställ in fältet id till 1 och välj Utför.
  • Du kan också anropa GET /todoitems från en webbläsare genom att ange URI-https://localhost:<port>/todoitems/1. 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, POST data till appen och försök med 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-statusNotFound felkod.
  • 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 ha stöd för 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 Placera /todoitems/{id}>Prova.

  • Ställ in id fält till 1.

  • Ange begärandekroppen till 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 fältet ID till 1 och välj Kör.

    DELETE-begäran skickas till appen och svaret visas i fönstret Svar. Svarstexten är tom och Server-svaret statuskod ä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 publicera 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 översikten ö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.

Överblick

I den här självstudien skapas följande API:

API Beskrivning Begäransinnehåll Svarskropp
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 Ingen
DELETE /todoitems/{id}     Ta bort ett objekt Ingen Ingen

Förutsättningar

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 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å webbläsaren Firefox, 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 kunna stödja databasen och diagnostiken som används i den här handledningen.

  • På menyn Verktyg väljer du NuGet Package Manager > Manage NuGet Packages for Solution.
  • Välj fliken Bläddra.
  • Ange Microsoft.EntityFrameworkCore.InMemory i sökrutan och välj sedan Microsoft.EntityFrameworkCore.InMemory.
  • Markera kryssrutan Project i den högra rutan och välj sedan Install.
  • 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 beroendeinmatning (DI) container 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:et.

Testpubliceringsdata

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

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

    I Endpoints Explorer markera snabbmenyalternativet 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.
    • Trippelhashtaggen (###) är en avgränsare för begäran: vad som kommer efter det ä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 POST begäranderaden.

    .http-filfönster 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 Beskrivning Begäranens innehåll Svarsdel
GET /todoitems Hämta alla to-do objekt Ingen Matris med to-do objekt
GET /todoitems/complete Hämta alla slutförda to-do objekt Ingen Matris med to-do objekt
GET /todoitems/{id} Hämta ett objekt efter ID Ingen 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 slutpunkter från en webbläsare eller med hjälp av Endpoints Explorer. Följande steg gäller för Endpoints Explorer.

  • I Endpoints Explorerhö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 Explorerhögerklickar du på slutpunkten /todoitems/{id}GET 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 POST data till appen och prova 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-statusNotFound felkod.
  • 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 ha stöd för 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 Explorerhögerklickar du på slutpunkten PUT 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 Explorerhögerklickar du på slutpunkten DELETE och väljer Generera begäran.

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

  • Ersätt {id} i raden för DELETE-begäran med 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 raderingsbegä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 verifiera 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 publicera 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 (hur du laddar ned).

Nästa steg

Lära sig mer

Se snabbreferensen minimala API:er