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:
1 |
Console.WriteLine("Hello, World!"); |
Nice and concise isn’t it? Here is what running this program looks like:
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.
1 2 3 4 5 6 7 8 |
using System; namespace ConsoleApp10 { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } } |
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 methodMain()
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 namespaceBar
, the full name of the class isBar.Foo
. You can either refer to this class with its full nameBar.Foo
, or use the shorter nameFoo
as long as a clauseusing Bar;
is declared prior to the usage ofFoo
in the source file so the compiler can guess that the code refers to theBar.Foo
class. - C# 9 and C# 10 introduces some new features to automatically generate and take care of the class
Program
, the methodMain()
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 fileProgram.cs
. For example here is a small program that prints the 20 first Fibonacci numbers:
1 2 3 4 5 6 7 8 9 10 11 |
// Print on console the 20x first Fibonacci numbers int length = 20; int a = 0, b = 1, c = 0; Console.Write("{0} {1}", a, b); for (int i = 2; i < length; i++) { c = a + b; Console.Write(" {0}", c); a = b; b = c; } |
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// Using clause can be declared before the first top-level statement using MyNamespace; Console.WriteLine("Hello, World!"); // More than one top-level statements can be provided Console.WriteLine(SquareInstance(5)); Console.WriteLine(SquareStatic(6)); // Per convention the main method is defined with an args parameter: <Main>$(string[] args); // Thus you can use the parameter args to get console parameters. if (args.Length > 0) { Console.WriteLine(args[0]); } // Program is not a static class thus it can be instantiated. // However no method nor field can be declared in this generated class! var program = new Program(); // When declaring some methods after top-level statements, // they are declared as local function of <Main>$ int capturedVariable = SquareStatic(2); Console.WriteLine(SquareInstance(5)); int SquareInstance(int i) => capturedVariable * i * i; static int SquareStatic(int i) => i * i; // Top-level statements can be provided after local functions Console.WriteLine(SquareInstance(7)); // squareOf8 is not a field of the class Program but a variable of the method <Main>$() int squareOf8 = SquareStatic(8); // The using MyNamespace; clause is used here to resolve MyClass. Program c = new MyClass(); // Namespaces and types can be declared after the last top-level statement namespace MyNamespace { // Program is not sealed and can be used as a base class. Pretty useless isn'it? class MyClass : Program { } } |
Here is the decompiled assembly if you are curious (like me):
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:
1 2 3 4 5 6 7 8 |
// <auto-generated/> global using global::System; global using global::System.Collections.Generic; global using global::System.IO; global using global::System.Linq; global using global::System.Net.Http; global using global::System.Threading; global using global::System.Threading.Tasks; |
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.
1 2 3 4 |
<ItemGroup> <Using Remove="System.Linq" /> <Using Include="System.Diagnostics" /> </ItemGroup> |
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 theglobal 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 declareglobal 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 likeusing NUnit.Framework;
only for projects which haveTest
in name. This can be achieved with a Directory.Build.props file whose content is:
1 2 3 4 5 |
<Project> <ItemGroup Condition="$(MSBuildProjectName.Contains('Test'))"> <Using Include="NUnit.Framework" /> </ItemGroup> </Project> |
- 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 haveTest
in name. The Directory.Build.props file content can then be:
1 2 3 4 5 |
<Project> <ItemGroup Condition="$(MSBuildProjectName.Contains('Test'))"> <Compile Include=".\Shared\GlobalUsings4Tests.cs" /> </ItemGroup> </Project> |
Finally let’s notice that in this using clause below…
1 |
global using global::System; |
…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).
Conclusion
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.
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.
Sure you can download NDepend full feature trial and let us know what you think https://www.ndepend.com/download
It’s starting to look more like C. Objects were invented to help deal with the complexities of large programs.
Articles like this are very good. It would be useful to know which version of Visual Studio is required to use them.