De top-level statements a ejecución directa
Antes (hasta .NET 9):
Podías usar top-level statements, pero seguías necesitando un .csproj. La fricción estaba en la estructura, no en la sintaxis.
Ahora (.NET 10):
#:package CsvHelper@33.0.1#:package RestSharp@112.1.0
using CsvHelper;using RestSharp;using System.Globalization;
var ordenes = new CsvReader( new StreamReader("ordenes.csv"), CultureInfo.InvariantCulture).GetRecords<Orden>().ToList();
var client = new RestClient("https://api.empresa.com");
foreach (var orden in ordenes){ await client.PostJsonAsync("/ordenes", orden);}
Console.WriteLine($"{ordenes.Count} órdenes procesadas");
record Orden(string Id, decimal Total, DateTime Fecha);Ejecutas:
dotnet run procesar-ordenes.csPrimera ejecución: 2.8s (compila + cachea). Segunda: 85ms. Sin proyecto. Sin boilerplate.
Esto sí compite con Python en velocidad de desarrollo.
Por qué el caché cambia todo
La mayoría piensa que File-Based Apps es “sintaxis conveniente”. El diferenciador es el caché inteligente del CLI que persiste compilación entre ejecuciones.
Qué cachea:
- Compilación del código
- Resolución de dependencias
- Proyecto virtual generado
Cuándo se invalida:
- Cambias el código
- Modificas versiones de paquetes
- Actualizas el SDK
Escenario real - ETL de datos:
#:package Dapper@2.1.35#:package Npgsql@8.0.5
using Npgsql;using Dapper;
var connOrigen = new NpgsqlConnection("Server=legacy;Database=ventas_old");var connDestino = new NpgsqlConnection("Server=prod;Database=analytics");
var ventas = await connOrigen.QueryAsync<Venta>(@" SELECT id, monto, fecha, cliente_id FROM ventas WHERE fecha >= @fecha", new { fecha = DateTime.Today.AddMonths(-1) });
await connDestino.ExecuteAsync(@" INSERT INTO ventas_procesadas (venta_id, total, periodo, cliente) VALUES (@Id, @Monto, @Fecha, @ClienteId)", ventas);
Console.WriteLine($"Migradas {ventas.Count()} ventas");
record Venta(int Id, decimal Monto, DateTime Fecha, int ClienteId);Desarrollo iterativo:
- Primera run: 3.2s
- Ajustas la query, reexecutas: 90ms
- Cambias el mapping: 90ms
- Pruebas con otra fecha: 90ms
Sin proyecto. Sin rebuild manual. Caché hace el trabajo.
Scripts de automatización y procesamiento one-off
Limpiar logs antiguos:
var logsDir = new DirectoryInfo("/var/logs/app");var archivosViejos = logsDir.GetFiles() .Where(f => f.CreationTime < DateTime.Now.AddDays(-30));
foreach (var archivo in archivosViejos){ archivo.Delete(); Console.WriteLine($"Eliminado: {archivo.Name}");}Ejecutas localmente, funciona, commiteas el .cs al repo. Otros devs lo ejecutan directo.
Prototipos de APIs:
#:package RestSharp@112.1.0
var client = new RestClient("https://api.staging.com");var response = await client.GetAsync<Producto>("/productos/123");
Console.WriteLine(response?.Nombre ?? "No encontrado");
record Producto(string Nombre, decimal Precio);Migración manual de datos, limpieza de BD, análisis de logs. Lo ejecutas una vez o pocas veces. El caché elimina fricción en iteraciones.
Cuándo convertir a proyecto tradicional
Señales claras:
- Más de 200 líneas: La estructura de proyecto aporta más que resta
- Múltiples archivos: File-Based Apps es mono-archivo (hasta .NET 11)
- Ejecución programada en producción: CI/CD y contenedores no mantienen caché
- Performance crítica: Necesitas AOT, trimming, o optimizaciones de publicación
Conversión automática:
dotnet project convert mi-script.csGenera .csproj, preserva #:package como <PackageReference>. Ahora tienes estructura completa sin reescribir código.
Type-safety sin fricción de setup
File-Based Apps no reemplazan proyectos. Cierran la brecha con lenguajes de scripting sin sacrificar el sistema de tipos de C#.
Antes: “Para este script rápido uso Python aunque mi stack sea .NET”. Ahora: “Uso C# con type-safety, async/await, LINQ, y todo el ecosistema sin fricción de setup”.
El caché hace que iterar sea instantáneo. Las directivas #:package eliminan gestión manual de dependencias. Un archivo .cs es portable y ejecutable.
Usa File-Based Apps para exploración y automatización. Migra a proyecto cuando la complejidad o el deployment lo justifiquen. La barrera de entrada bajó. La decisión de cuándo escalar sigue siendo tuya.