El flujo habitual al crear una aplicacion de consola con el CLI de .NET produce un proyecto mucho mas compacto que el clasico esqueleto con class Program y Main. Las plantillas actuales apuestan por un archivo Program.cs minimalista donde la logica principal se escribe directamente, sin ceremonia intermedia. Este enfoque es el que se encuentra en cualquier tutorial actualizado y conviene conocerlo desde el primer dia.
Top-level statements en Program.cs
Las top-level statements, o sentencias de nivel superior, permiten escribir el codigo de entrada de la aplicacion sin declarar explicitamente una clase ni un metodo Main. El compilador se encarga de generar toda esa estructura por debajo. El resultado es un archivo Program.cs legible de un vistazo.
Al ejecutar el comando de creacion en una terminal, se genera un proyecto con un archivo muy breve.
dotnet new console -n MiPrograma
cd MiPrograma
dotnet run
El contenido del archivo Program.cs generado tiene este aspecto.
Console.WriteLine("Hola, .NET moderno.");
Una sola linea basta para compilar y ejecutar. Se pueden anadir mas sentencias, declarar variables locales e incluso definir funciones locales sin tocar la estructura clasica.
var nombre = "Sofia";
var saludo = ConstruirSaludo(nombre);
Console.WriteLine(saludo);
static string ConstruirSaludo(string persona) => $"Hola, {persona}.";
Las funciones locales declaradas con static no capturan estado y pueden convivir con el resto de sentencias. Esto resulta util para scripts, herramientas internas y casos donde la ceremonia clasica estorba.
Las top-level statements estan limitadas a un unico archivo por proyecto. Si la aplicacion crece, se mueve la logica a clases y se deja
Program.cscomo punto de arranque compacto.
Argumentos y retorno desde top-level
Aunque no se escriba Main de forma explicita, el compilador mantiene las capacidades tradicionales. Existe una variable implicita args de tipo string[] con los argumentos de la linea de comandos.
if (args.Length == 0)
{
Console.Error.WriteLine("Falta un argumento obligatorio.");
return 1;
}
Console.WriteLine($"Procesando archivo {args[0]}");
return 0;
Devolver un valor entero desde las top-level statements equivale al codigo de salida del proceso, igual que con Main tradicional. Esto encaja con herramientas de linea de comandos y pipelines de CI donde un codigo distinto de cero se interpreta como fallo.
flowchart LR
A[dotnet run] --> B[Compilador genera Main implicito]
B --> C[Ejecucion top-level]
C --> D{return int?}
D -->|si| E[ExitCode entero]
D -->|no| F[ExitCode 0]
Implicit usings en el archivo .csproj
Los proyectos modernos activan los implicit usings de forma predeterminada. La propiedad se encuentra en el archivo de proyecto y se llama ImplicitUsings. Cuando esta habilitada, el SDK inyecta automaticamente los namespaces mas habituales del tipo de proyecto.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Entre los namespaces que se inyectan en una aplicacion de consola estan System, System.IO, System.Linq, System.Collections.Generic y System.Threading.Tasks. Por eso es posible escribir Console.WriteLine o List<int> sin ningun using visible en el archivo.
El listado exacto depende del SDK y del tipo de proyecto. En una Web API se anaden otros namespaces como
Microsoft.AspNetCore.BuilderoMicrosoft.Extensions.Hosting.
Global usings para compartir imports
Las directivas global using permiten declarar un using una sola vez para todo el proyecto. Se colocan normalmente en un archivo llamado GlobalUsings.cs situado en la raiz del proyecto.
global using System.Text.Json;
global using System.Globalization;
global using static System.Math;
A partir de ese momento, cualquier archivo .cs del proyecto puede usar JsonSerializer, CultureInfo o la funcion Sqrt sin repetir imports. La variante global using static trae tambien los miembros estaticos del tipo indicado.
var radio = 3.0;
var area = PI * Pow(radio, 2);
Console.WriteLine($"Area: {area}");
Esta tecnica es util para centralizar los namespaces habituales del dominio de la aplicacion, como logging, validacion o acceso a datos, evitando repetir las mismas diez lineas al principio de cada archivo.
File-scoped namespaces
Un namespace con ambito de archivo se declara terminado en punto y coma y abarca el resto del contenido del archivo sin necesidad de llaves adicionales. La indentacion baja un nivel entero y la lectura gana en claridad.
namespace Facturacion.Servicios;
public class CalculadoraImpuestos
{
private const decimal Iva = 0.21m;
public decimal Aplicar(decimal baseImponible)
{
return baseImponible * (1 + Iva);
}
}
La forma clasica con llaves sigue siendo valida, pero la nueva convencion en proyectos nuevos es declararlo siempre con file-scoped namespace. Un unico archivo no puede contener dos namespaces distintos bajo este formato, lo cual tambien es una buena practica organizativa.
Mantener un namespace por archivo ayuda a que la estructura de carpetas del proyecto refleje la estructura logica del codigo.
Estructura completa de un proyecto moderno
Uniendo todas las piezas, un proyecto C# actual tiene esta apariencia general. El archivo de proyecto habilita los features del SDK, un archivo de usings globales concentra los imports comunes y el resto de archivos declaran namespaces con punto y coma.
El archivo MiApi.csproj activa las opciones del SDK.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
El archivo GlobalUsings.cs concentra los imports comunes del dominio.
global using Facturacion.Servicios;
global using Facturacion.Modelos;
El archivo Program.cs arranca la aplicacion con top-level statements.
var calculadora = new CalculadoraImpuestos();
var totales = args.Select(a => decimal.Parse(a)).Select(calculadora.Aplicar);
foreach (var total in totales)
{
Console.WriteLine($"Total con IVA: {total:C}");
}
El archivo Modelos/Factura.cs declara su namespace con ambito de archivo.
namespace Facturacion.Modelos;
public class Factura
{
public string Numero { get; init; } = string.Empty;
public decimal BaseImponible { get; init; }
}
Con esta combinacion, cada archivo se concentra en su responsabilidad, sin repetir using ni llaves innecesarias. El codigo queda mas denso en intencion y mas ligero en ceremonia.
Reestructurar proyectos antiguos
Un proyecto heredado suele tener Program.cs con la forma clasica, namespaces anidados y una lista larga de usings en cada archivo. Migrar al estilo moderno se hace de forma progresiva sin romper nada.
El primer paso es activar ImplicitUsings y Nullable en el .csproj. El compilador indicara que usings ya no son necesarios y los atenuara en el editor.
El segundo paso es extraer los usings repetidos a un GlobalUsings.cs. Es comun encontrar using Microsoft.Extensions.Logging y using System.Text.Json en decenas de archivos. Una sola linea global elimina todas esas repeticiones.
El tercer paso es convertir los namespaces con llaves en namespaces de archivo. La accion esta disponible en el IDE como refactor automatico y no cambia la compilacion, solo la sintaxis.
Realizar la migracion modulo a modulo y con commits pequenos facilita la revision y permite revertir cambios puntuales si algo se comporta de forma inesperada.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, C# es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de C#
Explora más contenido relacionado con C# y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Escribir proyectos C# modernos usando top-level statements, global usings, implicit usings y file-scoped namespaces para eliminar ceremonia innecesaria.