Con .NET 10, una API web completa cabe en un archivo de treinta líneas y se arranca con dotnet run. Aquí construimos una pequeña API de tareas con ASP.NET Core y la probamos con peticiones de verdad, sin navegador.
También te puede interesar
El servidor completo
ASP.NET Core es el marco web de .NET, y viene con él: no se instala nada. Su forma más corta se llama Minimal API: cada ruta es una línea con MapGet o MapPost y una función. Esta es una pequeña API de tareas, en un solo archivo:
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var tareas = new List<Tarea> { new(1, "Comprar café", false), new(2, "Escribir el informe", true) };
app.MapGet("/", () => "Hola desde C#");
app.MapGet("/tareas", () => tareas);
app.MapGet("/tareas/{id:int}", (int id) =>
tareas.FirstOrDefault(t => t.Id == id) is { } t ? Results.Ok(t) : Results.NotFound());
app.MapPost("/tareas", (NuevaTarea nueva) =>
{
if (string.IsNullOrWhiteSpace(nueva.Titulo)) return Results.BadRequest(new { error = "Falta el título" });
var t = new Tarea(tareas.Max(x => x.Id) + 1, nueva.Titulo, false);
tareas.Add(t);
return Results.Created($"/tareas/{t.Id}", t);
});
app.Run();
record Tarea(int Id, string Titulo, bool Hecha);
record NuevaTarea(string Titulo);dotnet run servidor.cs
La primera línea, #:sdk Microsoft.NET.Sdk.Web, le dice a dotnet run que es una aplicación web (en un proyecto se crea con dotnet new web). La segunda desactiva AOT, que .NET 10 activa en los archivos sueltos y que exigiría declarar los tipos de JSON de antemano (lo explica JSON en C#). Al arrancar, el servidor escucha en http://localhost:5000; abre esa dirección en el navegador y verás «Hola desde C#».
Probarlo desde el propio programa
Para comprobar la API sin navegador, este programa arranca el mismo servidor con StartAsync (que no se queda esperando, como Run), le hace peticiones con HttpClient y lo detiene. Es también la idea de las pruebas de integración de ASP.NET Core:
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.Net.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // sin los mensajes de arranque
builder.WebHost.UseUrls("http://127.0.0.1:5199");
var app = builder.Build();
// ... las mismas rutas de arriba ...
await app.StartAsync(); // arranca y sigue, en vez de quedarse esperando
using var cliente = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5199") };
async Task Ver(HttpResponseMessage r) =>
Console.WriteLine($"{(int)r.StatusCode} {await r.Content.ReadAsStringAsync()}");
await Ver(await cliente.GetAsync("/"));
await Ver(await cliente.GetAsync("/tareas"));
await Ver(await cliente.GetAsync("/tareas/2"));
await Ver(await cliente.GetAsync("/tareas/9"));
await Ver(await cliente.GetAsync("/tareas/dos"));
await Ver(await cliente.PostAsJsonAsync("/tareas", new { titulo = "Llamar a Luis" }));
await Ver(await cliente.PostAsJsonAsync("/tareas", new { titulo = "" }));
await Ver(await cliente.GetAsync("/tareas"));
await app.StopAsync();
// ... los dos record de arriba ...salida200 Hola desde C#
200 [{"id":1,"titulo":"Comprar café","hecha":false},{"id":2,"titulo":"Escribir el informe","hecha":true}]
200 {"id":2,"titulo":"Escribir el informe","hecha":true}
404
404
201 {"id":3,"titulo":"Llamar a Luis","hecha":false}
400 {"error":"Falta el título"}
200 [{"id":1,"titulo":"Comprar café","hecha":false},{"id":2,"titulo":"Escribir el informe","hecha":true},{"id":3,"titulo":"Llamar a Luis","hecha":false}]Fíjate en lo que el marco hace solo. Una lista o un record se devuelven como JSON, con los nombres en camelCase. /tareas/9 da 404 porque la función devuelve Results.NotFound(), y /tareas/dos también, pero por otra razón: la restricción {id:int} hace que esa ruta ni siquiera encaje. En el POST, el cuerpo JSON se convierte en un NuevaTarea, y Results.Created responde 201 con la cabecera Location apuntando a la tarea nueva.
List no es segura si llegan dos peticiones a la vez (ASP.NET Core las atiende en paralelo). En una aplicación real, los datos van a una base de datos (mira el ejemplo de SQLite) y los servicios se registran con builder.Services y se reciben como parámetros de las funciones.Resumen
| Para | Escribe |
|---|---|
| Una ruta GET | app.MapGet("/ruta", () => ...) |
| Un parámetro en la ruta | "/tareas/{id:int}" y (int id) => ... |
| Leer el cuerpo JSON | un parámetro de tu tipo: (NuevaTarea nueva) |
| Elegir el código | Results.Ok, NotFound, BadRequest, Created |
| Arrancar | app.Run(); en pruebas, StartAsync |
Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.