NDepend Blog

Improve your .NET code quality with NDepend

Modern C# Hello World

May 21, 2026 9 minutes read

Modern CSharp Hello World

The modern C# Hello World is a single line of code:

No class, no Main() method and no using directive to write. Three features do all the work behind the scene: top-level statements (C# 9), implicit using directives and global using directives (C# 10). This article explains each of them, first for beginners and then in depth for experienced C# developers.

With Visual Studio 2026 and any recent .NET SDK (.NET 9 or 10), creating a new console project produces exactly this one-line source. Nice and concise isn’t it? Here is what running this program looks like:

C#10 .NET 6 Hello World

In Visual Studio 2019, creating a new console project used to produce a much more verbose Hello World source, with the definition of a namespace, a class and a Main() method.

Both versions represent the same program and compile to equivalent code. The rest of this article walks through the features that shrink the verbose version down to one line.

C# Hello World for Beginners

(If you are already experienced with C# just skip this section.)

If you are a beginner you might want to know that:

  • C# requires a class because it is an Object-Oriented language: you cannot write any code outside of a class.
  • Every C# program needs a method named Main() to start. This is why we call Main() the entry-point method.
  • A namespace groups the various classes of a program into logical units. The word logical matters because a namespace doesn’t refer to anything physical like a file, a directory or even a compiled element. When you declare a class Foo in a namespace Bar, the full name of the class becomes Bar.Foo. You can refer to this class either with its full name Bar.Foo, or use the shorter name Foo as long as you declare a using Bar; clause before its usage so the compiler can resolve Bar.Foo.
  • C# 9 and C# 10 introduce new features that automatically generate the class Program, the method Main() and the using clauses for you. As a result you don’t need to know more about these concepts before writing your code directly in the source file Program.cs. For example, here is a small program that prints the first 20 Fibonacci numbers:

Now let’s dig into what’s happening behind the scene.

C# Hello World for Experienced C# Programmers

If you have been programming in C# for a while, let’s first decompile the assembly generated with ILSpy and notice that the compiled code is almost identical to the Visual Studio 2019 Hello World program from the introduction.

C#10 .NET 6 Hello World Decompiled

Three C# features combined make this one of the shortest Hello World source codes in the industry:

  • C# 9 top-level statements
  • C# 10 implicit using directives
  • C# 10 global using directives

C# 9 Top-level Statements

With top-level statements, C# 9 takes care of generating a class named Program with a method named <Main>$() that is de-facto the entry point of the executable assembly.

The exact signature of this generated entry point depends on whether your top-level code uses await and/or a return statement:

Top-level code contains Generated entry-point signature
Neither await nor return static void Main(string[] args)
return only static int Main(string[] args)
await only static async Task Main(string[] args)
await and return static async Task<int> Main(string[] args)

You might wonder why you’d want an await main method? The async Task Main entry point (available since C# 7.1) exists because you can’t use await in a non-async method. Without it, you’d have to block on async calls, which causes real problems.

The top-level statements feature is quite flexible. Here is what you can do with it:

Top-level code can also resume after local functions, and any namespace or type must come last:

Here is the decompiled assembly if you are curious (like me):

C#9 Top Level Statements Decompiled

There are however a few limitations with C# 9 top-level statements:

  • Only one source file of a C# project can contain top-level statements. This makes sense since there cannot be more than one entry-point method for an executable assembly.
  • The C# project must produce an executable assembly. A library assembly obviously doesn’t have an entry point.
  • The <Main>$() method cannot be called from user code because its identifier is not a valid C# identifier. However <Main>$ is a valid CLR identifier, so the runtime can still invoke it to start the program.

C# 10 Implicit Using Directives

In the minimal Console.WriteLine("Hello, World!"); source code, the namespace System declares the class Console. Until C# 10, we had to write a using System; clause for the compiler to resolve Console. C# 10 implicit using directives make this using clause useless.

If we look at the C# .csproj project file generated, we see the XML element <ImplicitUsings>enable</ImplicitUsings>. The value disable is also possible, which is equivalent to removing this XML element.

When <ImplicitUsings> has the value enable, the compiler adds a file named YourProjectName.GlobalUsings.g.cs under .\obj\Debug\netX.0 (where netX.0 matches the targeted .NET version). The compiler regenerates this file on every build, so never edit it: the next build wipes any change. For the Hello World console application above, the content of this generated file is:

We’ll detail the usage of the keyword global in the next section.

This file lets you know which namespaces the compiler imports. <Using> tags in the .csproj project file change this set, and the compiler then rewrites YourProjectName.GlobalUsings.g.cs to match.

Notice that the project kind drives which namespaces the compiler imports implicitly. The list above targets a console project. An ASP.NET Core application also pulls in namespaces like Microsoft.AspNetCore.Http or Microsoft.AspNetCore.Builder on top of the console ones. When in doubt, just create a blank project of the desired kind and look at the generated GlobalUsings.g.cs file.

C# 10 Global Using Directives

In the YourProjectName.GlobalUsings.g.cs content we saw the new C# 10 syntax with the keyword global using. It means the compiler imports the specified namespace into all C# source files of the current project. There are two ways to use this feature:

  • Either declare global using YourNamespace once in a source file of the project.
  • Or specify <Using Include="YourNamespace"> in the .csproj project file. This way works even when you specify no <ImplicitUsings> element.

The global using directive only works at the project level. To make it work at the solution level, you can adopt several strategies A) B) C) D):

  • A) You can define a source file named GlobalImports.cs with the global using clauses, and reference this source file from all C# projects of the solution.
  • B) You might already share an AssemblyInfo.cs file among all projects, which can also declare global using clauses.
  • C) Another option is to declare the <Using Include="YourNamespace"> XML element in a shared Directory.Build.props file located at the root of your repository. This opens a new range of flexibility. For example, you might want a global using clause like using NUnit.Framework; only for projects whose name contains Test. The following Directory.Build.props file achieves this:

  • D) You might prefer to put all your global using clauses for tests in a file .\Shared\GlobalUsings4Tests.cs and include this file for all projects whose name contains Test. The Directory.Build.props file content can then be:

Finally, let’s notice that in this using clause below…

global:: has nothing to do with the global using feature. It’s there to avoid collisions when various scopes declare several types or namespaces with the same name (full explanation here).

Bringing Back the Classic Program and Main()

Prefer the explicit class with a Main() method? You don’t have to give it up. From the command line, pass the --use-program-main option:

In Visual Studio, tick the Do not use top-level statements check box on the Additional information page while creating the project. Both styles produce the same IL, so the choice is purely a matter of taste and team convention.

.NET 10: dotnet run app.cs

The .NET team aims to make the learning curve smoother for newcomers. Since .NET 10, much like Node.js or Python, .NET now supports a single-file experience. You need no .csproj project file, only a single .cs file. A single command line then compiles and runs this app.cs file: dotnet run app.cs

You can find more about this option in this article: .NET 10.0 dotnet run app.cs or file-based program.

Frequently Asked Questions

Do I still need a Main() method in modern C#?
No. With top-level statements the compiler generates the Program class and the entry-point method for you. The Main() method still exists in the compiled assembly, you just don’t write it.

Can a project have more than one file with top-level statements?
No. An executable can only have one entry point, so only one source file may contain top-level statements. Every other file is a regular C# file.

How do I read command-line arguments without a Main() method?
The args string array is available directly in your top-level code. It is never null; its Length is zero when no argument is passed.

How do I use async/await or return an exit code?
Just write await or return in your top-level statements. The compiler picks the matching entry-point signature, up to static async Task<int> Main(string[] args).

Where is the GlobalUsings.g.cs file?
The compiler generates it under .\obj\Debug\netX.0 at compile time, so never edit it by hand.

How do I disable implicit usings?
Set <ImplicitUsings>disable</ImplicitUsings> in the .csproj file to control every namespace yourself.

Conclusion

This article walked through the C# features that bring the size of a Hello World program down to a single line: top-level statements (C# 9), implicit using directives and global using directives (C# 10), and finally the .NET 10 file-based program model. A beginner doesn’t even need to know what a class or a method is to start writing a small working program. On the other hand, implicit and global using directives are flexible enough to discard thousands of using clauses in any real-world application.

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!

Comments:

  1. Very nice. A good simple description. Based on the quality of this article, I poked around your site. I need to spend some more time around here. … The product sounds interesting. It might even get me to update some of my habits.

  2. Jim Lonero says:

    It’s starting to look more like C. Objects were invented to help deal with the complexities of large programs.

  3. Roland Wales says:

    Articles like this are very good. It would be useful to know which version of Visual Studio is required to use them.

Leave a Reply

Your email address will not be published. Required fields are marked *