← maurobernal.com.ar

dotnet run script.cs y las nuevas herramientas de .NET 10: OpenAPI, HttpClient y más

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 →

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.