AlgoMaster Logo

First C# Program

Last Updated: May 17, 2026

10 min read

The fastest way to feel at home in C# is to create a tiny console app, run it, and then poke at it until each piece makes sense. This lesson walks through that loop using a small e-commerce program we'll keep building on throughout the course. By the end, you'll know how to spin up a project, read Program.cs line by line, print and read input, and recognize the two or three errors you'll trip over first.

Creating the Project

The dotnet CLI scaffolds new projects from templates. The one we want is console. From a terminal, run:

That command creates a folder named first-cart with a working console application inside it. List the contents and you'll see a small handful of files and folders:

Here's what each one is for:

File or folderPurpose
Program.csThe C# source file. This is where your code goes.
first-cart.csprojThe project file. Lists the target framework, dependencies, and build settings. The dotnet CLI reads it to know how to compile your code.
bin/Build output. Compiled assemblies (.dll files) land here, organized by configuration (Debug, Release).
obj/Intermediate build files. The compiler uses this folder for temporary work. You don't edit anything here by hand.

You won't see bin/ or obj/ until the first build, but they'll appear as soon as you run dotnet build or dotnet run. Both folders are safe to delete, the next build recreates them.

The .csproj file is short and worth peeking at:

Two settings to remember for now: ImplicitUsings automatically imports common namespaces (like System) so you don't need using System; at the top of every file, and Nullable turns on nullable reference type checking. We'll see both of these show up in the next few sections.

The Default Program.cs

Open Program.cs. You'll see this:

That's the entire program. Two lines, one of them a comment. Run it:

That's the modern C# template, and it uses a feature called top-level statements. There's no class, no Main method, no using System;. You just write the code you want to run, and the compiler handles the boilerplate.

That looks like magic, so let's see what the compiler is really generating under the hood.

The Classic Equivalent

Before C# 9 (released in 2020), every console program had to look like this:

This is what Program.cs used to look like, and it's exactly what your top-level-statements version compiles to. Top-level statements are syntactic sugar: the compiler wraps your code in a hidden class with a hidden Main method, so the runtime still has something to call when the program starts.

You're free to write either form. New projects use top-level statements because they're shorter, but you'll see the classic form in older codebases, tutorials, and any place where the program has multiple classes or needs more structure. Both compile to the same thing.

Anatomy Line by Line

The classic form is more verbose, but every piece is doing a specific job. Walking through it tells you everything you need to know about C# program structure.

using System;

This is a using directive. It tells the compiler "when I write Console, look inside the System namespace for it." A namespace is a labeled container for types. System is the namespace that holds Console, String, Int32, DateTime, and most of the everyday types you'll use.

Without this directive, you'd have to write System.Console.WriteLine(...) every time. In modern .NET (6 and later), the project file's ImplicitUsings setting automatically imports System (and a few other common namespaces) as a global using, which is why you don't see using System; at the top of new templates. It's still there, just invisible.

namespace FirstCart;

This declares a namespace for your code. FirstCart is just a name we picked. Any types declared below this line live inside the FirstCart namespace, so the full name of our class is FirstCart.Program.

The trailing semicolon makes this a file-scoped namespace (introduced in C# 10). Everything in the file belongs to it. The older brace-based syntax wraps the rest of the file in { ... } and is equivalent but noisier.

class Program

A class is the most common building block in C#. Here it's just a container for the entry point. The name Program is convention, the runtime doesn't actually care what the class is called. What it cares about is finding the Main method inside.

static void Main(string[] args)

This is the entry point. The runtime calls this method to start your program. Each piece of the signature has a job:

  • `static` means the method belongs to the class itself, not to an instance of it. The runtime has to call Main before any objects of Program exist, so it can't be an instance method.
  • `void` is the return type. void means "returns nothing." You can also write static int Main(...) and return an integer exit code, but void is the default.
  • `Main` is the method name. The runtime looks for a method called Main (with a capital M). main, MAIN, or Start won't work.
  • `string[] args` is the parameter. It's an array of strings holding the command-line arguments you passed when running the program. If you run dotnet run -- coupon10 freeship, then args[0] is "coupon10" and args[1] is "freeship". You can also write Main() with no parameters, that works too.

Console.WriteLine("Hello, World!");

Console is a class in the System namespace that represents the standard input/output streams of your terminal. WriteLine is a static method on Console that prints whatever you pass it, then moves to a new line. The string "Hello, World!" is the argument. Every statement ends with a semicolon.

Useful Console Members

Console does more than just WriteLine. The four you'll reach for most often:

MethodWhat it does
Console.WriteLine(...)Prints a value and adds a newline.
Console.Write(...)Prints a value with no newline. Useful when prompting for input on the same line.
Console.ReadLine()Reads one line of input from the user. Returns string? (the line, or null if input ended).
Console.ReadKey()Reads a single key press. Returns a ConsoleKeyInfo. Often used to pause until the user presses anything.

We'll use WriteLine, Write, and ReadLine in this lesson. ReadKey shows up later when we look at small interactive programs.

Modifying the Program

The "Hello, World!" greeting is fine for a first run, but we're building toward a shopping cart. Let's evolve the program in three steps.

Step 1: Print a Welcome Message

Replace the body of the program with:

Run it again:

Same shape, different message. Nothing surprising.

Step 2: Introduce a Variable

Hardcoding messages gets old fast. Let's print a personalized greeting using a variable:

Two new things here:

  • string customer = "Alice"; declares a variable of type string and assigns it the value "Alice". The type goes first, then the name, then the value.
  • $"Welcome, {customer}!" is a string interpolation. The $ prefix tells the compiler "treat the bits inside { } as expressions to evaluate and insert here." So {customer} is replaced with the value of customer. It's cleaner than "Welcome, " + customer + "!", and it's the modern C# way to build strings.

You can put any expression inside the braces, not just variable names. $"Total: {2 + 2}" prints Total: 4.

Step 3: Add a Number

A cart has a total, so we need a number type that can hold money. The right choice is decimal:

Three things to unpack:

  • `decimal` is the type C# uses for money. It avoids the rounding quirks of float and double, which can't represent values like 0.1 exactly. For prices, totals, and anything financial, use decimal.
  • The `m` suffix in 49.99m tells the compiler "this is a decimal literal." Without it, 49.99 would be a double, and assigning a double to a decimal is a compile error because the conversion isn't implicit. The m stands for "money."
  • The `:C` format specifier inside the interpolation formats the number as currency using the current culture's settings. On a US machine that's $49.99. On a UK machine you'd see £49.99. The _String Formatting_ lesson covers format strings in detail.

Reading Input

Output is half the story. Most programs also read input. Console.ReadLine is the simplest way to do that:

Sample run:

Two things to notice. First, we used Console.Write (no newline) for the prompt so the user types on the same line. Second, the variable is declared as string?, not string. The ? marks it as nullable, meaning it might be null. Console.ReadLine returns string? because it returns null when input ends (for example, if the user closes the input stream). Treating the return as nullable forces you to think about that case.

But what we usually want isn't a string. We want a number, so we can do math with it. The cleanest way to parse a string into a number safely is int.TryParse:

Sample run with valid input:

Sample run with invalid input:

int.TryParse does two things at once. It tries to parse the string as an integer. If the parse succeeds, it puts the result into the out parameter (here, items) and returns true. If the parse fails, it returns false and items is set to 0. The out keyword declares a variable that the method fills in, and the int items syntax both declares the variable and uses it in the same line.

The other option is int.Parse(input), but that throws a FormatException if the input isn't a valid number, which means your program crashes on bad input. TryParse is the safe default.

Common Mistakes

The first errors you'll see in C# are almost always small. Here are two you'll hit early.

Missing Semicolon

What's wrong with this code?

The first statement is missing its semicolon. The compiler reports:

CS1002 is the error code for "semicolon expected." It points at column 42 of line 1, which is right after the closing parenthesis where the semicolon should be.

Fix: Add the semicolon:

Every C# statement ends with ;. Forgetting one is a habit you'll lose in your first week, but you'll see CS1002 plenty of times before then.

Calling .Length on a Nullable String

What's wrong with this code?

This compiles with a warning (CS8602: Dereference of a possibly null reference), and if the user closes the input stream or ReadLine returns null for any other reason, the program throws:

The warning is the compiler telling you that name is string? (nullable), so calling .Length on it isn't safe. The runtime exception is what happens when that warning becomes a real null at run time.

Fix (option 1): Use the null-conditional operator ?., which short-circuits to null instead of throwing:

The ?.Length evaluates to null if name is null, and the ?? 0 then substitutes 0.

Fix (option 2): Check explicitly before using:

Inside the if, the compiler knows name can't be null, so the warning goes away and the call is safe.

Running with Arguments

You can pass command-line arguments to a dotnet run invocation using the -- separator:

The -- tells dotnet "everything after this belongs to the program, not to me." Without it, dotnet run would try to interpret coupon10 as one of its own options.

In top-level statements, the arguments are available through a magic variable called args:

Sample run:

In the classic Main(string[] args) form, args is the parameter you declared. In top-level statements, the compiler provides it implicitly with the same name. Either way, args is an array of strings, indexed from 0.

The Full Lifecycle

When you run dotnet run, several things happen in sequence:

dotnet run is actually two commands combined: it calls dotnet build to compile your source into an assembly (a .dll file with your bytecode-equivalent), then it launches the .NET runtime to execute that assembly. The compiled output lives in bin/Debug/net8.0/first-cart.dll.

You can also run those steps separately:

Same result, just more typing. For now, treat dotnet run as a black box that compiles and runs.

Summary

  • dotnet new console -n first-cart scaffolds a console project. It creates Program.cs (your code), a .csproj file (build settings), and bin/ and obj/ folders for build output.
  • The modern Program.cs uses top-level statements: you write the entry-point code directly, and the compiler wraps it in a hidden class with a hidden Main method. The classic form with class Program { static void Main(string[] args) { ... } } is equivalent and still common.
  • Console.WriteLine prints a value followed by a newline. Console.Write prints with no newline. Console.ReadLine reads a line and returns string? because input can end.
  • Use decimal for money. Decimal literals need the m suffix (49.99m), or the compiler treats them as double. Format with :C to get currency output.
  • Use int.TryParse(input, out int value) to parse user input safely. It returns true on success and false on bad input, without throwing.
  • Modern C# enables nullable reference types by default. string? means "might be null." Calling members on a possibly-null reference triggers CS8602 and risks NullReferenceException at runtime. Guard with ?., ??, or an is not null check.
  • Pass command-line arguments with dotnet run -- arg1 arg2. The -- separates dotnet's own options from your program's arguments. Read them through the args array.

Now that the program runs, the next lesson follows the code from source to running native instructions: how the C# compiler produces IL, what an assembly is, and how the JIT turns it into machine code at runtime.