Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Note
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 10 de este artículo.
Warning
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la política de compatibilidad de .NET y .NET Core. 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 contengan más características, consulte Creación de una API web.
Overview
En este tutorial se crea la siguiente API:
| API | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
POST /todoitems |
Incorporación de un nuevo elemento | Elemento de tareas pendientes | Elemento de tareas pendientes |
PUT /todoitems/{id} |
Actualizar un elemento existente | Elemento de tareas pendientes | None |
DELETE /todoitems/{id} |
Eliminar un elemento | None | None |
Prerequisites
Visual Studio 2022 con la carga de trabajo ASP.NET y desarrollo web.
Creación de un proyecto de API
Inicie Visual Studio 2022 y seleccione Crear un nuevo proyecto.
En el cuadro de diálogo Crear un nuevo proyecto:
- Escriba
Emptyen el cuadro de búsqueda Buscar plantillas . - Seleccione la plantilla ASP.NET Core Empty y seleccione Siguiente.
- Escriba
Asigne al proyecto el nombre TodoApi y seleccione Siguiente.
En el cuadro de diálogo Información adicional:
- Seleccione .NET 9.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:
- Crea los elementos WebApplicationBuilder y WebApplication con valores predeterminados preconfigurados.
- Crea un punto de conexión
/HTTP GET que devuelveHello World!.
Ejecutar la aplicación
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:
Seleccione Sí si confía en el certificado SSL de IIS Express.
Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener información sobre cómo confiar en el explorador Firefox, consulta Firefox SEC_ERROR_INADEQUATE_KEY_USAGE error de certificado.
Visual Studio inicia el Kestrel servidor web 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.
Cierra la ventana del explorador.
Incorporació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, seleccione Administrador de paquetes NuGet > Administrar paquetes NuGet para la solución.
- Seleccione la pestaña Examinar.
- Seleccione Incluir versión preliminar.
- Escriba Microsoft.EntityFrameworkCore.InMemory en el cuadro de búsqueda y, después, seleccione
Microsoft.EntityFrameworkCore.InMemory. - Active la casilla Proyecto en el panel derecho y, después, seleccione Instalar.
- Siga 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, cree un archivo llamado
Todo.cscon 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 administra la aplicación.
- Cree un archivo llamado
TodoDb.cscon 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 la 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
- Reemplace el contenido del archivo
Program.cspor 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 la base de datos al contenedor de inserción de dependencias (DI) y habilita la visualización de 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.
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.
La solicitud POST se envía a la aplicación y la respuesta se muestra en el panel Respuesta.
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 | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
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
Pruebe la aplicación llamando a los GET puntos de conexión desde un explorador o mediante el Explorador de puntos de conexió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 la respuesta es similar al siguiente formato 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
/todoitems/{id}y seleccione Generar solicitud. El siguiente contenido se agrega al archivoTodoApi.http:GET {{TodoApi_HostAddress}}/todoitems/{id} ###Reemplace
{id}con1.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 la respuesta es similar al siguiente formato 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, envíe datos POST a la aplicación y vuelva a intentar 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 excepciones no controladas. 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 404NotFound.
- En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver
itemgenera 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}por1.Agregue las líneas siguientes inmediatamente después de la línea de solicitud PUT:
Content-Type: application/json { "id": 1, "name": "feed fish", "isComplete": false }El código anterior agrega un encabezado Content-Type y un cuerpo de solicitud JSON.
Seleccione el vínculo Enviar solicitud que está encima de la nueva línea de solicitud PUT.
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 por1. 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>atodoItems.Map<HttpVerb>. - Quita el prefijo de dirección URL
/todoitemsde las llamadas de métodoMap<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 obtener más información, vea TypedResults vs 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 prueba unitaria puede comprobar que se devuelve un objeto de tipo Ok<Todo[]> desde el método de controlador. 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);
}
Evitar el exceso de contabilizació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:
- Evite el exceso de contabilizació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 descargar).
Pasos siguientes
- Configure las opciones de serialización json.
- Controlar errores y excepciones: la página de excepciones del desarrollador está habilitada de forma predeterminada en el entorno de desarrollo para aplicaciones de API mínimas. Para obtener información sobre cómo controlar errores y excepciones, consulte Control de errores en las API de ASP.NET Core.
- Para obtener un ejemplo de prueba de una aplicación de API mínima, consulte este ejemplo de GitHub.
- Compatibilidad con OpenAPI en API mínimas.
- Inicio rápido: Publicación en Azure.
- Organización de las API mínimas de ASP.NET Core.
Learn more
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 controladores, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contengan más características, consulte Creación de una API web.
Overview
En este tutorial se crea la siguiente API:
| API | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
POST /todoitems |
Incorporación de un nuevo elemento | Elemento de tareas pendientes | Elemento de tareas pendientes |
PUT /todoitems/{id} |
Actualizar un elemento existente | Elemento de tareas pendientes | None |
DELETE /todoitems/{id} |
Eliminar un elemento | None | None |
Prerequisites
Visual Studio 2022 con la carga de trabajo ASP.NET y desarrollo web.
Creación de un proyecto de API
Inicie Visual Studio 2022 y seleccione Crear un nuevo proyecto.
En el cuadro de diálogo Crear un nuevo proyecto:
- Escriba
Emptyen el cuadro de búsqueda Buscar plantillas . - Seleccione la plantilla ASP.NET Core Empty y seleccione Siguiente.
- Escriba
Asigne al proyecto el nombre TodoApi y seleccione Siguiente.
En el cuadro de diálogo Información adicional:
- Selección de .NET 7.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:
- Crea los elementos WebApplicationBuilder y WebApplication con valores predeterminados preconfigurados.
- Crea un punto de conexión HTTP GET
/que devuelveHello World!:
Ejecutar la aplicación
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:
Seleccione Sí si confía en el certificado SSL de IIS Express.
Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener información sobre cómo confiar en el explorador Firefox, consulta Firefox SEC_ERROR_INADEQUATE_KEY_USAGE error de certificado.
Visual Studio inicia el Kestrel servidor web 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.
Incorporació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, seleccione 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. - Active la casilla Proyecto en el panel derecho.
- En la lista desplegable Versión , seleccione la versión 7 más reciente disponible, por ejemplo
7.0.17, y, a continuación, seleccione Instalar. - Siga las instrucciones anteriores para agregar el paquete
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCorecon la última versión 7 disponible.
Clases de contexto de base de datos y modelo
En la carpeta del proyecto, cree 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 administra la aplicación.
Cree 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 la 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
Reemplace 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 la base de datos al contenedor de inserción de dependencias (DI) y habilita la visualización de 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; puede seguir los pasos introductorios de este tutorial de pruebas de API con su 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, consulte ASP.NET documentación de api web 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 documentos y interfaz de usuario de Swagger.
Configuración del middleware de Swagger
Agregue el código resaltado siguiente antes de que se defina
appen la líneavar 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 de API más sólidos, consulte 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
appen la líneavar 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>/swaggery mostrar la página de pruebas de API generada por Swagger.
En la página de pruebas de API de Swagger, seleccione Post /todoitems>Probar.
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, escriba JSON para un elemento de tarea, sin especificar el valor opcional
id:{ "name":"walk dog", "isComplete":true }Seleccione Ejecutar.
Swagger proporciona un panel Responses debajo del botón Execute.
Tenga en cuenta algunos detalles útiles:
- cURL: Swagger proporciona un ejemplo de comando cURL en la sintaxis de Unix/Linux, que se puede ejecutar en la línea de comandos con cualquier shell de Bash que use la sintaxis de Unix/Linux, 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
idse ha establecido en1. - 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 | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
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>Pruébelo>Ejecutar.
Como alternativa, llame a GET /todoitems desde un explorador escribiendo 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>Pruébelo.
- Establezca el campo id en
1y seleccione Ejecutar.
Como alternativa, llame a GET /todoitems desde un explorador escribiendo el URI
https://localhost:<port>/todoitems/1. Por ejemplo:https://localhost:5001/todoitems/1La 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, envíe datos POST a la aplicación y vuelva a intentar 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 excepciones no controladas. 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 404NotFound.
- En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver
itemgenera 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}>Pruébelo.
Establezca el campo id en
1.Establezca el cuerpo de la solicitud en el siguiente JSON:
{ "name": "feed fish", "isComplete": false }Seleccione 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}>Pruébelo.
Establezca el campo ID en
1y 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>atodoItems.Map<HttpVerb>. - Quita el prefijo de dirección URL
/todoitemsde las llamadas de métodoMap<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 obtener más información, vea TypedResults vs 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 prueba unitaria puede comprobar que se devuelve un objeto de tipo Ok<Todo[]> desde el método de controlador. 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);
}
Evitar el exceso de contabilizació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:
- Evite el exceso de contabilizació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 descargar).
Pasos siguientes
- Configure las opciones de serialización json.
- Controlar errores y excepciones: la página de excepciones del desarrollador está habilitada de forma predeterminada en el entorno de desarrollo para aplicaciones de API mínimas. Para obtener información sobre cómo controlar errores y excepciones, consulte Control de errores en las API de ASP.NET Core.
- Para obtener un ejemplo de prueba de una aplicación de API mínima, consulte este ejemplo de GitHub.
- Compatibilidad con OpenAPI en API mínimas.
- Inicio rápido: Publicación en Azure.
- Organización de las API mínimas de ASP.NET Core.
Learn more
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 controladores, consulte Introducción a las API. Para ver un tutorial sobre cómo crear un proyecto de API basado en controladores que contengan más características, consulte Creación de una API web.
Overview
En este tutorial se crea la siguiente API:
| API | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
POST /todoitems |
Incorporación de un nuevo elemento | Elemento de tareas pendientes | Elemento de tareas pendientes |
PUT /todoitems/{id} |
Actualizar un elemento existente | Elemento de tareas pendientes | None |
DELETE /todoitems/{id} |
Eliminar un elemento | None | None |
Prerequisites
- Visual Studio 2022 con la carga de trabajo ASP.NET y desarrollo web.
- SDK de .NET 6
Creación de un proyecto de API
Inicie Visual Studio 2022 y seleccione Crear un nuevo proyecto.
En el cuadro de diálogo Crear un nuevo proyecto:
- Escriba
Emptyen el cuadro de búsqueda Buscar plantillas . - Seleccione la plantilla ASP.NET Core Empty y seleccione Siguiente.
- Escriba
Asigne al proyecto el nombre TodoApi y seleccione Siguiente.
En el cuadro de diálogo Información adicional:
- Selección de .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:
- Crea los elementos WebApplicationBuilder y WebApplication con valores predeterminados preconfigurados.
- Crea un punto de conexión HTTP GET
/que devuelveHello World!:
Ejecutar la aplicación
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:
Seleccione Sí si confía en el certificado SSL de IIS Express.
Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener información sobre cómo confiar en el explorador Firefox, consulta Firefox SEC_ERROR_INADEQUATE_KEY_USAGE error de certificado.
Visual Studio inicia el Kestrel servidor web 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.
Incorporació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, seleccione 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. - Active la casilla Proyecto en el panel derecho.
- En la lista desplegable Versión , seleccione la versión 7 más reciente disponible, por ejemplo
6.0.28, y, a continuación, seleccione Instalar. - Siga las instrucciones anteriores para agregar el paquete
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCorecon la última versión 7 disponible.
Clases de contexto de base de datos y modelo
En la carpeta del proyecto, cree 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 administra la aplicación.
Cree 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 la 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
Reemplace 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 la base de datos al contenedor de inserción de dependencias (DI) y habilita la visualización de 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; puede seguir los pasos introductorios de este tutorial de pruebas de API con su 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, consulte ASP.NET documentación de api web 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 documentos y interfaz de usuario de Swagger.
Configuración del middleware de Swagger
En Program.cs, agregue las instrucciones
usingsiguientes en la parte superior :using NSwag.AspNetCore;Agregue el código resaltado siguiente antes de que se defina
appen la líneavar 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 de API más sólidos, consulte 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
appen la líneavar 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>/swaggery mostrar la página de pruebas de API generada por Swagger.
En la página de pruebas de API de Swagger, seleccione Post /todoitems>Probar.
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, escriba JSON para un elemento de tarea, sin especificar el valor opcional
id:{ "name":"walk dog", "isComplete":true }Seleccione Ejecutar.
Swagger proporciona un panel Responses debajo del botón Execute.
Tenga en cuenta algunos detalles útiles:
- cURL: Swagger proporciona un ejemplo de comando cURL en la sintaxis de Unix/Linux, que se puede ejecutar en la línea de comandos con cualquier shell de Bash que use la sintaxis de Unix/Linux, 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
idse ha establecido en1. - 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 | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
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>Pruébelo>Ejecutar.
Como alternativa, llame a GET /todoitems desde un explorador escribiendo 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>Pruébelo.
- Establezca el campo id en
1y seleccione Ejecutar.
Como alternativa, llame a GET /todoitems desde un explorador escribiendo el URI
https://localhost:<port>/todoitems/1. Por ejemplo,https://localhost:5001/todoitems/1La 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, envíe datos POST a la aplicación y vuelva a intentar 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 excepciones no controladas. 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 404NotFound.
- En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver
itemgenera 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}>Pruébelo.
Establezca el campo id en
1.Establezca el cuerpo de la solicitud en el siguiente JSON:
{ "name": "feed fish", "isComplete": false }Seleccione 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}>Pruébelo.
Establezca el campo ID en
1y 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.
Evitar el exceso de contabilizació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:
- Evite el exceso de contabilizació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.
Publicación en Azure
Para obtener 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 contengan más características, consulte Creación de una API web.
Overview
En este tutorial se crea la siguiente API:
| API | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
POST /todoitems |
Incorporación de un nuevo elemento | Elemento de tareas pendientes | Elemento de tareas pendientes |
PUT /todoitems/{id} |
Actualizar un elemento existente | Elemento de tareas pendientes | None |
DELETE /todoitems/{id} |
Eliminar un elemento | None | None |
Prerequisites
Visual Studio 2022 con la carga de trabajo ASP.NET y desarrollo web.
Creación de un proyecto de API
Inicie Visual Studio 2022 y seleccione Crear un nuevo proyecto.
En el cuadro de diálogo Crear un nuevo proyecto:
- Escriba
Emptyen el cuadro de búsqueda Buscar plantillas . - Seleccione la plantilla ASP.NET Core Empty y seleccione Siguiente.
- Escriba
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
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:
- Crea los elementos WebApplicationBuilder y WebApplication con valores predeterminados preconfigurados.
- Crea un punto de conexión HTTP GET
/que devuelveHello World!:
Ejecutar la aplicación
Presione Ctrl+F5 para ejecutarla sin el depurador.
Visual Studio muestra el cuadro de diálogo siguiente:
Seleccione Sí si confía en el certificado SSL de IIS Express.
Se muestra el cuadro de diálogo siguiente:
Si acepta confiar en el certificado de desarrollo, seleccione Sí.
Para obtener información sobre cómo confiar en el explorador Firefox, consulta Firefox SEC_ERROR_INADEQUATE_KEY_USAGE error de certificado.
Visual Studio inicia el Kestrel servidor web 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.
Cierra la ventana del explorador.
Incorporació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, seleccione 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. - Active la casilla Proyecto en el panel derecho y, después, seleccione Instalar.
- Siga 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, cree un archivo llamado
Todo.cscon 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 administra la aplicación.
- Cree un archivo llamado
TodoDb.cscon 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 la 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
- Reemplace el contenido del archivo
Program.cspor 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 la base de datos al contenedor de inserción de dependencias (DI) y habilita la visualización de 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.
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.
La solicitud POST se envía a la aplicación y la respuesta se muestra en el panel Respuesta.
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 | Description | Cuerpo de la solicitud | Cuerpo de 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 | Elemento de tareas pendientes |
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
Pruebe la aplicación llamando a los GET puntos de conexión desde un explorador o mediante el Explorador de puntos de conexió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 la respuesta es similar al siguiente formato 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
/todoitems/{id}y seleccione Generar solicitud. El siguiente contenido se agrega al archivoTodoApi.http:GET {{TodoApi_HostAddress}}/todoitems/{id} ###Reemplace
{id}con1.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 la respuesta es similar al siguiente formato 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, envíe datos POST a la aplicación y vuelva a intentar 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 excepciones no controladas. 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 404NotFound.
- En caso contrario, el método devuelve 200 con un cuerpo de respuesta JSON. Devolver
itemgenera 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}por1.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 Enviar solicitud que está encima de la nueva línea de solicitud PUT.
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 por1. 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>atodoItems.Map<HttpVerb>. - Quita el prefijo de dirección URL
/todoitemsde las llamadas de métodoMap<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 obtener más información, vea TypedResults vs 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 prueba unitaria puede comprobar que se devuelve un objeto de tipo Ok<Todo[]> desde el método de controlador. 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);
}
Evitar el exceso de contabilizació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:
- Evite el exceso de contabilizació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 descargar).
Pasos siguientes
- Configure las opciones de serialización json.
- Controlar errores y excepciones: la página de excepciones del desarrollador está habilitada de forma predeterminada en el entorno de desarrollo para aplicaciones de API mínimas. Para obtener información sobre cómo controlar errores y excepciones, consulte Control de errores en las API de ASP.NET Core.
- Para obtener un ejemplo de prueba de una aplicación de API mínima, consulte este ejemplo de GitHub.
- Compatibilidad con OpenAPI en API mínimas.
- Inicio rápido: Publicación en Azure.
- Organización de las API mínimas de ASP.NET Core.
Learn more
Consulte Referencia rápida de las API mínimas