Compartir a través de


Tutorial: Creación de una API mínima con ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la directiva de compatibilidad de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

Por Rick Anderson y Tom Dykstra

Las API mínimas están diseñadas para crear API HTTP con dependencias mínimas. Son ideales para microservicios y aplicaciones que desean incluir solo los archivos, las características y las dependencias mínimas en ASP.NET Core.

En este tutorial se enseñan los conceptos básicos de la compilación de una API mínima con ASP.NET Core. Otro enfoque para crear API en ASP.NET Core es usar controladores. Para obtener ayuda para elegir entre las API mínimas y las API basadas en controlador, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contiene más características, vea Creación de una API web.

Información general

En este tutorial se crea la siguiente API:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtener tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
POST /todoitems Incorporación de un nuevo elemento Tarea pendiente Tarea pendiente
PUT /todoitems/{id} Actualizar un elemento existente Tarea pendiente None
DELETE /todoitems/{id}     Eliminar un elemento None None

Requisitos previos

Creación de un proyecto de API

  • Inicie Visual Studio 2022 y seleccione Crear un proyecto.

  • En el cuadro de diálogo Crear un proyecto:

    • Escriba Empty en el cuadro de búsqueda Buscar plantillas.
    • Seleccione la plantilla ASP.NET Core vacío y seleccione Siguiente.

    Crear un proyecto en Visual Studio

  • Asigne al proyecto el nombre TodoApi y seleccione Siguiente.

  • En el cuadro de diálogo Información adicional:

    • Seleccione .NET 9.0 (versión preliminar)
    • Anule la sección de No usar instrucciones de nivel superior.
    • Seleccione Crear

    Información adicional

Examen del código

El archivo Program.cs contiene el código siguiente:

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

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

app.Run();

El código anterior:

Ejecutar la aplicación

Presione Ctrl+F5 para ejecutarla sin el depurador.

Visual Studio muestra el cuadro de diálogo siguiente:

Este proyecto está configurado para usar SSL. Para evitar las advertencias de SSL en el explorador puede optar por confiar en el certificado autofirmado que ha generado IIS Express. ¿Desea confiar en el certificado SSL de IIS Express?

Haga clic en si confía en el certificado SSL de IIS Express.

Se muestra el cuadro de diálogo siguiente:

Cuadro de diálogo de advertencia de seguridad

Si acepta confiar en el certificado de desarrollo, seleccione .

Para obtener información sobre cómo confiar en el explorador Firefox, consulte Error de certificado SEC_ERROR_INADEQUATE_KEY_USAGE de Firefox.

Visual Studio inicia el servidor web Kestrel y abre una ventana del explorador.

Se muestra Hello World! en el explorador. El archivo Program.cs contiene una aplicación mínima pero completa.

Cierre la ventana del explorador.

Adición de paquetes NuGet

Se deben agregar paquetes NuGet para admitir la base de datos y los diagnósticos usados en este tutorial.

  • En el menú Herramientas, selecciona Administrador de paquetes NuGet > Administrar paquetes NuGet para la solución.
  • Selecciona la pestaña Examinar.
  • Selecciona Incluir versión preliminar.
  • Escribe Microsoft.EntityFrameworkCore.InMemory en el cuadro de búsqueda y, después, selecciona Microsoft.EntityFrameworkCore.InMemory.
  • Activa la casilla Proyecto en el panel derecho y, después, selecciona Instalar.
  • Sigue las instrucciones anteriores para agregar el paquete Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Clases de contexto de base de datos y modelo

  • En la carpeta del proyecto, crea un archivo llamado Todo.cs con el código siguiente:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

El código anterior crea el modelo para esta aplicación. Un modelo es una clase que representa los datos que la aplicación administra.

  • Crea un archivo llamado TodoDb.cs con el código siguiente:
using Microsoft.EntityFrameworkCore;

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

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

El código anterior define el contexto de base de datos, que es la clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos. Esta clase deriva de la clase Microsoft.EntityFrameworkCore.DbContext.

Adición del código de API

  • Reemplaza el contenido del archivo Program.cs por el código siguiente:
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();

El código resaltado siguiente agrega el contexto de base de datos al contenedor de inserción de dependencias (ID) y permite mostrar excepciones relacionadas con la base de datos:

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

El contenedor de ID proporciona acceso al contexto de la base de datos y a otros servicios.

En este tutorial se usan el Explorador de puntos de conexión y los archivos .http para probar la API.

Prueba de la publicación de datos

El código siguiente en Program.cs crea un punto de conexión HTTP POST /todoitems que agrega datos a la base de datos en memoria:

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

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

Ejecutar la aplicación. El explorador muestra un error 404 porque ya no hay un punto de conexión /.

El punto de conexión POST se usará para agregar datos a la aplicación.

  • Seleccione Ver>Otras ventanas>Explorador de puntos de conexión.

  • Haga clic con el botón derecho en el punto de conexión POST y seleccione Generar solicitud.

    Menú contextual del Explorador de puntos de conexión que resalta el elemento de menú Generar solicitud.

    Se crea un nuevo archivo en la carpeta del proyecto denominada TodoApi.http, con contenido similar al ejemplo siguiente:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • La primera línea crea una variable que se usa para todos los puntos de conexión.
    • La siguiente línea define una solicitud POST.
    • La línea triple hashtag (###) es un delimitador de solicitud: lo que viene después es para una solicitud diferente.
  • La solicitud POST necesita encabezados y un cuerpo. Para definir esas partes de la solicitud, agregue las líneas siguientes inmediatamente después de la línea de solicitud POST:

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

    El código anterior agrega un encabezado Content-Type y un cuerpo de solicitud JSON. El archivo TodoApi.http debería tener ahora un aspecto similar al del ejemplo siguiente, pero con su número de puerto:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Ejecutar la aplicación.

  • Seleccione el vínculo Enviar solicitud situado encima de la línea de solicitud POST.

    Ventana de archivo .http con el vínculo de ejecución resaltado.

    La solicitud POST se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

    Ventana del archivo .http con respuesta de la solicitud POST.

Examen de los puntos de conexión GET

La aplicación de ejemplo implementa varios puntos de conexión GET mediante la llamada a MapGet:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtención de todas las tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
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());

Prueba de los puntos de conexión GET

Llame a los puntos de conexión GET desde un explorador o con el Explorador de puntos de conexión para probar la aplicación. Los pasos siguientes son para el Explorador de puntos de conexión.

  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el primer punto de conexión GET y seleccione Generar solicitud.

    El siguiente contenido se agrega al archivo TodoApi.http:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Seleccione el vínculo Enviar solicitud que está encima de la nueva línea de solicitud GET.

    La solicitud GET se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

  • El cuerpo de respuesta es similar al siguiente JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión GET /todoitems/{id} y seleccione Generar solicitud. El siguiente contenido se agrega al archivo TodoApi.http:

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

  • Seleccione el vínculo Enviar solicitud situado encima de la nueva línea de solicitud GET.

    La solicitud GET se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

  • El cuerpo de respuesta es similar al siguiente JSON:

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

Esta aplicación utiliza una base de datos en memoria. Si se reinicia la aplicación, la solicitud GET no devuelve ningún dato. Si no se devuelven datos, aplique POST para los datos en la aplicación y rentente realizar la solicitud GET.

Valores devueltos

ASP.NET Core serializa automáticamente el objeto a JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta de este tipo de valor devuelto es 200 OK, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en errores 5xx.

Los tipos de valores devueltos pueden representar una gama amplia de códigos de estado HTTP. Por ejemplo, GET /todoitems/{id} puede devolver dos valores de estado diferentes:

  • Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de error de estado 404 NotFound.
  • En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una respuesta HTTP 200.

Examen del punto de conexión PUT

La aplicación de ejemplo implementa un único punto de conexión PUT mediante 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();
});

Este método es similar al método MapPost, salvo que usa HTTP PUT. Una respuesta correcta devuelve 204 (Sin contenido). Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los cambios. Para admitir actualizaciones parciales, use HTTP PATCH.

Prueba del punto de conexión PUT

En este ejemplo se usa una base de datos en memoria que se debe inicializar cada vez que se inicia la aplicación. Debe haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para asegurarse de que hay un elemento en la base de datos antes de realizar una llamada PUT.

Actualice el elemento de tarea que tiene Id = 1 y establezca su nombre en "feed fish".

  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión PUT y seleccione Generar solicitud.

    El siguiente contenido se agrega al archivo TodoApi.http:

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • En la línea de solicitud PUT, reemplace {id} por 1.

  • Agregue las líneas siguientes inmediatamente después de la línea de solicitud PUT:

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

    El código anterior agrega un encabezado Content-Type y un cuerpo de solicitud JSON.

  • Seleccione el vínculo Send request situado encima de la línea de solicitud PUT nueva.

    La solicitud PUT se envía a la aplicación y la respuesta se muestra en el panel Respuesta. El cuerpo de la respuesta está vacío y el código de estado es 204.

Examen y prueba del punto de conexión DELETE

La aplicación de ejemplo implementa un único punto de conexión DELETE mediante 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();
});
  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión DELETE y seleccione Generar solicitud.

    Se agrega una solicitud DELETE a TodoApi.http.

  • Reemplace {id} en la línea de solicitud DELETE por 1. La solicitud DELETE debe tener un aspecto similar al del ejemplo siguiente:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Seleccione el vínculo Enviar solicitud para la solicitud DELETE.

    La solicitud DELETE se envía a la aplicación y la respuesta se muestra en el panel Respuesta. El cuerpo de la respuesta está vacío y el código de estado es 204.

Uso de la API MapGroup

El código de la aplicación de ejemplo repite el prefijo de dirección URL todoitems cada vez que configura un punto de conexión. Las API suelen tener grupos de puntos de conexión con un prefijo de dirección URL común, y el método MapGroup está disponible para ayudar a organizar esos grupos. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata.

Reemplace el contenido de Program.cs por el código siguiente:

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

El código anterior tiene los cambios siguientes:

  • Agrega var todoItems = app.MapGroup("/todoitems"); para configurar el grupo con el prefijo de dirección URL /todoitems.
  • Cambia todos los métodos app.Map<HttpVerb> a todoItems.Map<HttpVerb>.
  • Quita el prefijo de dirección URL /todoitems de las llamadas de método Map<HttpVerb>.

Pruebe los puntos de conexión para comprobar que funcionan de la misma forma.

Uso de la API TypedResults

Devolver TypedResults en lugar de Results tiene varias ventajas, incluida la capacidad de prueba y devolver automáticamente los metadatos de tipo de respuesta para que OpenAPI describa el punto de conexión. Para más información, consulte TypedResults frente a Results.

Los métodos Map<HttpVerb> pueden llamar a los métodos de controlador de ruta en lugar de usar expresiones lambda. Para ver un ejemplo, actualice Program.cs con el código siguiente:

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

Ahora, el código Map<HttpVerb> llama a métodos en lugar de llamar a expresiones lambda:

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

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

Estos métodos devuelven objetos que implementan IResult y se definen 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();
}

Las pruebas unitarias pueden llamar a estos métodos y probar que devuelven el tipo correcto. Por ejemplo, si el método es GetAllTodos:

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

El código de la prueba unitaria puede comprobar que el método de controlador devuelve un objeto de tipo Ok<Todo[]>. Por ejemplo:

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

Prevención del exceso de publicación

Actualmente, la aplicación de ejemplo expone todo el objeto Todo. En las aplicaciones de producción, a menudo se usa un subconjunto del modelo para restringir los datos que se pueden introducir y devolver. Hay varias razones para ello y la seguridad es una de las principales. El subconjunto de un modelo se suele conocer como un objeto de transferencia de datos (DTO), modelo de entrada o modelo de vista. En este artículo, se usa DTO.

Se puede usar un DTO para:

  • Evitar el exceso de publicación.
  • Ocultar las propiedades que los clientes no deben ver.
  • Omitir algunas propiedades para reducir el tamaño de la carga.
  • Acoplar los gráficos de objetos que contienen objetos anidados. Los gráficos de objetos acoplados pueden ser más cómodos para los clientes.

Para mostrar el enfoque del DTO, actualice la clase Todo a fin de que incluya un 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; }
}

El campo secreto debe ocultarse en esta aplicación, pero una aplicación administrativa podría decidir exponerlo.

Compruebe que puede publicar y obtener el campo secreto.

Cree un archivo llamado TodoItemDTO.cs con el código siguiente:

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

Reemplace el contenido del archivo Program.cs por el código siguiente para usar este modelo 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();
}

Compruebe que puede publicar y obtener todos los campos excepto el campo secreto.

Solución de problemas con el ejemplo completo

Si experimenta un problema que no puede resolver, compare el código con el proyecto completado. Vea o descargue el proyecto completado (cómo descargarlo).

Pasos siguientes

Saber más

Consulte Referencia rápida de las API mínimas

Las API mínimas están diseñadas para crear API HTTP con dependencias mínimas. Son ideales para microservicios y aplicaciones que desean incluir solo los archivos, las características y las dependencias mínimas en ASP.NET Core.

En este tutorial se enseñan los conceptos básicos de la compilación de una API mínima con ASP.NET Core. Otro enfoque para crear API en ASP.NET Core es usar controladores. Para obtener ayuda para elegir entre las API mínimas y las API basadas en controlador, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contiene más características, vea Creación de una API web.

Información general

En este tutorial se crea la siguiente API:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtener tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
POST /todoitems Incorporación de un nuevo elemento Tarea pendiente Tarea pendiente
PUT /todoitems/{id} Actualizar un elemento existente Tarea pendiente None
DELETE /todoitems/{id}     Eliminar un elemento None None

Requisitos previos

Creación de un proyecto de API

  • Inicie Visual Studio 2022 y seleccione Crear un proyecto.

  • En el cuadro de diálogo Crear un proyecto:

    • Escriba Empty en el cuadro de búsqueda Buscar plantillas.
    • Seleccione la plantilla ASP.NET Core vacío y seleccione Siguiente.

    Crear un proyecto en Visual Studio

  • Asigne al proyecto el nombre TodoApi y seleccione Siguiente.

  • En el cuadro de diálogo Información adicional:

    • Seleccione .NET 7.0.
    • Anule la sección de No usar instrucciones de nivel superior.
    • Seleccione Crear

    Información adicional

Examen del código

El archivo Program.cs contiene el código siguiente:

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

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

app.Run();

El código anterior:

Ejecutar la aplicación

Presione Ctrl+F5 para ejecutarla sin el depurador.

Visual Studio muestra el cuadro de diálogo siguiente:

Este proyecto está configurado para usar SSL. Para evitar las advertencias de SSL en el explorador puede optar por confiar en el certificado autofirmado que ha generado IIS Express. ¿Desea confiar en el certificado SSL de IIS Express?

Haga clic en si confía en el certificado SSL de IIS Express.

Se muestra el cuadro de diálogo siguiente:

Cuadro de diálogo de advertencia de seguridad

Si acepta confiar en el certificado de desarrollo, seleccione .

Para obtener información sobre cómo confiar en el explorador Firefox, consulte Error de certificado SEC_ERROR_INADEQUATE_KEY_USAGE de Firefox.

Visual Studio inicia el servidor web Kestrel y abre una ventana del explorador.

Se muestra Hello World! en el explorador. El archivo Program.cs contiene una aplicación mínima pero completa.

Adición de paquetes NuGet

Se deben agregar paquetes NuGet para admitir la base de datos y los diagnósticos usados en este tutorial.

  • En el menú Herramientas, selecciona Administrador de paquetes NuGet > Administrar paquetes NuGet para la solución.
  • Seleccione la pestaña Examinar.
  • Escriba Microsoft.EntityFrameworkCore.InMemory en el cuadro de búsqueda y, después, seleccione Microsoft.EntityFrameworkCore.InMemory.
  • Seleccione la casilla Proyecto en el panel derecho.
  • En el menú desplegable Versión, seleccione la última versión 7 disponible, por ejemplo, 7.0.17 y, a continuación, seleccione Instalar.
  • Siga las instrucciones anteriores para agregar el paquete Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore con la última versión 7 disponible.

Clases de contexto de base de datos y modelo

En la carpeta del proyecto, crea un archivo llamado Todo.cs con el código siguiente:

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

El código anterior crea el modelo para esta aplicación. Un modelo es una clase que representa los datos que la aplicación administra.

Crea un archivo llamado TodoDb.cs con el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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

El código anterior define el contexto de base de datos, que es la clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos. Esta clase deriva de la clase Microsoft.EntityFrameworkCore.DbContext.

Adición del código de API

Reemplaza el contenido del archivo Program.cs por el código siguiente:

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

El código resaltado siguiente agrega el contexto de base de datos al contenedor de inserción de dependencias (ID) y permite mostrar excepciones relacionadas con la base de datos:

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

El contenedor de ID proporciona acceso al contexto de la base de datos y a otros servicios.

Creación de una interfaz de usuario de pruebas de API con Swagger

Existen muchas herramientas de prueba de API web disponibles entre las que elegir; puedes seguir los pasos introductorios de este tutorial de pruebas de API con tu herramienta preferida propia.

En este tutorial se usa el paquete .NET NSwag.AspNetCore, que integra las herramientas de Swagger para generar una interfaz de usuario de prueba de acuerdo con la especificación OpenAPI:

  • NSwag: biblioteca de .NET que integra Swagger directamente en las aplicaciones de ASP.NET Core y proporciona middleware y configuración.
  • Swagger: conjunto de herramientas de código abierto, como OpenAPIGenerator y SwaggerUI, que generan páginas de pruebas de API de acuerdo con la especificación OpenAPI.
  • Especificación OpenAPI: documento que describe las capacidades de la API, según las anotaciones de atributo y XML en los controladores y modelos.

Para obtener más información sobre el uso de OpenAPI y NSwag con ASP.NET, consulta Documentación de la API web de ASP.NET Core con Swagger/OpenAPI.

Instalación de herramientas de Swagger

  • Ejecute el siguiente comando:

    dotnet add package NSwag.AspNetCore
    

El comando anterior agrega el paquete NSwag.AspNetCore, que contiene herramientas para generar interfaz de usuario y documentos de Swagger.

Configuración del middleware de Swagger

  • Agregue el código resaltado siguiente antes de que se defina app en la línea 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();
    

En el código anterior:

  • builder.Services.AddEndpointsApiExplorer();: habilita el Explorador de API, un servicio que proporciona metadatos sobre la API HTTP. Swagger usa el Explorador de API para generar el documento de Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: agrega el generador de documentos OpenAPI de Swagger a los servicios de la aplicación y lo configura para proporcionar más información sobre la API, como su título y versión. Para obtener información sobre cómo proporcionar detalles más sólidos de la API, consulta Introducción a NSwag y ASP.NET Core.

  • Agregue el código resaltado siguiente a la línea siguiente después de que se defina app en la línea 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";
        });
    }
    

    El código anterior habilita el middleware de Swagger para servir el documento JSON generado y la interfaz de usuario de Swagger. Swagger solo se habilita en un entorno de desarrollo. Si Swagger se habilita en un entorno de producción, pueden exponerse detalles potencialmente confidenciales sobre la implementación y la estructura de la API.

Prueba de la publicación de datos

El código siguiente en Program.cs crea un punto de conexión HTTP POST /todoitems que agrega datos a la base de datos en memoria:

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

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

Ejecutar la aplicación. El explorador muestra un error 404 porque ya no hay un punto de conexión /.

El punto de conexión POST se usará para agregar datos a la aplicación.

  • Con la aplicación todavía en ejecución, use el explorador para ir a https://localhost:<port>/swagger y mostrar la página de pruebas de API generada por Swagger.

    Página de pruebas de API generada por Swagger

  • En la página de pruebas de API de Swagger, seleccione Post /todoitems>Try it out.

  • Tenga en cuenta que el campo Request body contiene un formato de ejemplo generado que refleja los parámetros de la API.

  • En el cuerpo de la solicitud, escribe JSON para un elemento de tarea, sin especificar el valor id opcional:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Seleccione Execute(Ejecutar).

    Swagger con Post

Swagger proporciona un panel Responses debajo del botón Execute.

Swagger con respuesta Post

Tenga en cuenta algunos detalles útiles:

  • cURL: Swagger proporciona un comando cURL de ejemplo en la sintaxis de Unix/Linux, que se puede ejecutar en la línea de comandos con cualquier shell de Bash que utilice dicha sintaxis, incluido Git Bash desde Git para Windows.
  • Dirección URL de la solicitud: representación simplificada de la solicitud HTTP realizada por el código JavaScript de la interfaz de usuario de Swagger para la llamada API. Las solicitudes reales pueden incluir detalles como encabezados, parámetros de consulta y un cuerpo de la solicitud.
  • Respuesta del servidor: incluye los encabezados y el cuerpo de la respuesta. El cuerpo de la respuesta muestra que 1 se ha establecido en id.
  • Código de respuesta: se ha devuelto un código de estado 201 HTTP, que indica que la solicitud se ha procesado correctamente y ha dado lugar a la creación de un nuevo recurso.

Examen de los puntos de conexión GET

La aplicación de ejemplo implementa varios puntos de conexión GET mediante la llamada a MapGet:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtención de todas las tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
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());

Prueba de los puntos de conexión GET

Llame a los puntos de conexión desde un explorador o Swagger para probar la aplicación.

  • En Swagger, seleccione GET /todoitems>Try it out>Execute.

  • Como alternativa, llame a GET /todoitems desde un explorador al especificar el URI http://localhost:<port>/todoitems. Por ejemplo: http://localhost:5001/todoitems

La llamada a GET /todoitems genera una respuesta similar a la siguiente:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Llame a GET /todoitems/{id} en Swagger para devolver datos de un identificador específico:

    • Seleccione GET /todoitems>Try it out.
    • Establezca el campo id en 1 y seleccione Execute.
  • Como alternativa, llame a GET /todoitems desde un explorador al especificar el URI https://localhost:<port>/todoitems/1. Por ejemplo: https://localhost:5001/todoitems/1

  • La respuesta es similar a lo siguiente:

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

Esta aplicación utiliza una base de datos en memoria. Si se reinicia la aplicación, la solicitud GET no devuelve ningún dato. Si no se devuelven datos, aplique POST para los datos en la aplicación y rentente realizar la solicitud GET.

Valores devueltos

ASP.NET Core serializa automáticamente el objeto a JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta de este tipo de valor devuelto es 200 OK, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en errores 5xx.

Los tipos de valores devueltos pueden representar una gama amplia de códigos de estado HTTP. Por ejemplo, GET /todoitems/{id} puede devolver dos valores de estado diferentes:

  • Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de error de estado 404 NotFound.
  • En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una respuesta HTTP 200.

Examen del punto de conexión PUT

La aplicación de ejemplo implementa un único punto de conexión PUT mediante 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();
});

Este método es similar al método MapPost, salvo que usa HTTP PUT. Una respuesta correcta devuelve 204 (Sin contenido). Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los cambios. Para admitir actualizaciones parciales, use HTTP PATCH.

Prueba del punto de conexión PUT

En este ejemplo se usa una base de datos en memoria que se debe inicializar cada vez que se inicia la aplicación. Debe haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para asegurarse de que hay un elemento en la base de datos antes de realizar una llamada PUT.

Actualice el elemento de tarea que tiene Id = 1 y establezca su nombre en "feed fish".

Use Swagger para enviar una solicitud PUT:

  • Seleccione Put /todoitems/{id}>Try it out.

  • Establezca el campo id en 1.

  • Establece el cuerpo de la solicitud en el siguiente JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Seleccione Execute(Ejecutar).

Examen y prueba del punto de conexión DELETE

La aplicación de ejemplo implementa un único punto de conexión DELETE mediante 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 Swagger para enviar una solicitud DELETE:

  • Seleccione DELETE /todoitems/{id}>Try it out.

  • Establezca el campo ID en 1 y seleccione Execute.

    La solicitud DELETE se envía a la aplicación y la respuesta se muestra en el panel Responses. El cuerpo de la respuesta está vacío y el código de estado de Server response es 204.

Uso de la API MapGroup

El código de la aplicación de ejemplo repite el prefijo de dirección URL todoitems cada vez que configura un punto de conexión. Las API suelen tener grupos de puntos de conexión con un prefijo de dirección URL común, y el método MapGroup está disponible para ayudar a organizar esos grupos. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata.

Reemplace el contenido de Program.cs por el código siguiente:

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

El código anterior tiene los cambios siguientes:

  • Agrega var todoItems = app.MapGroup("/todoitems"); para configurar el grupo con el prefijo de dirección URL /todoitems.
  • Cambia todos los métodos app.Map<HttpVerb> a todoItems.Map<HttpVerb>.
  • Quita el prefijo de dirección URL /todoitems de las llamadas de método Map<HttpVerb>.

Pruebe los puntos de conexión para comprobar que funcionan de la misma forma.

Uso de la API TypedResults

Devolver TypedResults en lugar de Results tiene varias ventajas, incluida la capacidad de prueba y devolver automáticamente los metadatos de tipo de respuesta para que OpenAPI describa el punto de conexión. Para más información, consulte TypedResults frente a Results.

Los métodos Map<HttpVerb> pueden llamar a los métodos de controlador de ruta en lugar de usar expresiones lambda. Para ver un ejemplo, actualice Program.cs con el código siguiente:

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

Ahora, el código Map<HttpVerb> llama a métodos en lugar de llamar a expresiones lambda:

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

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

Estos métodos devuelven objetos que implementan IResult y se definen 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();
}

Las pruebas unitarias pueden llamar a estos métodos y probar que devuelven el tipo correcto. Por ejemplo, si el método es GetAllTodos:

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

El código de la prueba unitaria puede comprobar que el método de controlador devuelve un objeto de tipo Ok<Todo[]>. Por ejemplo:

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

Prevención del exceso de publicación

Actualmente, la aplicación de ejemplo expone todo el objeto Todo. En las aplicaciones de producción, a menudo se usa un subconjunto del modelo para restringir los datos que se pueden introducir y devolver. Hay varias razones para ello y la seguridad es una de las principales. El subconjunto de un modelo se suele conocer como un objeto de transferencia de datos (DTO), modelo de entrada o modelo de vista. En este artículo, se usa DTO.

Se puede usar un DTO para:

  • Evitar el exceso de publicación.
  • Ocultar las propiedades que los clientes no deben ver.
  • Omitir algunas propiedades para reducir el tamaño de la carga.
  • Acoplar los gráficos de objetos que contienen objetos anidados. Los gráficos de objetos acoplados pueden ser más cómodos para los clientes.

Para mostrar el enfoque del DTO, actualice la clase Todo a fin de que incluya un 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; }
}

El campo secreto debe ocultarse en esta aplicación, pero una aplicación administrativa podría decidir exponerlo.

Compruebe que puede publicar y obtener el campo secreto.

Cree un archivo llamado TodoItemDTO.cs con el código siguiente:

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

Reemplace el contenido del archivo Program.cs por el código siguiente para usar este modelo 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>();
}

Compruebe que puede publicar y obtener todos los campos excepto el campo secreto.

Solución de problemas con el ejemplo completo

Si experimenta un problema que no puede resolver, compare el código con el proyecto completado. Vea o descargue el proyecto completado (cómo descargarlo).

Pasos siguientes

Saber más

Consulte Referencia rápida de las API mínimas

Las API mínimas están diseñadas para crear API HTTP con dependencias mínimas. Son ideales para microservicios y aplicaciones que desean incluir solo los archivos, las características y las dependencias mínimas en ASP.NET Core.

En este tutorial se enseñan los conceptos básicos de la compilación de una API mínima con ASP.NET Core. Otro enfoque para crear API en ASP.NET Core es usar controladores. Para obtener ayuda para elegir entre las API mínimas y las API basadas en controlador, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contiene más características, vea Creación de una API web.

Información general

En este tutorial se crea la siguiente API:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtener tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
POST /todoitems Incorporación de un nuevo elemento Tarea pendiente Tarea pendiente
PUT /todoitems/{id} Actualizar un elemento existente Tarea pendiente None
DELETE /todoitems/{id}     Eliminar un elemento None None

Requisitos previos

Creación de un proyecto de API

  • Inicie Visual Studio 2022 y seleccione Crear un proyecto.

  • En el cuadro de diálogo Crear un proyecto:

    • Escriba Empty en el cuadro de búsqueda Buscar plantillas.
    • Seleccione la plantilla ASP.NET Core vacío y seleccione Siguiente.

    Crear un proyecto en Visual Studio

  • Asigne al proyecto el nombre TodoApi y seleccione Siguiente.

  • En el cuadro de diálogo Información adicional:

    • Seleccionar .NET 6.0
    • Anule la sección de No usar instrucciones de nivel superior.
    • Seleccione Crear

Examen del código

El archivo Program.cs contiene el código siguiente:

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

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

app.Run();

El código anterior:

Ejecutar la aplicación

Presione Ctrl+F5 para ejecutarla sin el depurador.

Visual Studio muestra el cuadro de diálogo siguiente:

Este proyecto está configurado para usar SSL. Para evitar las advertencias de SSL en el explorador puede optar por confiar en el certificado autofirmado que ha generado IIS Express. ¿Desea confiar en el certificado SSL de IIS Express?

Haga clic en si confía en el certificado SSL de IIS Express.

Se muestra el cuadro de diálogo siguiente:

Cuadro de diálogo de advertencia de seguridad

Si acepta confiar en el certificado de desarrollo, seleccione .

Para obtener información sobre cómo confiar en el explorador Firefox, consulte Error de certificado SEC_ERROR_INADEQUATE_KEY_USAGE de Firefox.

Visual Studio inicia el servidor web Kestrel y abre una ventana del explorador.

Se muestra Hello World! en el explorador. El archivo Program.cs contiene una aplicación mínima pero completa.

Adición de paquetes NuGet

Se deben agregar paquetes NuGet para admitir la base de datos y los diagnósticos usados en este tutorial.

  • En el menú Herramientas, selecciona Administrador de paquetes NuGet > Administrar paquetes NuGet para la solución.
  • Seleccione la pestaña Examinar.
  • Escriba Microsoft.EntityFrameworkCore.InMemory en el cuadro de búsqueda y, después, seleccione Microsoft.EntityFrameworkCore.InMemory.
  • Seleccione la casilla Proyecto en el panel derecho.
  • En el menú desplegable Versión, seleccione la última versión 7 disponible, por ejemplo, 6.0.28 y, a continuación, seleccione Instalar.
  • Siga las instrucciones anteriores para agregar el paquete Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore con la última versión 7 disponible.

Clases de contexto de base de datos y modelo

En la carpeta del proyecto, crea un archivo llamado Todo.cs con el código siguiente:

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

El código anterior crea el modelo para esta aplicación. Un modelo es una clase que representa los datos que la aplicación administra.

Crea un archivo llamado TodoDb.cs con el código siguiente:

using Microsoft.EntityFrameworkCore;

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

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

El código anterior define el contexto de base de datos, que es la clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos. Esta clase deriva de la clase Microsoft.EntityFrameworkCore.DbContext.

Adición del código de API

Reemplaza el contenido del archivo Program.cs por el código siguiente:

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

El código resaltado siguiente agrega el contexto de base de datos al contenedor de inserción de dependencias (ID) y permite mostrar excepciones relacionadas con la base de datos:

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

El contenedor de ID proporciona acceso al contexto de la base de datos y a otros servicios.

Creación de una interfaz de usuario de pruebas de API con Swagger

Existen muchas herramientas de prueba de API web disponibles entre las que elegir; puedes seguir los pasos introductorios de este tutorial de pruebas de API con tu herramienta preferida propia.

En este tutorial se usa el paquete .NET NSwag.AspNetCore, que integra las herramientas de Swagger para generar una interfaz de usuario de prueba de acuerdo con la especificación OpenAPI:

  • NSwag: biblioteca de .NET que integra Swagger directamente en las aplicaciones de ASP.NET Core y proporciona middleware y configuración.
  • Swagger: conjunto de herramientas de código abierto, como OpenAPIGenerator y SwaggerUI, que generan páginas de pruebas de API de acuerdo con la especificación OpenAPI.
  • Especificación OpenAPI: documento que describe las capacidades de la API, según las anotaciones de atributo y XML en los controladores y modelos.

Para obtener más información sobre el uso de OpenAPI y NSwag con ASP.NET, consulta Documentación de la API web de ASP.NET Core con Swagger/OpenAPI.

Instalación de herramientas de Swagger

  • Ejecute el siguiente comando:

    dotnet add package NSwag.AspNetCore
    

El comando anterior agrega el paquete NSwag.AspNetCore, que contiene herramientas para generar interfaz de usuario y documentos de Swagger.

Configuración del middleware de Swagger

  • En Program.cs, agregue las instrucciones using siguientes en la parte superior :

    using NSwag.AspNetCore;
    
  • Agregue el código resaltado siguiente antes de que se defina app en la línea 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();
    

En el código anterior:

  • builder.Services.AddEndpointsApiExplorer();: habilita el Explorador de API, un servicio que proporciona metadatos sobre la API HTTP. Swagger usa el Explorador de API para generar el documento de Swagger.

  • builder.Services.AddOpenApiDocument(config => {...});: agrega el generador de documentos OpenAPI de Swagger a los servicios de la aplicación y lo configura para proporcionar más información sobre la API, como su título y versión. Para obtener información sobre cómo proporcionar detalles más sólidos de la API, consulta Introducción a NSwag y ASP.NET Core.

  • Agregue el código resaltado siguiente a la línea siguiente después de que se defina app en la línea 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";
        });
    }
    
    

    El código anterior habilita el middleware de Swagger para servir el documento JSON generado y la interfaz de usuario de Swagger. Swagger solo se habilita en un entorno de desarrollo. Si Swagger se habilita en un entorno de producción, pueden exponerse detalles potencialmente confidenciales sobre la implementación y la estructura de la API.

Prueba de la publicación de datos

El código siguiente en Program.cs crea un punto de conexión HTTP POST /todoitems que agrega datos a la base de datos en memoria:

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

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

Ejecutar la aplicación. El explorador muestra un error 404 porque ya no hay un punto de conexión /.

El punto de conexión POST se usará para agregar datos a la aplicación.

  • Con la aplicación todavía en ejecución, use el explorador para ir a https://localhost:<port>/swagger y mostrar la página de pruebas de API generada por Swagger.

    Página de pruebas de API generada por Swagger

  • En la página de pruebas de API de Swagger, seleccione Post /todoitems>Try it out.

  • Tenga en cuenta que el campo Request body contiene un formato de ejemplo generado que refleja los parámetros de la API.

  • En el cuerpo de la solicitud, escribe JSON para un elemento de tarea, sin especificar el valor id opcional:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • Seleccione Execute(Ejecutar).

    Swagger con datos de Post

Swagger proporciona un panel Responses debajo del botón Execute.

Panel de Swagger con respuesta Post

Tenga en cuenta algunos detalles útiles:

  • cURL: Swagger proporciona un comando cURL de ejemplo en la sintaxis de Unix/Linux, que se puede ejecutar en la línea de comandos con cualquier shell de Bash que utilice dicha sintaxis, incluido Git Bash desde Git para Windows.
  • Dirección URL de la solicitud: representación simplificada de la solicitud HTTP realizada por el código JavaScript de la interfaz de usuario de Swagger para la llamada API. Las solicitudes reales pueden incluir detalles como encabezados, parámetros de consulta y un cuerpo de la solicitud.
  • Respuesta del servidor: incluye los encabezados y el cuerpo de la respuesta. El cuerpo de la respuesta muestra que 1 se ha establecido en id.
  • Código de respuesta: se ha devuelto un código de estado 201 HTTP, que indica que la solicitud se ha procesado correctamente y ha dado lugar a la creación de un nuevo recurso.

Examen de los puntos de conexión GET

La aplicación de ejemplo implementa varios puntos de conexión GET mediante la llamada a MapGet:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtención de todas las tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
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());

Prueba de los puntos de conexión GET

Llame a los puntos de conexión desde un explorador o Swagger para probar la aplicación.

  • En Swagger, seleccione GET /todoitems>Try it out>Execute.

  • Como alternativa, llame a GET /todoitems desde un explorador al especificar el URI http://localhost:<port>/todoitems. Por ejemplo: http://localhost:5001/todoitems

La llamada a GET /todoitems genera una respuesta similar a la siguiente:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • Llame a GET /todoitems/{id} en Swagger para devolver datos de un identificador específico:

    • Seleccione GET /todoitems>Try it out.
    • Establezca el campo id en 1 y seleccione Execute.
  • Como alternativa, llame a GET /todoitems desde un explorador al especificar el URI https://localhost:<port>/todoitems/1. Por ejemplo, https://localhost:5001/todoitems/1

  • La respuesta es similar a lo siguiente:

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

Esta aplicación utiliza una base de datos en memoria. Si se reinicia la aplicación, la solicitud GET no devuelve ningún dato. Si no se devuelven datos, aplique POST para los datos en la aplicación y rentente realizar la solicitud GET.

Valores devueltos

ASP.NET Core serializa automáticamente el objeto a JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta de este tipo de valor devuelto es 200 OK, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en errores 5xx.

Los tipos de valores devueltos pueden representar una gama amplia de códigos de estado HTTP. Por ejemplo, GET /todoitems/{id} puede devolver dos valores de estado diferentes:

  • Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de error de estado 404 NotFound.
  • En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una respuesta HTTP 200.

Examen del punto de conexión PUT

La aplicación de ejemplo implementa un único punto de conexión PUT mediante 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();
});

Este método es similar al método MapPost, salvo que usa HTTP PUT. Una respuesta correcta devuelve 204 (Sin contenido). Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los cambios. Para admitir actualizaciones parciales, use HTTP PATCH.

Prueba del punto de conexión PUT

En este ejemplo se usa una base de datos en memoria que se debe inicializar cada vez que se inicia la aplicación. Debe haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para asegurarse de que hay un elemento en la base de datos antes de realizar una llamada PUT.

Actualice el elemento de tarea que tiene Id = 1 y establezca su nombre en "feed fish".

Use Swagger para enviar una solicitud PUT:

  • Seleccione Put /todoitems/{id}>Try it out.

  • Establezca el campo id en 1.

  • Establece el cuerpo de la solicitud en el siguiente JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • Seleccione Execute(Ejecutar).

Examen y prueba del punto de conexión DELETE

La aplicación de ejemplo implementa un único punto de conexión DELETE mediante 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 Swagger para enviar una solicitud DELETE:

  • Seleccione DELETE /todoitems/{id}>Try it out.

  • Establezca el campo ID en 1 y seleccione Execute.

    La solicitud DELETE se envía a la aplicación y la respuesta se muestra en el panel Responses. El cuerpo de la respuesta está vacío y el código de estado de Server response es 204.

Prevención del exceso de publicación

Actualmente, la aplicación de ejemplo expone todo el objeto Todo. En las aplicaciones de producción, a menudo se usa un subconjunto del modelo para restringir los datos que se pueden introducir y devolver. Hay varias razones para ello y la seguridad es una de las principales. El subconjunto de un modelo se suele conocer como un objeto de transferencia de datos (DTO), modelo de entrada o modelo de vista. En este artículo, se usa DTO.

Se puede usar un DTO para:

  • Evitar el exceso de publicación.
  • Ocultar las propiedades que los clientes no deben ver.
  • Omitir algunas propiedades para reducir el tamaño de la carga.
  • Acoplar los gráficos de objetos que contienen objetos anidados. Los gráficos de objetos acoplados pueden ser más cómodos para los clientes.

Para mostrar el enfoque del DTO, actualice la clase Todo a fin de que incluya un 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; }
}

El campo secreto debe ocultarse en esta aplicación, pero una aplicación administrativa podría decidir exponerlo.

Compruebe que puede publicar y obtener el campo secreto.

Cree un archivo llamado TodoItemDTO.cs con el código siguiente:

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

Reemplace el contenido del archivo Program.cs por el código siguiente para usar este modelo 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>();
}

Compruebe que puede publicar y obtener todos los campos excepto el campo secreto.

Prueba de la API mínima

Para obtener un ejemplo de prueba de una aplicación de API mínima, consulte este ejemplo de GitHub.

Publicar en Azure

Para obtener más información sobre la implementación en Azure, consulte Inicio rápido: Implementación de una aplicación web de ASP.NET.

Recursos adicionales

Las API mínimas están diseñadas para crear API HTTP con dependencias mínimas. Son ideales para microservicios y aplicaciones que desean incluir solo los archivos, las características y las dependencias mínimas en ASP.NET Core.

En este tutorial se enseñan los conceptos básicos de la compilación de una API mínima con ASP.NET Core. Otro enfoque para crear API en ASP.NET Core es usar controladores. Para obtener ayuda para elegir entre las API mínimas y las API basadas en controlador, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contiene más características, vea Creación de una API web.

Información general

En este tutorial se crea la siguiente API:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtener tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
POST /todoitems Incorporación de un nuevo elemento Tarea pendiente Tarea pendiente
PUT /todoitems/{id} Actualizar un elemento existente Tarea pendiente None
DELETE /todoitems/{id}     Eliminar un elemento None None

Requisitos previos

Creación de un proyecto de API

  • Inicie Visual Studio 2022 y seleccione Crear un proyecto.

  • En el cuadro de diálogo Crear un proyecto:

    • Escriba Empty en el cuadro de búsqueda Buscar plantillas.
    • Seleccione la plantilla ASP.NET Core vacío y seleccione Siguiente.

    Crear un proyecto en Visual Studio

  • Asigne al proyecto el nombre TodoApi y seleccione Siguiente.

  • En el cuadro de diálogo Información adicional:

    • Seleccione .NET 8.0 (Compatibilidad a largo plazo)
    • Anule la sección de No usar instrucciones de nivel superior.
    • Seleccione Crear

    Información adicional

Examen del código

El archivo Program.cs contiene el código siguiente:

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

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

app.Run();

El código anterior:

Ejecutar la aplicación

Presione Ctrl+F5 para ejecutarla sin el depurador.

Visual Studio muestra el cuadro de diálogo siguiente:

Este proyecto está configurado para usar SSL. Para evitar las advertencias de SSL en el explorador puede optar por confiar en el certificado autofirmado que ha generado IIS Express. ¿Desea confiar en el certificado SSL de IIS Express?

Haga clic en si confía en el certificado SSL de IIS Express.

Se muestra el cuadro de diálogo siguiente:

Cuadro de diálogo de advertencia de seguridad

Si acepta confiar en el certificado de desarrollo, seleccione .

Para obtener información sobre cómo confiar en el explorador Firefox, consulte Error de certificado SEC_ERROR_INADEQUATE_KEY_USAGE de Firefox.

Visual Studio inicia el servidor web Kestrel y abre una ventana del explorador.

Se muestra Hello World! en el explorador. El archivo Program.cs contiene una aplicación mínima pero completa.

Cierre la ventana del explorador.

Adición de paquetes NuGet

Se deben agregar paquetes NuGet para admitir la base de datos y los diagnósticos usados en este tutorial.

  • En el menú Herramientas, selecciona Administrador de paquetes NuGet > Administrar paquetes NuGet para la solución.
  • Seleccione la pestaña Examinar.
  • Escriba Microsoft.EntityFrameworkCore.InMemory en el cuadro de búsqueda y, después, seleccione Microsoft.EntityFrameworkCore.InMemory.
  • Activa la casilla Proyecto en el panel derecho y, después, selecciona Instalar.
  • Sigue las instrucciones anteriores para agregar el paquete Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Clases de contexto de base de datos y modelo

  • En la carpeta del proyecto, crea un archivo llamado Todo.cs con el código siguiente:
public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

El código anterior crea el modelo para esta aplicación. Un modelo es una clase que representa los datos que la aplicación administra.

  • Crea un archivo llamado TodoDb.cs con el código siguiente:
using Microsoft.EntityFrameworkCore;

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

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

El código anterior define el contexto de base de datos, que es la clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos. Esta clase deriva de la clase Microsoft.EntityFrameworkCore.DbContext.

Adición del código de API

  • Reemplaza el contenido del archivo Program.cs por el código siguiente:
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();

El código resaltado siguiente agrega el contexto de base de datos al contenedor de inserción de dependencias (ID) y permite mostrar excepciones relacionadas con la base de datos:

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

El contenedor de ID proporciona acceso al contexto de la base de datos y a otros servicios.

En este tutorial se usan el Explorador de puntos de conexión y los archivos .http para probar la API.

Prueba de la publicación de datos

El código siguiente en Program.cs crea un punto de conexión HTTP POST /todoitems que agrega datos a la base de datos en memoria:

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

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

Ejecutar la aplicación. El explorador muestra un error 404 porque ya no hay un punto de conexión /.

El punto de conexión POST se usará para agregar datos a la aplicación.

  • Seleccione Ver>Otras ventanas>Explorador de puntos de conexión.

  • Haga clic con el botón derecho en el punto de conexión POST y seleccione Generar solicitud.

    Menú contextual del Explorador de puntos de conexión que resalta el elemento de menú Generar solicitud.

    Se crea un nuevo archivo en la carpeta del proyecto denominada TodoApi.http, con contenido similar al ejemplo siguiente:

    @TodoApi_HostAddress = https://localhost:7031
    
    Post {{TodoApi_HostAddress}}/todoitems
    
    ###
    
    • La primera línea crea una variable que se usa para todos los puntos de conexión.
    • La siguiente línea define una solicitud POST.
    • La línea triple hashtag (###) es un delimitador de solicitud: lo que viene después es para una solicitud diferente.
  • La solicitud POST necesita encabezados y un cuerpo. Para definir esas partes de la solicitud, agregue las líneas siguientes inmediatamente después de la línea de solicitud POST:

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

    El código anterior agrega un encabezado Content-Type y un cuerpo de solicitud JSON. El archivo TodoApi.http debería tener ahora un aspecto similar al del ejemplo siguiente, pero con su número de puerto:

    @TodoApi_HostAddress = https://localhost:7057
    
    Post {{TodoApi_HostAddress}}/todoitems
    Content-Type: application/json
    
    {
      "name":"walk dog",
      "isComplete":true
    }
    
    ###
    
  • Ejecutar la aplicación.

  • Seleccione el vínculo Enviar solicitud situado encima de la línea de solicitud POST.

    Ventana de archivo .http con el vínculo de ejecución resaltado.

    La solicitud POST se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

    Ventana del archivo .http con respuesta de la solicitud POST.

Examen de los puntos de conexión GET

La aplicación de ejemplo implementa varios puntos de conexión GET mediante la llamada a MapGet:

API Descripción Cuerpo de la solicitud Cuerpo de la respuesta
GET /todoitems Obtener todas las tareas pendientes None Matriz de tareas pendientes
GET /todoitems/complete Obtención de todas las tareas pendientes completadas None Matriz de tareas pendientes
GET /todoitems/{id} Obtener un elemento por identificador None Tarea pendiente
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());

Prueba de los puntos de conexión GET

Llame a los puntos de conexión GET desde un explorador o con el Explorador de puntos de conexión para probar la aplicación. Los pasos siguientes son para el Explorador de puntos de conexión.

  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el primer punto de conexión GET y seleccione Generar solicitud.

    El siguiente contenido se agrega al archivo TodoApi.http:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • Seleccione el vínculo Enviar solicitud que está encima de la nueva línea de solicitud GET.

    La solicitud GET se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

  • El cuerpo de respuesta es similar al siguiente JSON:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
      }
    ]
    
  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión GET /todoitems/{id} y seleccione Generar solicitud. El siguiente contenido se agrega al archivo TodoApi.http:

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

  • Seleccione el vínculo Enviar solicitud situado encima de la nueva línea de solicitud GET.

    La solicitud GET se envía a la aplicación y la respuesta se muestra en el panel Respuesta.

  • El cuerpo de respuesta es similar al siguiente JSON:

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

Esta aplicación utiliza una base de datos en memoria. Si se reinicia la aplicación, la solicitud GET no devuelve ningún dato. Si no se devuelven datos, aplique POST para los datos en la aplicación y rentente realizar la solicitud GET.

Valores devueltos

ASP.NET Core serializa automáticamente el objeto a JSON y escribe el JSON en el cuerpo del mensaje de respuesta. El código de respuesta de este tipo de valor devuelto es 200 OK, suponiendo que no haya ninguna excepción no controlada. Las excepciones no controladas se convierten en errores 5xx.

Los tipos de valores devueltos pueden representar una gama amplia de códigos de estado HTTP. Por ejemplo, GET /todoitems/{id} puede devolver dos valores de estado diferentes:

  • Si no hay ningún elemento que coincida con el identificador solicitado, el método devolverá un código de error de estado 404 NotFound.
  • En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver item genera una respuesta HTTP 200.

Examen del punto de conexión PUT

La aplicación de ejemplo implementa un único punto de conexión PUT mediante 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();
});

Este método es similar al método MapPost, salvo que usa HTTP PUT. Una respuesta correcta devuelve 204 (Sin contenido). Según la especificación HTTP, una solicitud PUT requiere que el cliente envíe toda la entidad actualizada, no solo los cambios. Para admitir actualizaciones parciales, use HTTP PATCH.

Prueba del punto de conexión PUT

En este ejemplo se usa una base de datos en memoria que se debe inicializar cada vez que se inicia la aplicación. Debe haber un elemento en la base de datos antes de que realice una llamada PUT. Llame a GET para asegurarse de que hay un elemento en la base de datos antes de realizar una llamada PUT.

Actualice el elemento de tarea que tiene Id = 1 y establezca su nombre en "feed fish".

  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión PUT y seleccione Generar solicitud.

    El siguiente contenido se agrega al archivo TodoApi.http:

    Put {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • En la línea de solicitud PUT, reemplace {id} por 1.

  • Agregue las líneas siguientes inmediatamente después de la línea de solicitud PUT:

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

    El código anterior agrega un encabezado Content-Type y un cuerpo de solicitud JSON.

  • Seleccione el vínculo Send request situado encima de la línea de solicitud PUT nueva.

    La solicitud PUT se envía a la aplicación y la respuesta se muestra en el panel Respuesta. El cuerpo de la respuesta está vacío y el código de estado es 204.

Examen y prueba del punto de conexión DELETE

La aplicación de ejemplo implementa un único punto de conexión DELETE mediante 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();
});
  • En el Explorador de puntos de conexión, haga clic con el botón derecho en el punto de conexión DELETE y seleccione Generar solicitud.

    Se agrega una solicitud DELETE a TodoApi.http.

  • Reemplace {id} en la línea de solicitud DELETE por 1. La solicitud DELETE debe tener un aspecto similar al del ejemplo siguiente:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • Seleccione el vínculo Enviar solicitud para la solicitud DELETE.

    La solicitud DELETE se envía a la aplicación y la respuesta se muestra en el panel Respuesta. El cuerpo de la respuesta está vacío y el código de estado es 204.

Uso de la API MapGroup

El código de la aplicación de ejemplo repite el prefijo de dirección URL todoitems cada vez que configura un punto de conexión. Las API suelen tener grupos de puntos de conexión con un prefijo de dirección URL común, y el método MapGroup está disponible para ayudar a organizar esos grupos. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata.

Reemplace el contenido de Program.cs por el código siguiente:

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

El código anterior tiene los cambios siguientes:

  • Agrega var todoItems = app.MapGroup("/todoitems"); para configurar el grupo con el prefijo de dirección URL /todoitems.
  • Cambia todos los métodos app.Map<HttpVerb> a todoItems.Map<HttpVerb>.
  • Quita el prefijo de dirección URL /todoitems de las llamadas de método Map<HttpVerb>.

Pruebe los puntos de conexión para comprobar que funcionan de la misma forma.

Uso de la API TypedResults

Devolver TypedResults en lugar de Results tiene varias ventajas, incluida la capacidad de prueba y devolver automáticamente los metadatos de tipo de respuesta para que OpenAPI describa el punto de conexión. Para más información, consulte TypedResults frente a Results.

Los métodos Map<HttpVerb> pueden llamar a los métodos de controlador de ruta en lugar de usar expresiones lambda. Para ver un ejemplo, actualice Program.cs con el código siguiente:

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

Ahora, el código Map<HttpVerb> llama a métodos en lugar de llamar a expresiones lambda:

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

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

Estos métodos devuelven objetos que implementan IResult y se definen 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();
}

Las pruebas unitarias pueden llamar a estos métodos y probar que devuelven el tipo correcto. Por ejemplo, si el método es GetAllTodos:

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

El código de la prueba unitaria puede comprobar que el método de controlador devuelve un objeto de tipo Ok<Todo[]>. Por ejemplo:

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

Prevención del exceso de publicación

Actualmente, la aplicación de ejemplo expone todo el objeto Todo. En las aplicaciones de producción, a menudo se usa un subconjunto del modelo para restringir los datos que se pueden introducir y devolver. Hay varias razones para ello y la seguridad es una de las principales. El subconjunto de un modelo se suele conocer como un objeto de transferencia de datos (DTO), modelo de entrada o modelo de vista. En este artículo, se usa DTO.

Se puede usar un DTO para:

  • Evitar el exceso de publicación.
  • Ocultar las propiedades que los clientes no deben ver.
  • Omitir algunas propiedades para reducir el tamaño de la carga.
  • Acoplar los gráficos de objetos que contienen objetos anidados. Los gráficos de objetos acoplados pueden ser más cómodos para los clientes.

Para mostrar el enfoque del DTO, actualice la clase Todo a fin de que incluya un 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; }
}

El campo secreto debe ocultarse en esta aplicación, pero una aplicación administrativa podría decidir exponerlo.

Compruebe que puede publicar y obtener el campo secreto.

Cree un archivo llamado TodoItemDTO.cs con el código siguiente:

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

Reemplace el contenido del archivo Program.cs por el código siguiente para usar este modelo 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();
}

Compruebe que puede publicar y obtener todos los campos excepto el campo secreto.

Solución de problemas con el ejemplo completo

Si experimenta un problema que no puede resolver, compare el código con el proyecto completado. Vea o descargue el proyecto completado (cómo descargarlo).

Pasos siguientes

Saber más

Consulte Referencia rápida de las API mínimas