AlgoMaster Logo

First C# Program

Last Updated: May 22, 2026

Medium Priority
8 min read

The fastest way to learn C# is to create a small console app, run it, and explore each piece. This lesson walks through that loop using a small e-commerce program that will continue throughout the course. The lesson covers how to spin up a project, read Program.cs line by line, print and read input, and recognize early errors.

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. The contents:

What each one is for:

File or folderPurpose
Program.csThe C# source file. The code goes here.
first-cart.csprojThe project file. Lists the target framework, dependencies, and build settings. The dotnet CLI reads it to compile the 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. Don't edit anything here by hand.

bin/ and obj/ don't appear until the first build. They appear after running dotnet build or dotnet run. Both folders are safe to delete; the next build recreates them.

The .csproj file is short:

Two settings matter for now: ImplicitUsings automatically imports common namespaces (like System) so using System; isn't needed at the top of every file, and Nullable turns on nullable reference type checking. Both appear in the next few sections.

The Default Program.cs

Open Program.cs:

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

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

The compiler-generated equivalent reveals what's happening.

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 the top-level-statements version compiles to. Top-level statements are syntactic sugar: the compiler wraps the code in a hidden class with a hidden Main method, so the runtime still has something to call when the program starts.

Either form is valid. New projects use top-level statements because they're shorter, but the classic form appears 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 covers C# program structure.

using System;

This is a using directive. It tells the compiler "when Console appears, 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.

Without this directive, every call would need to be written as System.Console.WriteLine(...). 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 using System; doesn't appear at the top of new templates. It's still there, just invisible.

namespace FirstCart;

This declares a namespace for the code. FirstCart is the chosen name. Any types declared below this line live inside the FirstCart namespace, so the full name of the 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 a container for the entry point. The name Program is convention; the runtime doesn't care what the class is called. The runtime cares about finding the Main method inside.

static void Main(string[] args)

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

  • `static` means the method belongs to the class itself, not to an instance. 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." static int Main(...) returns 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 passed when running the program. With dotnet run -- coupon10 freeship, args[0] is "coupon10" and args[1] is "freeship". Main() with no parameters also works.

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

Console is a class in the System namespace representing the standard input/output streams of the terminal. WriteLine is a static method on Console that prints whatever is passed to 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 WriteLine. The four common methods:

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 works for a first run, but the goal is a shopping cart. The program evolves in three steps.

Step 1: Print a Welcome Message

Replace the body of the program with:

Run it again:

Same shape, different message.

Step 2: Introduce a Variable

Hardcoded messages aren't reusable. 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.

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

Step 3: Add a Number

A cart has a total, requiring a number type for money. decimal is the choice:

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 the output is £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 note. First, Console.Write (no newline) is used 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 handling that case.

But the common requirement isn't a string, it's a number for math. The standard 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 on invalid input, which means the program crashes on bad input. TryParse is the safe default.

Common Mistakes

The first errors in C# are almost always small. Two common ones early on:

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 common early mistake.

Calling .Length on a Nullable String

What's wrong with this code?

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

The warning indicates 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

Command-line arguments can be passed 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 an implicit variable called args:

Sample run:

In the classic Main(string[] args) form, args is the declared parameter. 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

Running dotnet run triggers several steps in sequence:

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

The steps can also run separately:

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