En esta página

Introducción a ASP.NET Core 10 y Minimal APIs

14 min lectura TextoCap. 4 — ASP.NET Core

¿Qué es ASP.NET Core?

ASP.NET Core es el framework web de .NET para construir APIs, aplicaciones web, WebSockets y microservicios. Es de código abierto, multiplataforma y uno de los frameworks web más rápidos del mundo según los benchmarks de TechEmpower.

ASP.NET Core 10 trae:

  • Minimal APIs maduros y optimizados
  • Native AOT mejorado para binarios ultra-pequeños
  • OpenAPI 3.1 con generación automática de documentación
  • Rate limiting nativo
  • WebSockets mejorados

Crear un proyecto web

# Crear una Minimal API
dotnet new webapi -n CatalogoApi -f net10.0 --use-minimal-apis

# Crear una API con controladores
dotnet new webapi -n CatalogoApi -f net10.0

# Ejecutar en modo desarrollo
dotnet run --launch-profile https

# Ejecutar con hot reload
dotnet watch run

El proyecto generado tiene esta estructura:

CatalogoApi/
├── Program.cs              ← Punto de entrada + configuración
├── CatalogoApi.csproj      ← Archivo de proyecto
├── appsettings.json        ← Configuración de la app
├── appsettings.Development.json ← Configuración de desarrollo
└── Properties/
    └── launchSettings.json ← Perfiles de ejecución local

El builder pattern de ASP.NET Core

ASP.NET Core usa el builder pattern para configurar la aplicación en dos fases:

Fase 1: builder.Services.*  → Registrar servicios (DI container)
           ↓
        builder.Build()     → Crear la aplicación
           ↓
Fase 2: app.Use*            → Configurar middleware
        app.Map*            → Definir endpoints
           ↓
        app.Run()           → Iniciar el servidor
var builder = WebApplication.CreateBuilder(args);

// Fase 1: Servicios
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddScoped<IProductoRepo, ProductoRepo>();
builder.Services.AddTransient<IEmailService, SmtpEmailService>();

var app = builder.Build();

// Fase 2: Middleware y endpoints
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", () => "¡Bienvenido a la API!");
app.Run();

Lifetimes de servicios

Lifetime Se crea Cuando usar
Singleton Una vez por aplicación Caché, configuración, clientes HTTP
Scoped Una vez por solicitud HTTP DbContext, Unit of Work
Transient Cada vez que se inyecta Servicios ligeros, stateless
builder.Services.AddSingleton<IConfiguracion, Configuracion>();   // compartido
builder.Services.AddScoped<IUsuarioService, UsuarioService>();     // por request
builder.Services.AddTransient<IEmailSender, EmailSender>();        // nueva instancia

Minimal API endpoints

Las Minimal APIs definen endpoints con funciones lambda directamente:

// GET simple — devuelve texto plano
app.MapGet("/", () => "API en línea");

// GET con parámetro de ruta y query string
app.MapGet("/productos/{id:int}", (int id, bool? incluirDetalle) =>
{
    // id viene de la ruta, incluirDetalle del query string (?incluirDetalle=true)
    return Results.Ok(new { id, incluirDetalle });
});

// POST con body JSON
app.MapPost("/productos", (CrearProductoDto dto) =>
{
    // ASP.NET deserializa el JSON automáticamente
    Console.WriteLine($"Creando: {dto.Nombre}");
    return Results.Created("/productos/1", dto);
});

// Agrupar endpoints con RouteGroupBuilder
var grupo = app.MapGroup("/api/v1/productos");
grupo.MapGet("/",     ObtenerTodos);
grupo.MapGet("/{id}", ObtenerPorId);
grupo.MapPost("/",    Crear);

Inyección de dependencias en endpoints

// La DI funciona por parámetros en los endpoints
app.MapGet("/users", (IUserService userService, ILogger<Program> logger) =>
{
    logger.LogInformation("Consultando usuarios");
    return Results.Ok(userService.ObtenerTodos());
});

// También puedes inyectar desde el contenedor directamente
app.MapGet("/config", (IServiceProvider sp) =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return Results.Ok(new { version = config["App:Version"] });
});

Configuración (appsettings.json)

{
  "App": {
    "Nombre": "Catálogo API",
    "Version": "1.0.0",
    "MaxResultados": 50
  },
  "ConnectionStrings": {
    "Default": "Host=localhost;Database=catalogo;Username=app;Password=pass"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}
// Leer configuración fuertemente tipada
public class AppSettings
{
    public string Nombre { get; set; } = string.Empty;
    public string Version { get; set; } = string.Empty;
    public int MaxResultados { get; set; } = 20;
}

builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("App"));

// Inyectar en endpoints
app.MapGet("/info", (IOptions<AppSettings> opts) =>
    Results.Ok(new { opts.Value.Nombre, opts.Value.Version }));

Variables de entorno y entornos de ejecución

// Detectar el entorno actual
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseDeveloperExceptionPage();
}
else if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

// Variables de entorno sobreescriben appsettings.json
// ASPNETCORE_ENVIRONMENT=Production
// ConnectionStrings__Default=...

Práctica

  1. API completa: Crea una Minimal API con endpoints CRUD para una entidad Tarea(int Id, string Titulo, bool Completada) usando un servicio en memoria.
  2. Configuración: Agrega una sección "Api": { "MaxItems": 10 } en appsettings.json y úsala en el endpoint GET /tareas para limitar los resultados.
  3. Swagger: Asegúrate de que tu API tenga Swagger habilitado en desarrollo. Usa .WithName() y .WithSummary() para documentar los endpoints.

En la siguiente lección aprenderemos a organizar APIs más grandes usando controladores, atributos de routing y validación con DataAnnotations.

Minimal APIs para proyectos pequeños y microservicios
Minimal APIs son perfectas para microservicios, APIs de propósito único y proyectos que priorizan la simplicidad. Para APIs grandes con muchos endpoints, considera usar Controllers (ApiController) que ofrecen mejor organización con herencia y filtros.
Results helpers
ASP.NET Core 10 incluye el tipo estático Results con helpers para las respuestas más comunes: Results.Ok(), Results.Created(), Results.NotFound(), Results.BadRequest(), Results.NoContent(), Results.Unauthorized(), Results.Problem(). Usa siempre estos helpers en lugar de devolver datos directamente.
Registra los servicios en el orden correcto
Los servicios se registran ANTES de llamar a builder.Build(). El middleware se configura DESPUÉS del Build(). Si registras un servicio después del Build, obtendrás una excepción en tiempo de ejecución. La separación entre configuración de servicios y middleware es fundamental en ASP.NET Core.
// Program.cs — ASP.NET Core 10 Minimal API completa
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

// ── Registrar servicios (Dependency Injection) ───────
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IProductoService, ProductoService>();

var app = builder.Build();

// ── Configurar el pipeline de middleware ─────────────
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// ── Definir endpoints ────────────────────────────────
app.MapGet("/productos", (IProductoService svc) =>
    Results.Ok(svc.ObtenerTodos()))
    .WithName("GetProductos")
    .WithOpenApi();

app.MapGet("/productos/{id:int}", (int id, IProductoService svc) =>
{
    var producto = svc.ObtenerPorId(id);
    return producto is null
        ? Results.NotFound(new { mensaje = $"Producto {id} no encontrado" })
        : Results.Ok(producto);
});

app.MapPost("/productos", (CrearProductoDto dto, IProductoService svc) =>
{
    var producto = svc.Crear(dto);
    return Results.Created($"/productos/{producto.Id}", producto);
});

app.MapPut("/productos/{id:int}", (int id, ActualizarProductoDto dto, IProductoService svc) =>
{
    var actualizado = svc.Actualizar(id, dto);
    return actualizado is null ? Results.NotFound() : Results.Ok(actualizado);
});

app.MapDelete("/productos/{id:int}", (int id, IProductoService svc) =>
    svc.Eliminar(id) ? Results.NoContent() : Results.NotFound());

app.Run();