ZetCode

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.

Program.cs
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.

Program.cs
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:

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Utils.cs
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.

Program.cs
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.

GlobalUsings.cs
global using System.Text;
global using System.Globalization;
Program.cs
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

Top-level statements

In this article we have explained the structure of a C# program.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.