Home /C# /Crash Course

C# Course

C# Crash Course

The Essential Reference Guide — Modern C# 12 & .NET

1. Hello World & Program Structure

Modern C# (9+) supports top-level statements — you no longer need a class or Main method for simple programs. The traditional structure still works and is still used in larger projects.

// Modern C# — top-level statements (C# 9+)
Console.WriteLine("Hello, World!");
Console.WriteLine("Welcome to C#!");
// Traditional structure — still required in larger projects
namespace MyApp;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

C# vs Java

Coming from Java? C# is very similar — both are compiled, statically typed, and OOP-first. Key differences: C# has var, LINQ, async/await as first-class features, and uses Console.WriteLine instead of System.out.println.

2. Variables & Data Types

C# is statically typed. Use var for local type inference — the compiler still enforces the type.

// Explicit types
int     age     = 25;
double  gpa     = 3.85;
bool    active  = true;
char    grade   = 'A';
string  name    = "Ayman";
decimal price   = 9.99m;     // m suffix for decimal (financial)

// var — inferred by compiler
var score   = 95;             // int
var message = "Hello";        // string

// Nullable types — can hold null
int?    nullableInt    = null;
string? nullableString = null;

// Constants
const double PI = 3.14159;

// String interpolation
Console.WriteLine($"Name: {name}, Score: {score}");

Tip

Use string? (nullable reference type) to make your intent explicit. Enable <Nullable>enable</Nullable> in your .csproj for compiler warnings on null misuse.

3. Operators

// Arithmetic
Console.WriteLine(10 + 3);   // 13
Console.WriteLine(10 / 3);   // 3  (integer division)
Console.WriteLine(10 % 3);   // 1  (remainder)
Console.WriteLine(2 * 3.0);   // 6.0

// Null-coalescing — return right side if left is null
string? input  = null;
string  result = input ?? "default";   // "default"

// Null-conditional — safe member access
int? len = input?.Length;              // null (no crash)

// Null-coalescing assignment
input ??= "fallback";                  // sets only if null

// Logical
Console.WriteLine(true && false);   // false
Console.WriteLine(true || false);   // true
Console.WriteLine(!true);           // false

4. Control Flow

int score = 85;

// if / else if / else
if (score >= 90)      Console.WriteLine("A");
else if (score >= 80) Console.WriteLine("B");   // ← runs
else                   Console.WriteLine("F");

// Ternary
string pass = (score >= 60) ? "Pass" : "Fail";

// Switch expression (C# 8+) — elegant and exhaustive
string letter = score switch
{
    >= 90              => "A",
    >= 80 and < 90   => "B",
    >= 70              => "C",
    _                  => "F"    // _ is the discard / default
};

// Switch on type (pattern matching)
object obj = 42;
string desc = obj switch
{
    int n    => $"Integer: {n}",
    string s => $"String: {s}",
    _         => "Unknown"
};

Modern C#

The switch expression (not statement) is the preferred modern form — it's an expression (returns a value), requires exhaustive cases, and eliminates fall-through bugs entirely.

5. Loops

// for
for (int i = 0; i < 5; i++)
    Console.WriteLine(i);

// while
int count = 0;
while (count < 3) { Console.WriteLine(count); count++; }

// foreach — the most common loop in C#
string[] names = { "Ayman", "Sara", "Ali" };
foreach (var n in names)
    Console.WriteLine(n);

// do-while
do { Console.WriteLine("runs at least once"); } while (false);

// break / continue
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;   // skip 3
    if (i == 7) break;      // stop at 7
    Console.Write(i + " ");
}

6. Arrays & Collections

// Array — fixed size
int[] scores = { 90, 85, 78 };
Console.WriteLine(scores[0]);        // 90
Console.WriteLine(scores.Length);    // 3

// List<T> — dynamic, most common collection
var names = new List<string> { "Ayman", "Sara" };
names.Add("Ali");
names.Remove("Sara");
Console.WriteLine(names.Count);       // 2
Console.WriteLine(names.Contains("Ali"));  // True

// Dictionary<K,V> — key-value pairs
var grades = new Dictionary<string, int>
{
    ["Ayman"] = 95,
    ["Sara"]  = 88
};
grades["Ali"] = 79;
Console.WriteLine(grades["Ayman"]);  // 95

// HashSet<T> — unique values, no duplicates
var tags = new HashSet<string> { "cs", "python", "cs" };
Console.WriteLine(tags.Count);        // 2 — duplicate removed

7. Methods

// Standard method
static int Add(int a, int b) => a + b;     // expression body

// Optional parameters
static void Greet(string name, string prefix = "Hello")
    => Console.WriteLine($"{prefix}, {name}!");

Greet("Ayman");              // Hello, Ayman!
Greet("Sara", "Welcome");   // Welcome, Sara!

// Named arguments
Greet(prefix: "Hi", name: "Ali");

// params — variable number of arguments
static int Sum(params int[] nums)
    => nums.Sum();    // using LINQ Sum()

Console.WriteLine(Sum(1, 2, 3, 4));   // 10

// Local function — defined inside another method
static int Factorial(int n)
{
    return Inner(n);
    int Inner(int x) => x <= 1 ? 1 : x * Inner(x - 1);
}

8. OOP & Classes

public class Student
{
    // Auto-implemented properties — cleaner than Java getters/setters
    public string Name  { get; set; }
    public int    Grade { get; init; }   // init = set once at construction

    // Constructor
    public Student(string name, int grade)
        => (Name, Grade) = (name, grade);   // tuple assignment

    // Computed property
    public string LetterGrade => Grade switch
    {
        >= 90 => "A", >= 80 => "B",
        >= 70 => "C", _    => "F"
    };

    // Override ToString
    public override string ToString()
        => $"{Name} → {LetterGrade} ({Grade})";
}

// Usage
var s = new Student("Ayman", 92);
Console.WriteLine(s);          // Ayman → A (92)

// Object initializer syntax
var s2 = new Student("Sara", 85) { Name = "Sara A." };

Records (C# 9+)

For immutable data objects, use record instead of class. Records auto-generate equality, ToString, and with expressions for non-destructive mutation. See Section 12.

9. Interfaces & Generics

// Interface — defines a contract
public interface IPrintable
{
    void Print();
    string Summary { get; }   // interfaces can have properties
}

// Implement the interface
public class Report : IPrintable
{
    public string Title { get; set; } = "";
    public string Summary => $"Report: {Title}";
    public void Print() => Console.WriteLine(Summary);
}

// Generic class — T is a type parameter
public class Box<T>
{
    public T Value { get; }
    public Box(T value) => Value = value;
    public override string ToString() => $"Box[{Value}]";
}

var sBox = new Box<string>("Hello");
var iBox = new Box<int>(42);
Console.WriteLine(sBox);    // Box[Hello]
Console.WriteLine(iBox);    // Box[42]

// Generic method with constraint
static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(10, 20));          // 20
Console.WriteLine(Max("apple", "mango")); // mango

10. LINQ

LINQ (Language Integrated Query) lets you query any collection using readable, SQL-like syntax directly in C#. It works on arrays, lists, databases, XML, and more.

using System.Linq;

int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Method syntax (most common in production)
var evens  = nums.Where(n => n % 2 == 0);         // [2,4,6,8,10]
var squares = nums.Select(n => n * n);             // [1,4,9,16...]
var top3   = nums.OrderByDescending(n => n)
                  .Take(3);                          // [10,9,8]

Console.WriteLine(nums.Sum());                     // 55
Console.WriteLine(nums.Average());                 // 5.5
Console.WriteLine(nums.Max());                     // 10
Console.WriteLine(nums.Any(n => n > 9));           // True
Console.WriteLine(nums.All(n => n > 0));           // True
Console.WriteLine(nums.Count(n => n % 2 == 0));   // 5

// Query syntax (closer to SQL)
var query =
    from n in nums
    where n % 2 == 0
    orderby n descending
    select n * n;   // [100, 64, 36, 16, 4]

// LINQ on objects
var students = new[] {
    new { Name = "Ayman", Grade = 95 },
    new { Name = "Sara",  Grade = 88 },
    new { Name = "Ali",   Grade = 72 }
};

var top = students
    .Where(s => s.Grade >= 85)
    .OrderByDescending(s => s.Grade)
    .Select(s => s.Name);

foreach (var name in top)
    Console.WriteLine(name);   // Ayman, Sara

LINQ is Lazy

LINQ queries are not executed until you iterate them (deferred execution). Call .ToList() or .ToArray() to materialise the results immediately and avoid repeated evaluation.

11. Async / Await

async and await let you write non-blocking code that reads like synchronous code. Essential for web requests, file I/O, and database calls.

using System.Net.Http;

// Mark a method async — it returns Task or Task<T>
static async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    // await suspends this method without blocking the thread
    string content = await client.GetStringAsync(url);
    return content;
}

// Call an async method with await
var data = await FetchDataAsync("https://example.com");
Console.WriteLine(data.Length);

// Run multiple tasks in parallel
var task1 = FetchDataAsync("https://a.com");
var task2 = FetchDataAsync("https://b.com");
var results = await Task.WhenAll(task1, task2);

// async void — only for event handlers (never use otherwise)
static async void OnButtonClick(object sender, EventArgs e)
{
    await Task.Delay(1000);   // non-blocking 1-second pause
    Console.WriteLine("Done");
}

Warning

Never call .Result or .Wait() on a Task from synchronous code — it causes deadlocks. Always await it instead, and make the calling method async too.

12. Modern C# Features

C# evolves rapidly. These features from C# 9–12 are now standard in modern codebases.

Records — immutable data objects

// record — auto-generates constructor, ToString, equality
public record Point(double X, double Y);

var p1 = new Point(1.0, 2.0);
var p2 = new Point(1.0, 2.0);
Console.WriteLine(p1 == p2);       // True — value equality
Console.WriteLine(p1);              // Point { X = 1, Y = 2 }

// 'with' — non-destructive mutation
var p3 = p1 with { Y = 99.0 };      // new Point(1.0, 99.0)

Pattern Matching

object shape = new { Width = 4, Height = 5 };

// is pattern
if (shape is { } s)
    Console.WriteLine("Not null");

// List patterns (C# 11)
int[] arr = { 1, 2, 3 };
if (arr is [1, .., 3])
    Console.WriteLine("Starts with 1, ends with 3");

Primary Constructors (C# 12)

// Parameters available throughout the class body
public class Student(string name, int grade)
{
    public string Name  => name;
    public int    Grade => grade;
    public override string ToString() => $"{name}: {grade}";
}

Delegates & Lambda Expressions

// Func<in, out> — built-in delegate for methods that return a value
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4));      // 7

// Action<in> — built-in delegate for void methods
Action<string> print = msg => Console.WriteLine(msg);
print("Hello!");

// Predicate — returns bool (used in LINQ)
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4));      // True

// Pass a lambda to a method
var evens = new[] { 1, 2, 3, 4 }
    .Where(n => n % 2 == 0)
    .ToList();