The .NET SDK, the dotnet CLI and Your First Program
Getting started with C#: installing the .NET SDK, the dotnet command, running a single file, top-level statements, reading input and writing output, and what a project adds.
The .NET SDK
The .NET SDK (software development
kit) is the download you need to write .NET programs. It contains
the C# compiler, the .NET runtime and base class library, project
templates, and the dotnet command that ties them
together. Microsoft also offers the runtime on its own:
enough to run finished programs, not to build them. On a
developer machine you want the SDK; on a server that only runs an
application, the runtime is enough.
Common ways to install it:
-
Windows: the installer from
dotnet.microsoft.com/download,
winget install Microsoft.DotNet.SDK.10, or as part of Visual Studio. - macOS: the installer from the same page.
-
Linux: the distribution's package manager (many
distributions package .NET, and Microsoft publishes its own
package feed), or Microsoft's
dotnet-install.shscript, which installs into your home folder without root rights.
Install the current long-term support version; as of October 2026 that is .NET 10. Then check the installation in a terminal:
dotnet --version
dotnet --list-sdks
dotnet --list-runtimes
10.0.401 10.0.401 [/home/amara/.dotnet/sdk] Microsoft.AspNetCore.App 10.0.12 [/home/amara/.dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 10.0.12 [/home/amara/.dotnet/shared/Microsoft.NETCore.App]
The SDK and the runtime have separate version numbers: SDK 10.0.401
ships runtime 10.0.12. Several SDKs and runtimes can be installed
side by side, and by default the dotnet command uses
the newest SDK. dotnet --info prints all of this plus
details about the operating system.
The dotnet CLI
The dotnet CLI (command-line
interface) is the single dotnet command with
subcommands for everything you do with a program: create, build,
run, test, publish, and manage packages. It works the same on
Windows, Linux and macOS, and editors and build pipelines call the
same commands under the hood.
| Command | What it does |
|---|---|
dotnet run app.cs |
Compile and run a single C# file |
dotnet new console -o Name |
Create a new project from a template in folder Name |
dotnet build |
Compile the project in the current folder |
dotnet run |
Build the project if needed, then run it |
dotnet test |
Run the tests (see Testing and Code Quality) |
dotnet package add Name |
Add a NuGet package (older form:
dotnet add package)
|
dotnet publish |
Produce the files to deploy |
dotnet format |
Apply the code style rules to your files |
dotnet new list |
Show the installed project templates |
Every command has help: dotnet --help lists the
commands, and dotnet build --help the options of one
command. .NET 10 added "noun first" forms such as
dotnet package add and
dotnet reference add; the older
dotnet add package still works and is what most
existing documentation shows.
Running a single file
The quickest way to run C# is a
file-based app: one
.cs file that the dotnet command compiles
and runs directly, without a project file. This arrived with .NET 10
(November 2025). Create a file hello.cs with one line:
Console.WriteLine("Hello from Kestrel Energy!");
Hello from Kestrel Energy!
Then run it from a terminal. The terminal works in one folder at a
time, its
working directory, and dotnet looks for hello.cs there; so
first go to the folder that contains the file with cd:
cd ~/projects/csharp # the folder that contains hello.cs
dotnet run hello.cs
dotnet hello.cs # shorter form, same result
The first run takes a few seconds: behind the scenes the SDK creates a hidden project, compiles your file to IL and stores the result in a cache folder. Later runs of an unchanged file start much faster because the build is reused. Every example on these pages is checked exactly this way, as a file-based app on .NET 10.
Directives in the file
What a project file would normally say can go at the top of the
file, in lines starting with #:. The most common ones:
#:package Humanizer@2.14.1
#:property TreatWarningsAsErrors=true
// The program starts here
Console.WriteLine("Directives come first.");
-
#:package Name@versionadds a NuGet package (it is downloaded on the first build, so it needs network access). -
#:property Name=Valuesets a build property, the same properties a.csprojfile contains. -
#:sdkchooses a different SDK, for example#:sdk Microsoft.NET.Sdk.Webfor a small web application.
Running a file as a script
On Linux and macOS a file-based app can run like a shell script. Put a shebang line first, save the file with Unix line endings, and make it executable:
#!/usr/bin/env -S dotnet --
Console.WriteLine("Checking meter files...");
chmod +x check-meters.cs
./check-meters.cs
This makes C# a reasonable choice for small automation jobs, next to Bash, Python and PowerShell.
Top-level statements
Every C# program needs an entry point: the place where
execution starts. Traditionally that is a method called
Main inside a class. With
top-level statements
(since C# 9) you write the statements directly at the top of one
file, and the compiler wraps them in a generated class and
Main method. These two programs do the same thing:
// Modern form: top-level statements
Console.WriteLine("Meter report");
Console.WriteLine("Readings processed: 3");
Meter report Readings processed: 3
// Classic form: an explicit class with a Main method
class Program
{
static void Main()
{
Console.WriteLine("Meter report");
Console.WriteLine("Readings processed: 3");
}
}
Meter report Readings processed: 3
You will see the classic form in older code, in books and in projects created before .NET 6 (November 2021). Both are valid C#. The rules for top-level statements are simple:
- Only one file in a program may contain them.
- They come first in the file. Methods written among them are local functions; classes, records and other types go after the statements.
-
The command-line arguments are available in a ready-made variable
args, astring[].
PrintHeader("Outage summary");
Console.WriteLine($"Arguments given: {args.Length}");
Console.WriteLine(new Region("North", 1250));
// A method used by the statements above (a local function)
void PrintHeader(string title)
{
Console.WriteLine(title);
Console.WriteLine(new string('-', title.Length));
}
// Types come after the top-level statements
record Region(string Name, int CustomersAffected);
Outage summary
--------------
Arguments given: 0
Region { Name = North, CustomersAffected = 1250 }
Run as dotnet run outage.cs -- North East, the same
program would report two arguments: everything after
-- is passed to your program instead of to
dotnet. Putting a type before the statements
is a compiler error:
record Region(string Name, int CustomersAffected);
Console.WriteLine(new Region("North", 1250));
example.cs(3,1): error CS8803: Top-level statements must precede namespace and type declarations.
Small mistakes are reported the same way, with file, line and column. A forgotten semicolon is the classic one:
Console.WriteLine("Meter report")
Console.WriteLine("Readings processed: 3");
example.cs(1,34): error CS1002: ; expected
Writing output
The Console class writes to the terminal (the
standard output stream). Console.WriteLine writes its argument followed by a
line break; Console.Write writes without the line
break, so the next output continues on the same line. Without an
argument, WriteLine() prints an empty line.
string ticket = "INC-20417";
int customers = 1250;
Console.Write("Ticket: ");
Console.WriteLine(ticket);
Console.WriteLine();
Console.WriteLine("Customers affected: " + customers);
Console.WriteLine($"{ticket} affects {customers} customers");
Console.WriteLine(customers / 1000.0);
Ticket: INC-20417 Customers affected: 1250 INC-20417 affects 1250 customers 1.25
WriteLine accepts any value and prints its text form.
The + operator joins strings, and converts a number to
text when one side is a string. The $"…" form, an
interpolated string, is usually clearer; it is covered in
Strings and String Interpolation. Error messages belong on the standard error stream:
Console.Error.WriteLine("…").
Reading input
Console.ReadLine() waits until the user types a line
and presses Enter, then returns that line as a string,
without the line break. In the examples below the typed input is fed
in automatically and, unlike in a real terminal, does not appear in
the output; only what the program prints is shown.
Console.Write("Your name: ");
string? name = Console.ReadLine();
Console.WriteLine();
Console.WriteLine($"Hello, {name}. Welcome to the service desk.");
Your name: Hello, Amara. Welcome to the service desk.
The type is string?, with a question mark, because
ReadLine returns null (no value) when
there is no more input, for example when the input comes from a file
that has ended. The question mark tells the compiler that this
variable may hold null; without it, the compiler warns:
greet.cs(2,15): warning CS8600: Converting null literal or possible null value to non-nullable type.
It is a warning, so the program still runs. Why this matters, and
how to handle null properly, is explained in
Nullable Reference Types and Null Safety. Here is what happens when there is no input at all:
string? line = Console.ReadLine();
Console.WriteLine(line is null ? "No input: end of stream." : $"Read: {line}");
No input: end of stream.
Converting input to numbers
Input is always text. To calculate with it, convert it:
int.Parse for whole numbers,
decimal.Parse or double.Parse for numbers
with decimals. Unlike Python or JavaScript, C# will not let you
multiply a string by a number at all; the compiler refuses.
decimal tariff = 0.27m; // euros per kWh
Console.Write("Meter reading this month (kWh): ");
string input = Console.ReadLine() ?? "";
int kwh = int.Parse(input);
decimal cost = kwh * tariff;
Console.WriteLine();
Console.WriteLine($"Usage: {kwh} kWh, cost: EUR {cost:F2}");
Meter reading this month (kWh): Usage: 342 kWh, cost: EUR 92.34
?? "" means "if ReadLine returned
null, use an empty string instead". The
m after 0.27 makes it a
decimal, the type for money (see
Variables, Types and Operators).
int.Parse throws an exception when the text is not a
number, and the program stops:
Console.Write("Meter reading (kWh): ");
int kwh = int.Parse(Console.ReadLine() ?? "");
Console.WriteLine($"Usage: {kwh} kWh");
Meter reading (kWh): Unhandled exception. System.FormatException: The input string 'twelve' was not in a correct format.
For input from people, use int.TryParse. It returns
true or false instead of throwing, and
hands the number back through an out variable. A loop
can then ask again until the input is valid:
int kwh;
while (true)
{
Console.Write("Meter reading (kWh): ");
string input = Console.ReadLine() ?? "";
if (int.TryParse(input, out kwh) && kwh >= 0)
{
break;
}
Console.WriteLine($"'{input}' is not a valid reading, try again.");
}
Console.WriteLine($"Accepted: {kwh} kWh");
Meter reading (kWh): 'abc' is not a valid reading, try again. Meter reading (kWh): '-5' is not a valid reading, try again. Meter reading (kWh): Accepted: 120 kWh
Each prompt and the message after it end up on one line here, because the typed answers are not shown. Loops are explained in Loops, and the try-parse pattern in Exceptions and Error Handling.
What a project adds
A single file is fine for a script or an experiment. Real applications have many files, use packages, and need settings for how they are built; that is what a project is for. The classic starting point is a console application: a program without a graphical interface that reads from and writes to the terminal. Create one with a template:
dotnet new console -o MeterReport
cd MeterReport
dotnet run
The template "Console App" was created successfully. Processing post-creation actions... Restoring /home/amara/MeterReport/MeterReport.csproj: Determining projects to restore... Restored /home/amara/MeterReport/MeterReport.csproj (in 421 ms). Restore succeeded. Hello, World!
The folder now contains:
MeterReport/
├── MeterReport.csproj the project file
├── Program.cs Console.WriteLine("Hello, World!");
└── obj/ restore and build information
The project file is a short XML file. It says that the result is an executable program, which .NET version it targets, and that implicit usings and nullable checking are on:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
After a build, the compiled program is in
bin/Debug/net10.0/: MeterReport.dll (the
IL), a small launcher MeterReport (or
MeterReport.exe on Windows), and configuration files
for the runtime. In a project, every .cs file in the
folder is compiled together, so you can split code over as many
files as you like.
A file-based app can grow into a project at any time:
dotnet project convert hello.cs creates a folder with a
.csproj and the file, translating the
#: directives into project settings. Projects, packages
and building are covered in
Projects, NuGet and Building.
Editors
Any text editor will do for a file-based app, but an editor that understands C# shows errors while you type, completes names, and lets you step through code in a debugger. The common choices:
- Visual Studio (Windows), Microsoft's full development environment; the Community edition is free for individuals, education, open-source work and small teams.
- Visual Studio Code (Windows, Linux, macOS) with Microsoft's C# extensions. The C# Dev Kit extension adds project management and testing; unlike the open-source base extension it is closed source, free for individuals, education, open-source work and commercial teams of up to five developers.
- JetBrains Rider (Windows, Linux, macOS), a commercial development environment that is also offered free for non-commercial use.
All three use the same SDK and the same dotnet
commands, so a project made in one opens in the others. For finding
and fixing errors in general, see
Debugging and Testing.