Partager via


Tutoriel : Créer une API minimale avec ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Par Rick Anderson et Ryan Nowak

Les API minimales sont conçues pour créer des API HTTP ayant des dépendances minimales. Elles sont idéales pour les microservices et les applications pour lesquels vous voulez inclure seulement le minimum de fichiers, de fonctionnalités et de dépendances dans ASP.NET Core.

Ce tutoriel décrit les principes fondamentaux liés à la génération d’une API minimale avec ASP.NET Core. Une autre approche de la création d’API dans ASP.NET Core consiste à utiliser des contrôleurs. Pour obtenir de l’aide avec le choix entre les API minimales et les API basées sur un contrôleur, consultez Vue d’ensemble des API. Pour obtenir un tutoriel sur la création d’un projet d’API basée sur des contrôleurs qui contiennent d’autres fonctionnalités, consultez Créer une API web.

Vue d’ensemble

Ce didacticiel crée l’API suivante :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenir les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
POST /todoitems Ajouter un nouvel élément Tâche Tâche
PUT /todoitems/{id} Mettre à jour un élément existant Tâche Aucune
DELETE /todoitems/{id}     Supprimer un élément Aucune None

Prerequisites

Créez un projet d’API

  • Démarrez Visual Studio 2022 et sélectionnez Créer un projet.

  • Dans la boîte de dialogue Créer un projet :

    • Entrez Empty dans la zone de recherche Rechercher des modèles.
    • Sélectionnez le modèle ASP.NET Core vide, puis Suivant.

    Visual Studio – Créer un projet

  • Nommez le projet TodoApi, puis sélectionnez Suivant.

  • Dans la boîte de dialogue Informations supplémentaires :

    • Sélectionnez .NET 9.0 (préversion).
    • Décochez la case N’utilisez pas d’instructions de niveau supérieur
    • Sélectionnez Créer

    Informations supplémentaires

Examiner le code

Le fichier Program.cs contient le code suivant :

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

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

app.Run();

Le code précédent :

Exécuter l’application

Appuyez sur Ctrl+F5 pour exécuter sans le débogueur.

Visual Studio affiche la boîte de dialogue suivante :

Ce projet est configuré pour utiliser SSL. Pour éviter les avertissements SSL dans le navigateur, vous pouvez choisir d’approuver le certificat auto-signé généré par IIS Express. Voulez-vous approuver le certificat SSL d’IIS Express ?

Sélectionnez Oui si vous faites confiance au certificat SSL d’IIS Express.

La boîte de dialogue suivante s’affiche :

Boîte de dialogue Avertissement de sécurité

Sélectionnez Oui si vous acceptez d’approuver le certificat de développement.

Pour plus d’informations sur l’approbation du navigateur Firefox, consultez Erreur de certificat Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio lance le Kestrel serveur web et ouvre une fenêtre de navigateur.

Hello World! s’affiche dans le navigateur. Le fichier Program.cs contient une application minimale mais complète.

Fermez la fenêtre du navigateur.

Ajouter des packages NuGet

Les packages NuGet doivent être ajoutés pour prendre en charge la base de données et les diagnostics utilisés dans ce tutoriel.

  • Dans le menu Outils, sélectionnez Gestionnaire de package NuGet > Gérer les packages NuGet pour la solution.
  • Sélectionnez l’onglet Parcourir.
  • Sélectionnez Inclure la préversion.
  • Entrez Microsoft.EntityFrameworkCore.InMemory dans la zone de recherche, puis sélectionnez Microsoft.EntityFrameworkCore.InMemory.
  • Cochez la case Projet dans le volet droit, puis sélectionnez Installer.
  • Suivez les instructions précédentes pour ajouter le package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Classes de contexte de modèle et de base de données

  • Dans le dossier du projet, créez un fichier nommé Todo.cs avec le code suivant :
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Le code précédent crée le modèle pour cette application. Un modèle est un ensemble de classes qui représentent les données gérées par l’application.

  • Créez un fichier nommé TodoDb.cs avec le code suivant :
using Microsoft.EntityFrameworkCore;

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

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

Le précédent code définit le contexte de base de données, qui est la classe principale qui coordonne les fonctionnalités d’Entity Framework pour un modèle de données. Cette classe dérive de la classe Microsoft.EntityFrameworkCore.DbContext.

Ajouter le code de l’API

  • Remplacez le contenu du fichier Program.cs par le code suivant :
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();

Le code suivant mis en surbrillance ajoute le contexte de base de données au conteneur d’injection de dépendances et permet d’afficher les exceptions liées à la base de données :

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

Le conteneur d’injection de dépendances fournit l’accès au contexte de base de données et à d’autres services.

Ce didacticiel utilise l’Explorateur des points de terminaison et les fichiers .http pour tester l’API.

Testez les données de publication

Le code suivant dans Program.cs crée un point de terminaison HTTP POST /todoitems qui ajoute des données à la base de données en mémoire :

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

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

Exécutez l'application. Le navigateur affiche une erreur 404, car il n’y a plus de point de terminaison /.

Le point de terminaison POST sera utilisé pour ajouter des données à l’application.

  • Sélectionnez Afficher, Autres fenêtres et Explorateur de points de terminaison.

  • Cliquez avec le bouton droit sur le point de terminaison POST, puis sélectionnez Générer une requête.

    Menu contextuel de l’Explorateur de points de terminaison mettant en évidence l’élément de menu Générer une requête.

    Un nouveau fichier est créé dans le dossier de projet nommé TodoApi.http, avec un contenu similaire à l’exemple suivant :

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • La première ligne crée une variable qui est utilisée pour tous les points de terminaison.
    • La ligne suivante définit une requête POST.
    • La triple hashtag (###) ligne est un délimiteur de requête : ce qui arrive après qu’il est pour une autre requête.
  • La requête POST a besoin d’en-têtes et d’un corps. Pour définir ces parties de la requête, ajoutez les lignes suivantes immédiatement après la ligne de requête POST :

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

    Le code précédent ajoute un en-tête Content-Type et un corps de la demande JSON. Le fichier TodoApi.http doit maintenant ressembler à l’exemple suivant, mais avec votre numéro de port :

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Exécutez l'application.

  • Sélectionnez le lien Envoyer une requête au-dessus de la ligne de requête POST.

    Fenêtre du fichier .http avec le lien d’exécution mis en évidence.

    La requête POST est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

    Fenêtre du fichier .http avec la réponse de la requête POST.

Examinez les points de terminaison GET

L’échantillon d’application implémente plusieurs points de terminaison GET en appelant MapGet :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenez tous les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
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());

Testez les points de terminaison GET

Testez l’application en appelant les GET points d’extrémité à partir d’un navigateur ou en utilisant l’Explorateur des points de terminaison. Les étapes suivantes concernent l’Explorateur des points de terminaison.

  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le premier GET un point de terminaison et sélectionnez Générer une requête.

    Le contenu suivant est ajouté au fichier TodoApi.http :

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Sélectionnez le lien Envoyer une requête au-dessus de la nouvelle ligne de requête GET.

    La requête GET est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

  • Le corps de la réponse est similaire à JSON suivant :

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison /todoitems/{id}GET, puis sélectionnez Générer une requête. Le contenu suivant est ajouté au fichier TodoApi.http :

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Remplacez {id} par 1.

  • Sélectionnez le lien Envoyer une requête au-dessus de la nouvelle ligne de requête GET.

    La requête GET est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

  • Le corps de la réponse est similaire à JSON suivant :

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

Cette application utilise une base de données en mémoire. Si l’application est redémarrée, la requête GET ne retourne aucune donnée. Si aucune donnée n’est retournée, publiez (POST) les données dans l’application et réessayez la requête GET.

Valeurs de retour

ASP.NET Core sérialise automatiquement l’objet en JSON et écrit le JSON dans le corps du message de réponse. Le code de réponse pour ce type de retour est 200 OK, en supposant qu’il n’existe pas d’exception non gérée. Les exceptions non gérées sont converties en erreurs 5xx.

Les types de retour peuvent représenter une large plage de codes d’état HTTP. Par exemple, GET /todoitems/{id} peut retourner deux valeurs d’état différentes :

  • Si aucun élément ne correspond à l’ID demandé, la méthode retourne un code d’erreur Code de statut 404NotFound.
  • Sinon, la méthode retourne 200 avec un corps de réponse JSON. Le retour de item entraîne une réponse HTTP 200.

Examinez le point de terminaison PUT

L’échantillon d’application implémente un point de terminaison PUT unique à l’aide de 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();
});

Cette méthode est similaire à la méthode MapPost, sauf qu’elle utilise HTTP PUT. Une réponse réussie retourne 204 (Aucun contenu). D’après la spécification HTTP, une requête PUT nécessite que le client envoie toute l’entité mise à jour, et pas seulement les changements. Pour prendre en charge les mises à jour partielles, utilisez HTTP PATCH.

Testez le point de terminaison PUT

Cet exemple utilise une base de données en mémoire qui doit être initialisée à chaque démarrage de l’application. La base de données doit contenir un élément avant que vous ne passiez un appel PUT. Appelez GET pour vérifier qu’un élément existe dans la base de données avant d’effectuer un appel PUT.

Mettez à jour l’élément de tâche avec Id = 1 et définissez son nom sur "feed fish".

  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison PUT et sélectionnez Générer une requête.

    Le contenu suivant est ajouté au fichier TodoApi.http :

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Dans la ligne de requête PUT, remplacez {id} par 1.

  • Ajoutez les lignes suivantes immédiatement après la ligne de requête PUT :

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

    Le code précédent ajoute un en-tête Content-Type et un corps de la demande JSON.

  • Sélectionnez le lien Send request (Envoyer une requête) qui se trouve au-dessus de la ligne de la nouvelle requête PUT.

    La requête PUT est envoyée à l’application et la réponse s’affiche dans le volet Réponse. Le corps de la réponse est vide et le code d’état est 204.

Examiner et tester le point de terminaison DELETE

L’échantillon d’application implémente un seul point de terminaison DELETE à l’aide de 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();
});
  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison DELETE et sélectionnez Générer une requête.

    Une requête DELETE est ajoutée à TodoApi.http.

  • Remplacez {id} dans la ligne de requête DELETE par 1. La requête DELETE doit ressembler à l’exemple suivant :

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Sélectionnez le lien Envoyer une requête pour la demande DELETE.

    La requête DELETE est envoyée à l’application et la réponse s’affiche dans le volet Réponse. Le corps de la réponse est vide et le code d’état est 204.

Utiliser l’API MapGroup

L’échantillon de code d’application répète le préfixe d’URL todoitems chaque fois qu’il configure un point de terminaison. Les API ont souvent des groupes de points de terminaison avec un préfixe d’URL commun, et la méthode MapGroup est disponible pour vous aider à organiser ces groupes. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata.

Remplacez le contenu de Program.cs par le code suivant :

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

Le code précédent dispose des modifications suivantes :

  • Ajoute var todoItems = app.MapGroup("/todoitems"); pour configurer le groupe à l’aide du préfixe d’URL /todoitems.
  • Remplace toutes les méthodes app.Map<HttpVerb> par todoItems.Map<HttpVerb>.
  • Supprime le préfixe d’URL /todoitems des appels de méthode Map<HttpVerb>.

Testez les points de terminaison pour vérifier qu’ils fonctionnent de la même façon.

Utilisez l’API TypedResults

Retourner TypedResults plutôt que Results présente plusieurs avantages, notamment la testabilité et le retour automatique des métadonnées de type de réponse pour OpenAPI afin de décrire le point de terminaison. Pour plus d'informations, consultez TypedResults vs Results.

Les méthodes Map<HttpVerb> peuvent appeler des méthodes de gestionnaire de routage au lieu d’utiliser des expressions lambda. Pour voir un exemple, mettez à jour Program.cs avec le code suivant :

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

Le code Map<HttpVerb> appelle maintenant des méthodes au lieu des lambda :

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

Ces méthodes retournent des objets qui implémentent IResult et sont définis par 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();
}

Les tests unitaires peuvent appeler ces méthodes et vérifier qu’elles retournent le type correct. Par exemple, si la méthode est GetAllTodos :

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

Le code de test unitaire peut vérifier qu’un objet de type Ok<Todo[]> est retourné à partir de la méthode de gestionnaire. Par exemple :

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

Empêcher la sur-publication

Actuellement, l’exemple d’application expose l’ensemble de l’objet Todo. Dans les applications de production, un sous-ensemble du modèle est souvent utilisé pour restreindre les données qui peuvent être entrées et retournées. Il y a plusieurs raisons à cela, et la sécurité en est une majeure. Le sous-ensemble d’un modèle est généralement appelé objet de transfert de données (DTO), modèle d’entrée ou modèle de vue. DTO est utilisé dans cet article.

Un DTO peut être utilisé pour :

  • Empêcher la sur-publication.
  • Masquer les propriétés que les clients ne sont pas censés voir.
  • Omettez certaines propriétés afin de réduire la taille de la charge utile.
  • Aplatir les graphes d’objets qui contiennent des objets imbriqués. Les graphiques d’objets aplatis peuvent être plus pratiques pour les clients.

Pour illustrer l’approche DTO, mettez à jour la classe Todo pour inclure un champ secret :

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

Le champ secret doit être masqué dans cette application, mais une application administrative peut choisir de l’exposer.

Vérifiez que vous pouvez publier et obtenir le champ secret.

Créez un fichier nommé TodoItemDTO.cs avec le code suivant :

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

Remplacez le contenu du fichier Program.cs par le code suivant pour utiliser ce modèle DTO :

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

Vérifiez que vous pouvez publier et obtenir tous les champs à l’exception du champ secret.

Résolution des problèmes avec l’exemple terminé

Si vous rencontrez un problème que vous ne pouvez pas résoudre, comparez votre code au projet terminé. Afficher ou télécharger le projet terminé (comment télécharger).

Étapes suivantes

En savoir plus

Consultez Informations de référence rapides sur les API minimales.

Les API minimales sont conçues pour créer des API HTTP ayant des dépendances minimales. Elles sont idéales pour les microservices et les applications qui ne nécessitent qu’un minimum de fichiers, de fonctionnalités et de dépendances dans ASP.NET Core.

Ce tutoriel décrit les principes fondamentaux liés à la génération d’une API minimale avec ASP.NET Core. Une autre approche de la création d’API dans ASP.NET Core consiste à utiliser des contrôleurs. Pour obtenir de l’aide sur le choix entre les API minimales et les API basées sur un contrôleur, consultez Vue d’ensemble des API. Pour obtenir un tutoriel sur la création d’un projet d’API basée sur des contrôleurs qui contiennent d’autres fonctionnalités, consultez Créer une API web.

Vue d’ensemble

Ce didacticiel crée l’API suivante :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenir les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
POST /todoitems Ajouter un nouvel élément Tâche Tâche
PUT /todoitems/{id} Mettre à jour un élément existant Tâche Aucune
DELETE /todoitems/{id}     Supprimer un élément Aucune None

Prerequisites

Créez un projet d’API

  • Démarrez Visual Studio 2022 et sélectionnez Créer un projet.

  • Dans la boîte de dialogue Créer un projet :

    • Entrez Empty dans la zone de recherche Rechercher des modèles.
    • Sélectionnez le modèle ASP.NET Core vide, puis Suivant.

    Visual Studio – Créer un projet

  • Nommez le projet TodoApi, puis sélectionnez Suivant.

  • Dans la boîte de dialogue Informations supplémentaires :

    • Sélectionner .NET 7.0
    • Décochez la case N’utilisez pas d’instructions de niveau supérieur
    • Sélectionnez Créer

    Informations supplémentaires

Examiner le code

Le fichier Program.cs contient le code suivant :

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

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

app.Run();

Le code précédent :

Exécuter l’application

Appuyez sur Ctrl+F5 pour exécuter sans le débogueur.

Visual Studio affiche la boîte de dialogue suivante :

Ce projet est configuré pour utiliser SSL. Pour éviter les avertissements SSL dans le navigateur, vous pouvez choisir d’approuver le certificat auto-signé généré par IIS Express. Voulez-vous approuver le certificat SSL d’IIS Express ?

Sélectionnez Oui si vous faites confiance au certificat SSL d’IIS Express.

La boîte de dialogue suivante s’affiche :

Boîte de dialogue Avertissement de sécurité

Sélectionnez Oui si vous acceptez d’approuver le certificat de développement.

Pour plus d’informations sur l’approbation du navigateur Firefox, consultez Erreur de certificat Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio lance le Kestrel serveur web et ouvre une fenêtre de navigateur.

Hello World! s’affiche dans le navigateur. Le fichier Program.cs contient une application minimale mais complète.

Ajouter des packages NuGet

Les packages NuGet doivent être ajoutés pour prendre en charge la base de données et les diagnostics utilisés dans ce tutoriel.

  • Dans le menu Outils, sélectionnez Gestionnaire de package NuGet > Gérer les packages NuGet pour la solution.
  • Sélectionnez l’onglet Parcourir.
  • Entrez Microsoft.EntityFrameworkCore.InMemory dans la zone de recherche, puis sélectionnez Microsoft.EntityFrameworkCore.InMemory.
  • Cochez la case Projet dans le volet droit.
  • Dans la liste déroulante Version, sélectionnez la version 7 la plus récente disponible, par exemple 7.0.17, puis sélectionnez Installer.
  • Suivez les instructions précédentes pour ajouter le package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore avec la version 7 la plus récente disponible.

Classes de contexte de modèle et de base de données

Dans le dossier du projet, créez un fichier nommé Todo.cs avec le code suivant :

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

Le code précédent crée le modèle pour cette application. Un modèle est un ensemble de classes qui représentent les données gérées par l’application.

Créez un fichier nommé TodoDb.cs avec le code suivant :

using Microsoft.EntityFrameworkCore;

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

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

Le précédent code définit le contexte de base de données, qui est la classe principale qui coordonne les fonctionnalités d’Entity Framework pour un modèle de données. Cette classe dérive de la classe Microsoft.EntityFrameworkCore.DbContext.

Ajouter le code de l’API

Remplacez le contenu du fichier Program.cs par le code suivant :

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

Le code suivant mis en surbrillance ajoute le contexte de base de données au conteneur d’injection de dépendances et permet d’afficher les exceptions liées à la base de données :

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

Le conteneur d’injection de dépendances fournit l’accès au contexte de base de données et à d’autres services.

Créer une interface utilisateur de test d’API avec Swagger

Vous pouvez choisir parmi de nombreux outils de test d’API web, et vous pouvez suivre les étapes de cette introduction aux tests d’API de ce tutoriel avec l’outil que vous préférez.

Ce tutoriel utilise le package .NET NSwag.AspNetCore, qui intègre des outils Swagger pour générer une interface utilisateur de test conforme à la spécification OpenAPI :

  • NSwag : une bibliothèque .NET qui intègre Swagger directement dans des applications ASP.NET Core, en fournissant un intergiciel et une configuration.
  • Swagger : un ensemble d’outils open source, comme OpenAPIGenerator et SwaggerUI, générant des pages de test d’API qui suivent la spécification OpenAPI.
  • Spécification OpenAPI : un document qui décrit les fonctionnalités de l’API, en se basant sur les annotations XML et d’attribut présentes dans les contrôleurs et les modèles.

Pour plus d’informations sur l’utilisation d’OpenAPI et de NSwag avec ASP.NET, consultez Documentation des API web ASP.NET Core avec Swagger / OpenAPI.

Installer les outils Swagger

  • Exécutez la commande suivante :

    dotnet add package NSwag.AspNetCore
    

La commande précédente ajoute le package NSwag.AspNetCore, qui contient des outils pour générer des documents et une interface utilisateur Swagger.

Configurer l’intergiciel Swagger

  • Ajoutez le code mis en surbrillance suivant avant la définition de app dans la ligne 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();
    

Dans le code précédent :

  • builder.Services.AddEndpointsApiExplorer(); : active l’Explorateur d’API, qui est un service fournissant des métadonnées sur l’API HTTP. L’Explorateur d’API est utilisé par Swagger pour générer le document Swagger.

  • builder.Services.AddOpenApiDocument(config => {...}); : ajoute le générateur de documents Swagger OpenAPI aux services d’application et le configure pour fournir plus d’informations sur l’API, comme son titre et sa version. Pour plus d’informations sur la fourniture de détails d’API plus robustes, consultez Bien démarrer avec NSwag et ASP.NET Core.

  • Ajoutez le code en surbrillance suivant à la ligne qui se trouve après la définition de app dans la ligne 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";
        });
    }
    

    Le code précédent permet à l’intergiciel Swagger de servir le document JSON et l’interface utilisateur Swagger générés. Swagger est activé seulement dans un environnement de développement. L’activation de Swagger dans un environnement de production peut exposer des détails potentiellement sensibles sur la structure et l’implémentation de l’API.

Testez les données de publication

Le code suivant dans Program.cs crée un point de terminaison HTTP POST /todoitems qui ajoute des données à la base de données en mémoire :

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

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

Exécutez l'application. Le navigateur affiche une erreur 404, car il n’y a plus de point de terminaison /.

Le point de terminaison POST sera utilisé pour ajouter des données à l’application.

  • Avec l’application toujours en cours d’exécution, dans le navigateur, accédez à https://localhost:<port>/swagger pour afficher la page de test de l’API générée par Swagger.

    Page de test d’API générée par Swagger

  • Dans la page de test de l’API Swagger, sélectionnez POST /todoitems>Try it out (Essayer).

  • Notez que le champ Request body (Corps de la requête) contient un exemple de format généré reflétant les paramètres de l’API.

  • Dans le corps de la requête, entrez JSON pour un élément de tâche, sans spécifier l’élément facultatif id :

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Sélectionnez Exécuter.

    Swagger avec Post

Swagger fournit un volet Responses (Réponses) sous le bouton Execute (Exécuter).

Swagger avec une réponse Post

Notez quelques-unes des informations utiles :

  • cURL : Swagger fournit un exemple de commande cURL selon la syntaxe Unix/Linux, qui peut être exécutée sur la ligne de commande avec n’importe quel shell bash utilisant la syntaxe Unix/Linux, y compris Git Bash de Git pour Windows.
  • Request URL (URL de la requête) : une représentation simplifiée de la requête HTTP effectuée par le code JavaScript de l’interface utilisateur Swagger pour l’appel d’API. Les requêtes réelles peuvent inclure des détails comme des en-têtes, des paramètres de requête et un corps de requête.
  • Server response (Réponse du serveur) : inclut le corps et les en-têtes de la réponse. Le corps de la réponse montre que l’élément id a été défini sur 1.
  • Code de réponse : un code d’état HTTP 201 a été retourné, indiquant que la demande a été correctement traitée et a entraîné la création d’une nouvelle ressource.

Examinez les points de terminaison GET

L’échantillon d’application implémente plusieurs points de terminaison GET en appelant MapGet :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenez tous les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
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());

Testez les points de terminaison GET

Testez l’application en appelant les points de terminaison depuis un navigateur ou depuis Swagger.

  • Dans Swagger, sélectionnez GET /todoitems>Try it out (Essayer)>Execute (Exécuter).

  • Vous pouvez également appeler GET /todoitems depuis un navigateur en entrant l’URI http://localhost:<port>/todoitems. Par exemple, http://localhost:5001/todoitems

L’appel à GET /todoitems génère une réponse similaire à ce qui suit :

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Appelez GET /todoitems/{id} dans Swagger pour retourner des données d’un ID spécifique :

    • Sélectionnez GET /todoitems>Try it out (Essayer).
    • Définissez le champ id sur 1, puis sélectionnez Execute (Exécuter).
  • Vous pouvez également appeler GET /todoitems depuis un navigateur en entrant l’URI https://localhost:<port>/todoitems/1. Par exemple, https://localhost:5001/todoitems/1

  • La réponse est similaire à celle-ci :

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

Cette application utilise une base de données en mémoire. Si l’application est redémarrée, la requête GET ne retourne aucune donnée. Si aucune donnée n’est retournée, publiez (POST) les données dans l’application et réessayez la requête GET.

Valeurs de retour

ASP.NET Core sérialise automatiquement l’objet en JSON et écrit le JSON dans le corps du message de réponse. Le code de réponse pour ce type de retour est 200 OK, en supposant qu’il n’existe pas d’exception non gérée. Les exceptions non gérées sont converties en erreurs 5xx.

Les types de retour peuvent représenter une large plage de codes d’état HTTP. Par exemple, GET /todoitems/{id} peut retourner deux valeurs d’état différentes :

  • Si aucun élément ne correspond à l’ID demandé, la méthode retourne un code d’erreur Code de statut 404NotFound.
  • Sinon, la méthode retourne 200 avec un corps de réponse JSON. Le retour de item entraîne une réponse HTTP 200.

Examinez le point de terminaison PUT

L’échantillon d’application implémente un point de terminaison PUT unique à l’aide de 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();
});

Cette méthode est similaire à la méthode MapPost, sauf qu’elle utilise HTTP PUT. Une réponse réussie retourne 204 (Aucun contenu). D’après la spécification HTTP, une requête PUT nécessite que le client envoie toute l’entité mise à jour, et pas seulement les changements. Pour prendre en charge les mises à jour partielles, utilisez HTTP PATCH.

Testez le point de terminaison PUT

Cet exemple utilise une base de données en mémoire qui doit être initialisée à chaque démarrage de l’application. La base de données doit contenir un élément avant que vous ne passiez un appel PUT. Appelez GET pour vérifier qu’un élément existe dans la base de données avant d’effectuer un appel PUT.

Mettez à jour l’élément de tâche avec Id = 1 et définissez son nom sur "feed fish".

Utilisez Swagger pour envoyer une requête PUT :

  • Sélectionnez Put /todoitems/{id}>Try it out (Essayer).

  • Définissez le champ id sur 1.

  • Définissez le corps de la requête sur le JSON suivant :

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Sélectionnez Exécuter.

Examiner et tester le point de terminaison DELETE

L’échantillon d’application implémente un seul point de terminaison DELETE à l’aide de 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();
});

Utilisez Swagger pour envoyer une requête DELETE :

  • Sélectionnez DELETE /todoitems/{id}>Try it out (Essayer).

  • Définissez le champ ID sur 1, puis sélectionnez Execute (Exécuter).

    La requête DELETE est envoyée à l’application et la réponse est affichée dans le volet Responses (Réponses). Le corps de la réponse est vide et le code d’état de Server response (Réponse du serveur) est 204.

Utiliser l’API MapGroup

L’échantillon de code d’application répète le préfixe d’URL todoitems chaque fois qu’il configure un point de terminaison. Les API ont souvent des groupes de points de terminaison avec un préfixe d’URL commun, et la méthode MapGroup est disponible pour vous aider à organiser ces groupes. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata.

Remplacez le contenu de Program.cs par le code suivant :

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

Le code précédent dispose des modifications suivantes :

  • Ajoute var todoItems = app.MapGroup("/todoitems"); pour configurer le groupe à l’aide du préfixe d’URL /todoitems.
  • Remplace toutes les méthodes app.Map<HttpVerb> par todoItems.Map<HttpVerb>.
  • Supprime le préfixe d’URL /todoitems des appels de méthode Map<HttpVerb>.

Testez les points de terminaison pour vérifier qu’ils fonctionnent de la même façon.

Utilisez l’API TypedResults

Retourner TypedResults plutôt que Results présente plusieurs avantages, notamment la testabilité et le retour automatique des métadonnées de type de réponse pour OpenAPI afin de décrire le point de terminaison. Pour plus d'informations, consultez TypedResults vs Results.

Les méthodes Map<HttpVerb> peuvent appeler des méthodes de gestionnaire de routage au lieu d’utiliser des expressions lambda. Pour voir un exemple, mettez à jour Program.cs avec le code suivant :

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

Le code Map<HttpVerb> appelle maintenant des méthodes au lieu des lambda :

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

Ces méthodes retournent des objets qui implémentent IResult et sont définis par 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();
}

Les tests unitaires peuvent appeler ces méthodes et vérifier qu’elles retournent le type correct. Par exemple, si la méthode est GetAllTodos :

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

Le code de test unitaire peut vérifier qu’un objet de type Ok<Todo[]> est retourné à partir de la méthode de gestionnaire. Par exemple :

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

Empêcher la sur-publication

Actuellement, l’exemple d’application expose l’ensemble de l’objet Todo. Dans les applications de production, un sous-ensemble du modèle est souvent utilisé pour restreindre les données qui peuvent être entrées et retournées. Il y a plusieurs raisons à cela, et la sécurité en est une majeure. Le sous-ensemble d’un modèle est généralement appelé objet de transfert de données (DTO), modèle d’entrée ou modèle de vue. DTO est utilisé dans cet article.

Un DTO peut être utilisé pour :

  • Empêcher la sur-publication.
  • Masquer les propriétés que les clients ne sont pas censés voir.
  • Omettez certaines propriétés afin de réduire la taille de la charge utile.
  • Aplatir les graphes d’objets qui contiennent des objets imbriqués. Les graphiques d’objets aplatis peuvent être plus pratiques pour les clients.

Pour illustrer l’approche DTO, mettez à jour la classe Todo pour inclure un champ secret :

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

Le champ secret doit être masqué dans cette application, mais une application administrative peut choisir de l’exposer.

Vérifiez que vous pouvez publier et obtenir le champ secret.

Créez un fichier nommé TodoItemDTO.cs avec le code suivant :

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

Remplacez le contenu du fichier Program.cs par le code suivant pour utiliser ce modèle DTO :

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

Vérifiez que vous pouvez publier et obtenir tous les champs à l’exception du champ secret.

Résolution des problèmes avec l’exemple terminé

Si vous rencontrez un problème que vous ne pouvez pas résoudre, comparez votre code au projet terminé. Afficher ou télécharger le projet terminé (comment télécharger).

Étapes suivantes

En savoir plus

Consultez Informations de référence rapides sur les API minimales.

Les API minimales sont conçues pour créer des API HTTP ayant des dépendances minimales. Elles sont idéales pour les microservices et les applications qui ne nécessitent qu’un minimum de fichiers, de fonctionnalités et de dépendances dans ASP.NET Core.

Ce tutoriel décrit les principes fondamentaux liés à la génération d’une API minimale avec ASP.NET Core. Une autre approche de la création d’API dans ASP.NET Core consiste à utiliser des contrôleurs. Pour obtenir de l’aide sur le choix entre les API minimales et les API basées sur un contrôleur, consultez Vue d’ensemble des API. Pour obtenir un tutoriel sur la création d’un projet d’API basée sur des contrôleurs qui contiennent d’autres fonctionnalités, consultez Créer une API web.

Vue d’ensemble

Ce didacticiel crée l’API suivante :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenir les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
POST /todoitems Ajouter un nouvel élément Tâche Tâche
PUT /todoitems/{id} Mettre à jour un élément existant Tâche Aucune
DELETE /todoitems/{id}     Supprimer un élément Aucune None

Prerequisites

Créez un projet d’API

  • Démarrez Visual Studio 2022 et sélectionnez Créer un projet.

  • Dans la boîte de dialogue Créer un projet :

    • Entrez Empty dans la zone de recherche Rechercher des modèles.
    • Sélectionnez le modèle ASP.NET Core vide, puis Suivant.

    Visual Studio – Créer un projet

  • Nommez le projet TodoApi, puis sélectionnez Suivant.

  • Dans la boîte de dialogue Informations supplémentaires :

    • Sélectionnez .NET 6.0
    • Décochez la case N’utilisez pas d’instructions de niveau supérieur
    • Sélectionnez Créer

Examiner le code

Le fichier Program.cs contient le code suivant :

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

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

app.Run();

Le code précédent :

Exécuter l’application

Appuyez sur Ctrl+F5 pour exécuter sans le débogueur.

Visual Studio affiche la boîte de dialogue suivante :

Ce projet est configuré pour utiliser SSL. Pour éviter les avertissements SSL dans le navigateur, vous pouvez choisir d’approuver le certificat auto-signé généré par IIS Express. Voulez-vous approuver le certificat SSL d’IIS Express ?

Sélectionnez Oui si vous faites confiance au certificat SSL d’IIS Express.

La boîte de dialogue suivante s’affiche :

Boîte de dialogue Avertissement de sécurité

Sélectionnez Oui si vous acceptez d’approuver le certificat de développement.

Pour plus d’informations sur l’approbation du navigateur Firefox, consultez Erreur de certificat Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio lance le Kestrel serveur web et ouvre une fenêtre de navigateur.

Hello World! s’affiche dans le navigateur. Le fichier Program.cs contient une application minimale mais complète.

Ajouter des packages NuGet

Les packages NuGet doivent être ajoutés pour prendre en charge la base de données et les diagnostics utilisés dans ce tutoriel.

  • Dans le menu Outils, sélectionnez Gestionnaire de package NuGet > Gérer les packages NuGet pour la solution.
  • Sélectionnez l’onglet Parcourir.
  • Entrez Microsoft.EntityFrameworkCore.InMemory dans la zone de recherche, puis sélectionnez Microsoft.EntityFrameworkCore.InMemory.
  • Cochez la case Projet dans le volet droit.
  • Dans la liste déroulante Version, sélectionnez la version 7 la plus récente disponible, par exemple 6.0.28, puis sélectionnez Installer.
  • Suivez les instructions précédentes pour ajouter le package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore avec la version 7 la plus récente disponible.

Classes de contexte de modèle et de base de données

Dans le dossier du projet, créez un fichier nommé Todo.cs avec le code suivant :

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

Le code précédent crée le modèle pour cette application. Un modèle est un ensemble de classes qui représentent les données gérées par l’application.

Créez un fichier nommé TodoDb.cs avec le code suivant :

using Microsoft.EntityFrameworkCore;

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

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

Le précédent code définit le contexte de base de données, qui est la classe principale qui coordonne les fonctionnalités d’Entity Framework pour un modèle de données. Cette classe dérive de la classe Microsoft.EntityFrameworkCore.DbContext.

Ajouter le code de l’API

Remplacez le contenu du fichier Program.cs par le code suivant :

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

Le code suivant mis en surbrillance ajoute le contexte de base de données au conteneur d’injection de dépendances et permet d’afficher les exceptions liées à la base de données :

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

Le conteneur d’injection de dépendances fournit l’accès au contexte de base de données et à d’autres services.

Créer une interface utilisateur de test d’API avec Swagger

Vous pouvez choisir parmi de nombreux outils de test d’API web, et vous pouvez suivre les étapes de cette introduction aux tests d’API de ce tutoriel avec l’outil que vous préférez.

Ce tutoriel utilise le package .NET NSwag.AspNetCore, qui intègre des outils Swagger pour générer une interface utilisateur de test conforme à la spécification OpenAPI :

  • NSwag : une bibliothèque .NET qui intègre Swagger directement dans des applications ASP.NET Core, en fournissant un intergiciel et une configuration.
  • Swagger : un ensemble d’outils open source, comme OpenAPIGenerator et SwaggerUI, générant des pages de test d’API qui suivent la spécification OpenAPI.
  • Spécification OpenAPI : un document qui décrit les fonctionnalités de l’API, en se basant sur les annotations XML et d’attribut présentes dans les contrôleurs et les modèles.

Pour plus d’informations sur l’utilisation d’OpenAPI et de NSwag avec ASP.NET, consultez Documentation des API web ASP.NET Core avec Swagger / OpenAPI.

Installer les outils Swagger

  • Exécutez la commande suivante :

    dotnet add package NSwag.AspNetCore
    

La commande précédente ajoute le package NSwag.AspNetCore, qui contient des outils pour générer des documents et une interface utilisateur Swagger.

Configurer l’intergiciel Swagger

  • En haut du fichier Program.cs, ajoutez les instructions using suivantes :

    using NSwag.AspNetCore;
    
  • Ajoutez le code mis en surbrillance suivant avant la définition de app dans la ligne 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();
    

Dans le code précédent :

  • builder.Services.AddEndpointsApiExplorer(); : active l’Explorateur d’API, qui est un service fournissant des métadonnées sur l’API HTTP. L’Explorateur d’API est utilisé par Swagger pour générer le document Swagger.

  • builder.Services.AddOpenApiDocument(config => {...}); : ajoute le générateur de documents Swagger OpenAPI aux services d’application et le configure pour fournir plus d’informations sur l’API, comme son titre et sa version. Pour plus d’informations sur la fourniture de détails d’API plus robustes, consultez Bien démarrer avec NSwag et ASP.NET Core.

  • Ajoutez le code en surbrillance suivant à la ligne qui se trouve après la définition de app dans la ligne 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";
        });
    }
    
    

    Le code précédent permet à l’intergiciel Swagger de servir le document JSON et l’interface utilisateur Swagger générés. Swagger est activé seulement dans un environnement de développement. L’activation de Swagger dans un environnement de production peut exposer des détails potentiellement sensibles sur la structure et l’implémentation de l’API.

Testez les données de publication

Le code suivant dans Program.cs crée un point de terminaison HTTP POST /todoitems qui ajoute des données à la base de données en mémoire :

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

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

Exécutez l'application. Le navigateur affiche une erreur 404, car il n’y a plus de point de terminaison /.

Le point de terminaison POST sera utilisé pour ajouter des données à l’application.

  • Avec l’application toujours en cours d’exécution, dans le navigateur, accédez à https://localhost:<port>/swagger pour afficher la page de test de l’API générée par Swagger.

    Page de test d’API générée par Swagger

  • Dans la page de test de l’API Swagger, sélectionnez POST /todoitems>Try it out (Essayer).

  • Notez que le champ Request body (Corps de la requête) contient un exemple de format généré reflétant les paramètres de l’API.

  • Dans le corps de la requête, entrez JSON pour un élément de tâche, sans spécifier l’élément facultatif id :

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Sélectionnez Exécuter.

    Swagger avec des données Post

Swagger fournit un volet Responses (Réponses) sous le bouton Execute (Exécuter).

Swagger avec le volet de réponse Post

Notez quelques-unes des informations utiles :

  • cURL : Swagger fournit un exemple de commande cURL selon la syntaxe Unix/Linux, qui peut être exécutée sur la ligne de commande avec n’importe quel shell bash utilisant la syntaxe Unix/Linux, y compris Git Bash de Git pour Windows.
  • Request URL (URL de la requête) : une représentation simplifiée de la requête HTTP effectuée par le code JavaScript de l’interface utilisateur Swagger pour l’appel d’API. Les requêtes réelles peuvent inclure des détails comme des en-têtes, des paramètres de requête et un corps de requête.
  • Server response (Réponse du serveur) : inclut le corps et les en-têtes de la réponse. Le corps de la réponse montre que l’élément id a été défini sur 1.
  • Code de réponse : un code d’état HTTP 201 a été retourné, indiquant que la demande a été correctement traitée et a entraîné la création d’une nouvelle ressource.

Examinez les points de terminaison GET

L’échantillon d’application implémente plusieurs points de terminaison GET en appelant MapGet :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenez tous les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
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());

Testez les points de terminaison GET

Testez l’application en appelant les points de terminaison depuis un navigateur ou depuis Swagger.

  • Dans Swagger, sélectionnez GET /todoitems>Try it out (Essayer)>Execute (Exécuter).

  • Vous pouvez également appeler GET /todoitems depuis un navigateur en entrant l’URI http://localhost:<port>/todoitems. Par exemple, http://localhost:5001/todoitems

L’appel à GET /todoitems génère une réponse similaire à ce qui suit :

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Appelez GET /todoitems/{id} dans Swagger pour retourner des données d’un ID spécifique :

    • Sélectionnez GET /todoitems>Try it out (Essayer).
    • Définissez le champ id sur 1, puis sélectionnez Execute (Exécuter).
  • Vous pouvez également appeler GET /todoitems depuis un navigateur en entrant l’URI https://localhost:<port>/todoitems/1. Par exemple, https://localhost:5001/todoitems/1

  • La réponse est similaire à celle-ci :

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

Cette application utilise une base de données en mémoire. Si l’application est redémarrée, la requête GET ne retourne aucune donnée. Si aucune donnée n’est retournée, publiez (POST) les données dans l’application et réessayez la requête GET.

Valeurs de retour

ASP.NET Core sérialise automatiquement l’objet en JSON et écrit le JSON dans le corps du message de réponse. Le code de réponse pour ce type de retour est 200 OK, en supposant qu’il n’existe pas d’exception non gérée. Les exceptions non gérées sont converties en erreurs 5xx.

Les types de retour peuvent représenter une large plage de codes d’état HTTP. Par exemple, GET /todoitems/{id} peut retourner deux valeurs d’état différentes :

  • Si aucun élément ne correspond à l’ID demandé, la méthode retourne un code d’erreur Code de statut 404NotFound.
  • Sinon, la méthode retourne 200 avec un corps de réponse JSON. Le retour de item entraîne une réponse HTTP 200.

Examinez le point de terminaison PUT

L’échantillon d’application implémente un point de terminaison PUT unique à l’aide de 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();
});

Cette méthode est similaire à la méthode MapPost, sauf qu’elle utilise HTTP PUT. Une réponse réussie retourne 204 (Aucun contenu). D’après la spécification HTTP, une requête PUT nécessite que le client envoie toute l’entité mise à jour, et pas seulement les changements. Pour prendre en charge les mises à jour partielles, utilisez HTTP PATCH.

Testez le point de terminaison PUT

Cet exemple utilise une base de données en mémoire qui doit être initialisée à chaque démarrage de l’application. La base de données doit contenir un élément avant que vous ne passiez un appel PUT. Appelez GET pour vérifier qu’un élément existe dans la base de données avant d’effectuer un appel PUT.

Mettez à jour l’élément de tâche avec Id = 1 et définissez son nom sur "feed fish".

Utilisez Swagger pour envoyer une requête PUT :

  • Sélectionnez Put /todoitems/{id}>Try it out (Essayer).

  • Définissez le champ id sur 1.

  • Définissez le corps de la requête sur le JSON suivant :

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Sélectionnez Exécuter.

Examiner et tester le point de terminaison DELETE

L’échantillon d’application implémente un seul point de terminaison DELETE à l’aide de 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();
});

Utilisez Swagger pour envoyer une requête DELETE :

  • Sélectionnez DELETE /todoitems/{id}>Try it out (Essayer).

  • Définissez le champ ID sur 1, puis sélectionnez Execute (Exécuter).

    La requête DELETE est envoyée à l’application et la réponse est affichée dans le volet Responses (Réponses). Le corps de la réponse est vide et le code d’état de Server response (Réponse du serveur) est 204.

Empêcher la sur-publication

Actuellement, l’exemple d’application expose l’ensemble de l’objet Todo. Dans les applications de production, un sous-ensemble du modèle est souvent utilisé pour restreindre les données qui peuvent être entrées et retournées. Il y a plusieurs raisons à cela, et la sécurité en est une majeure. Le sous-ensemble d’un modèle est généralement appelé objet de transfert de données (DTO), modèle d’entrée ou modèle de vue. DTO est utilisé dans cet article.

Un DTO peut être utilisé pour :

  • Empêcher la sur-publication.
  • Masquer les propriétés que les clients ne sont pas censés voir.
  • Omettez certaines propriétés afin de réduire la taille de la charge utile.
  • Aplatir les graphes d’objets qui contiennent des objets imbriqués. Les graphiques d’objets aplatis peuvent être plus pratiques pour les clients.

Pour illustrer l’approche DTO, mettez à jour la classe Todo pour inclure un champ secret :

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

Le champ secret doit être masqué dans cette application, mais une application administrative peut choisir de l’exposer.

Vérifiez que vous pouvez publier et obtenir le champ secret.

Créez un fichier nommé TodoItemDTO.cs avec le code suivant :

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

Remplacez le contenu du fichier Program.cs par le code suivant pour utiliser ce modèle DTO :

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

Vérifiez que vous pouvez publier et obtenir tous les champs à l’exception du champ secret.

Tester l’API minimale

Pour obtenir un échantillon de test d’une application API minimale, consultez cet échantillon GitHub.

Publier sur Azure

Pour plus d’informations sur le déploiement sur Azure, consultez Démarrage rapide : Déployer une application web ASP.NET.

Ressources supplémentaires

Les API minimales sont conçues pour créer des API HTTP ayant des dépendances minimales. Elles sont idéales pour les microservices et les applications pour lesquels vous voulez inclure seulement le minimum de fichiers, de fonctionnalités et de dépendances dans ASP.NET Core.

Ce tutoriel décrit les principes fondamentaux liés à la génération d’une API minimale avec ASP.NET Core. Une autre approche de la création d’API dans ASP.NET Core consiste à utiliser des contrôleurs. Pour obtenir de l’aide avec le choix entre les API minimales et les API basées sur un contrôleur, consultez Vue d’ensemble des API. Pour obtenir un tutoriel sur la création d’un projet d’API basée sur des contrôleurs qui contiennent d’autres fonctionnalités, consultez Créer une API web.

Vue d’ensemble

Ce didacticiel crée l’API suivante :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenir les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
POST /todoitems Ajouter un nouvel élément Tâche Tâche
PUT /todoitems/{id} Mettre à jour un élément existant Tâche Aucune
DELETE /todoitems/{id}     Supprimer un élément Aucune None

Prerequisites

Créez un projet d’API

  • Démarrez Visual Studio 2022 et sélectionnez Créer un projet.

  • Dans la boîte de dialogue Créer un projet :

    • Entrez Empty dans la zone de recherche Rechercher des modèles.
    • Sélectionnez le modèle ASP.NET Core vide, puis Suivant.

    Visual Studio – Créer un projet

  • Nommez le projet TodoApi, puis sélectionnez Suivant.

  • Dans la boîte de dialogue Informations supplémentaires :

    • Sélectionnez .NET 8.0 (support à long terme)
    • Décochez la case N’utilisez pas d’instructions de niveau supérieur
    • Sélectionnez Créer

    Informations supplémentaires

Examiner le code

Le fichier Program.cs contient le code suivant :

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

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

app.Run();

Le code précédent :

Exécuter l’application

Appuyez sur Ctrl+F5 pour exécuter sans le débogueur.

Visual Studio affiche la boîte de dialogue suivante :

Ce projet est configuré pour utiliser SSL. Pour éviter les avertissements SSL dans le navigateur, vous pouvez choisir d’approuver le certificat auto-signé généré par IIS Express. Voulez-vous approuver le certificat SSL d’IIS Express ?

Sélectionnez Oui si vous faites confiance au certificat SSL d’IIS Express.

La boîte de dialogue suivante s’affiche :

Boîte de dialogue Avertissement de sécurité

Sélectionnez Oui si vous acceptez d’approuver le certificat de développement.

Pour plus d’informations sur l’approbation du navigateur Firefox, consultez Erreur de certificat Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

Visual Studio lance le Kestrel serveur web et ouvre une fenêtre de navigateur.

Hello World! s’affiche dans le navigateur. Le fichier Program.cs contient une application minimale mais complète.

Fermez la fenêtre du navigateur.

Ajouter des packages NuGet

Les packages NuGet doivent être ajoutés pour prendre en charge la base de données et les diagnostics utilisés dans ce tutoriel.

  • Dans le menu Outils, sélectionnez Gestionnaire de package NuGet > Gérer les packages NuGet pour la solution.
  • Sélectionnez l’onglet Parcourir.
  • Entrez Microsoft.EntityFrameworkCore.InMemory dans la zone de recherche, puis sélectionnez Microsoft.EntityFrameworkCore.InMemory.
  • Cochez la case Projet dans le volet droit, puis sélectionnez Installer.
  • Suivez les instructions précédentes pour ajouter le package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Classes de contexte de modèle et de base de données

  • Dans le dossier du projet, créez un fichier nommé Todo.cs avec le code suivant :
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Le code précédent crée le modèle pour cette application. Un modèle est un ensemble de classes qui représentent les données gérées par l’application.

  • Créez un fichier nommé TodoDb.cs avec le code suivant :
using Microsoft.EntityFrameworkCore;

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

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

Le précédent code définit le contexte de base de données, qui est la classe principale qui coordonne les fonctionnalités d’Entity Framework pour un modèle de données. Cette classe dérive de la classe Microsoft.EntityFrameworkCore.DbContext.

Ajouter le code de l’API

  • Remplacez le contenu du fichier Program.cs par le code suivant :
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();

Le code suivant mis en surbrillance ajoute le contexte de base de données au conteneur d’injection de dépendances et permet d’afficher les exceptions liées à la base de données :

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

Le conteneur d’injection de dépendances fournit l’accès au contexte de base de données et à d’autres services.

Ce didacticiel utilise l’Explorateur des points de terminaison et les fichiers .http pour tester l’API.

Testez les données de publication

Le code suivant dans Program.cs crée un point de terminaison HTTP POST /todoitems qui ajoute des données à la base de données en mémoire :

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

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

Exécutez l'application. Le navigateur affiche une erreur 404, car il n’y a plus de point de terminaison /.

Le point de terminaison POST sera utilisé pour ajouter des données à l’application.

  • Sélectionnez Afficher, Autres fenêtres et Explorateur de points de terminaison.

  • Cliquez avec le bouton droit sur le point de terminaison POST, puis sélectionnez Générer une requête.

    Menu contextuel de l’Explorateur de points de terminaison mettant en évidence l’élément de menu Générer une requête.

    Un nouveau fichier est créé dans le dossier de projet nommé TodoApi.http, avec un contenu similaire à l’exemple suivant :

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • La première ligne crée une variable qui est utilisée pour tous les points de terminaison.
    • La ligne suivante définit une requête POST.
    • La triple hashtag (###) ligne est un délimiteur de requête : ce qui arrive après qu’il est pour une autre requête.
  • La requête POST a besoin d’en-têtes et d’un corps. Pour définir ces parties de la requête, ajoutez les lignes suivantes immédiatement après la ligne de requête POST :

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

    Le code précédent ajoute un en-tête Content-Type et un corps de la demande JSON. Le fichier TodoApi.http doit maintenant ressembler à l’exemple suivant, mais avec votre numéro de port :

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Exécutez l'application.

  • Sélectionnez le lien Envoyer une requête au-dessus de la ligne de requête POST.

    Fenêtre du fichier .http avec le lien d’exécution mis en évidence.

    La requête POST est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

    Fenêtre du fichier .http avec la réponse de la requête POST.

Examinez les points de terminaison GET

L’échantillon d’application implémente plusieurs points de terminaison GET en appelant MapGet :

API Description Corps de la requête Corps de réponse
GET /todoitems Obtenir toutes les tâches Aucune Tableau de tâches
GET /todoitems/complete Obtenez tous les éléments de tâche terminés None Tableau de tâches
GET /todoitems/{id} Obtenir un élément par ID Aucune Tâche
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());

Testez les points de terminaison GET

Testez l’application en appelant les GET points d’extrémité à partir d’un navigateur ou en utilisant l’Explorateur des points de terminaison. Les étapes suivantes concernent l’Explorateur des points de terminaison.

  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le premier GET un point de terminaison et sélectionnez Générer une requête.

    Le contenu suivant est ajouté au fichier TodoApi.http :

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Sélectionnez le lien Envoyer une requête au-dessus de la nouvelle ligne de requête GET.

    La requête GET est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

  • Le corps de la réponse est similaire à JSON suivant :

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison /todoitems/{id}GET, puis sélectionnez Générer une requête. Le contenu suivant est ajouté au fichier TodoApi.http :

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Remplacez {id} par 1.

  • Sélectionnez le lien Envoyer une requête au-dessus de la nouvelle ligne de requête GET.

    La requête GET est envoyée à l’application et la réponse s’affiche dans le volet Réponse.

  • Le corps de la réponse est similaire à JSON suivant :

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

Cette application utilise une base de données en mémoire. Si l’application est redémarrée, la requête GET ne retourne aucune donnée. Si aucune donnée n’est retournée, publiez (POST) les données dans l’application et réessayez la requête GET.

Valeurs de retour

ASP.NET Core sérialise automatiquement l’objet en JSON et écrit le JSON dans le corps du message de réponse. Le code de réponse pour ce type de retour est 200 OK, en supposant qu’il n’existe pas d’exception non gérée. Les exceptions non gérées sont converties en erreurs 5xx.

Les types de retour peuvent représenter une large plage de codes d’état HTTP. Par exemple, GET /todoitems/{id} peut retourner deux valeurs d’état différentes :

  • Si aucun élément ne correspond à l’ID demandé, la méthode retourne un code d’erreur Code de statut 404NotFound.
  • Sinon, la méthode retourne 200 avec un corps de réponse JSON. Le retour de item entraîne une réponse HTTP 200.

Examinez le point de terminaison PUT

L’échantillon d’application implémente un point de terminaison PUT unique à l’aide de 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();
});

Cette méthode est similaire à la méthode MapPost, sauf qu’elle utilise HTTP PUT. Une réponse réussie retourne 204 (Aucun contenu). D’après la spécification HTTP, une requête PUT nécessite que le client envoie toute l’entité mise à jour, et pas seulement les changements. Pour prendre en charge les mises à jour partielles, utilisez HTTP PATCH.

Testez le point de terminaison PUT

Cet exemple utilise une base de données en mémoire qui doit être initialisée à chaque démarrage de l’application. La base de données doit contenir un élément avant que vous ne passiez un appel PUT. Appelez GET pour vérifier qu’un élément existe dans la base de données avant d’effectuer un appel PUT.

Mettez à jour l’élément de tâche avec Id = 1 et définissez son nom sur "feed fish".

  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison PUT et sélectionnez Générer une requête.

    Le contenu suivant est ajouté au fichier TodoApi.http :

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Dans la ligne de requête PUT, remplacez {id} par 1.

  • Ajoutez les lignes suivantes immédiatement après la ligne de requête PUT :

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

    Le code précédent ajoute un en-tête Content-Type et un corps de la demande JSON.

  • Sélectionnez le lien Send request (Envoyer une requête) qui se trouve au-dessus de la ligne de la nouvelle requête PUT.

    La requête PUT est envoyée à l’application et la réponse s’affiche dans le volet Réponse. Le corps de la réponse est vide et le code d’état est 204.

Examiner et tester le point de terminaison DELETE

L’échantillon d’application implémente un seul point de terminaison DELETE à l’aide de 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();
});
  • Dans l’Explorateur de points de terminaison, cliquez avec le bouton droit de la souris sur le point de terminaison DELETE et sélectionnez Générer une requête.

    Une requête DELETE est ajoutée à TodoApi.http.

  • Remplacez {id} dans la ligne de requête DELETE par 1. La requête DELETE doit ressembler à l’exemple suivant :

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Sélectionnez le lien Envoyer une requête pour la demande DELETE.

    La requête DELETE est envoyée à l’application et la réponse s’affiche dans le volet Réponse. Le corps de la réponse est vide et le code d’état est 204.

Utiliser l’API MapGroup

L’échantillon de code d’application répète le préfixe d’URL todoitems chaque fois qu’il configure un point de terminaison. Les API ont souvent des groupes de points de terminaison avec un préfixe d’URL commun, et la méthode MapGroup est disponible pour vous aider à organiser ces groupes. Cela réduit le code répétitif et permet de personnaliser des groupes entiers de points de terminaison avec un seul appel à des méthodes comme RequireAuthorization et WithMetadata.

Remplacez le contenu de Program.cs par le code suivant :

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

Le code précédent dispose des modifications suivantes :

  • Ajoute var todoItems = app.MapGroup("/todoitems"); pour configurer le groupe à l’aide du préfixe d’URL /todoitems.
  • Remplace toutes les méthodes app.Map<HttpVerb> par todoItems.Map<HttpVerb>.
  • Supprime le préfixe d’URL /todoitems des appels de méthode Map<HttpVerb>.

Testez les points de terminaison pour vérifier qu’ils fonctionnent de la même façon.

Utilisez l’API TypedResults

Retourner TypedResults plutôt que Results présente plusieurs avantages, notamment la testabilité et le retour automatique des métadonnées de type de réponse pour OpenAPI afin de décrire le point de terminaison. Pour plus d'informations, consultez TypedResults vs Results.

Les méthodes Map<HttpVerb> peuvent appeler des méthodes de gestionnaire de routage au lieu d’utiliser des expressions lambda. Pour voir un exemple, mettez à jour Program.cs avec le code suivant :

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

Le code Map<HttpVerb> appelle maintenant des méthodes au lieu des lambda :

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

Ces méthodes retournent des objets qui implémentent IResult et sont définis par 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();
}

Les tests unitaires peuvent appeler ces méthodes et vérifier qu’elles retournent le type correct. Par exemple, si la méthode est GetAllTodos :

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

Le code de test unitaire peut vérifier qu’un objet de type Ok<Todo[]> est retourné à partir de la méthode de gestionnaire. Par exemple :

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

Empêcher la sur-publication

Actuellement, l’exemple d’application expose l’ensemble de l’objet Todo. Dans les applications de production, un sous-ensemble du modèle est souvent utilisé pour restreindre les données qui peuvent être entrées et retournées. Il y a plusieurs raisons à cela, et la sécurité en est une majeure. Le sous-ensemble d’un modèle est généralement appelé objet de transfert de données (DTO), modèle d’entrée ou modèle de vue. DTO est utilisé dans cet article.

Un DTO peut être utilisé pour :

  • Empêcher la sur-publication.
  • Masquer les propriétés que les clients ne sont pas censés voir.
  • Omettez certaines propriétés afin de réduire la taille de la charge utile.
  • Aplatir les graphes d’objets qui contiennent des objets imbriqués. Les graphiques d’objets aplatis peuvent être plus pratiques pour les clients.

Pour illustrer l’approche DTO, mettez à jour la classe Todo pour inclure un champ secret :

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

Le champ secret doit être masqué dans cette application, mais une application administrative peut choisir de l’exposer.

Vérifiez que vous pouvez publier et obtenir le champ secret.

Créez un fichier nommé TodoItemDTO.cs avec le code suivant :

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

Remplacez le contenu du fichier Program.cs par le code suivant pour utiliser ce modèle DTO :

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

Vérifiez que vous pouvez publier et obtenir tous les champs à l’exception du champ secret.

Résolution des problèmes avec l’exemple terminé

Si vous rencontrez un problème que vous ne pouvez pas résoudre, comparez votre code au projet terminé. Afficher ou télécharger le projet terminé (comment télécharger).

Étapes suivantes

En savoir plus

Consultez Informations de référence rapides sur les API minimales.