Installing C# and Your First Program: dotnet new console and the Project File
Installing the .NET 10 SDK, the first project with dotnet new console, top-level statements and the Main the compiler generates, TargetFramework, ImplicitUsings, Nullable and LangVersion in the csproj, dotnet run and build, choosing an IDE, and reading your first compiler error.
Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.
Guess First
When you create a new C# project, the Program.cs file contains exactly this one line:
Console.WriteLine("Hello, World!");
Nothing else. No class, no Main method, not even a using line telling the compiler where Console comes from. If you've read an older C# book, you know a program has to start with static void Main. Does this file compile? If it does, where is Main?
It compiles and prints Hello, World!. The compiler and the project settings fill in every piece that seems to be missing. In this post we install the .NET SDK, create our first project, find out where those invisible pieces come from, and get our first compiler error on purpose.
Installing the SDK
As we saw in the previous post, to write code you install the SDK, not just the runtime. For a new installation today, the right choice is the .NET 10 SDK: it's the LTS release and is supported until November 14, 2028. The official download page is dotnet.microsoft.com, with a ready-made installer for every operating system. If you prefer a package manager, winget install Microsoft.DotNet.SDK.10 works on Windows and brew install --cask dotnet-sdk with Homebrew on macOS. On Linux you can use your distribution's package manager or Microsoft's install script.
Once the installation finishes, open a new terminal and check that the SDK is in place:
$ dotnet --version
10.0.402
The last part of the version number may differ on your machine; what matters is that it starts with 10. If you get a "command not found" error, close the terminal and open it again: the installer may have updated PATH, but the terminal that was already open hasn't seen it yet.
The First Project: dotnet new console
The dotnet command is the single door through which the SDK does all its work. For a new console application:
$ dotnet new console -o Bookstore
The template "Console App" was created successfully.
Processing post-creation actions...
Restoring .../Bookstore/Bookstore.csproj:
Restore succeeded.
new console means "create a new project from the console template", and -o Bookstore puts the project into a new folder named Bookstore. The project name comes from the folder name too. Three things appear in the folder: Program.cs, Bookstore.csproj and an obj folder. The restore on the last line prepares the packages the project needs; the obj folder holds the intermediate files of that step and of the build.
Go into the folder and run the application:
$ cd Bookstore
$ dotnet run
Hello, World!
dotnet run builds the project and runs it right away. It's the command you'll use most during development.
Top-Level Statements: Where Is Main?
Back to the answer of Guess First. The lines in Program.cs are top-level statements: statements written at the top level of the file, not inside a class or method. This feature arrived in C# 9 and has been the default of the console template since .NET 6. When the compiler sees these lines, it generates a Program class with an entry method inside it for you. When I decompiled the built .dll back to C# with ILSpy, I saw this:
[CompilerGenerated]
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
ILSpy shows the method as Main to keep it readable; its real name in IL is <Main>$. That name contains characters you can't write in C# code, so it can never clash with any method you write. If you want to see the same template in its classic form, dotnet new console --use-program-main generates this:
namespace Classic;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
Both turn into the same program. In this series we use top-level statements to keep the examples short. Notice that the generated method has a string[] args parameter; in top-level code you reach that parameter directly as args:
string name = args.Length > 0 ? args[0] : "reader";
Console.WriteLine($"Hello, {name}! Welcome to the Bookstore.");
Console.WriteLine($"Today is {DateTime.Now.DayOfWeek}.");
$ dotnet run
Hello, reader! Welcome to the Bookstore.
Today is Saturday.
$ dotnet run -- Ada
Hello, Ada! Welcome to the Bookstore.
Today is Saturday.
Everything after -- goes to your program, not to dotnet run. We'll cover the ?: operator and strings written with $"..." in detail in upcoming posts; for now it's enough to read it as "if there's an argument take the first one, otherwise use reader".
Top-level statements come with two rules. Only one file in a project can contain top-level code, because a program has a single entry point. Second, you can write type definitions such as classes in the same file, but they must come after the top-level statements. We'll see both with real error messages in the pitfalls section.
The Project File: Bookstore.csproj
The project file the template created is this short:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
This is an MSBuild file; the .NET build system compiles the project according to these settings. The Sdk="Microsoft.NET.Sdk" line pulls in the hundreds of default settings a .NET project needs in one go. That's why you don't have to list the .cs files in the folder one by one; they're all included in the build automatically.
OutputType is Exe, meaning an executable application. If we were writing a library, this line wouldn't be there. TargetFramework says which .NET version the project targets. By writing net10.0 you decide both which BCL APIs you can use and the compiler's default language version.
With ImplicitUsings on, the SDK generates global using lines for the most common namespaces on your behalf. That's why you can use Console without writing using System;. The generated file is obj/Debug/net10.0/Bookstore.GlobalUsings.g.cs:
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
With Nullable on, the compiler warns you when you use a value that might be null without checking it. This topic has its own post; for now leave it on and take the warnings seriously.
Notice that there's no line in the file stating the language version. The compiler derives it from TargetFramework. To see it, you can write a single line in Program.cs: #error version. This directive stops the build on purpose and prints the compiler version:
.../Bookstore/Program.cs(1,8): error CS8304: Compiler version: '5.9.0-1.26505.111 (a8dd0577c5c13f43a2f57fe33b4ff11b90f6371e)'. Language version: 14.0. Compiler path: '/usr/local/share/dotnet/sdk/10.0.402/Roslyn/bincore/Microsoft.CodeAnalysis.CSharp.dll'.
In a project targeting net10.0, the language version is 14.0. The code in this series sticks to C# 10; if you want the project to keep you within that limit too, add one line to the PropertyGroup:
<LangVersion>10</LangVersion>
The same #error version now says Language version: 10.0. Choosing an older language version is a supported setting; the opposite, choosing a version newer than the TargetFramework default, isn't supported by Microsoft. Don't forget to delete the #error line when you're done experimenting.
dotnet build and the Output Folder
dotnet run builds and runs every time. When you only want to build, use dotnet build:
$ dotnet build
Restore complete (0.1s)
Bookstore net10.0 succeeded (0.1s) → bin/Debug/net10.0/Bookstore.dll
Build succeeded in 0.3s
The output goes to the bin/Debug/net10.0 folder. Debug is the default build configuration; when you ship, you get an optimized build with -c Release. Inside the folder:
$ ls bin/Debug/net10.0
Bookstore
Bookstore.deps.json
Bookstore.dll
Bookstore.pdb
Bookstore.runtimeconfig.json
The real product is Bookstore.dll: the assembly that contains the IL code. Bookstore.pdb holds the debugging information; it's the reason you see line numbers in exception messages. runtimeconfig.json describes which runtime the app runs on, and deps.json its dependencies. The Bookstore file with no extension is the launcher on macOS and Linux; on Windows you'll see Bookstore.exe instead. You can start the application either way:
$ dotnet bin/Debug/net10.0/Bookstore.dll
Hello, World!
$ ./bin/Debug/net10.0/Bookstore
Hello, World!
Choosing an IDE
An IDE isn't required; we did everything in this post with a text editor and a terminal. But for code completion, instant error highlighting and a debugger, an IDE makes your life much easier. On Windows, Visual Studio 2026 is the most common choice, and the Community edition is free for individual use. Options that run on every platform are Visual Studio Code with the C# Dev Kit extension and JetBrains Rider. Visual Studio for Mac was retired in August 2024; if you're on a Mac, look at VS Code or Rider. Whichever you choose, it uses the same SDK and the same dotnet commands behind the scenes. This series doesn't describe IDE-specific steps; everything can also be done from the terminal.
The First Error: Reading a Compiler Message
You can't learn programming without errors, so let's get one. Delete the semicolon at the end of the line in Program.cs and build:
$ dotnet build
Restore complete (0.1s)
Bookstore net10.0 failed with 1 error(s) (0.1s)
.../Bookstore/Program.cs(1,35): error CS1002: ; expected
Build failed with 1 error(s) in 0.3s
The line starts with the full path of the file; I shortened it here. Read the message from left to right: which file (Program.cs), which line and column ((1,35)), what kind of problem (error), which code (CS1002) and the description (; expected). Column 35 is right after the closing parenthesis; that's exactly where the compiler expected the semicolon. In C#, every statement ends with a semicolon.
Now put the semicolon back but write WriteLine as Writeline:
.../Bookstore/Program.cs(1,9): error CS0117: 'Console' does not contain a definition for 'Writeline'
C# is case-sensitive: Writeline and WriteLine are different names. Error codes are also very handy for searching; search for CS0117 and you'll find the explanation of this error and its typical causes in Microsoft's documentation.
The third error is about the language version. If you added LangVersion 10 to the project and use a feature that came with C# 11, such as a raw string literal, the compiler stops you:
string json = """{ "title": "Clean Code" }""";
Console.WriteLine(json);
.../Bookstore/Program.cs(1,15): error CS8936: Feature 'raw string literals' is not available in C# 10.0. Please use language version 11.0 or greater.
The message tells you both the problem and the fix. When you're working on an older project, this error tells you right away why a newer syntax you found online doesn't compile.
Common Pitfalls
Writing top-level code in two files. If you add a second .cs file to the project and put a Console.WriteLine in it too, the build stops: error CS8802: Only one compilation unit can have top-level statements. There must be a single entry point; other files hold classes and methods.
Writing a class above the top-level code. When you define a class first and write Console.WriteLine below it in the same file, the compiler says: error CS8803: Top-level statements must precede namespace and type declarations. Running code first, type definitions after.
Running dotnet run in the wrong folder. In a folder without a project file you get Couldn't find a project to run. Either move to the folder with the .csproj or pass the path with --project.
Setting LangVersion to latest. On an older project this shortcut is tempting when you want a new feature, but Microsoft doesn't support that combination. With features that need runtime or BCL support, you'll hit a compile error or unexpected behavior. If you want new language features, upgrade the TargetFramework.
A Small Experiment
★☆☆ Create the project with dotnet new console -o Bookstore, run it, and find the GlobalUsings.g.cs file in the obj folder.
★★☆ Change the args example so it prints all of the program's arguments together with their count. Hint: args.Length and string.Join(", ", args).
★★★ Write <ImplicitUsings>disable</ImplicitUsings> in the .csproj and build. Which error do you get? Make the program compile again by adding a single line at the top of the file.
You now have a working project and an eye that can read compiler messages. In the next post we move on to variables and data types: C#'s built-in types, what var really does, const, literals, default values and naming rules.