Uno de los cambios de .NET 10 que más me llamó la atención no es del compilador ni del runtime: es de las herramientas. dotnet run script.cs permite ejecutar un archivo C# sin crear un proyecto. Y el soporte OpenAPI nativo en Minimal APIs me hizo desinstalar Swashbuckle de varias soluciones. Repaso todo el tooling nuevo.
dotnet run script.cs: C# sin proyecto
Antes de .NET 10, ejecutar un archivo C# requería crear un proyecto, un .csproj, restaurar paquetes… Para un script rápido o una prueba de concepto, era excesivo. Ahora podés ejecutar un archivo directamente.
// script.cs — archivo suelto, sin .csproj
using System.Net.Http.Json;
var http = new HttpClient();
var usuarios = await http.GetFromJsonAsync<List<Usuario>>(
"https://jsonplaceholder.typicode.com/users"
);
foreach (var u in usuarios ?? [])
Console.WriteLine($"{u.Id}: {u.Name} ({u.Email})");
record Usuario(int Id, string Name, string Email);
# Ejecutar directamente
dotnet run script.cs
# Con referencias a paquetes NuGet (directivas en el archivo)
# #:package Newtonsoft.Json@13.0.3
# Con argumentos
dotnet run script.cs -- arg1 arg2
# Ver el IL generado sin ejecutar
dotnet run --no-build script.cs
Uso esto constantemente para probar APIs externas, procesar archivos CSV, o generar datos de prueba. Antes abría LINQPad; ahora uso la terminal directamente.
OpenAPI built-in en Minimal APIs (.NET 9+)
Desde .NET 9, la documentación OpenAPI está integrada en el framework. No hace falta Swashbuckle ni NSwag para tener un endpoint /openapi/v1.json funcional.
// Program.cs — OpenAPI nativo en .NET 9/10
var builder = WebApplication.CreateBuilder(args);
// Solo agregar esto:
builder.Services.AddOpenApi();
var app = builder.Build();
// Endpoint para servir el documento OpenAPI
app.MapOpenApi(); // /openapi/v1.json por defecto
// Opcional: UI de Swagger (sigue siendo un paquete externo)
// app.UseSwaggerUI(options => options.SwaggerEndpoint("/openapi/v1.json", "Mi API"));
// Minimal API con metadata para OpenAPI
app.MapGet("/api/pedidos/{id}", async (int id, IRepositorio repo) =>
{
var pedido = await repo.ObtenerAsync(id);
return pedido is null ? Results.NotFound() : Results.Ok(pedido);
})
.WithName("ObtenerPedido")
.WithSummary("Obtiene un pedido por ID")
.WithDescription("Retorna el pedido completo con sus líneas de detalle")
.WithTags("Pedidos")
.Produces<Pedido>(200)
.Produces(404);
app.Run();
HttpClient: mejoras en .NET 10
// .NET 10: HttpClient con Resilience nativo (Microsoft.Extensions.Http.Resilience)
builder.Services.AddHttpClient<IMiApiClient, MiApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.externa.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(); // retry + circuit breaker + timeout automáticos
// Configuración personalizada del pipeline de resiliencia
builder.Services.AddHttpClient<IMiApiClient, MiApiClient>()
.AddResilienceHandler("mi-pipeline", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential
});
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 5
});
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
Mejoras en Minimal APIs
// .NET 9/10: TypedResults para respuestas con tipos bien definidos
app.MapPost("/api/usuarios", async (CrearUsuarioRequest req, IServicio svc) =>
{
if (string.IsNullOrEmpty(req.Email))
return TypedResults.ValidationProblem(new Dictionary<string, string[]>
{
["email"] = ["El email es requerido"]
});
var usuario = await svc.CrearAsync(req);
return TypedResults.Created($"/api/usuarios/{usuario.Id}", usuario);
});
// Route groups para organizar endpoints
var pedidosGroup = app.MapGroup("/api/pedidos")
.WithTags("Pedidos")
.RequireAuthorization();
pedidosGroup.MapGet("/", ObtenerTodos);
pedidosGroup.MapGet("/{id}", ObtenerPorId);
pedidosGroup.MapPost("/", Crear);
pedidosGroup.MapPut("/{id}", Actualizar);
pedidosGroup.MapDelete("/{id}", Eliminar);
Blazor en .NET 10: render modes y streaming
// .NET 10: componentes Blazor con render modes explícitos
@page "/dashboard"
@rendermode InteractiveServer
<h1>Dashboard</h1>
@if (datos is null)
{
<p>Cargando...</p>
}
else
{
@foreach (var item in datos)
{
<div>@item.Nombre: @item.Valor</div>
}
}
@code {
private List<Dato>? datos;
protected override async Task OnInitializedAsync()
{
// Streaming: el componente renderiza mientras carga
await foreach (var dato in servicio.GetStreamAsync())
{
datos ??= [];
datos.Add(dato);
StateHasChanged(); // actualiza el DOM incremental
}
}
}
← Rendimiento extremo en .NET 10: Stack allocation, Native AOT y el GC que trabaja menos | Serie .NET 8 → .NET 10 | Próximo: Guía práctica: cómo migré mis proyectos de .NET 8 a .NET 10 sin romper producción →