Tutorial: Criar uma API mínima com o ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Por Rick Anderson e Tom Dykstra

APIs mínimas são arquitetadas para criar APIs HTTP com dependências mínimas. Elas são ideais para microsserviços e aplicativos que desejam incluir apenas os arquivos, recursos e dependências mínimos no ASP.NET Core.

Este tutorial ensina os conceitos básicos da criação de uma API mínima com o ASP.NET Core. Outra abordagem para criar APIs no ASP.NET Core é usar controladores. Para obter ajuda na escolha entre APIs mínimas e APIs baseadas em controlador, veja Visão geral de APIs. Para obter um tutorial sobre como criar um projeto de API com base em controladores que contêm mais recursos, consulte Criar uma API Web.

Visão geral

Este tutorial cria a seguinte API:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
POST /todoitems Adicionar um novo item Item de tarefas pendentes Item de tarefas pendentes
PUT /todoitems/{id} Atualizar um item existente Item de tarefas pendentes Nenhum
DELETE /todoitems/{id}     Excluir um item Nenhum Nenhum

Pré-requisitos

Criar um projeto de API

  • Inicie o Visual Studio 2022 e selecione Criar um novo projeto.

  • Na caixa de diálogo Criar um projeto:

    • Insira Empty na caixa de pesquisa Pesquisar modelos .
    • Selecione o modelo ASP.NET Core Vazio e clique em Avançar.

    Visual Studio Criar um projeto

  • Nomeie o projeto como TodoApi e clique em Avançar.

  • Na caixa de diálogo Informações adicionais:

    • Selecione .NET 9.0 (Versão prévia)
    • Desmarque Não usar declarações de nível superior
    • Escolha Criar

    Informações adicionais

Examinar o código

O arquivo Program.cs contém o seguinte código:

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

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

app.Run();

O código anterior:

Executar o aplicativo

Pressione Ctrl + F5 para execução sem o depurador.

O Visual Studio exibe a caixa de diálogo a seguir:

Este projeto está configurado para usar SSL. para evitar avisos de SSL no navegador, você pode optar por confiar no certificado autoassinado que o IIS Express gerou. Gostaria de confiar no certificado SSL do IIS Express?

Selecione Sim se você confia no certificado SSL do IIS Express.

A seguinte caixa de diálogo é exibida:

Caixa de diálogo de aviso de segurança

Selecione Sim se você concordar com confiar no certificado de desenvolvimento.

Para obter informações sobre como confiar no navegador Firefox, confira Erro de certificado Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

O Visual Studio inicia o Kestrelservidor Web e abre uma janela do navegador.

Hello World! é exibido no navegador. O arquivo Program.cs contém um aplicativo mínimo, mas completo.

Feche a janela do navegador.

Adicionar pacotes do NuGet

Os pacotes NuGet devem ser adicionados para dar suporte ao banco de dados e diagnósticos usados neste tutorial.

  • No menu Ferramentas, selecione Gerenciador de Pacotes do NuGet > Gerenciar Pacotes do NuGet para a Solução.
  • Selecione a guia Procurar.
  • Selecione Incluir Pré-lançamento.
  • Na caixa de pesquisa, insira Microsoft.EntityFrameworkCore.InMemory e selecione Microsoft.EntityFrameworkCore.InMemory.
  • Marque a caixa de seleção Projeto no painel direito e selecione Instalar.
  • Use as instruções anteriores para adicionar o pacote Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

As classes de contexto de modelo e banco de dados

  • Na pasta do projeto, crie um arquivo chamado Todo.cs com o seguinte código:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

O código anterior cria o modelo para este aplicativo. Um modelo é um conjunto de classes que representam os dados gerenciados pelo aplicativo.

  • Crie um arquivo chamado TodoDb.cs com o código a seguir:
using Microsoft.EntityFrameworkCore;

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

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

O código anterior define o contexto de banco de dados, que é a classe principal que coordena a funcionalidade do Entity Framework para um modelo de dados. A classe é derivada da classe Microsoft.EntityFrameworkCore.DbContext.

Adicionar o código da API

  • Substitua o conteúdo do arquivo Program.cs pelo seguinte código:
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();

O código realçado a seguir adiciona o contexto do banco de dados ao contêiner de DI (injeção de dependência) e permite exibir exceções relacionadas ao banco de dados:

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

O contêiner de DI fornece acesso ao contexto do banco de dados e a outros serviços.

Este tutorial usa o Gerenciador de Pontos de Extremidade e arquivos .http para testar a API.

Testar os dados de postagem

O código a seguir em Program.cs cria um ponto de extremidade HTTP POST/todoitems que adiciona dados ao banco de dados na memória:

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

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

Execute o aplicativo. O navegador exibe um erro 404 porque não há mais um ponto de extremidade /.

O ponto de extremidade POST será usado para adicionar dados ao aplicativo.

  • Selecione Exibir>Outras Janelas>Gerenciador de Pontos de Extremidade.

  • Clique com o botão direito do mouse no ponto de extremidade POST e selecione Gerar solicitação.

    Menu de contexto do Gerenciador de Pontos de Extremidade realçando o item de menu Gerar Solicitação.

    Um novo arquivo é criado na pasta do projeto chamada TodoApi.http, com conteúdo semelhante ao seguinte exemplo:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • A primeira linha cria uma variável que é usada para todos os pontos de extremidade.
    • A próxima linha define uma solicitação POST.
    • A linha tripla de hashtag (###) é um delimitador de solicitação: o que vem depois dela é para uma solicitação diferente.
  • A solicitação POST precisa de cabeçalhos e um corpo. Para definir essas partes da solicitação, adicione as seguintes linhas imediatamente após a linha de solicitação POST:

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

    O código anterior adiciona um cabeçalho Content-Type e um corpo de solicitação JSON. O arquivo TodoApi.http agora deve se parece com o exemplo a seguir, mas com o número da porta:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Execute o aplicativo.

  • Selecione o link Enviar solicitação acima da linha de solicitação POST.

    Janela do arquivo .http com o link de execução realçado.

    A solicitação POST é enviada ao aplicativo e a resposta é exibida no painel Resposta.

    Janela do arquivo .http com resposta da solicitação POST.

Examinar os pontos de extremidade GET

O aplicativo de exemplo implementa vários pontos de extremidade GET chamando MapGet:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
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());

Testar os pontos de extremidade GET

Teste o aplicativo chamando os pontos de extremidade GET de um navegador ou usando o Gerenciador de Pontos de Extremidade. As etapas a seguir são para o Gerenciador de Pontos de Extremidade.

  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no primeiro ponto de extremidade GET e selecione Gerar solicitação.

    O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Selecione o link Enviar solicitação acima da nova linha de solicitação GET.

    A solicitação GET é enviada ao aplicativo e a resposta é exibida no painel Resposta.

  • O corpo de resposta é semelhante ao seguinte JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no /todoitems/{id} GET e selecione Gerar solicitação. O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Substitua {id} por 1.

  • Selecione o link Enviar solicitação acima da nova linha solicitação GET.

    A solicitação GET é enviada ao aplicativo e a resposta é exibida no painel Resposta.

  • O corpo de resposta é semelhante ao seguinte JSON:

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

Este aplicativo usa um banco de dados em memória. Se o aplicativo for reiniciado, a solicitação GET não retornará nenhum dado. Se nenhum dado for retornado, envie os dados para o aplicativo via POST e tente a solicitação GET novamente.

Valores de retorno

O ASP.NET Core serializa automaticamente o objeto em JSON e grava o JSON no corpo da mensagem de resposta. O código de resposta para esse tipo de retorno é 200 OK, supondo que não haja nenhuma exceção sem tratamento. As exceções sem tratamento são convertidas em erros 5xx.

Os tipos de retorno podem representar uma ampla variedade de códigos de status HTTP. Por exemplo, GET /todoitems/{id} pode retornar dois valores de status diferentes:

  • Se nenhum item corresponder às ID solicitadas, o método retornará um código de erro status 404 NotFound.
  • Caso contrário, o método retornará 200 com um corpo de resposta JSON. Retornar item resulta em uma resposta HTTP 200.

Examinar o ponto de extremidade PUT

O aplicativo de exemplo implementa um único ponto de extremidade PUT usando 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();
});

Esse método é semelhante ao método MapPost, exceto que ele usa HTTP PUT. Uma resposta bem-sucedida retorna 204 (sem conteúdo). De acordo com a especificação de HTTP, uma solicitação PUT exige que o cliente envie a entidade inteira atualizada, não apenas as alterações. Para dar suporte a atualizações parciais, use HTTP PATCH.

Testar o ponto de extremidade PUT

Este exemplo usa um banco de dados em memória que precisará ser iniciado sempre que o aplicativo for iniciado. Deverá haver um item no banco de dados antes de você fazer uma chamada PUT. Chame GET para garantir a existência de um item no banco de dados antes de fazer uma chamada PUT.

Atualize o item pendente que tem Id = 1 e defina o nome dele como "feed fish".

  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no ponto de extremidade PUT e selecione Gerar solicitação.

    O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Na linha de solicitação PUT, substitua {id} por 1.

  • Adicione as seguintes linhas imediatamente após a linha de solicitação PUT:

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

    O código anterior adiciona um cabeçalho Content-Type e um corpo de solicitação JSON.

  • Selecione o link Enviar solicitação acima da nova linha de solicitação PUT.

    A solicitação PUT é enviada ao aplicativo e a resposta é exibida no painel Resposta. O corpo da resposta está vazio e código de status é 204.

Examinar e testar o ponto de extremidade DELETE

O aplicativo de exemplo implementa um único ponto de extremidade DELETE usando 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();
});
  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse do ponto de extremidade DELETE e selecione Gerar solicitação.

    Uma solicitação DELETE é adicionada a TodoApi.http.

  • Substitua {id} na linha de solicitação DELETE com 1. A solicitação DELETE deve ter uma aparência semelhante ao exemplo a seguir:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Selecione o link Enviar solicitação para a solicitação DELETE.

    A solicitação DELETE é enviada ao aplicativo e a resposta é exibida no painel Resposta. O corpo da resposta está vazio e código de status é 204.

Usar a API do MapGroup

O código do aplicativo de exemplo repete o prefixo de URL todoitems sempre que configura um ponto de extremidade. As APIs geralmente têm grupos de pontos de extremidade com um prefixo de URL comum e o método MapGroup está disponível para ajudar a organizar esses grupos. Isso reduz o código repetitivo e permite personalizar grupos inteiros de pontos de extremidade com uma única chamada a métodos como RequireAuthorization e WithMetadata.

Substitua o conteúdo de Program.cs pelo seguinte código:

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

O código anterior faz as seguintes alterações:

  • Adiciona var todoItems = app.MapGroup("/todoitems"); para configurar o grupo usando o prefixo /todoitems da URL.
  • Altera todos os métodos app.Map<HttpVerb> para todoItems.Map<HttpVerb>.
  • Remove o prefixo /todoitems da URL das chamadas de método Map<HttpVerb>.

Teste os pontos de extremidade para verificar se eles funcionam da mesma forma.

Usar a API TypedResults

Retornar TypedResults em vez de Results tem várias vantagens, incluindo capacidade de teste e retornar automaticamente os metadados de tipo de resposta para OpenAPI para descrever o ponto de extremidade. Para obter mais informações, consulte TypedResults vs Resultados.

Os métodos Map<HttpVerb> podem chamar métodos de manipulador de rotas em vez de usar lambdas. Para ver um exemplo, atualize Program.cs com o seguinte código:

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

O código Map<HttpVerb> agora chama métodos em vez de lambdas:

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

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

Esses métodos retornam objetos que implementam IResult e são definidos por 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();
}

Os testes de unidade podem chamar esses métodos e testar se eles retornam o tipo correto. Por exemplo, se o método for GetAllTodos:

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

O código de teste de unidade pode verificar se um objeto do tipo Ok<Todo[]> é retornado do método de manipulador. Por exemplo:

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

Impedir o excesso de postagem

Atualmente, o aplicativo de exemplo expõe todo o objeto Todo. Aplicativos de produção Em aplicativos de produção, um subconjunto do modelo geralmente é usado para restringir os dados que podem ser inseridos e retornados. Há várias razões por trás disso, e a segurança é uma das principais. O subconjunto de um modelo geralmente é chamado de DTO (Objeto de Transferência de Dados), modelo de entrada ou modelo de exibição. O DTO é usado neste artigo.

Um DTO pode ser usado para:

  • Impedir o excesso de postagem.
  • Oculte propriedades que os clientes não deveriam visualizar.
  • Omita algumas propriedades para reduzir o tamanho do conteúdo.
  • Nivelar gráficos de objetos que contenham objetos aninhados. Os grafos de objeto nivelados podem ser mais convenientes para os clientes.

Para demonstrar a abordagem de DTO, atualize a classe Todo para incluir um campo secreto:

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

O campo secreto precisa ser ocultado neste aplicativo, mas um aplicativo administrativo poderia optar por mostrá-lo.

Verifique se você pode postar e obter o campo secreto.

Crie um arquivo chamado TodoItemDTO.cs com o código a seguir:

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

Substitua o conteúdo do arquivo Program.cs pelo seguinte código para usar este modelo de 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();
}

Verifique se você pode postar e obter todos os campos, exceto o campo de segredo.

Solução de problemas com o exemplo concluído

Se você encontrar um problema que não possa resolver, compare seu código com o projeto concluído. Exibir ou baixar projeto concluído (como baixar).

Próximas etapas

Saiba mais

Confira Referência rápida sobre as APIs mínimas

APIs mínimas são arquitetadas para criar APIs HTTP com dependências mínimas. Elas são ideais para microsserviços e aplicativos que desejam incluir apenas os arquivos, recursos e dependências mínimos no ASP.NET Core.

Este tutorial ensina os conceitos básicos da criação de uma API mínima com o ASP.NET Core. Outra abordagem para criar APIs no ASP.NET Core é usar controladores. Para obter ajuda na escolha entre APIs mínimas e APIs baseadas em controlador, confira a visão geral das APIs. Para obter um tutorial sobre como criar um projeto de API com base em controladores que contêm mais recursos, consulte Criar uma API Web.

Visão geral

Este tutorial cria a seguinte API:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
POST /todoitems Adicionar um novo item Item de tarefas pendentes Item de tarefas pendentes
PUT /todoitems/{id} Atualizar um item existente Item de tarefas pendentes Nenhum
DELETE /todoitems/{id}     Excluir um item Nenhum Nenhum

Pré-requisitos

Criar um projeto de API

  • Inicie o Visual Studio 2022 e selecione Criar um novo projeto.

  • Na caixa de diálogo Criar um projeto:

    • Insira Empty na caixa de pesquisa Pesquisar modelos .
    • Selecione o modelo ASP.NET Core Vazio e clique em Avançar.

    Visual Studio Criar um projeto

  • Nomeie o projeto como TodoApi e clique em Avançar.

  • Na caixa de diálogo Informações adicionais:

    • Selecione o .NET 7.0
    • Desmarque Não usar declarações de nível superior
    • Escolha Criar

    Informações adicionais

Examinar o código

O arquivo Program.cs contém o seguinte código:

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

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

app.Run();

O código anterior:

Executar o aplicativo

Pressione Ctrl + F5 para execução sem o depurador.

O Visual Studio exibe a caixa de diálogo a seguir:

Este projeto está configurado para usar SSL. para evitar avisos de SSL no navegador, você pode optar por confiar no certificado autoassinado que o IIS Express gerou. Gostaria de confiar no certificado SSL do IIS Express?

Selecione Sim se você confia no certificado SSL do IIS Express.

A seguinte caixa de diálogo é exibida:

Caixa de diálogo de aviso de segurança

Selecione Sim se você concordar com confiar no certificado de desenvolvimento.

Para obter informações sobre como confiar no navegador Firefox, confira Erro de certificado Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

O Visual Studio inicia o Kestrelservidor Web e abre uma janela do navegador.

Hello World! é exibido no navegador. O arquivo Program.cs contém um aplicativo mínimo, mas completo.

Adicionar pacotes do NuGet

Os pacotes NuGet devem ser adicionados para dar suporte ao banco de dados e diagnósticos usados neste tutorial.

  • No menu Ferramentas, selecione Gerenciador de Pacotes do NuGet > Gerenciar Pacotes do NuGet para a Solução.
  • Selecione a guia Procurar.
  • Na caixa de pesquisa, insira Microsoft.EntityFrameworkCore.InMemory e selecione Microsoft.EntityFrameworkCore.InMemory.
  • Marque a caixa de seleção Projeto no painel direito.
  • Na lista suspensa Versão, selecione a versão 7 mais recente disponível, por exemplo 7.0.17, e selecione Instalar.
  • Siga as instruções anteriores para adicionar o pacote Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore com a versão 7 mais recente disponível.

As classes de contexto de modelo e banco de dados

Na pasta do projeto, crie um arquivo chamado Todo.cs com o seguinte código:

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

O código anterior cria o modelo para este aplicativo. Um modelo é um conjunto de classes que representam os dados gerenciados pelo aplicativo.

Crie um arquivo chamado TodoDb.cs com o código a seguir:

using Microsoft.EntityFrameworkCore;

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

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

O código anterior define o contexto de banco de dados, que é a classe principal que coordena a funcionalidade do Entity Framework para um modelo de dados. A classe é derivada da classe Microsoft.EntityFrameworkCore.DbContext.

Adicionar o código da API

Substitua o conteúdo do arquivo Program.cs pelo seguinte código:

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

O código realçado a seguir adiciona o contexto do banco de dados ao contêiner de DI (injeção de dependência) e permite exibir exceções relacionadas ao banco de dados:

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

O contêiner de DI fornece acesso ao contexto do banco de dados e a outros serviços.

Criar interface do usuário de teste de API com o Swagger

Há muitas ferramentas de teste de API Web disponíveis para escolher e você pode seguir as etapas de teste de API introdutórias deste tutorial com sua ferramenta preferida.

Este tutorial utiliza o pacote .NET NSwag.AspNetCore, que integra as ferramentas do Swagger para gerar um teste de interface do usuário aderindo à especificação OpenAPI:

  • NSwag: uma biblioteca .NET que integra o Swagger diretamente aos aplicativos ASP.NET Core, fornecendo middleware e configuração.
  • Swagger: um conjunto de ferramentas de código aberto, como OpenAPIGenerator e SwaggerUI, que geram páginas de teste de API que seguem a especificação OpenAPI.
  • Especificação do OpenAPI: um documento que descreve os recursos da API, com base no XML e anotações de atributo dentro dos controladores e modelos.

Para obter mais informações sobre como usar o OpenAPI e o NSwag com ASP.NET, consulte Documentação da API Web do ASP.NET Core com Swagger/OpenAPI.

Instalar ferramentas do Swagger

  • Execute o comando a seguir:

    dotnet add package NSwag.AspNetCore
    

O comando anterior adiciona o pacote NSwag.AspNetCore, que contém ferramentas para gerar documentos e interface do usuário do Swagger.

Configurar o middleware do Swagger

  • Adicionar o código realçado a seguir antes app de ser definido na linha 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();
    

No código anterior:

  • builder.Services.AddEndpointsApiExplorer();: habilita o Gerenciador de API, que é um serviço que fornece metadados sobre a API HTTP. O Gerenciador de API é usado pelo Swagger para gerar o documento do Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: adiciona o gerador de documentos OpenAPI do Swagger aos serviços de aplicativo e o configura para fornecer mais informações sobre a API, como o título e a versão dela. Para obter informações sobre como fornecer detalhes mais robustos sobre a API, consulte Introdução ao NSwag e ao ASP.NET Core

  • Adicione o código realçado a seguir à próxima linha depois de app ser definido na linha 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";
        });
    }
    

    O código anterior habilita o middleware do Swagger para exibir o documento JSON gerado e a IU do Swagger. O Swagger só é habilitado em um ambiente de desenvolvimento. Habilitar o Swagger em um ambiente de produção pode expor detalhes potencialmente confidenciais sobre a estrutura e a implementação da API.

Testar os dados de postagem

O código a seguir em Program.cs cria um ponto de extremidade HTTP POST/todoitems que adiciona dados ao banco de dados na memória:

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

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

Execute o aplicativo. O navegador exibe um erro 404 porque não há mais um ponto de extremidade /.

O ponto de extremidade POST será usado para adicionar dados ao aplicativo.

  • Com o aplicativo ainda em execução, no navegador, navegue até https://localhost:<port>/swagger para exibir a página de teste de API gerada pelo Swagger.

    Página de teste de API gerada pelo Swagger

  • Na página de teste da API do Swagger, selecione Post /todoitems>Experimentar.

  • Observe que o campo Corpo da solicitação contém um formato de exemplo gerado que reflete os parâmetros da API.

  • No corpo de solicitação, insira o JSON para um item de tarefas pendentes, sem especificar o id opcional:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Selecione Executar.

    Swagger com Post

O Swagger fornece um painel Respostas abaixo do botão Executar.

Swagger com resposta Post

Observe alguns dos detalhes úteis:

  • cURL: o Swagger fornece um comando cURL de exemplo na sintaxe Unix/Linux, que pode ser executado na linha de comando com qualquer shell bash que use a sintaxe Unix/Linux, incluindo o Git Bash do Git para Windows.
  • URL de solicitação: uma representação simplificada da solicitação HTTP feita pelo código JavaScript da interface do usuário do Swagger para a chamada à API. As solicitações reais podem incluir detalhes, como cabeçalhos e parâmetros de consulta e um corpo da solicitação.
  • Resposta do servidor: inclui o corpo da resposta e os cabeçalhos. O corpo da resposta mostra que id foi definido como 1.
  • Código de resposta: um código de status 201 HTTP foi retornado, indicando que a solicitação foi processada com êxito e resultou na criação de um novo recurso.

Examinar os pontos de extremidade GET

O aplicativo de exemplo implementa vários pontos de extremidade GET chamando MapGet:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
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());

Testar os pontos de extremidade GET

Teste o aplicativo chamando os pontos de extremidade de um navegador ou do Swagger.

  • No Swagger, selecione GET /todoitems>Experimentar>Executar.

  • Como alternativa, chame GET /todoitems de um navegador inserindo o URI http://localhost:<port>/todoitems. Por exemplo, http://localhost:5001/todoitems

Uma resposta semelhante à seguinte é produzida pela chamada a GET /todoitems:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Chame GET /todoitems/{id} no Swagger para retornar dados de uma ID específica:

    • Selecione GET /todoitems>Experimentar.
    • Defina o campo de id para 1 e selecione Executar.
  • Como alternativa, chame GET /todoitems de um navegador inserindo o URI https://localhost:<port>/todoitems/1. Por exemplo, https://localhost:5001/todoitems/1

  • A resposta é semelhante ao descrito a seguir:

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

Este aplicativo usa um banco de dados em memória. Se o aplicativo for reiniciado, a solicitação GET não retornará nenhum dado. Se nenhum dado for retornado, envie os dados para o aplicativo via POST e tente a solicitação GET novamente.

Valores de retorno

O ASP.NET Core serializa automaticamente o objeto em JSON e grava o JSON no corpo da mensagem de resposta. O código de resposta para esse tipo de retorno é 200 OK, supondo que não haja nenhuma exceção sem tratamento. As exceções sem tratamento são convertidas em erros 5xx.

Os tipos de retorno podem representar uma ampla variedade de códigos de status HTTP. Por exemplo, GET /todoitems/{id} pode retornar dois valores de status diferentes:

  • Se nenhum item corresponder às ID solicitadas, o método retornará um código de erro status 404 NotFound.
  • Caso contrário, o método retornará 200 com um corpo de resposta JSON. Retornar item resulta em uma resposta HTTP 200.

Examinar o ponto de extremidade PUT

O aplicativo de exemplo implementa um único ponto de extremidade PUT usando 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();
});

Esse método é semelhante ao método MapPost, exceto que ele usa HTTP PUT. Uma resposta bem-sucedida retorna 204 (sem conteúdo). De acordo com a especificação de HTTP, uma solicitação PUT exige que o cliente envie a entidade inteira atualizada, não apenas as alterações. Para dar suporte a atualizações parciais, use HTTP PATCH.

Testar o ponto de extremidade PUT

Este exemplo usa um banco de dados em memória que precisará ser iniciado sempre que o aplicativo for iniciado. Deverá haver um item no banco de dados antes de você fazer uma chamada PUT. Chame GET para garantir a existência de um item no banco de dados antes de fazer uma chamada PUT.

Atualize o item pendente que tem Id = 1 e defina o nome dele como "feed fish".

Use o Swagger para enviar uma solicitação PUT:

  • Selecione Put /todoitems/{id}>Experimentar.

  • Defina o campo id como 1.

  • Defina o corpo de solicitação para o seguinte JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Selecione Executar.

Examinar e testar o ponto de extremidade DELETE

O aplicativo de exemplo implementa um único ponto de extremidade DELETE usando 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();
});

Use o Swagger para enviar uma solicitação DELETE:

  • Selecione DELETE /todoitems/{id}>Experimentar.

  • Defina o campo ID como 1 e selecione Executar.

    A solicitação DELETE é enviada ao aplicativo e a resposta é exibida no painel Respostas. O corpo da resposta está vazio e código de status Resposta do servidor é 204.

Usar a API do MapGroup

O código do aplicativo de exemplo repete o prefixo de URL todoitems sempre que configura um ponto de extremidade. As APIs geralmente têm grupos de pontos de extremidade com um prefixo de URL comum e o método MapGroup está disponível para ajudar a organizar esses grupos. Isso reduz o código repetitivo e permite personalizar grupos inteiros de pontos de extremidade com uma única chamada a métodos como RequireAuthorization e WithMetadata.

Substitua o conteúdo de Program.cs pelo seguinte código:

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

O código anterior faz as seguintes alterações:

  • Adiciona var todoItems = app.MapGroup("/todoitems"); para configurar o grupo usando o prefixo /todoitems da URL.
  • Altera todos os métodos app.Map<HttpVerb> para todoItems.Map<HttpVerb>.
  • Remove o prefixo /todoitems da URL das chamadas de método Map<HttpVerb>.

Teste os pontos de extremidade para verificar se eles funcionam da mesma forma.

Usar a API TypedResults

Retornar TypedResults em vez de Results tem várias vantagens, incluindo capacidade de teste e retornar automaticamente os metadados de tipo de resposta para OpenAPI para descrever o ponto de extremidade. Para obter mais informações, consulte TypedResults vs Resultados.

Os métodos Map<HttpVerb> podem chamar métodos de manipulador de rotas em vez de usar lambdas. Para ver um exemplo, atualize Program.cs com o seguinte código:

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

O código Map<HttpVerb> agora chama métodos em vez de lambdas:

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

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

Esses métodos retornam objetos que implementam IResult e são definidos por 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();
}

Os testes de unidade podem chamar esses métodos e testar se eles retornam o tipo correto. Por exemplo, se o método for GetAllTodos:

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

O código de teste de unidade pode verificar se um objeto do tipo Ok<Todo[]> é retornado do método de manipulador. Por exemplo:

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

Impedir o excesso de postagem

Atualmente, o aplicativo de exemplo expõe todo o objeto Todo. Aplicativos de produção Em aplicativos de produção, um subconjunto do modelo geralmente é usado para restringir os dados que podem ser inseridos e retornados. Há várias razões por trás disso, e a segurança é uma das principais. O subconjunto de um modelo geralmente é chamado de DTO (Objeto de Transferência de Dados), modelo de entrada ou modelo de exibição. O DTO é usado neste artigo.

Um DTO pode ser usado para:

  • Impedir o excesso de postagem.
  • Oculte propriedades que os clientes não deveriam visualizar.
  • Omita algumas propriedades para reduzir o tamanho do conteúdo.
  • Nivelar gráficos de objetos que contenham objetos aninhados. Os grafos de objeto nivelados podem ser mais convenientes para os clientes.

Para demonstrar a abordagem de DTO, atualize a classe Todo para incluir um campo secreto:

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

O campo secreto precisa ser ocultado neste aplicativo, mas um aplicativo administrativo poderia optar por mostrá-lo.

Verifique se você pode postar e obter o campo secreto.

Crie um arquivo chamado TodoItemDTO.cs com o código a seguir:

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

Substitua o conteúdo do arquivo Program.cs pelo seguinte código para usar este modelo de 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>();
}

Verifique se você pode postar e obter todos os campos, exceto o campo de segredo.

Solução de problemas com o exemplo concluído

Se você encontrar um problema que não possa resolver, compare seu código com o projeto concluído. Exibir ou baixar projeto concluído (como baixar).

Próximas etapas

Saiba mais

Confira Referência rápida sobre as APIs mínimas

APIs mínimas são arquitetadas para criar APIs HTTP com dependências mínimas. Elas são ideais para microsserviços e aplicativos que desejam incluir apenas os arquivos, recursos e dependências mínimos no ASP.NET Core.

Este tutorial ensina os conceitos básicos da criação de uma API mínima com o ASP.NET Core. Outra abordagem para criar APIs no ASP.NET Core é usar controladores. Para obter ajuda na escolha entre APIs mínimas e APIs baseadas em controlador, confira a visão geral das APIs. Para obter um tutorial sobre como criar um projeto de API com base em controladores que contêm mais recursos, consulte Criar uma API Web.

Visão geral

Este tutorial cria a seguinte API:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
POST /todoitems Adicionar um novo item Item de tarefas pendentes Item de tarefas pendentes
PUT /todoitems/{id} Atualizar um item existente Item de tarefas pendentes Nenhum
DELETE /todoitems/{id}     Excluir um item Nenhum Nenhum

Pré-requisitos

Criar um projeto de API

  • Inicie o Visual Studio 2022 e selecione Criar um novo projeto.

  • Na caixa de diálogo Criar um projeto:

    • Insira Empty na caixa de pesquisa Pesquisar modelos .
    • Selecione o modelo ASP.NET Core Vazio e clique em Avançar.

    Visual Studio Criar um projeto

  • Nomeie o projeto como TodoApi e clique em Avançar.

  • Na caixa de diálogo Informações adicionais:

    • Selecionar o .NET 6.0
    • Desmarque Não usar declarações de nível superior
    • Escolha Criar

Examinar o código

O arquivo Program.cs contém o seguinte código:

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

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

app.Run();

O código anterior:

Executar o aplicativo

Pressione Ctrl + F5 para execução sem o depurador.

O Visual Studio exibe a caixa de diálogo a seguir:

Este projeto está configurado para usar SSL. para evitar avisos de SSL no navegador, você pode optar por confiar no certificado autoassinado que o IIS Express gerou. Gostaria de confiar no certificado SSL do IIS Express?

Selecione Sim se você confia no certificado SSL do IIS Express.

A seguinte caixa de diálogo é exibida:

Caixa de diálogo de aviso de segurança

Selecione Sim se você concordar com confiar no certificado de desenvolvimento.

Para obter informações sobre como confiar no navegador Firefox, confira Erro de certificado Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

O Visual Studio inicia o Kestrelservidor Web e abre uma janela do navegador.

Hello World! é exibido no navegador. O arquivo Program.cs contém um aplicativo mínimo, mas completo.

Adicionar pacotes do NuGet

Os pacotes NuGet devem ser adicionados para dar suporte ao banco de dados e diagnósticos usados neste tutorial.

  • No menu Ferramentas, selecione Gerenciador de Pacotes do NuGet > Gerenciar Pacotes do NuGet para a Solução.
  • Selecione a guia Procurar.
  • Na caixa de pesquisa, insira Microsoft.EntityFrameworkCore.InMemory e selecione Microsoft.EntityFrameworkCore.InMemory.
  • Marque a caixa de seleção Projeto no painel direito.
  • Na lista suspensa Versão, selecione a versão 7 mais recente disponível, por exemplo 6.0.28, e selecione Instalar.
  • Siga as instruções anteriores para adicionar o pacote Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore com a versão 7 mais recente disponível.

As classes de contexto de modelo e banco de dados

Na pasta do projeto, crie um arquivo chamado Todo.cs com o seguinte código:

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

O código anterior cria o modelo para este aplicativo. Um modelo é um conjunto de classes que representam os dados gerenciados pelo aplicativo.

Crie um arquivo chamado TodoDb.cs com o código a seguir:

using Microsoft.EntityFrameworkCore;

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

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

O código anterior define o contexto de banco de dados, que é a classe principal que coordena a funcionalidade do Entity Framework para um modelo de dados. A classe é derivada da classe Microsoft.EntityFrameworkCore.DbContext.

Adicionar o código da API

Substitua o conteúdo do arquivo Program.cs pelo seguinte código:

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

O código realçado a seguir adiciona o contexto do banco de dados ao contêiner de DI (injeção de dependência) e permite exibir exceções relacionadas ao banco de dados:

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

O contêiner de DI fornece acesso ao contexto do banco de dados e a outros serviços.

Criar interface do usuário de teste de API com o Swagger

Há muitas ferramentas de teste de API Web disponíveis para escolher e você pode seguir as etapas de teste de API introdutórias deste tutorial com sua ferramenta preferida.

Este tutorial utiliza o pacote .NET NSwag.AspNetCore, que integra as ferramentas do Swagger para gerar um teste de interface do usuário aderindo à especificação OpenAPI:

  • NSwag: uma biblioteca .NET que integra o Swagger diretamente aos aplicativos ASP.NET Core, fornecendo middleware e configuração.
  • Swagger: um conjunto de ferramentas de código aberto, como OpenAPIGenerator e SwaggerUI, que geram páginas de teste de API que seguem a especificação OpenAPI.
  • Especificação do OpenAPI: um documento que descreve os recursos da API, com base no XML e anotações de atributo dentro dos controladores e modelos.

Para obter mais informações sobre como usar o OpenAPI e o NSwag com ASP.NET, consulte Documentação da API Web do ASP.NET Core com Swagger/OpenAPI.

Instalar ferramentas do Swagger

  • Execute o comando a seguir:

    dotnet add package NSwag.AspNetCore
    

O comando anterior adiciona o pacote NSwag.AspNetCore, que contém ferramentas para gerar documentos e interface do usuário do Swagger.

Configurar o middleware do Swagger

  • No Program.cs, adicione os seguintes demonstrativos do using à parte superior:

    using NSwag.AspNetCore;
    
  • Adicionar o código realçado a seguir antes app de ser definido na linha 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();
    

No código anterior:

  • builder.Services.AddEndpointsApiExplorer();: habilita o Gerenciador de API, que é um serviço que fornece metadados sobre a API HTTP. O Gerenciador de API é usado pelo Swagger para gerar o documento do Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: adiciona o gerador de documentos OpenAPI do Swagger aos serviços de aplicativo e o configura para fornecer mais informações sobre a API, como o título e a versão dela. Para obter informações sobre como fornecer detalhes mais robustos sobre a API, consulte Introdução ao NSwag e ao ASP.NET Core

  • Adicione o código realçado a seguir à próxima linha depois de app ser definido na linha 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";
        });
    }
    
    

    O código anterior habilita o middleware do Swagger para exibir o documento JSON gerado e a IU do Swagger. O Swagger só é habilitado em um ambiente de desenvolvimento. Habilitar o Swagger em um ambiente de produção pode expor detalhes potencialmente confidenciais sobre a estrutura e a implementação da API.

Testar os dados de postagem

O código a seguir em Program.cs cria um ponto de extremidade HTTP POST/todoitems que adiciona dados ao banco de dados na memória:

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

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

Execute o aplicativo. O navegador exibe um erro 404 porque não há mais um ponto de extremidade /.

O ponto de extremidade POST será usado para adicionar dados ao aplicativo.

  • Com o aplicativo ainda em execução, no navegador, navegue até https://localhost:<port>/swagger para exibir a página de teste de API gerada pelo Swagger.

    Página de teste de API gerada pelo Swagger

  • Na página de teste da API do Swagger, selecione Post /todoitems>Experimentar.

  • Observe que o campo Corpo da solicitação contém um formato de exemplo gerado que reflete os parâmetros da API.

  • No corpo de solicitação, insira o JSON para um item de tarefas pendentes, sem especificar o id opcional:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Selecione Executar.

    Swagger com dados de Post

O Swagger fornece um painel Respostas abaixo do botão Executar.

Swagger com painel de resposta Post

Observe alguns dos detalhes úteis:

  • cURL: o Swagger fornece um comando cURL de exemplo na sintaxe Unix/Linux, que pode ser executado na linha de comando com qualquer shell bash que use a sintaxe Unix/Linux, incluindo o Git Bash do Git para Windows.
  • URL de solicitação: uma representação simplificada da solicitação HTTP feita pelo código JavaScript da interface do usuário do Swagger para a chamada à API. As solicitações reais podem incluir detalhes, como cabeçalhos e parâmetros de consulta e um corpo da solicitação.
  • Resposta do servidor: inclui o corpo da resposta e os cabeçalhos. O corpo da resposta mostra que id foi definido como 1.
  • Código de resposta: um código de status 201 HTTP foi retornado, indicando que a solicitação foi processada com êxito e resultou na criação de um novo recurso.

Examinar os pontos de extremidade GET

O aplicativo de exemplo implementa vários pontos de extremidade GET chamando MapGet:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
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());

Testar os pontos de extremidade GET

Teste o aplicativo chamando os pontos de extremidade de um navegador ou do Swagger.

  • No Swagger, selecione GET /todoitems>Experimentar>Executar.

  • Como alternativa, chame GET /todoitems de um navegador inserindo o URI http://localhost:<port>/todoitems. Por exemplo, http://localhost:5001/todoitems

Uma resposta semelhante à seguinte é produzida pela chamada a GET /todoitems:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Chame GET /todoitems/{id} no Swagger para retornar dados de uma ID específica:

    • Selecione GET /todoitems>Experimentar.
    • Defina o campo de id para 1 e selecione Executar.
  • Como alternativa, chame GET /todoitems de um navegador inserindo o URI https://localhost:<port>/todoitems/1. Por exemplo, https://localhost:5001/todoitems/1

  • A resposta é semelhante ao descrito a seguir:

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

Este aplicativo usa um banco de dados em memória. Se o aplicativo for reiniciado, a solicitação GET não retornará nenhum dado. Se nenhum dado for retornado, envie os dados para o aplicativo via POST e tente a solicitação GET novamente.

Valores de retorno

O ASP.NET Core serializa automaticamente o objeto em JSON e grava o JSON no corpo da mensagem de resposta. O código de resposta para esse tipo de retorno é 200 OK, supondo que não haja nenhuma exceção sem tratamento. As exceções sem tratamento são convertidas em erros 5xx.

Os tipos de retorno podem representar uma ampla variedade de códigos de status HTTP. Por exemplo, GET /todoitems/{id} pode retornar dois valores de status diferentes:

  • Se nenhum item corresponder às ID solicitadas, o método retornará um código de erro status 404 NotFound.
  • Caso contrário, o método retornará 200 com um corpo de resposta JSON. Retornar item resulta em uma resposta HTTP 200.

Examinar o ponto de extremidade PUT

O aplicativo de exemplo implementa um único ponto de extremidade PUT usando 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();
});

Esse método é semelhante ao método MapPost, exceto que ele usa HTTP PUT. Uma resposta bem-sucedida retorna 204 (sem conteúdo). De acordo com a especificação de HTTP, uma solicitação PUT exige que o cliente envie a entidade inteira atualizada, não apenas as alterações. Para dar suporte a atualizações parciais, use HTTP PATCH.

Testar o ponto de extremidade PUT

Este exemplo usa um banco de dados em memória que precisará ser iniciado sempre que o aplicativo for iniciado. Deverá haver um item no banco de dados antes de você fazer uma chamada PUT. Chame GET para garantir a existência de um item no banco de dados antes de fazer uma chamada PUT.

Atualize o item pendente que tem Id = 1 e defina o nome dele como "feed fish".

Use o Swagger para enviar uma solicitação PUT:

  • Selecione Put /todoitems/{id}>Experimentar.

  • Defina o campo id como 1.

  • Defina o corpo de solicitação para o seguinte JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Selecione Executar.

Examinar e testar o ponto de extremidade DELETE

O aplicativo de exemplo implementa um único ponto de extremidade DELETE usando 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();
});

Use o Swagger para enviar uma solicitação DELETE:

  • Selecione DELETE /todoitems/{id}>Experimentar.

  • Defina o campo ID como 1 e selecione Executar.

    A solicitação DELETE é enviada ao aplicativo e a resposta é exibida no painel Respostas. O corpo da resposta está vazio e código de status Resposta do servidor é 204.

Impedir o excesso de postagem

Atualmente, o aplicativo de exemplo expõe todo o objeto Todo. Aplicativos de produção Em aplicativos de produção, um subconjunto do modelo geralmente é usado para restringir os dados que podem ser inseridos e retornados. Há várias razões por trás disso, e a segurança é uma das principais. O subconjunto de um modelo geralmente é chamado de DTO (Objeto de Transferência de Dados), modelo de entrada ou modelo de exibição. O DTO é usado neste artigo.

Um DTO pode ser usado para:

  • Impedir o excesso de postagem.
  • Oculte propriedades que os clientes não deveriam visualizar.
  • Omita algumas propriedades para reduzir o tamanho do conteúdo.
  • Nivelar gráficos de objetos que contenham objetos aninhados. Os grafos de objeto nivelados podem ser mais convenientes para os clientes.

Para demonstrar a abordagem de DTO, atualize a classe Todo para incluir um campo secreto:

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

O campo secreto precisa ser ocultado neste aplicativo, mas um aplicativo administrativo poderia optar por mostrá-lo.

Verifique se você pode postar e obter o campo secreto.

Crie um arquivo chamado TodoItemDTO.cs com o código a seguir:

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

Substitua o conteúdo do arquivo Program.cs pelo seguinte código para usar este modelo de 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>();
}

Verifique se você pode postar e obter todos os campos, exceto o campo de segredo.

Testar APIs mínimas

Para obter um exemplo de teste de um aplicativo de API mínima, consulte este exemplo do GitHub.

Publicar no Azure

Para obter informações sobre como implantar no Azure, consulte Início Rápido: Implantar um aplicativo Web ASP.NET.

Recursos adicionais

APIs mínimas são arquitetadas para criar APIs HTTP com dependências mínimas. Elas são ideais para microsserviços e aplicativos que desejam incluir apenas os arquivos, recursos e dependências mínimos no ASP.NET Core.

Este tutorial ensina os conceitos básicos da criação de uma API mínima com o ASP.NET Core. Outra abordagem para criar APIs no ASP.NET Core é usar controladores. Para obter ajuda na escolha entre APIs mínimas e APIs baseadas em controlador, veja Visão geral de APIs. Para obter um tutorial sobre como criar um projeto de API com base em controladores que contêm mais recursos, consulte Criar uma API Web.

Visão geral

Este tutorial cria a seguinte API:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
POST /todoitems Adicionar um novo item Item de tarefas pendentes Item de tarefas pendentes
PUT /todoitems/{id} Atualizar um item existente Item de tarefas pendentes Nenhum
DELETE /todoitems/{id}     Excluir um item Nenhum Nenhum

Pré-requisitos

Criar um projeto de API

  • Inicie o Visual Studio 2022 e selecione Criar um novo projeto.

  • Na caixa de diálogo Criar um projeto:

    • Insira Empty na caixa de pesquisa Pesquisar modelos .
    • Selecione o modelo ASP.NET Core Vazio e clique em Avançar.

    Visual Studio Criar um projeto

  • Nomeie o projeto como TodoApi e clique em Avançar.

  • Na caixa de diálogo Informações adicionais:

    • Selecione .NET 8.0 (suporte a longo prazo)
    • Desmarque Não usar declarações de nível superior
    • Escolha Criar

    Informações adicionais

Examinar o código

O arquivo Program.cs contém o seguinte código:

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

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

app.Run();

O código anterior:

Executar o aplicativo

Pressione Ctrl + F5 para execução sem o depurador.

O Visual Studio exibe a caixa de diálogo a seguir:

Este projeto está configurado para usar SSL. para evitar avisos de SSL no navegador, você pode optar por confiar no certificado autoassinado que o IIS Express gerou. Gostaria de confiar no certificado SSL do IIS Express?

Selecione Sim se você confia no certificado SSL do IIS Express.

A seguinte caixa de diálogo é exibida:

Caixa de diálogo de aviso de segurança

Selecione Sim se você concordar com confiar no certificado de desenvolvimento.

Para obter informações sobre como confiar no navegador Firefox, confira Erro de certificado Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.

O Visual Studio inicia o Kestrelservidor Web e abre uma janela do navegador.

Hello World! é exibido no navegador. O arquivo Program.cs contém um aplicativo mínimo, mas completo.

Feche a janela do navegador.

Adicionar pacotes do NuGet

Os pacotes NuGet devem ser adicionados para dar suporte ao banco de dados e diagnósticos usados neste tutorial.

  • No menu Ferramentas, selecione Gerenciador de Pacotes do NuGet > Gerenciar Pacotes do NuGet para a Solução.
  • Selecione a guia Procurar.
  • Na caixa de pesquisa, insira Microsoft.EntityFrameworkCore.InMemory e selecione Microsoft.EntityFrameworkCore.InMemory.
  • Marque a caixa de seleção Projeto no painel direito e selecione Instalar.
  • Use as instruções anteriores para adicionar o pacote Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

As classes de contexto de modelo e banco de dados

  • Na pasta do projeto, crie um arquivo chamado Todo.cs com o seguinte código:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

O código anterior cria o modelo para este aplicativo. Um modelo é um conjunto de classes que representam os dados gerenciados pelo aplicativo.

  • Crie um arquivo chamado TodoDb.cs com o código a seguir:
using Microsoft.EntityFrameworkCore;

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

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

O código anterior define o contexto de banco de dados, que é a classe principal que coordena a funcionalidade do Entity Framework para um modelo de dados. A classe é derivada da classe Microsoft.EntityFrameworkCore.DbContext.

Adicionar o código da API

  • Substitua o conteúdo do arquivo Program.cs pelo seguinte código:
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();

O código realçado a seguir adiciona o contexto do banco de dados ao contêiner de DI (injeção de dependência) e permite exibir exceções relacionadas ao banco de dados:

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

O contêiner de DI fornece acesso ao contexto do banco de dados e a outros serviços.

Este tutorial usa o Gerenciador de Pontos de Extremidade e arquivos .http para testar a API.

Testar os dados de postagem

O código a seguir em Program.cs cria um ponto de extremidade HTTP POST/todoitems que adiciona dados ao banco de dados na memória:

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

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

Execute o aplicativo. O navegador exibe um erro 404 porque não há mais um ponto de extremidade /.

O ponto de extremidade POST será usado para adicionar dados ao aplicativo.

  • Selecione Exibir>Outras Janelas>Gerenciador de Pontos de Extremidade.

  • Clique com o botão direito do mouse no ponto de extremidade POST e selecione Gerar solicitação.

    Menu de contexto do Gerenciador de Pontos de Extremidade realçando o item de menu Gerar Solicitação.

    Um novo arquivo é criado na pasta do projeto chamada TodoApi.http, com conteúdo semelhante ao seguinte exemplo:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • A primeira linha cria uma variável que é usada para todos os pontos de extremidade.
    • A próxima linha define uma solicitação POST.
    • A linha tripla de hashtag (###) é um delimitador de solicitação: o que vem depois dela é para uma solicitação diferente.
  • A solicitação POST precisa de cabeçalhos e um corpo. Para definir essas partes da solicitação, adicione as seguintes linhas imediatamente após a linha de solicitação POST:

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

    O código anterior adiciona um cabeçalho Content-Type e um corpo de solicitação JSON. O arquivo TodoApi.http agora deve se parece com o exemplo a seguir, mas com o número da porta:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Execute o aplicativo.

  • Selecione o link Enviar solicitação acima da linha de solicitação POST.

    Janela do arquivo .http com o link de execução realçado.

    A solicitação POST é enviada ao aplicativo e a resposta é exibida no painel Resposta.

    Janela do arquivo .http com resposta da solicitação POST.

Examinar os pontos de extremidade GET

O aplicativo de exemplo implementa vários pontos de extremidade GET chamando MapGet:

API Descrição Corpo da solicitação Corpo da resposta
GET /todoitems Obter todos os itens de tarefas pendentes Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/complete Obter itens pendentes concluídos Nenhum Matriz de itens de tarefas pendentes
GET /todoitems/{id} Obter um item por ID Nenhum Item de tarefas pendentes
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());

Testar os pontos de extremidade GET

Teste o aplicativo chamando os pontos de extremidade GET de um navegador ou usando o Gerenciador de Pontos de Extremidade. As etapas a seguir são para o Gerenciador de Pontos de Extremidade.

  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no primeiro ponto de extremidade GET e selecione Gerar solicitação.

    O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Selecione o link Enviar solicitação acima da nova linha de solicitação GET.

    A solicitação GET é enviada ao aplicativo e a resposta é exibida no painel Resposta.

  • O corpo de resposta é semelhante ao seguinte JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no /todoitems/{id} GET e selecione Gerar solicitação. O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Substitua {id} por 1.

  • Selecione o link Enviar solicitação acima da nova linha solicitação GET.

    A solicitação GET é enviada ao aplicativo e a resposta é exibida no painel Resposta.

  • O corpo de resposta é semelhante ao seguinte JSON:

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

Este aplicativo usa um banco de dados em memória. Se o aplicativo for reiniciado, a solicitação GET não retornará nenhum dado. Se nenhum dado for retornado, envie os dados para o aplicativo via POST e tente a solicitação GET novamente.

Valores de retorno

O ASP.NET Core serializa automaticamente o objeto em JSON e grava o JSON no corpo da mensagem de resposta. O código de resposta para esse tipo de retorno é 200 OK, supondo que não haja nenhuma exceção sem tratamento. As exceções sem tratamento são convertidas em erros 5xx.

Os tipos de retorno podem representar uma ampla variedade de códigos de status HTTP. Por exemplo, GET /todoitems/{id} pode retornar dois valores de status diferentes:

  • Se nenhum item corresponder às ID solicitadas, o método retornará um código de erro status 404 NotFound.
  • Caso contrário, o método retornará 200 com um corpo de resposta JSON. Retornar item resulta em uma resposta HTTP 200.

Examinar o ponto de extremidade PUT

O aplicativo de exemplo implementa um único ponto de extremidade PUT usando 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();
});

Esse método é semelhante ao método MapPost, exceto que ele usa HTTP PUT. Uma resposta bem-sucedida retorna 204 (sem conteúdo). De acordo com a especificação de HTTP, uma solicitação PUT exige que o cliente envie a entidade inteira atualizada, não apenas as alterações. Para dar suporte a atualizações parciais, use HTTP PATCH.

Testar o ponto de extremidade PUT

Este exemplo usa um banco de dados em memória que precisará ser iniciado sempre que o aplicativo for iniciado. Deverá haver um item no banco de dados antes de você fazer uma chamada PUT. Chame GET para garantir a existência de um item no banco de dados antes de fazer uma chamada PUT.

Atualize o item pendente que tem Id = 1 e defina o nome dele como "feed fish".

  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse no ponto de extremidade PUT e selecione Gerar solicitação.

    O conteúdo a seguir é adicionado ao arquivo TodoApi.http:

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • Na linha de solicitação PUT, substitua {id} por 1.

  • Adicione as seguintes linhas imediatamente após a linha de solicitação PUT:

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

    O código anterior adiciona um cabeçalho Content-Type e um corpo de solicitação JSON.

  • Selecione o link Enviar solicitação acima da nova linha de solicitação PUT.

    A solicitação PUT é enviada ao aplicativo e a resposta é exibida no painel Resposta. O corpo da resposta está vazio e código de status é 204.

Examinar e testar o ponto de extremidade DELETE

O aplicativo de exemplo implementa um único ponto de extremidade DELETE usando 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();
});
  • No Gerenciador de Pontos de Extremidade, clique com o botão direito do mouse do ponto de extremidade DELETE e selecione Gerar solicitação.

    Uma solicitação DELETE é adicionada a TodoApi.http.

  • Substitua {id} na linha de solicitação DELETE com 1. A solicitação DELETE deve ter uma aparência semelhante ao exemplo a seguir:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Selecione o link Enviar solicitação para a solicitação DELETE.

    A solicitação DELETE é enviada ao aplicativo e a resposta é exibida no painel Resposta. O corpo da resposta está vazio e código de status é 204.

Usar a API do MapGroup

O código do aplicativo de exemplo repete o prefixo de URL todoitems sempre que configura um ponto de extremidade. As APIs geralmente têm grupos de pontos de extremidade com um prefixo de URL comum e o método MapGroup está disponível para ajudar a organizar esses grupos. Isso reduz o código repetitivo e permite personalizar grupos inteiros de pontos de extremidade com uma única chamada a métodos como RequireAuthorization e WithMetadata.

Substitua o conteúdo de Program.cs pelo seguinte código:

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

O código anterior faz as seguintes alterações:

  • Adiciona var todoItems = app.MapGroup("/todoitems"); para configurar o grupo usando o prefixo /todoitems da URL.
  • Altera todos os métodos app.Map<HttpVerb> para todoItems.Map<HttpVerb>.
  • Remove o prefixo /todoitems da URL das chamadas de método Map<HttpVerb>.

Teste os pontos de extremidade para verificar se eles funcionam da mesma forma.

Usar a API TypedResults

Retornar TypedResults em vez de Results tem várias vantagens, incluindo capacidade de teste e retornar automaticamente os metadados de tipo de resposta para OpenAPI para descrever o ponto de extremidade. Para obter mais informações, consulte TypedResults vs Resultados.

Os métodos Map<HttpVerb> podem chamar métodos de manipulador de rotas em vez de usar lambdas. Para ver um exemplo, atualize Program.cs com o seguinte código:

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

O código Map<HttpVerb> agora chama métodos em vez de lambdas:

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

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

Esses métodos retornam objetos que implementam IResult e são definidos por 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();
}

Os testes de unidade podem chamar esses métodos e testar se eles retornam o tipo correto. Por exemplo, se o método for GetAllTodos:

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

O código de teste de unidade pode verificar se um objeto do tipo Ok<Todo[]> é retornado do método de manipulador. Por exemplo:

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

Impedir o excesso de postagem

Atualmente, o aplicativo de exemplo expõe todo o objeto Todo. Aplicativos de produção Em aplicativos de produção, um subconjunto do modelo geralmente é usado para restringir os dados que podem ser inseridos e retornados. Há várias razões por trás disso, e a segurança é uma das principais. O subconjunto de um modelo geralmente é chamado de DTO (Objeto de Transferência de Dados), modelo de entrada ou modelo de exibição. O DTO é usado neste artigo.

Um DTO pode ser usado para:

  • Impedir o excesso de postagem.
  • Oculte propriedades que os clientes não deveriam visualizar.
  • Omita algumas propriedades para reduzir o tamanho do conteúdo.
  • Nivelar gráficos de objetos que contenham objetos aninhados. Os grafos de objeto nivelados podem ser mais convenientes para os clientes.

Para demonstrar a abordagem de DTO, atualize a classe Todo para incluir um campo secreto:

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

O campo secreto precisa ser ocultado neste aplicativo, mas um aplicativo administrativo poderia optar por mostrá-lo.

Verifique se você pode postar e obter o campo secreto.

Crie um arquivo chamado TodoItemDTO.cs com o código a seguir:

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

Substitua o conteúdo do arquivo Program.cs pelo seguinte código para usar este modelo de 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();
}

Verifique se você pode postar e obter todos os campos, exceto o campo de segredo.

Solução de problemas com o exemplo concluído

Se você encontrar um problema que não possa resolver, compare seu código com o projeto concluído. Exibir ou baixar projeto concluído (como baixar).

Próximas etapas

Saiba mais

Confira Referência rápida sobre as APIs mínimas