NDepend Blog

Improve your .NET code quality with NDepend

Modern C# Hello World

November 23, 2021 6 minutes read

Modern CSharp Hello World

With Visual Studio 2022 when you create a new console project based on .NET 6, the Hello World source code generated is now as simple as that:

Nice and concise isn’t it? Here is what running this program looks like:

C#10 .NET 6 Hello World

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

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:

  • A class is required because C# is an Object-Oriented language: No code can be specified outside of a class.
  • A method Main() is required to start a C# program. This is why the method Main() is qualified as an entry-point method.
  • Namespace are used to group the various classes of a program into logical units. The word logical is important because a namespace doesn’t refer to anything physical like a file, a directory or even a compiled element. When a class Foo is declared in a namespace Bar, the full name of the class is Bar.Foo. You can either refer to this class with its full name Bar.Foo, or use the shorter name Foo as long as a clause using Bar; is declared prior to the usage of Foo in the source file so the compiler can guess that the code refers to the Bar.Foo class.
  • C# 9 and C# 10 introduces some new features to automatically generate and take care of the class Program, the method Main() and the namespaces stuff. 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 20 first Fibonacci numbers:

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

C# Hello World for Experienced C# Programmers

If you are already programming with C# for a while, let’s first decompile the assembly generated with IL Spy and realize that the compiled code is almost identical to the Visual Studio 2019 Hello World program shown in introduction.

C#10 .NET 6 Hello World Decompiled

Here are the new C# 9 and C# 10 features that makes possible one of the shortest Hello World source code 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 top-level statements feature is quite flexible. Here is what you can do with it:

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

C#9 Top Level Statements Decompiled

However there are 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 generate an executable assembly. Obviously a library assembly doesn’t have an entry-point.
  • The <Main>$() method cannot be called by user code since its identifier is not a valid C# identifier. However <Main>$ is a valid CLR identifier, hence the runtime can invoke it to start the program.

C# 10 Implicit using directives.

In the minimal Console.WriteLine("Hello, World!"); source code, the class Console is declared in the namespace System. Thus, until now we had to declare a using System; clause for the C# compiler to resolve the class Console in the namespace System. However C# 10 implicit using directives makes this using clause useless.

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

When the element <ImplicitUsings> has the value enable, a file named YourProjectName.GlobalUsings.g.cs is added in .\obj\Debug\net6.0. This source file is auto-generated at compile-time and must not be edited, else changes are erased at next compilation. For the above Hello World console application the content of this generated file is:

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

This file let’s you know which namepaces are imported by the compiler. The set of namespaces imported can be changed with some tags <Using> in the .csproj project file. The source file YourProjectName.GlobalUsings.g.cs is then updated to reflect the modified set of namespaces imported.

Notice that the set of namespaces implicitly imported depends on the kind of project. Those listed above are for a console project. But for an ASP.NET Core application some namespaces like Microsoft.AspNetCore.Http  or Microsoft.AspNetCore.Builder are implicitly imported in addition to the ones imported for a console project. When in doubt, just create a blank project of your choice and double check 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 that the specified namespace is imported for all C# source files of the current project. There are two way 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 no <ImplicitUsings> element is specified.

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

  • A) You can just 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 have an AssemblyInfo.cs file shared among all projects that can also be used to 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 which have Test in name. This can be achieved with a Directory.Build.props file whose content is:

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

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

global:: has noting to do with the global using feature. It’s here to avoid some collisions when several types or namespaces with a same name are declared in various scopes (full explanation here).


This article explained some new C# features that are here to reduce the size of your C# sources. 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, the feature implicit and global using directives are flexible enough to discard thousands of using clauses in any real-world application.


  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.

Comments are closed.