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.sh script, 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@version adds a NuGet package (it is downloaded on the first build, so it needs network access).
  • #:property Name=Value sets a build property, the same properties a .csproj file contains.
  • #:sdk chooses a different SDK, for example #:sdk Microsoft.NET.Sdk.Web for 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, a string[].
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.