Beginner Guide

C# for Dummies

From Hello, World! to your first class — no prior programming experience needed. Copy-paste C# you can run in any .NET 8 console app.

Beginner Friendly Plain English Copy & Paste
01

What is C#?

So you want to learn C# but you've never written a line of code. Or you've coded a little — maybe a Python tutorial, maybe a bit of JavaScript — and now you want a "real" backend language. You're in the right place.

C# (pronounced "C-sharp") is a modern, friendly programming language made by Microsoft. It runs on .NET — a free, cross-platform toolbox that lets you build websites, desktop apps, games, mobile apps, and cloud services with the same language. One language, all the things.

Brain Power

Before reading on, try to guess: when you write C# code, what actually runs it on the computer? The language itself can't do anything — something has to translate your code into instructions the CPU understands.

(The answer in one phrase: the .NET runtime. The next three cards are basically "what does that mean?".)

The two-sentence story: C# is the language you write. .NET is the runtime that executes it (plus thousands of pre-built libraries for free). You write a .cs file, you run dotnet run, and a program happens. That's the whole stack.
1.1

What is C# and .NET?

Think of it like cooking: C# is the language you speak (the recipe), and .NET is the kitchen — the pots, pans, oven, and ingredients you use. You write C# code, .NET runs it on Windows, macOS, or Linux.

// C#  = the language (syntax: how you write code)
// .NET = the platform (runtime + libraries that execute it)

// You install the .NET SDK once, then create a new app:
//   dotnet new console -n HelloApp
//   cd HelloApp
//   dotnet run

// Inside HelloApp you get a Program.cs file.
// That's where your C# code lives.

// .NET 8 ships thousands of useful classes for free:
//  - Console        → write to the terminal
//  - File           → read/write files
//  - HttpClient     → call web APIs
//  - List<T>        → resizable lists
//  - DateTime       → dates and times
//
// You don't need to write any of these — you just USE them.
1.2

Your first Hello, World!

Modern C# (since .NET 6) lets you skip all the ceremony. A whole program can be a single line inside Program.cs. This style is called top-level statements.

// Program.cs — that's the whole file. Seriously.
Console.WriteLine("Hello, World!");

// Run it:
//   dotnet run
// Output:
//   Hello, World!

// You can also read input back from the user:
Console.Write("What's your name? ");
string? name = Console.ReadLine();   // user types something, hits Enter
Console.WriteLine($"Nice to meet you, {name}!");

// The $ in front of the string means "interpolated":
// anything inside { } gets replaced with that variable's value.
1.3

Comments, namespaces & using

Comments are notes for humans — the compiler ignores them. Namespaces are folders for code. using says "I want to use stuff from that folder without typing the full path every time".

// ── Single-line comment ──────────────────────
// This line is ignored by the compiler.

/* Multi-line comment.
   Useful when you need to write more than
   one line of explanation. */

/// <summary>
/// XML doc comment — shows up in IntelliSense
/// when someone hovers over your method or class.
/// </summary>

// ── using directives at the top of the file ──
using System;            // gives you Console, DateTime, etc.
using System.IO;         // gives you File, Directory
using System.Linq;       // gives you Where, Select, etc.

// (In .NET 6+ many of these are added automatically
//  as "implicit usings", so you may not see them.)

// ── Namespaces = folders for code ────────────
namespace MyApp.Models   // file-scoped namespace (one per file)
{
    public class User { /* ... */ }
}

// Then elsewhere:
//   using MyApp.Models;   // now you can just write `User`
//                          // instead of MyApp.Models.User
Section 1 wrap — what you now know
  • C# = the language you write. .NET = the runtime + thousands of free libraries that execute it.
  • One language covers web, desktop, mobile, cloud, games. Cross-platform on Windows, macOS, Linux.
  • Modern C# lets you skip all boilerplate — a one-line Hello, World! is a valid program (top-level statements).
  • Comments are for humans; the compiler ignores them. // for one-line, /* */ for blocks, /// for docs.
  • namespace = folder for code. using = "let me refer to things in that folder by short name".
02

Variables & Types

Imagine a kitchen full of labelled jars. The label says what's inside: "sugar", "flour", "salt". You can refill a jar, but you can't pour flour into the sugar jar without making a mess.

That's a variable in C#. A labelled box that holds a value. And because C# is strongly typed, every box also remembers what kind of value it's allowed to hold — number, text, date, list. The compiler is the kitchen inspector. Try to put a string in an int box and it stops you before you ship the code.

Brain Power

If int can hold a number and string can hold text, why does C# need so many number types — int, long, double, decimal?

(Hint: think about money. Why might "3.14" be fine and "$19.99" be a disaster as the same kind of number?)

The answer is precision. double is fast but has tiny rounding errors — that's fine for physics simulations and unacceptable for prices. decimal is slower but exact — that's why you use it for money. The right type is whichever one matches your actual data, not whichever one looks shortest.
2.1

Numbers, strings & booleans

The most common types you'll use every day. Use var when the type is obvious from the right-hand side — the compiler figures it out for you.

// ── Whole numbers ────────────────────────────
int    age      = 35;                  // -2 billion to +2 billion
long   bigNum   = 9_000_000_000L;      // bigger range, suffix L

// ── Decimal numbers ──────────────────────────
double pi       = 3.14159;             // fast, slight rounding error
decimal price   = 19.99m;              // exact — USE FOR MONEY (suffix m)

// ── Text ─────────────────────────────────────
string greeting = "Hello";
char   letter   = 'A';                 // single character, single quotes

// ── True / false ─────────────────────────────
bool   isActive = true;

// ── Let the compiler infer the type ──────────
var    name     = "Spyros";            // compiler picks string
var    count    = 42;                  // compiler picks int

// ── Value vs reference types (plain English) ─
// VALUE types     (int, double, bool, struct) → the BOX holds the value itself.
//                  Copying the box copies the value.
// REFERENCE types (string, arrays, classes)    → the BOX holds an ADDRESS
//                  pointing to the actual data somewhere in memory.
//                  Copying the box copies the address — both point to the same thing.
2.2

Strings — interpolation & basics

A string is a piece of text. C# gives you very handy ways to build, compare, and inspect strings — no need to remember everything, just keep this card open.

string name = "Anna";
int    age  = 30;

// ── Interpolation — the modern way ───────────
// The $ lets you embed variables directly inside { }.
string hello = $"Hello {name}, you are {age} years old.";
// → "Hello Anna, you are 30 years old."

// ── Multi-line strings ───────────────────────
string poem = $"""
    Roses are red,
    Violets are blue,
    {name} is awesome,
    and so are you.
    """;

// ── Useful built-in methods ──────────────────
string s = "  Hello World  ";

int    len     = s.Length;            // 15 (including spaces)
string upper   = s.ToUpper();         // "  HELLO WORLD  "
string lower   = s.ToLower();         // "  hello world  "
string trimmed = s.Trim();            // "Hello World"
bool   hasHi   = s.Contains("Hello"); // true
bool   starts  = s.StartsWith("  H"); // true
string fixed_  = s.Replace("World", "C#"); // "  Hello C#  "

// ── Safe null/empty checks ───────────────────
string? maybe = null;
bool blank = string.IsNullOrWhiteSpace(maybe); // true (null/empty/spaces)
2.3

Nullable & default values

null means "there's no value here". By default, value types (like int) can't be null. Adding a ? to the type says "this can be null". C# also gives you tidy operators to handle nulls gracefully.

// ── Non-nullable vs nullable ─────────────────
int     a = 5;          // can never be null
int?    b = null;       // nullable int — can be null OR an int

string  s = "hi";       // (in nullable context) can't be null
string? t = null;       // nullable string — can be null

// ── Default values ───────────────────────────
int    zero    = default;    // 0
bool   off     = default;    // false
string empty   = default!;   // null (! says "I know, trust me")

// ── Null-coalescing  ??  ─────────────────────
// "If the left side is null, use the right side instead."
string? userInput = null;
string  name      = userInput ?? "Guest";   // → "Guest"

int? maybeAge = null;
int  age      = maybeAge ?? 18;             // → 18

// ── Null-conditional  ?.  ────────────────────
// "Only call this member if the object isn't null. Otherwise return null."
string? city = null;
int? len = city?.Length;        // → null (no NullReferenceException!)

// Chains nicely:
//   user?.Address?.City?.ToUpper()
// returns null at the FIRST null link instead of crashing.
Section 2 wrap — what you now know
  • Variable = labelled box for a value. C# is strongly typed — every box has a type and the compiler enforces it.
  • Use int / long for whole numbers, double for science, decimal for money (it's the exact one — never use double for currency).
  • string = text. Add ? to any type to say "this can be null" (string?, int?).
  • var = "let the compiler infer the type". Use it when the right-hand side makes the type obvious.
  • Null-coalescing ?? = "if null, use this default". Null-conditional ?. = "only call this if the object isn't null." Together they kill 90% of NullReferenceExceptions.
  • String interpolation: $"Hello {name}" beats concatenation every time.
03

Conditions & Loops

Every interesting program does exactly two things that "hello world" can't: it decides, and it repeats.

Conditions let your code choose a path — "if the user is logged in, show the dashboard; otherwise show the login screen." That's if and switch.

Loops let your code do the same thing many times — "for every order in the list, calculate the total." That's for, foreach, and while.

Together, conditions and loops are the heart of every program you'll ever write. Master these and you can read 80% of any C# codebase.

Brain Power

If for, foreach, and while all loop — why three of them?

(Hint: think about what you have before the loop starts. Do you know how many iterations? Do you have a collection? Or do you only know "keep going until something happens"?)

for = "I know exactly how many times to repeat." foreach = "I have a collection, go through every item." while = "Keep going as long as some condition holds." Same goal, three different mental models. Pick the one that matches what you actually know at the start of the loop — your code reads better.
3.1

if, else if, else & ternary

if runs a block only if a condition is true. Chain else if for more options, and else as the fallback. For tiny "this-or-that" decisions, use the ternary ?: operator.

int score = 78;

// ── Classic if / else if / else ──────────────
if (score >= 90)
{
    Console.WriteLine("Grade A");
}
else if (score >= 75)
{
    Console.WriteLine("Grade B");        // ← this one runs
}
else if (score >= 50)
{
    Console.WriteLine("Grade C");
}
else
{
    Console.WriteLine("Try again");
}

// ── Combine conditions with && (and) and || (or) ──
int age = 20;
bool hasTicket = true;

if (age >= 18 && hasTicket)
    Console.WriteLine("Welcome in!");

// ── Ternary — one-line "if / else" ───────────
string label = score >= 50 ? "Pass" : "Fail";
//                    ^^ condition  ^^ if true   ^^ if false

// Use ternary for SIMPLE choices. For anything fancy, use if/else
// so your code stays readable.
3.2

switch expressions

When you have many branches that compare the same value, a switch is cleaner than a wall of if/else if. Modern C# (8+) has a delightful arrow form that returns a value directly.

// ── Modern switch expression — uses  =>  arrows ──
string day = "Tue";

string kind = day switch
{
    "Mon" or "Tue" or "Wed" or "Thu" or "Fri" => "Weekday",
    "Sat" or "Sun"                            => "Weekend",
    _                                         => "Unknown"   // _ = default
};

Console.WriteLine(kind);   // Weekday

// ── Switch on a number, with ranges ──────────
int score = 78;

string grade = score switch
{
    >= 90 => "A",
    >= 75 => "B",         // ← matches first, returns "B"
    >= 50 => "C",
    _     => "F"
};

// ── Classic switch statement (older but still valid) ──
switch (day)
{
    case "Sat":
    case "Sun":
        Console.WriteLine("Relax!");
        break;            // don't forget break — falls through otherwise
    default:
        Console.WriteLine("Work day");
        break;
}
3.3

for, foreach, while, do-while

Four flavours of loop. Use foreach 90% of the time. Reach for for when you need the index. Use while / do-while when you don't know in advance how many times to repeat.

// ── for — when you need a counter ────────────
for (int i = 0; i < 5; i++)
{
    Console.WriteLine($"i = {i}");   // prints 0,1,2,3,4
}

// ── foreach — walk through a collection ──────
string[] fruits = { "apple", "banana", "cherry" };

foreach (string fruit in fruits)
{
    Console.WriteLine(fruit);
}

// ── while — repeat as long as a condition is true ──
int countdown = 3;
while (countdown > 0)
{
    Console.WriteLine(countdown);
    countdown--;
}
// → 3, 2, 1

// ── do-while — always runs at least once ─────
string? input;
do
{
    Console.Write("Type 'quit' to exit: ");
    input = Console.ReadLine();
} while (input != "quit");

// ── break and continue ───────────────────────
for (int i = 0; i < 10; i++)
{
    if (i == 3) continue;   // skip 3, jump to next iteration
    if (i == 7) break;      // stop the loop completely
    Console.WriteLine(i);
}
// → 0, 1, 2, 4, 5, 6
Section 3 wrap — what you now know
  • if / else if / else = pick one branch out of many. Use && for AND, || for OR.
  • switch expression (the arrow form) is the modern way to map one value to many outputs in one place.
  • Ternary cond ? a : b = one-line if/else. Use it for short values, not statements.
  • for = "I know exactly how many iterations." foreach = "I have a collection." while = "until a condition flips."
  • break = leave the loop now. continue = skip to the next iteration. Both are valid — overusing them is not.
  • Conditions + loops together make up the skeleton of every program. Master these and you can read 80% of real C# code.
04

Methods

The microwave in your kitchen has a button labelled "Popcorn". You press it, and it does everything — start, set the time, adjust the power, beep when done. You don't have to know the steps. You press the button, popcorn happens.

A method in C# is that button. A named, reusable block of code that does one job. Inputs go in (parameters), an output may come out (return value), and everything in the middle is hidden inside.

Write the logic once. Call it from anywhere. If the logic changes, you change one place. That's it — that's the entire pitch for methods, and it's why every codebase on Earth is mostly methods calling other methods.

Brain Power

You see two methods: void Save() and int Save(). What's the difference, and which one would you write for saving a file?

(Hint: the keyword void means "nothing". Why might "save a file" not need to return anything? When might it?)

void = "I do work but I don't hand anything back." int = "I do work AND give you a number." For "save a file" you might use void if you just need it done. You'd use bool if the caller needs to know "did it succeed?". The return type is part of the contract — pick it based on what the caller actually needs.
4.1

Defining and calling methods

A method needs: a return type (or void for nothing), a name, and a list of parameters in parentheses. Then you call it by typing its name and passing arguments.

// ── A method that returns a value ────────────
int Add(int a, int b)
{
    return a + b;
}

int sum = Add(3, 4);          // sum = 7
Console.WriteLine(sum);

// ── A method with no return (void) ───────────
void Greet(string name)
{
    Console.WriteLine($"Hi, {name}!");
    // no `return` needed
}

Greet("Spyros");              // Hi, Spyros!

// ── Multiple parameters ──────────────────────
double Average(double x, double y, double z)
{
    return (x + y + z) / 3;
}

Console.WriteLine(Average(10, 20, 30));   // 20

// ── Named arguments (clearer at call site) ───
void Book(string from, string to, int seats)
{
    Console.WriteLine($"{seats} seats: {from} → {to}");
}

Book(from: "Athens", to: "Berlin", seats: 2);
4.2

Overloading & default parameters

Overloading = several methods with the same name but different parameter lists. The compiler picks the right one. Default parameters let you make some arguments optional.

// ── Overloading: same name, different signatures ──
int Multiply(int a, int b)            => a * b;
double Multiply(double a, double b)   => a * b;
int Multiply(int a, int b, int c)     => a * b * c;

Multiply(2, 3);          // calls the (int,int) version → 6
Multiply(2.5, 4.0);      // calls the (double,double) version → 10.0
Multiply(2, 3, 4);       // calls the three-int version → 24

// ── Default parameter values ─────────────────
void SendEmail(string to, string subject = "Hello", bool important = false)
{
    Console.WriteLine($"To: {to} | Subject: {subject} | !{important}");
}

SendEmail("anna@example.com");
//   → To: anna@example.com | Subject: Hello | !False

SendEmail("bob@example.com", "Urgent", important: true);
//   → To: bob@example.com  | Subject: Urgent | !True

// Rule: default-valued parameters must come AFTER required ones.
4.3

Expression-bodied & local functions

For one-line methods, the fat-arrow => form is shorter and clearer. And you can declare local functions — methods inside other methods — when a helper only belongs to one place.

// ── Expression-bodied members ────────────────
// Instead of:
int SquareLong(int x) { return x * x; }

// Write:
int Square(int x) => x * x;
string Hello(string name) => $"Hi {name}!";
bool IsAdult(int age) => age >= 18;

// Works for properties too:
public class Person
{
    public string First { get; set; } = "";
    public string Last  { get; set; } = "";
    public string Full  => $"{First} {Last}";   // computed every read
}

// ── Local functions — helpers that live inside a method ──
double CalcInvoice(double[] lines, double taxRate)
{
    double subtotal = Sum(lines);
    double tax      = subtotal * taxRate;
    return subtotal + tax;

    // Local function — only callable from inside CalcInvoice
    static double Sum(double[] xs)
    {
        double total = 0;
        foreach (var x in xs) total += x;
        return total;
    }
}

// Use local functions to keep small helpers close to where they're used,
// without polluting the rest of your class.
Section 4 wrap — what you now know
  • Method = named, reusable block of code. Write the logic once, call it everywhere.
  • Parameters = inputs. Return type = output. void means "I do work but return nothing".
  • Overloading = same name, different parameter shapes. The compiler picks the right one based on what you pass.
  • Default values let parameters be optional: void Greet(string name = "world").
  • Expression-bodied methods (=>) keep one-liners tidy: int Square(int x) => x * x;
  • Naming rule of thumb: methods are verbs, properties are nouns. SendEmail() and EmailAddress — not the other way around.
05

Classes & Objects

You've been collecting variables one at a time — a customer's name, their email, their age. Three loose variables. Now add 1,000 customers and you've got 3,000 loose variables. It doesn't scale.

This is what classes solve. A class is a blueprint for a "thing" — it bundles related data (name, email, age) and the behaviour that goes with it (sendEmail, updateProfile) into a single named template.

An object is an actual instance built from that blueprint. Class = cookie cutter. Object = the cookie. You define the cookie cutter once and stamp out as many cookies as you need.

Brain Power

If a class bundles data and behaviour, why does C# also have something called a record? Aren't they doing the same job?

(Hint: class is for things that change over time — a User whose email gets updated. record is for immutable values — a Money(99.50, USD) you'd never edit, only replace.)

Class = "this thing has state and changes". Record = "this is just a value, treat it like a number". Once you can tell which one a particular concept is, half the design decisions in your codebase make themselves.
5.1

Your first class

Use the class keyword. Inside it you put fields (private storage), properties (controlled access to data), and methods (what the object can do).

// ── Define the blueprint ─────────────────────
public class Dog
{
    // FIELD — private storage (convention: _name)
    private int _energy = 100;

    // PROPERTIES — public, controlled access
    public string Name  { get; set; } = "";
    public int    Age   { get; set; }

    // METHODS — what a Dog can do
    public void Bark()
    {
        Console.WriteLine($"{Name}: Woof!");
        _energy -= 5;
    }

    public void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping...");
        _energy = 100;
    }

    public int GetEnergy() => _energy;
}

// ── Make objects (instances) ─────────────────
var rex  = new Dog { Name = "Rex",  Age = 4 };
var luna = new Dog { Name = "Luna", Age = 2 };

rex.Bark();     // Rex: Woof!
luna.Bark();    // Luna: Woof!
Console.WriteLine(rex.GetEnergy());   // 95

// Each object keeps its OWN data — changing rex.Age doesn't
// affect luna.Age. They're independent cookies from the same cutter.
5.2

Constructors, this & initializers

A constructor is a special method that runs when you create an object — it's where you set up its starting state. this refers to "the current object". Object initializers (the { Prop = value } syntax) are a shorter way to set properties at creation time.

public class Person
{
    public string Name { get; set; }
    public int    Age  { get; set; }

    // ── Constructor: same name as the class, no return type ──
    public Person(string name, int age)
    {
        // `this.Name` = the property of THIS object
        //  `name`     = the parameter passed in
        this.Name = name;
        this.Age  = age;
    }

    // ── Default constructor (no parameters) ──
    public Person()
    {
        Name = "Unknown";
        Age  = 0;
    }
}

// ── Use the constructors ─────────────────────
var anna = new Person("Anna", 30);
var nobody = new Person();              // uses the default ctor

// ── Object initializer — set properties at creation ──
var bob = new Person                    // calls default ctor first...
{
    Name = "Bob",                       // ...then sets these
    Age  = 25
};

// You can mix: parameters for the must-haves,
// initializer for the optionals.
5.3

Records vs classes

A record is a lightweight class designed for holding data. Records are immutable by default, compare by value (not by reference), and give you a constructor, properties, and ToString for free in one line.

// ── A classic class — needs lots of boilerplate ──
public class PointClass
{
    public int X { get; init; }
    public int Y { get; init; }
    public PointClass(int x, int y) { X = x; Y = y; }
}

// ── A record — same thing in ONE line ────────
public record Point(int X, int Y);

var p1 = new Point(1, 2);
var p2 = new Point(1, 2);

// ── Value equality (records compare by content) ──
Console.WriteLine(p1 == p2);            // True   ← !!!
// Classes compare by reference, so PointClass would print False.

// ── Friendly ToString for free ───────────────
Console.WriteLine(p1);                  // Point { X = 1, Y = 2 }

// ── Non-destructive update with `with` ───────
var p3 = p1 with { Y = 99 };            // copy p1 but change Y
Console.WriteLine(p3);                  // Point { X = 1, Y = 99 }

// ── When to use what? ────────────────────────
//   record →  data containers (DTOs, API models, value objects)
//   class  →  things with behaviour and changing state (services, etc.)
Section 5 wrap — what you now know
  • class = blueprint. object = instance built from the blueprint.
  • Fields hold data (private by convention). Properties expose controlled access. Methods are behaviour.
  • Constructors run when you write new — that's where the object's initial state gets set up.
  • record = immutable value type. Use it for "data that gets replaced, not edited" (money amounts, DTOs, events).
  • Use class for things that change over time (Users, Orders). Use record for data values (Money, Address).
  • Encapsulation: keep fields private, expose only what callers truly need. This is the single highest-leverage habit in object-oriented design.
06

Collections & LINQ basics

You've been working with one value at a time. Real programs are about many values: a list of orders, a dictionary of users, an array of scores, a set of allowed tags.

C# ships with collections — pre-built classes that handle the "many values" problem for you. List<T> grows and shrinks. Dictionary<K, V> looks things up by key. HashSet<T> stores unique values.

And then there's LINQ — the secret weapon. It's a mini-language for asking questions of any collection: "give me the top 5 customers by total spent" becomes a single readable line of code. It's the part of C# you'll fall in love with.

Brain Power

You have a list of 1,000 orders and you want to find one by its ID. You loop through with a foreach and check each one. That works — but on a list of 1,000,000 it's painfully slow.

What collection would you use instead? And why is it dramatically faster?

The answer is Dictionary<int, Order>. Looking up by key is O(1) — instant — regardless of size. List<T>.Find is O(n) — has to scan every element. Picking the right collection for the job is one of the highest-leverage choices you make. Three lines of code, 1,000× speed-up.
6.1

Arrays, List<T> & Dictionary<K,V>

Arrays are fixed-size. List<T> grows and shrinks. Dictionary<K,V> is a lookup table — keys map to values. The <T> means "of what type" (e.g. List<string> = a list of strings).

// ── Array — fixed size, fast ─────────────────
int[] scores = { 90, 85, 70 };
Console.WriteLine(scores[0]);          // 90
Console.WriteLine(scores.Length);      // 3
// scores.Add(60);   ❌ arrays don't grow

// ── List<T> — resizable ──────────────────────
List<string> cities = new() { "Athens", "Berlin" };

cities.Add("Paris");                   // CREATE
string first = cities[0];              // READ    → "Athens"
cities[1]    = "Madrid";               // UPDATE
cities.Remove("Paris");                // DELETE
Console.WriteLine(cities.Count);       // 2

// ── Dictionary<TKey, TValue> — lookups ───────
Dictionary<string, int> ages = new()
{
    ["Anna"] = 30,
    ["Bob"]  = 25
};

ages["Carol"] = 40;                    // CREATE
int annaAge = ages["Anna"];            // READ    → 30
ages["Bob"] = 26;                      // UPDATE
ages.Remove("Carol");                  // DELETE

// Safer read — avoid KeyNotFoundException:
if (ages.TryGetValue("Anna", out int a))
    Console.WriteLine($"Anna is {a}"); // Anna is 30
6.2

foreach & iterating collections

foreach is the easiest way to walk through every item in a collection. It works on arrays, lists, dictionaries, strings, and pretty much anything that holds a sequence of values.

// ── foreach on a list ────────────────────────
List<string> cities = new() { "Athens", "Berlin", "Paris" };

foreach (string city in cities)
{
    Console.WriteLine(city);
}
// → Athens / Berlin / Paris

// ── foreach on a dictionary ──────────────────
Dictionary<string, int> ages = new()
{
    ["Anna"] = 30,
    ["Bob"]  = 25
};

foreach (var kvp in ages)              // kvp = KeyValuePair
{
    Console.WriteLine($"{kvp.Key} is {kvp.Value}");
}
// → Anna is 30 / Bob is 25

// ── foreach on a string (it's a sequence of chars) ──
foreach (char c in "Hi!")
    Console.WriteLine(c);
// → H / i / !

// ── Need the index too? Use a classic for, or .Select ──
for (int i = 0; i < cities.Count; i++)
    Console.WriteLine($"{i}: {cities[i]}");
6.3

LINQ basics — Where, Select, OrderBy

LINQ (Language Integrated Query) lets you filter, sort, and transform collections with short, readable chains. Once you get the hang of it, you'll wonder how you lived without it.

using System.Linq;     // needed for LINQ (often implicit in .NET 8)

List<int> numbers = new() { 3, 1, 4, 1, 5, 9, 2, 6 };

// ── Where → keep items that match a condition ──
var evens = numbers.Where(n => n % 2 == 0).ToList();
// → [4, 2, 6]

// ── Select → transform each item ─────────────
var doubled = numbers.Select(n => n * 2).ToList();
// → [6, 2, 8, 2, 10, 18, 4, 12]

// ── Any / Count → quick questions ────────────
bool hasNine = numbers.Any(n => n == 9);       // true
int  bigOnes = numbers.Count(n => n > 3);      // 4 (4, 5, 9, 6)

// ── OrderBy / OrderByDescending → sort ───────
var sorted   = numbers.OrderBy(n => n).ToList();           // [1,1,2,3,4,5,6,9]
var biggest  = numbers.OrderByDescending(n => n).First();  // 9

// ── Chain them together — read top-to-bottom ──
var result = numbers
    .Where(n => n > 2)         // keep numbers > 2
    .Select(n => n * 10)       // multiply each by 10
    .OrderBy(n => n)           // sort ascending
    .ToList();
// → [30, 40, 50, 60, 90]

// Works the same on lists of objects:
List<Person> people = ...;
var adultNames = people
    .Where(p => p.Age >= 18)
    .OrderBy(p => p.Name)
    .Select(p => p.Name)
    .ToList();
Section 6 wrap — what you now know
  • List<T> = grows and shrinks, indexed access. The default container when you need "many of these".
  • Dictionary<K, V> = lookup by key in O(1). Use this whenever you find yourself looping to find one item.
  • HashSet<T> = unordered, no duplicates, fast contains-check. The right tool for "have I seen this before?".
  • LINQ = query language for any collection. The five verbs to master: Where, Select, OrderBy, GroupBy, Aggregate.
  • LINQ is lazy — nothing runs until you call ToList(), Count(), First(), etc. Knowing this prevents the "infinite loop on a database query" bug.
  • Picking the right collection for the operation you do most is one of the highest-leverage choices in performance.
07

Errors & Files

Until now every example "just worked". Welcome to reality. Real programs talk to the outside world, and the outside world is a chaos engine. Files go missing. Networks drop. Users paste "Carrot" into a number field. Disks fill up. Permissions get revoked.

C# handles trouble with exceptions — a structured way for code to say "something went wrong, here's what." You catch what you can fix, you let the rest bubble up, you log everything.

For files, the runtime ships with one-line helpers: File.ReadAllText, File.WriteAllLines, File.Exists. No streams, no buffers, no ceremony. Combine those two ideas — exceptions + file helpers — and you can write real programs that handle real-world failures gracefully.

Brain Power

Beginner reflex: wrap every method call in try/catch, because "what if it throws?"

Why is that actually the wrong habit? When should you catch — and when should you let the exception fly?

Rule of thumb: catch what you can handle, let everything else bubble up. If the file is missing and you have a default to fall back to — catch it. If the disk is full and you don't know what to do about it — don't catch it, let the program crash with a clear stack trace so the user (or the logs) can show what happened. Empty catch blocks that swallow errors are the #1 way bugs hide in production.
7.1

try / catch / finally & throw

Wrap risky code in a try block. If something goes wrong, the matching catch runs. finally always runs — perfect for cleanup. Use throw to signal that something is wrong from your own code.

// ── Basic try / catch ────────────────────────
try
{
    Console.Write("Enter a number: ");
    int n = int.Parse(Console.ReadLine()!);   // can throw if not a number
    Console.WriteLine($"100 / {n} = {100 / n}"); // can throw if n == 0
}
catch (FormatException)
{
    Console.WriteLine("That wasn't a number!");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Can't divide by zero.");
}
catch (Exception ex)                          // catch-all (last)
{
    Console.WriteLine($"Something else broke: {ex.Message}");
}
finally
{
    Console.WriteLine("Done. (always runs)");
}

// ── Throw your own exceptions ────────────────
void Withdraw(decimal balance, decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException("Amount must be positive.");
    if (amount > balance)
        throw new InvalidOperationException("Not enough funds.");
    // ... do the withdrawal ...
}

// Rule of thumb: catch ONLY exceptions you can do something
// useful about. Don't catch everything just to hide bugs.
7.2

Reading and writing text files

The System.IO.File class has one-liner helpers for the most common file jobs. No fiddly streams required — pass a path, get text back (or write text out).

using System.IO;

string path = "notes.txt";

// ── Write text to a file (overwrites if it exists) ──
File.WriteAllText(path, "Hello from C#!\n");

// ── Append text to the end of a file ─────────
File.AppendAllText(path, "Another line.\n");

// ── Write a list of lines (each item = one line) ──
string[] lines = { "Line 1", "Line 2", "Line 3" };
File.WriteAllLines(path, lines);

// ── Read the whole file back as one string ───
string all = File.ReadAllText(path);
Console.WriteLine(all);

// ── Read the file as an array of lines ───────
string[] back = File.ReadAllLines(path);
foreach (var line in back)
    Console.WriteLine(line);

// ── Check before you read ────────────────────
if (File.Exists(path))
{
    Console.WriteLine("File is there!");
}
else
{
    Console.WriteLine("Nope, missing.");
}

// File operations can throw IOException — wrap in try/catch
// if the file might be locked, read-only, or off a network share.
7.3

Putting it all together — a tiny app

A complete console app that uses everything you learned: a class, a list, LINQ, a file, and try/catch. Copy it into a fresh dotnet new console project and run it.

using System.IO;

// ── A small data model ───────────────────────
public record Task(string Title, bool Done);

// ── A "service" class that manages tasks ─────
public class TaskList
{
    private readonly List<Task> _tasks = new();
    private readonly string    _path;

    public TaskList(string path)
    {
        _path = path;
        if (File.Exists(_path))
        {
            foreach (var line in File.ReadAllLines(_path))
            {
                var parts = line.Split('|');
                _tasks.Add(new Task(parts[0], parts[1] == "1"));
            }
        }
    }

    public void Add(string title) => _tasks.Add(new Task(title, false));

    public IEnumerable<Task> Pending() =>
        _tasks.Where(t => !t.Done).OrderBy(t => t.Title);

    public void Save() =>
        File.WriteAllLines(_path,
            _tasks.Select(t => $"{t.Title}|{(t.Done ? 1 : 0)}"));
}

// ── Top-level program ────────────────────────
try
{
    var list = new TaskList("tasks.txt");
    list.Add("Learn C# basics");
    list.Add("Build a console app");

    Console.WriteLine("Pending tasks:");
    foreach (var t in list.Pending())
        Console.WriteLine($" - {t.Title}");

    list.Save();
    Console.WriteLine("Saved. Run again to see them load from disk!");
}
catch (Exception ex)
{
    Console.WriteLine($"Oops: {ex.Message}");
}

// What you used in 30 lines:
//   classes, records, properties, constructors, list<t>, linq,
//   string interpolation, file i/o, try/catch — the whole tutorial. ✅
Section 7 wrap — what you now know
  • Exception = structured signal that "something went wrong". Use try / catch / finally to handle it.
  • Catch what you can handle. Let everything else bubble up with a clear stack trace. Empty catch blocks are the #1 way bugs hide.
  • throw = raise an exception. throw new ArgumentException("...") tells the caller "you gave me invalid input".
  • File I/O in one line each: File.ReadAllText, File.ReadAllLines, File.WriteAllText, File.Exists.
  • using on disposable resources (files, connections, streams) guarantees clean-up — even if an exception is thrown.
  • Always log before you re-throw. Future-you debugging at 2am will thank present-you for the breadcrumbs.
Mastery Move — where to go next

You've now seen all 7 building blocks of C#. To master the language, the natural next jumps are:

1. OOP & SOLID for Dummies — turn the class basics from Section 5 into proper object-oriented design. Encapsulation, inheritance, polymorphism, the SOLID principles. Every senior C# job interview tests this material.

2. C# Deep-Dive Tutorial — async/await, generics, advanced LINQ, pattern matching, modern C# 9–13 features. The "now I can read any codebase" tier.

3. ASP.NET Core Minimal API — build your first REST API in .NET 8. The fastest way to put what you've learned online.

4. Algorithms & Data Structures — for technical interviews and writing genuinely fast .NET code.

You don't need to do these in order — pick the one that matches your goal. Coming back here to refresh the basics is allowed and encouraged.