C# program structure
last modified October 16, 2026
In this article we explain the structure of a C# program: the Main
method and top-level statements.
Every C# application has an entry point, the place where the execution
starts. Traditionally, the entry point is a static method called
Main. Since C# 9, we can write top-level statements
instead; the compiler then generates the Main method for us. The
console templates of the .NET SDK use top-level statements.
C# Main method
The following is a classic C# program with an explicit Main
method.
namespace HelloApp;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello there!");
}
}
The program consists of a namespace, a class, and the Main method.
static void Main(string[] args)
The Main method must be static. It can be declared in
any class and the class does not have to be called Program. The
args parameter holds the command line arguments; it can be omitted
if we do not need it.
$ dotnet run Hello there!
The Main method can have one of the following signatures:
static void Main() static void Main(string[] args) static int Main() static int Main(string[] args) static async Task Main() static async Task Main(string[] args) static async Task<int> Main() static async Task<int> Main(string[] args)
The method can be public or private; the access
modifier does not matter for the entry point.
C# top-level statements
With top-level statements, we write the code of the entry point directly in a file, without the class and the method.
Console.WriteLine("Hello there!");
This one-line program is equivalent to the previous example. The compiler wraps
the statements in a generated class with a Main method.
$ dotnet run Hello there!
Top-level statements have the following rules:
- only one file in a project can contain top-level statements
- a project with top-level statements cannot have another
Mainmethod used as the entry point - the statements must precede type declarations and namespace declarations in the file
- methods declared in the file become local functions
C# types after top-level statements
Types such as classes, records, or enums can be declared in the same file after the top-level statements.
var p = new Person("John Doe", Occupation.Gardener);
Greet(p);
void Greet(Person person)
{
Console.WriteLine($"Hello, {person.Name} the {person.Occupation}");
}
record Person(string Name, Occupation Occupation);
enum Occupation { Gardener, Teacher, Driver }
The file contains statements, a local function, a record, and an enum.
void Greet(Person person)
{
Console.WriteLine($"Hello, {person.Name} the {person.Occupation}");
}
The Greet function is a local
function of the generated Main method. It can be called only
from the top-level statements.
$ dotnet run Hello, John Doe the Gardener
If we placed a statement after the type declarations, the compiler would report the CS8803 error: top-level statements must precede namespace and type declarations.
C# command line arguments
In top-level statements, the command line arguments are available through the
implicit args variable.
Console.WriteLine($"Number of arguments: {args.Length}");
foreach (var arg in args)
{
Console.WriteLine(arg);
}
We print the number of the arguments and the arguments themselves. Unlike in C, the array does not contain the name of the program.
$ dotnet run -- "John Doe" gardener 34 Number of arguments: 3 John Doe gardener 34
The arguments after -- are passed to the program; the arguments
before it are processed by the dotnet run command.
C# exit code
A program returns an exit code to the operating system. By convention, zero
means success and other values indicate an error. In top-level statements, we
return the exit code with the return statement.
if (args.Length == 0)
{
Console.Error.WriteLine("usage: app <file>");
return 2;
}
Console.WriteLine($"processing {args[0]}");
return 0;
If no argument is provided, the program prints a usage message to the standard
error output and returns 2. When top-level statements contain a
return statement with a value, the generated Main
method returns int.
$ dotnet run usage: app <file> $ echo $? 2
In the shell, we check the exit code of the last command with the
$? variable. Alternatively, the exit code can be set with the
Environment.Exit method, which terminates the process immediately.
C# async entry point
Top-level statements can use the await operator. In such a case,
the compiler generates an asynchronous Main method.
Console.WriteLine("Starting tasks");
var t1 = Compute("task 1", 300);
var t2 = Compute("task 2", 100);
int[] res = await Task.WhenAll(t1, t2);
Console.WriteLine($"Sum: {res.Sum()}");
async Task<int> Compute(string name, int delay)
{
await Task.Delay(delay);
Console.WriteLine($"{name} finished");
return delay;
}
We run two asynchronous operations concurrently and wait for both of them.
$ dotnet run Starting tasks task 2 finished task 1 finished Sum: 400
The shorter task finishes first, even though it was started second.
C# program in multiple files
Larger programs consist of multiple files. Only one of them contains the top-level statements; the other files contain types.
namespace HelloApp.Text;
public static class Utils
{
public static string Reverse(string s)
{
var chars = s.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
}
The Utils.cs file declares a static class in the
HelloApp.Text namespace. It uses a file-scoped namespace
declaration, which applies to the whole file and saves one level of
indentation.
using HelloApp.Text;
Console.WriteLine(Utils.Reverse("falcon"));
In the Program.cs file, we import the namespace and use the
class.
$ dotnet run noclaf
C# implicit and global usings
We have not imported the System namespace in our programs, yet we
could use the Console class. This is because the project files
generated by the .NET SDK enable implicit usings.
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
With the ImplicitUsings property, the compiler automatically imports
common namespaces, such as System, System.IO,
System.Linq, System.Net.Http, or
System.Threading.Tasks.
We can add our own namespaces with the global using directive. It
applies to all files in the project. It is common to put such directives in a
separate file.
global using System.Text; global using System.Globalization;
var sb = new StringBuilder();
sb.Append("Price: ");
sb.Append(1234.5m.ToString("C", CultureInfo.GetCultureInfo("sk-SK")));
Console.WriteLine(sb);
The StringBuilder and CultureInfo classes are
available without using directives in Program.cs.
$ dotnet run Price: 1 234,50 €
Source
General structure of a C# program
In this article we have explained the structure of a C# program.
Author
List all C# tutorials.