.NET Full Course with C#

A comprehensive guide — 29 chapters across 8 parts


Part I: C# Fundamentals

Chapter 1

Introduction to .NET and C#

C# is a modern, type-safe, object-oriented programming language developed by Microsoft. It runs on the .NET platform and is widely used for web, desktop, mobile, cloud, and game development. .NET provides a comprehensive runtime environment (CoreCLR), a massive class library (BCL), and tools for building virtually any kind of application.

.NET Platform Architecture .NET Runtime (CoreCLR) Base Class Library (BCL) ASP.NET Core Web / API / MVC EF Core ORM / Data MAUI / WPF Desktop / Mobile Azure / ML.NET Cloud / ML Languages: C# / F# / VB.NET Tooling: VS / VS Code / Rider / .NET CLI Platforms: Windows / Linux / macOS / Docker / Cloud Open-source (MIT) · Cross-platform · High-performance .NET 6+ unifies all workloads under a single platform

Key Components of .NET

Hello, World!

The simplest C# program in modern .NET uses top-level statements (C# 9+):

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

This single line is a complete program. The traditional approach with an explicit Main method:

csharpusing System;

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

The C# compiler (Roslyn) compiles source code into Intermediate Language (IL) stored in an assembly. At runtime, the JIT compiler within CoreCLR compiles IL methods into native machine code as they are called. Modern .NET also supports Ahead-of-Time (AOT) compilation via Native AOT.

C# Version History

VersionReleasedKey Features
C# 1.02002Managed code, classes, structs, interfaces, events
C# 2.02005Generics, nullable types, anonymous methods, iterators
C# 3.02007LINQ, lambda expressions, extension methods, anonymous types
C# 4.02010Dynamic binding, named/optional params, co/contravariance
C# 5.02012Async/await, caller info attributes
C# 6.02015String interpolation, null-conditional, expression-bodied members
C# 7.0+2017Tuples, pattern matching, ref locals/returns, local functions
C# 8.02019Nullable reference types, async streams, default interface methods
C# 9.02020Records, init-only setters, top-level statements, pattern matching
C# 102021Global usings, file-scoped namespaces, record structs
C# 112022Raw string literals, generic math, required members, list patterns
C# 122023Primary constructors, collection expressions, aliases for any type

Key Takeaways

  • .NET is a cross-platform, open-source runtime and library ecosystem.
  • C# is the primary .NET language — modern, type-safe, and multi-paradigm.
  • C# compiles to IL, which is JIT-compiled to native code at runtime.
  • The language has evolved rapidly; stay current with the latest version.

Chapter 2

Variables, Data Types, and Operators

C# has a unified type system where all types inherit from a common base type: object (System.Object). Types fall into two categories: value types (stored on the stack) and reference types (stored on the heap, GC-managed).

C# Type System Value Types (Stack) Stored inline, copied, no GC Reference Types (Heap) Via pointer, GC-managed int, long, byte float, double, decimal bool, char struct, enum string class, record interface delegate, object Boxing: Value → object Heap alloc, performance cost Unboxing: object → Value Type check required Generics avoid boxing List<int> — no boxing cost Nullable<T> enables null for value types · Span<T> provides stack-allocated memory views

Value Types

CategoryTypesSizeRange
Integralbyte, sbyte8-bit0 to 255, -128 to 127
Integralshort, ushort16-bit-32,768 to 32,767; 0 to 65,535
Integralint, uint32-bit±2.1 billion; 0 to 4.3 billion
Integrallong, ulong64-bit±9.2 quintillion
Floatingfloat32-bit±1.5e-45 to ±3.4e38 (7 digits)
Floatingdouble64-bit±5.0e-324 to ±1.7e308 (15-16 digits)
Decimaldecimal128-bit28-29 digits (financial)
Booleanbooltrue / false
Characterchar16-bitUnicode (U+0000 to U+FFFF)

Variables

csharpint age = 25;
string name = "Alice";
var price = 19.99m;          // inferred as decimal
var message = "Hello";       // inferred as string
int x = 10, y = 20, z = 30; // multiple declarations

Operators

Arithmetic

csharpint sum = 10 + 5;         // 15
int quotient = 10 / 3;     // 3 (integer truncation)
double result = 10.0 / 3;  // 3.333...
int remainder = 10 % 3;    // 1

Null-Conditional (C# 6+)

csharpstring? name = GetName();
int length = name?.Length ?? 0;   // null-conditional + coalescing
name ??= "Default";               // null-coalescing assignment (C# 8+)

Type Conversions

csharpint num = 100;
long big = num;            // implicit
double d = num;            // implicit

double pi = 3.14159;
int truncated = (int)pi;   // explicit cast: 3

int parsed = int.Parse("123");
bool ok = int.TryParse("123", out int result);  // safe parsing

Key Takeaways

  • Value types stack-allocated; reference types heap-allocated (GC-managed).
  • Use var when type is obvious from the right side.
  • Prefer TryParse over Parse for user input.
  • Use decimal for financial, double for scientific computations.

Chapter 3

Control Flow and Iterations

Conditional Statements

if/else if/else

csharpint score = 85;
string grade = score >= 90 ? "A" : score >= 80 ? "B" : "C";

Switch Expression (C# 8+)

csharpstring category = day switch
{
    "Saturday" or "Sunday" => "Weekend",
    "Monday" => "Start",
    "Friday" => "End",
    _ => "Midweek"
};

Pattern Matching (C# 7+)

csharpstring Describe(object obj) => obj switch
{
    int n when n < 0 => "Negative",
    int n when n > 0 => "Positive",
    int _ => "Zero",
    string s => $"String: {s}",
    null => "Null",
    _ => "Unknown"
};

Loops

for

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

foreach

csharpint[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers)
    Console.WriteLine(num);

while / do-while

csharpint count = 0;
while (count < 5) count++;

int num;
do { /* runs at least once */ }
while (condition);

Jump Statements

csharpfor (int i = 0; i < 10; i++)
{
    if (i == 5) break;      // exit loop
    if (i % 2 == 0) continue; // skip even
    Console.Write(i);        // 1 3 5 7 9
}

Error Handling

csharptry
{
    int result = Divide(10, 0);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
finally
{
    Console.WriteLine("Cleanup runs always");
}

int Divide(int a, int b) => a / b;

Key Takeaways

  • Prefer switch expressions over switch statements for value-returning logic.
  • Use pattern matching with when clauses for complex conditions.
  • foreach is safer and more readable than indexed loops.
  • Always handle specific exceptions before general ones.

Chapter 4

Methods and Parameters

Methods promote code reuse and modularity. C# supports multiple parameter passing modes: by value, ref, out, in, and params.

Method Anatomy

csharppublic int Add(int a, int b) => a + b;
void PrintSum(int a, int b) => Console.WriteLine(a + b);

Parameter Passing

Pass by Value (default)

csharpvoid Modify(int x) { x = 100; }  // not visible to caller

ref — reference to variable

csharpvoid Swap(ref int a, ref int b)
{
    int t = a; a = b; b = t;
}
int x = 1, y = 2;
Swap(ref x, ref y);  // x=2, y=1

out — must assign before return

csharpbool TryParseInt(string input, out int result)
    => int.TryParse(input, out result);
if (TryParseInt("42", out int n)) Console.WriteLine(n);

in — read-only reference

csharpvoid Display(in int value) => Console.WriteLine(value);

params — variable arguments

csharpint Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3, 4, 5);  // 15

Named & Optional Parameters

csharpvoid CreateUser(string name, int age = 18, bool active = true) { }
CreateUser(name: "Alice", age: 30);
CreateUser("Bob", active: false);

Local Functions (C# 7+)

csharpint Fibonacci(int n)
{
    int Fib(int a, int b, int count) =>
        count == 0 ? a : Fib(b, a + b, count - 1);
    return Fib(0, 1, n);
}
Console.WriteLine(Fibonacci(10));  // 55

Overloading

csharppublic int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;

Key Takeaways

  • Use out for return-by-reference patterns; ref sparingly.
  • Expression-bodied members keep simple methods concise.
  • Local functions encapsulate helper logic within a method.
  • Named arguments improve readability with many optional params.

Part II: Object-Oriented Programming

Chapter 5

Classes and Objects

A class is a blueprint for creating objects. It defines data (fields, properties) and behaviors (methods, events). C# is fundamentally object-oriented, and classes are the primary building blocks.

Defining a Class

csharppublic class Person
{
    private string _name;
    private int _age;

    public Person(string name, int age) { _name = name; _age = age; }

    public string Name
    {
        get => _name;
        set => _name = value ?? throw new ArgumentNullException();
    }

    public int Age
    {
        get => _age;
        set => _age = value >= 0 ? value : throw new ArgumentException();
    }

    public string Email { get; set; }  // auto-property
    public DateTime CreatedAt { get; } = DateTime.UtcNow;  // read-only

    public string GetGreeting() => $"Hi, I'm {Name}, {Age}.";
    public override string ToString() => $"{Name} ({Age})";
}

Creating Objects

csharpPerson alice = new Person("Alice", 30);
var bob = new Person("Bob", 25) { Email = "bob@example.com" };
Person carol = new("Carol", 28);  // target-typed new (C# 9+)
Console.WriteLine(alice.GetGreeting());

Records (C# 9+)

Records provide value-based equality, immutability, and concise syntax:

csharppublic record PersonRecord(string Name, int Age);

var p1 = new PersonRecord("Alice", 30);
var p2 = new PersonRecord("Alice", 30);
Console.WriteLine(p1 == p2);  // true (value equality)
var p3 = p1 with { Age = 31 }; // non-destructive mutation

// Record struct (C# 10):
public readonly record struct Point(int X, int Y);

Static Members

csharppublic class MathUtils
{
    public static readonly double PI = 3.14159265;
    public static int Add(int a, int b) => a + b;
}

Console.WriteLine(MathUtils.PI);     // via type name
int sum = MathUtils.Add(3, 4);       // 7

Key Takeaways

  • Classes define object structure and behavior.
  • Use properties (not public fields) for encapsulation.
  • Records are ideal for immutable data carriers with value equality.
  • Static members belong to the type, not instances.

Chapter 6

Inheritance and Polymorphism

Inheritance allows a class to derive from another, reusing and extending behavior. C# supports single class inheritance but multiple interface implementation.

Inheritance Hierarchy Vehicle (Base Class) Car : Vehicle Bike : Vehicle Truck : Vehicle SportsCar : Car SUV : Car ElectricCar : Car, IElectric

Base and Derived Classes

csharppublic class Vehicle
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }

    public Vehicle(string make, string model, int year)
        => (Make, Model, Year) = (make, model, year);

    public virtual void StartEngine()
        => Console.WriteLine("Engine started");
}

public class Car : Vehicle
{
    public int NumberOfDoors { get; set; }

    public Car(string make, string model, int year, int doors)
        : base(make, model, year) => NumberOfDoors = doors;

    public override void StartEngine()
    {
        base.StartEngine();
        Console.WriteLine("Car engine running smoothly");
    }
}

Polymorphism

csharpList<Vehicle> vehicles = new()
{
    new Car("Toyota", "Camry", 2022, 4),
    new Bike("Harley", "Sportster", 2023, true),
    new Truck("Ford", "F-150", 2021, 3000)
};

foreach (Vehicle v in vehicles)
    v.StartEngine();  // polymorphic call

Abstract Classes

csharppublic abstract class Shape
{
    public string Color { get; set; }
    public Shape(string color) => Color = color;
    public abstract double GetArea();
    public virtual void Display()
        => Console.WriteLine($"{Color} shape area: {GetArea():F2}");
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public Circle(string color, double r) : base(color) => Radius = r;
    public override double GetArea() => Math.PI * Radius * Radius;
}

var shapes = new Shape[] { new Circle("Red", 5), new Circle("Blue", 3) };
foreach (var s in shapes) s.Display();

Access Modifiers

ModifierVisibility
publicAnywhere
privateSame class only
protectedClass + derived classes
internalSame assembly
protected internalDerived classes OR same assembly
private protectedDerived classes in same assembly (C# 7.2+)

Key Takeaways

  • Inheritance enables 'is-a' relationships and code reuse.
  • Polymorphism treats derived objects through base type.
  • Abstract classes define contracts with partial implementation.
  • Prefer composition over inheritance for 'has-a' relationships.

Chapter 7

Interfaces and Abstraction

An interface defines a contract — signatures without implementation. Classes implementing an interface must provide all member implementations. Interfaces enable polymorphism and loose coupling.

Defining and Implementing Interfaces

csharppublic interface IRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

public class InMemoryRepo<T> : IRepository<T> where T : class
{
    private readonly Dictionary<int, T> _data = new();
    private int _nextId = 1;

    public T GetById(int id) =>
        _data.TryGetValue(id, out var item) ? item : null;

    public IEnumerable<T> GetAll() => _data.Values;
    public void Add(T entity) { _data[_nextId++] = entity; }
    public void Update(T entity) { /* find and replace */ }
    public void Delete(int id) => _data.Remove(id);
}

Explicit Interface Implementation

csharppublic interface IWriter { void Write(string text); }
public interface ILogger { void Write(string msg); }

public class FileManager : IWriter, ILogger
{
    void IWriter.Write(string text)
        => Console.WriteLine($"Writing: {text}");
    void ILogger.Write(string msg)
        => Console.WriteLine($"Logging: {msg}");

    public void WriteBoth(string content)
    {
        ((IWriter)this).Write(content);
        ((ILogger)this).Write(content);
    }
}

Default Interface Methods (C# 8+)

csharppublic interface INotifier
{
    void Send(string message);
    void SendWithLog(string message)  // default implementation
    {
        Console.WriteLine($"Sending: {message}");
        Send(message);
    }
}

public class EmailNotifier : INotifier
{
    public void Send(string message)
        => Console.WriteLine($"Email: {message}");
    // SendWithLog inherited from interface
}

var n = new EmailNotifier();
((INotifier)n).SendWithLog("Hi");

Interface Segregation

csharp// Prefer small, focused interfaces:
public interface IPrinter { void Print(); }
public interface IScanner { void Scan(); }

public class MultiFunctionPrinter : IPrinter, IScanner
{
    public void Print() => Console.WriteLine("Printing...");
    public void Scan() => Console.WriteLine("Scanning...");
}

Key Takeaways

  • Interfaces define contracts; classes implement them.
  • Explicit implementation resolves naming conflicts.
  • Default interface methods allow adding members without breaking implementers.
  • Follow ISP — keep interfaces small and focused.

Chapter 8

Generics

Generics enable type-safe, reusable code without boxing or casting. Before generics, collections stored object, causing boxing overhead and runtime type errors.

Generic Methods

csharppublic static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;

int max = Max(5, 10);              // 10
double d = Max(3.14, 2.71);        // 3.14
string s = Max("apple", "zebra");  // "zebra"

Generic Classes

csharppublic class Stack<T>
{
    private T[] _items = new T[4];
    private int _count = 0;

    public void Push(T item)
    {
        if (_count == _items.Length)
            Array.Resize(ref _items, _items.Length * 2);
        _items[_count++] = item;
    }

    public T Pop() => _count > 0
        ? _items[--_count]
        : throw new InvalidOperationException("Empty");

    public T Peek() => _count > 0 ? _items[_count - 1]
        : throw new InvalidOperationException("Empty");

    public int Count => _count;
}

var stack = new Stack<int>();
stack.Push(10); stack.Push(20);
Console.WriteLine(stack.Pop());  // 20

Generic Constraints

csharppublic class Repo<T> where T : class, IEntity, new() { }
// T must be: reference type + IEntity + parameterless ctor

// Constraint types:
// where T : struct      — value type
// where T : class       — reference type
// where T : notnull     — non-nullable (C# 8+)
// where T : new()       — parameterless ctor
// where T : Enum        — enum (C# 7.3+)
// where T : Delegate    — delegate (C# 7.3+)
// where T : unmanaged   — unmanaged (C# 7.3+)

Covariance and Contravariance

csharp// Covariance (out) — T in output positions only
IEnumerable<string> strings = new[] { "a", "b" };
IEnumerable<object> objs = strings;  // OK

// Contravariance (in) — T in input positions only
IComparer<object> objComp = /* ... */;
IComparer<string> strComp = objComp;  // OK

Key Takeaways

  • Generics provide type safety without boxing overhead.
  • Constraints specify what types are valid for your generic.
  • Covariance (out) and contravariance (in) enable flexible type relationships.
  • Generic math (C# 11) enables type-safe arithmetic with INumber.

Part III: Collections and LINQ

Chapter 9

Arrays and Collections

Arrays are fixed-size collections. The System.Collections.Generic namespace provides dynamic collections: List, Dictionary, HashSet, Queue, Stack, and LinkedList.

Arrays

csharpint[] primes = { 2, 3, 5, 7, 11 };
Console.WriteLine(primes[0]);  // 2
Array.Sort(primes);
Array.Reverse(primes);
int idx = Array.IndexOf(primes, 7);
bool exists = Array.Exists(primes, n => n > 10);

// Multidimensional
int[,] matrix = { { 1, 2 }, { 3, 4 } };
// Jagged
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };

List

csharpList<string> fruits = new() { "Apple", "Banana" };
fruits.Add("Cherry");
fruits.AddRange(new[] { "Date", "Elderberry" });
fruits.Insert(1, "Blueberry");
fruits.Remove("Banana");
fruits.Sort();
int idx = fruits.IndexOf("Cherry");
bool has = fruits.Contains("Apple");
var filtered = fruits.Where(f => f.Length > 5).ToList();

Dictionary

csharpDictionary<string, int> ages = new()
{
    { "Alice", 30 }, { "Bob", 25 }
};
ages["Carol"] = 28;

if (ages.TryGetValue("Alice", out int age))
    Console.WriteLine(age);

foreach (var (name, years) in ages)
    Console.WriteLine($"{name}: {years}");

bool hasKey = ages.ContainsKey("David");
ages.Remove("Bob");

HashSet

csharpHashSet<int> set = new() { 1, 2, 3, 4, 5 };
set.Add(3);  // no-op (already present)
bool added = set.Add(6);  // true

HashSet<int> other = new() { 4, 5, 6, 7 };
set.UnionWith(other);       // 1,2,3,4,5,6,7
set.IntersectWith(other);   // 4,5,6
set.ExceptWith(other);
bool isSubset = set.IsSubsetOf(other);

Queue and Stack

csharpQueue<string> q = new();
q.Enqueue("First"); q.Enqueue("Second");
string next = q.Dequeue();  // "First"
string peek = q.Peek();     // "Second"

Stack<int> s = new();
s.Push(10); s.Push(20);
int top = s.Pop();  // 20

Performance Comparison

CollectionAccessSearchInsert/Delete
ArrayO(1)O(n)O(n)
List<T>O(1)O(n)O(n) / O(1)*
Dictionary<K,V>O(1)O(1)O(1)
HashSet<T>O(1)O(1)O(1)
Queue<T>O(1) peekO(n)O(1)
Stack<T>O(1) peekO(n)O(1)
LinkedList<T>O(n)O(n)O(1)

Key Takeaways

  • Choose the right collection for access, search, and mutation patterns.
  • Dictionary and HashSet provide O(1) lookups via hash codes.
  • List is the most versatile dynamic array.
  • Consider LinkedList for frequent insertions in the middle.

Chapter 10

LINQ Fundamentals

LINQ (Language Integrated Query) is a set of features that extends powerful query capabilities to the C# language syntax. LINQ enables you to query any data source that implements IEnumerable or IQueryable.

LINQ Architecture LINQ to Objects IEnumerable<T> LINQ to SQL IQueryable / SQL LINQ to XML XDocument LINQ to Entities EF Core Query Expression or Fluent Syntax from x in source where x.Price > 10 select x Standard Query Operators Where, Select, OrderBy, GroupBy, Join, Aggregate, Skip, Take, First, Count ... Deferred vs Immediate Execution IEnumerable — lazy; .ToList() / .Count() / .First() — eager

Query Expression Syntax

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

var evenNumbers = from n in numbers
                  where n % 2 == 0
                  orderby n descending
                  select n * 2;

// Result: 20, 16, 12, 8, 4

Fluent Syntax (Method Syntax)

csharpvar result = numbers
    .Where(n => n % 2 == 0)
    .OrderByDescending(n => n)
    .Select(n => n * 2)
    .ToList();

Common LINQ Operators

Filtering

csharpvar adults = people.Where(p => p.Age >= 18);
var first = people.OfType<Student>();  // filter by type

Projection

csharpvar names = people.Select(p => p.Name);
var anon = people.Select(p => new { p.Name, p.Age });

Ordering

csharpvar sorted = people.OrderBy(p => p.Name)
    .ThenByDescending(p => p.Age);

Aggregation

csharpint count = numbers.Count();
int sum = numbers.Sum();
double avg = numbers.Average();
int min = numbers.Min();
int max = numbers.Max();

Element Operators

csharpvar first = numbers.First();
var firstEven = numbers.First(n => n % 2 == 0);
var single = numbers.Single(n => n == 5);  // throws if not exactly one
var last = numbers.Last();
var atPos = numbers.ElementAt(3);

Quantifiers

csharpbool any = numbers.Any(n => n < 0);    // false
bool all = numbers.All(n => n > 0);    // true
bool contains = numbers.Contains(5);  // true

Deferred vs Immediate Execution

csharpvar query = numbers.Where(n => n > 5);  // NOT executed yet
numbers[0] = 100;  // changes affect the query!
var result = query.ToList();  // NOW executed (ToList forces it)

// Methods that force immediate execution:
// ToList(), ToArray(), ToDictionary(), ToHashSet()
// Count(), Sum(), Average(), Min(), Max()
// First(), Last(), Single(), Any(), All()

Key Takeaways

  • LINQ provides a consistent query model across data sources.
  • Deferred execution means queries run when enumerated, not when defined.
  • Fluent and query syntax are equivalent; use what's more readable.
  • Be aware of deferred execution when the data source changes.

Chapter 11

Advanced LINQ

GroupBy

csharpvar students = new[]
{
    new { Name = "Alice", Grade = "A", Age = 20 },
    new { Name = "Bob", Grade = "B", Age = 19 },
    new { Name = "Carol", Grade = "A", Age = 21 }
};

var byGrade = students.GroupBy(s => s.Grade);

foreach (var group in byGrade)
{
    Console.WriteLine($"Grade {group.Key}: {group.Count()} students");
    foreach (var s in group)
        Console.WriteLine($"  {s.Name}");
}

Join

csharpvar orders = new[] { /* order data */ };
var customers = new[] { /* customer data */ };

var query = from o in orders
            join c in customers on o.CustomerId equals c.Id
            select new { c.Name, o.Total, o.Date };

// Group join
var customersWithOrders = from c in customers
    join o in orders on c.Id equals o.CustomerId into orderGroup
    select new { c.Name, Orders = orderGroup };

SelectMany (Flatten)

csharpvar classes = new[]
{
    new { Name = "Math", Students = new[] { "A", "B" } },
    new { Name = "Science", Students = new[] { "C", "D" } }
};

var allStudents = classes.SelectMany(c => c.Students);
// "A", "B", "C", "D"

Zip

csharpvar names = new[] { "Alice", "Bob", "Carol" };
var scores = new[] { 85, 92, 78 };
var combined = names.Zip(scores, (n, s) => $"{n}: {s}");
// "Alice: 85", "Bob: 92", "Carol: 78"

Set Operations

csharpint[] set1 = { 1, 2, 3, 4 };
int[] set2 = { 3, 4, 5, 6 };

var union = set1.Union(set2);       // 1,2,3,4,5,6
var intersect = set1.Intersect(set2); // 3,4
var except = set1.Except(set2);     // 1,2
var distinct = set1.Distinct();     // remove duplicates

Aggregate

csharpint[] nums = { 1, 2, 3, 4, 5 };

// Manual sum using Aggregate
int sum = nums.Aggregate((acc, n) => acc + n);  // 15

// With seed
int product = nums.Aggregate(1, (acc, n) => acc * n);  // 120

// String building
string csv = nums.Aggregate("", (acc, n) => acc == ""
    ? n.ToString() : $"{acc},{n}");  // "1,2,3,4,5"

Deferred Execution Gotchas

csharp// ⚠ BAD: Query re-executed each time
var young = people.Where(p => p.Age < 30);
Console.WriteLine(young.Count());  // query runs here
Console.WriteLine(young.First());  // query runs AGAIN

// ✅ GOOD: Materialize once
var youngList = people.Where(p => p.Age < 30).ToList();
Console.WriteLine(youngList.Count);
Console.WriteLine(youngList[0]);

Key Takeaways

  • GroupBy partitions data; Join combines two sources via key.
  • SelectMany flattens nested collections into a single sequence.
  • Aggregate performs custom accumulations.
  • Always materialize (ToList/ToArray) if enumerating multiple times.

Part IV: Advanced C#

Chapter 12

Delegates, Events, and Lambda Expressions

Delegates

A delegate is a type-safe function pointer. It defines a signature that methods must match to be called through the delegate.

csharp// Declare a delegate type
public delegate int Operation(int x, int y);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;

// Instantiate and invoke
Operation op = Add;
Console.WriteLine(op(3, 4));      // 7

op = Multiply;
Console.WriteLine(op(3, 4));      // 12

// Multicast delegate (chaining)
Operation multi = Add;
multi += Multiply;
int result = multi(3, 4);  // returns 12 (last in chain)

Built-in Delegates

csharp// Func<TResult> — takes 0-16 params, returns value
Func<int, int, int> add = (a, b) => a + b;

// Action — takes 0-16 params, returns void
Action<string> print = msg => Console.WriteLine(msg);

// Predicate<T> — takes T, returns bool
Predicate<int> isEven = n => n % 2 == 0;

Console.WriteLine(add(5, 3));      // 8
print("Hello");                     // Hello
Console.WriteLine(isEven(4));       // True

Lambda Expressions

csharp// Lambda: (parameters) => expression or block
Func<int, int> square = x => x * x;
Func<int, int, int> sum = (x, y) => x + y;

// Statement lambda (with block body)
Func<int, int, int> max = (x, y) =>
{
    return x > y ? x : y;
};

// Lambda with discard (C# 9+)
Func<int, int, int> ignore = (_, _) => 42;

// Used heavily with LINQ
var evens = numbers.Where(n => n % 2 == 0).ToList();

Events

csharppublic class Button
{
    // Event declaration (uses EventHandler<T> by convention)
    public event EventHandler<EventArgs>? Clicked;

    public void Click()
    {
        Console.WriteLine("Button clicked");
        OnClicked();
    }

    protected virtual void OnClicked()
        => Clicked?.Invoke(this, EventArgs.Empty);
}

// Usage
var btn = new Button();
btn.Clicked += (sender, e) => Console.WriteLine("Event handled!");
btn.Click();  // "Button clicked" + "Event handled!"

Key Takeaways

  • Delegates are type-safe function pointers; they enable callback mechanisms.
  • Use Func/Action/Predicate instead of custom delegates for common patterns.
  • Lambdas are concise inline functions, essential for LINQ.
  • Events provide a publish-subscribe pattern built on delegates.

Chapter 13

Exception Handling

Exceptions represent runtime errors. C# provides structured exception handling with try/catch/finally blocks.

Try/Catch/Finally

csharptry
{
    string data = File.ReadAllText("config.json");
    int value = int.Parse(data);
    Console.WriteLine(value);
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"File not found: {ex.FileName}");
}
catch (FormatException ex) when (ex.Message.Contains("Input"))
{
    // Exception filter — catches only when condition is true
    Console.WriteLine($"Bad format: {ex.Message}");
}
catch (Exception ex)
{
    // Catch-all (rethrow if cannot handle)
    Console.WriteLine($"Unexpected error: {ex.Message}");
    throw;  // preserves stack trace
}
finally
{
    Console.WriteLine("This always executes");
}

Throwing Exceptions

csharppublic void Withdraw(decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException("Amount must be positive",
            nameof(amount));

    if (amount > Balance)
        throw new InsufficientFundsException(Balance, amount);

    Balance -= amount;
}

Custom Exceptions

csharppublic class InsufficientFundsException : Exception
{
    public decimal Balance { get; }
    public decimal Attempted { get; }

    public InsufficientFundsException(decimal balance, decimal attempted)
        : base($"Insufficient funds. Balance: {balance:C}, Attempted: {attempted:C}")
    {
        Balance = balance;
        Attempted = attempted;
    }
}

Best Practices

Key Takeaways

  • Use try/catch/finally for structured error handling.
  • Exception filters (when) enable conditional catching.
  • Create custom exceptions for domain-specific errors.
  • Fail fast — validate inputs early; avoid swallowing exceptions.

Chapter 14

Asynchronous Programming with async/await

Asynchronous programming is essential for building responsive applications. The async/await pattern (C# 5+) allows non-blocking operations without manual thread management.

Async / Await Execution Flow Main Thread calls async method async Task<string> FetchData() await httpClient.GetStringAsync() Thread Pool Thread HTTP / I/O operation State Machine Created captures context Task returned (incomplete) caller continues Continuation Scheduled SynchronizationContext I/O Completes callback fires State Machine: MoveNext() — resumes after await, executes remaining code Result unwrapped, Task marked complete, continuation invoked on captured context Original Thread (or ThreadPool) resumes execution Key: No thread is blocked during I/O — efficient resource usage

Basics of async/await

csharppublic async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    string result = await client.GetStringAsync(url);
    return result;
}

// Calling an async method
string data = await FetchDataAsync("https://api.example.com/data");
Console.WriteLine(data);

Async Return Types

csharp// Task<T> — returns a value
public async Task<int> GetCountAsync() => await CountItemsAsync();

// Task — no return value (like void)
public async Task SaveDataAsync(string data)
{
    await File.WriteAllTextAsync("data.txt", data);
}

// ValueTask<T> — avoids Task allocation when result is synchronous
public ValueTask<int> GetCachedValueAsync()
{
    if (_cached.HasValue)
        return new ValueTask<int>(_cached.Value);
    return new ValueTask<int>(ComputeExpensiveAsync());
}

// void — only for event handlers (fire-and-forget)
public async void Button_Click(object sender, RoutedEventArgs e)
{
    await DoWorkAsync();
}

Async Streams (C# 8+)

csharp// Producer
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

// Consumer
await foreach (int num in GenerateNumbersAsync())
{
    Console.WriteLine(num);
}

Task Combinators

csharp// Wait for all to complete
Task task1 = DoWorkAsync();
Task task2 = DoOtherWorkAsync();
await Task.WhenAll(task1, task2);

// Wait for first to complete
Task<string> first = await Task.WhenAny(FetchA(), FetchB());
string result = await first;

// Sequential composition
var data = await FetchAsync()
    .ContinueWith(t => Process(t.Result));

ConfigureAwait

csharp// In libraries (UI-agnostic), use ConfigureAwait(false)
public async Task<string> ReadFileAsync(string path)
{
    using var stream = File.OpenRead(path);
    byte[] buffer = new byte[stream.Length];
    await stream.ReadAsync(buffer, 0, buffer.Length)
        .ConfigureAwait(false);

    return Encoding.UTF8.GetString(buffer);
}

Common Pitfalls

Key Takeaways

  • async/await is non-blocking; no thread is consumed during I/O.
  • Await returns the result; the method resumes on the captured context.
  • Use Task.WhenAll for concurrency, Task.WhenAny for race conditions.
  • Never block on async code — it causes deadlocks.

Chapter 15

Reflection and Attributes

Attributes

Attributes add metadata to code elements (classes, methods, properties). They are used for serialization, validation, authorization, and more.

csharp[Serializable]
public class Person
{
    [Obsolete("Use Name instead")]
    public string FullName { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Range(0, 150)]
    public int Age { get; set; }
}

// Custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

[Author("Alice")]
public class Service { }

Reflection

csharpusing System.Reflection;

Type type = typeof(Person);

// Get all properties
foreach (PropertyInfo prop in type.GetProperties())
{
    Console.WriteLine($"{prop.Name}: {prop.PropertyType.Name}");
}

// Get custom attributes
var authorAttr = type.GetCustomAttribute<AuthorAttribute>();
if (authorAttr != null)
    Console.WriteLine($"Author: {authorAttr.Name}");

// Dynamic invocation
object instance = Activator.CreateInstance(typeof(Person))!;
PropertyInfo nameProp = type.GetProperty("Name")!;
nameProp.SetValue(instance, "Alice");
string name = (string)nameProp.GetValue(instance)!;
Console.WriteLine(name);  // Alice

// Invoke method
MethodInfo method = type.GetMethod("GetGreeting")!;
string result = (string)method.Invoke(instance, null)!;

Source Generators (C# 9+)

Source Generators run at compile time, analyzing code and generating additional source files. They replace much of what Reflection was used for, with better performance.

csharp// Example: partial class generated by a source generator
// The generator reads [GenerateDto] attribute and creates DTO classes
// Common libraries: System.Text.Json source gen, Regex source gen

Key Takeaways

  • Attributes add declarative metadata to code elements.
  • Reflection inspects types and members at runtime.
  • Use Reflection sparingly — it's slow and bypasses compile-time checks.
  • Source Generators provide compile-time code generation as a faster alternative.

Part V: .NET Platform

Chapter 16

Memory Management and Garbage Collection

.NET's automatic memory management frees developers from manual memory handling. The GC (Garbage Collector) manages allocation and deallocation of objects on the managed heap.

Garbage Collection Process Allocation on Gen 0 New objects created Gen 0 fills → GC Trigger Threads suspended briefly Mark Phase Find live refs Sweep Phase Free dead memory Compact Phase Defragment Promotion: Gen 0 → Gen 1 Survivors move up Finalization ~Finalize() LOH: objects ≥ 85KB, Gen 2, not compacted by default Modes: Workstation vs Server GC · Concurrent vs Non-concurrent

Generations

The managed heap is divided into three generations:

GC Process

  1. Mark: Starting from root references (static fields, stack locals, CPU registers), the GC traverses object graphs to find live objects.
  2. Sweep: Dead objects' memory is freed. Adjacent dead objects become a single free block.
  3. Compact: Live objects are moved together to reduce fragmentation (optional, not done on LOH by default).

IDisposable Pattern

csharppublic class ResourceHolder : IDisposable
{
    private IntPtr _nativeResource;  // native resource (must release)
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);  // don't call finalizer
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // Release managed resources (IDisposable fields)
        }

        // Release native resources
        if (_nativeResource != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_nativeResource);
            _nativeResource = IntPtr.Zero;
        }

        _disposed = true;
    }

    ~ResourceHolder() => Dispose(false);  // finalizer

    // Safe usage
    public void UseResource() { /* ... */ }
}

// Usage: using statement ensures Dispose is called
using (var resource = new ResourceHolder())
{
    resource.UseResource();
}  // Dispose called here even if exception occurs

// Using declaration (C# 8+)
using var file = new StreamReader("data.txt");
// Dispose called when file goes out of scope

GC Modes

ModeDescription
Workstation GCDefault for desktop apps; one heap per process.
Server GCDefault for ASP.NET Core; one heap per core, higher throughput.
Concurrent GCBackground collection minimizes pause times (default in .NET Core).
Non-concurrent GCSuspends all threads during collection; shorter total time but longer pauses.

Memory Best Practices

.NET Memory Layout Code Segment IL / JIT-compiled native code Stack Value types, params, return addresses (LIFO, per-thread) Managed Heap Gen 0 (short-lived) Gen 1 (buffer) Gen 2 (long-lived / LOH) GC Handle / Finalizer Queue Object references tracked for cleanup Loader / Metadata Type metadata, method tables, assembly info Thread Pool / TLS Worker threads, I/O completion ports

Key Takeaways

  • GC manages heap memory automatically; generations optimize collection frequency.
  • Implement IDisposable for native resource cleanup.
  • Server GC scales with CPU cores (default for ASP.NET Core).
  • Use Span, structs, and pooling to reduce GC pressure.

Chapter 17

File I/O and Serialization

File I/O

System.IO provides classes for reading/writing files, streams, and directories.

csharp// Reading files
string text = File.ReadAllText("data.txt");
string[] lines = File.ReadAllLines("data.txt");

// Writing files
File.WriteAllText("output.txt", "Hello, World!");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Stream-based I/O (efficient for large files)
using var reader = new StreamReader("largefile.txt");
string? line;
while ((line = await reader.ReadLineAsync()) != null)
    Process(line);

using var writer = new StreamWriter("log.txt", append: true);
await writer.WriteLineAsync($"{DateTime.UtcNow}: Event logged");

// File info
bool exists = File.Exists("data.txt");
FileInfo info = new("data.txt");
Console.WriteLine($"{info.Length} bytes, {info.LastWriteTime}");

Serialization

System.Text.Json (JSON)

csharpusing System.Text.Json;

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

// Serialize
var person = new Person { Name = "Alice", Age = 30 };
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person? deserialized = JsonSerializer.Deserialize<Person>(json);

// Options
var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};

// Source generator (C# 9+, faster)
[JsonSerializable(typeof(Person))]
partial class AppJsonContext : JsonSerializerContext { }

XML Serialization

csharpusing System.Xml.Serialization;

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
string xml = writer.ToString();

using var reader = new StringReader(xml);
Person? deserialized = (Person?)serializer.Deserialize(reader);

Streams

csharp// MemoryStream — in-memory buffer
using var memStream = new MemoryStream();
using var writer = new StreamWriter(memStream);
writer.Write("Hello");
writer.Flush();
memStream.Position = 0;
using var reader = new StreamReader(memStream);
string text = reader.ReadToEnd();

// FileStream — file I/O
using var fs = new FileStream("data.bin", FileMode.OpenOrCreate);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
await fs.WriteAsync(buffer, 0, bytesRead);

// Pipe — streaming between producers and consumers
// System.IO.Pipelines for high-performance I/O

Binary Serialization (Protobuf)

csharp// Using protobuf-net (NuGet)
[ProtoContract]
public class Person
{
    [ProtoMember(1)] public string Name { get; set; }
    [ProtoMember(2)] public int Age { get; set; }
}

using var ms = new MemoryStream();
Serializer.Serialize(ms, person);
byte[] data = ms.ToArray();
ms.Position = 0;
var deserialized = Serializer.Deserialize<Person>(ms);

Key Takeaways

  • Use async File I/O for non-blocking operations (ReadAsync/WriteAsync).
  • System.Text.Json is the preferred JSON serializer (fast, no reflection).
  • Streams provide a composable abstraction for byte-level I/O.
  • Consider protocol buffers (protobuf) for high-performance binary serialization.

Chapter 18

Dependency Injection

Dependency Injection (DI) is a design pattern where dependencies are provided (injected) to a class rather than created by the class itself. ASP.NET Core has built-in DI with first-class support.

Dependency Injection Container Program.cs / Startup Register Services DI Container Interface → Impl Map Consumer Constructor Service Lifetimes AddTransient (new each time) → AddScoped (once per request) → AddSingleton (once per app) services.AddScoped<IUserRepo, UserRepo>() Registration Container.Resolve<IUserRepo>() Resolution Injected into Controller / Service Via constructor parameter

Why DI?

Registering Services

csharpvar builder = WebApplication.CreateBuilder(args);

// Transient: new instance every time (lightweight, stateless)
builder.Services.AddTransient<IEmailService, EmailService>();

// Scoped: one instance per HTTP request (DbContext, unit of work)
builder.Services.AddScoped<IUserRepository, UserRepository>();

// Singleton: one instance for the entire application lifetime
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();

Constructor Injection

csharppublic class UserController : ControllerBase
{
    private readonly IUserRepository _repo;
    private readonly IEmailService _email;
    private readonly ILogger<UserController> _logger;

    // Dependencies are automatically resolved by the container
    public UserController(
        IUserRepository repo,
        IEmailService email,
        ILogger<UserController> logger)
    {
        _repo = repo;
        _email = email;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        _logger.LogInformation("Fetching user {Id}", id);
        var user = await _repo.GetByIdAsync(id);
        if (user == null) return NotFound();
        return Ok(user);
    }
}

Manual Resolution (Service Locator)

csharp// Avoid this pattern — prefer constructor injection
public class SomeClass
{
    private readonly IServiceProvider _services;

    public SomeClass(IServiceProvider services)
    {
        _services = services;
    }

    public void DoWork()
    {
        using var scope = _services.CreateScope();
        var repo = scope.ServiceProvider
            .GetRequiredService<IUserRepository>();
        // ... use repo
    }
}

DI Anti-Patterns

Key Takeaways

  • DI enables loose coupling, testability, and maintainability.
  • Choose lifetimes carefully: Transient, Scoped, or Singleton.
  • ASP.NET Core has built-in DI — no third-party container needed.
  • Inject interfaces, not concrete classes.

Part VI: Data Access

Chapter 19

ADO.NET Fundamentals

ADO.NET is the low-level data access technology in .NET. It provides direct connection to databases, commands, and data readers. EF Core and Dapper are built on top of ADO.NET.

Connecting to a Database

csharpusing System.Data;
using Microsoft.Data.SqlClient;

string connectionString = "Server=localhost;Database=MyDb;"
    + "User Id=sa;Password=MyPassword;TrustServerCertificate=true;";

using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

Console.WriteLine($"State: {connection.State}");

Executing Commands

csharp// ExecuteNonQuery — for INSERT, UPDATE, DELETE
using var cmd = new SqlCommand(
    "INSERT INTO Users (Name, Email) VALUES (@name, @email)",
    connection);

cmd.Parameters.AddWithValue("@name", "Alice");
cmd.Parameters.AddWithValue("@email", "alice@example.com");

int rows = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rows} row(s) affected");

// ExecuteScalar — returns single value
var scalarCmd = new SqlCommand("SELECT COUNT(*) FROM Users", connection);
int count = (int)(await scalarCmd.ExecuteScalarAsync())!;
Console.WriteLine($"Total users: {count}");

DataReader

csharpvar queryCmd = new SqlCommand(
    "SELECT Id, Name, Email FROM Users WHERE IsActive = 1",
    connection);

using var reader = await queryCmd.ExecuteReaderAsync();

while (await reader.ReadAsync())
{
    int id = reader.GetInt32(0);
    string name = reader.GetString(1);
    string email = reader.GetString(2);
    Console.WriteLine($"#{id}: {name} <{email}>");
}

DataTable and DataAdapter

csharpusing var adapter = new SqlDataAdapter(
    "SELECT * FROM Users", connection);

var table = new DataTable();
adapter.Fill(table);

foreach (DataRow row in table.Rows)
{
    Console.WriteLine($"{row["Id"]}: {row["Name"]}");
}

Async Operations

csharpawait connection.OpenAsync();
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteScalarAsync();
await reader.ReadAsync();
// Each has a synchronous counterpart (without Async suffix)

Connection Pooling

ADO.NET pools connections by connection string. The pool is managed automatically. To benefit from pooling:

Key Takeaways

  • ADO.NET provides direct database access with full control.
  • Always use parameters to prevent SQL injection.
  • Use async methods for non-blocking database operations.
  • Connection pooling is automatic — just dispose connections properly.

Chapter 20

Entity Framework Core - Getting Started

Entity Framework Core (EF Core) is a modern, lightweight, extensible ORM that enables .NET developers to work with databases using .NET objects. It eliminates the need for most of the data-access code developers typically need to write.

Entity Framework Core Pipeline DbContext / DbSet LINQ queries Expression Tree IQueryable Query Compilation Cache plan SQL Gen Provider-specific DB Provider SqlServer Change Tracker Detects modifications SaveChangesAsync INSERT/UPDATE/DELETE Migrations: Add-Migration / Update-Database / Schema versioning EF Core translates LINQ to SQL — no raw SQL needed for most operations Relationship Patterns: One-to-One / One-to-Many / Many-to-Many Fluent API / Data Annotations for configuration Lazy Loading · Eager Loading (.Include) · Explicit Loading (.Load)

Installing EF Core

csharpdotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

Defining the Model

csharppublic class Blog
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    // Navigation property
    public List<Comment> Comments { get; set; } = new();
}

public class Comment
{
    public int Id { get; set; }
    public string Text { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

Creating the DbContext

csharppublic class AppDbContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Comment> Comments => Set<Comment>();

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API configuration
        modelBuilder.Entity<Blog>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Title).HasMaxLength(200).IsRequired();
            entity.HasMany(e => e.Comments)
                  .WithOne(e => e.Blog)
                  .HasForeignKey(e => e.BlogId)
                  .OnDelete(DeleteBehavior.Cascade);
        });
    }
}

Registering and Using

csharp// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration
        .GetConnectionString("DefaultConnection")));

// Using in a service
public class BlogService
{
    private readonly AppDbContext _db;

    public BlogService(AppDbContext db) => _db = db;

    public async Task<List<Blog>> GetAllBlogsAsync()
        => await _db.Blogs.OrderByDescending(b => b.CreatedAt)
                           .ToListAsync();

    public async Task<Blog?> GetBlogAsync(int id)
        => await _db.Blogs.Include(b => b.Comments)
                          .FirstOrDefaultAsync(b => b.Id == id);

    public async Task<Blog> CreateBlogAsync(string title, string content)
    {
        var blog = new Blog { Title = title, Content = content };
        _db.Blogs.Add(blog);
        await _db.SaveChangesAsync();
        return blog;
    }
}

Migrations

csharp# Create migration
dotnet ef migrations add InitialCreate

# Apply to database
dotnet ef database update

# Generate SQL script
dotnet ef migrations script -o script.sql

# Remove last migration
dotnet ef migrations remove

Key Takeaways

  • EF Core translates LINQ queries to SQL automatically.
  • Use migrations for version-controlled schema changes.
  • Include() eager-loads related data, avoiding N+1 queries.
  • Register DbContext as Scoped (default in ASP.NET Core).

Chapter 21

EF Core - Advanced Concepts

Query Types

csharp// Tracking vs No-Tracking
var blogs = await _db.Blogs.AsNoTracking().ToListAsync();
// No-tracking is faster for read-only queries

// Raw SQL queries
var blogs = await _db.Blogs
    .FromSql($"SELECT * FROM Blogs WHERE Title LIKE '%' + {term} + '%'")
    .ToListAsync();

// Execute raw SQL
int affected = await _db.Database
    .ExecuteSqlAsync($"UPDATE Blogs SET Views = Views + 1");

// Compiled queries (EF Core 6+)
private static readonly Func<AppDbContext, int, Blog?> GetBlogById =
    EF.CompileQuery((AppDbContext ctx, int id) =>
        ctx.Blogs.Include(b => b.Comments)
                 .FirstOrDefault(b => b.Id == id));

// Usage: var blog = GetBlogById(_db, 1);

Change Tracker

csharpvar blog = await _db.Blogs.FindAsync(1);

// Track state
Console.WriteLine(_db.Entry(blog).State);  // Unchanged

blog.Title = "Updated";
Console.WriteLine(_db.Entry(blog).State);  // Modified

_db.Blogs.Remove(blog);
Console.WriteLine(_db.Entry(blog).State);  // Deleted

await _db.SaveChangesAsync();  // Generates SQL for all tracked changes

Concurrency Control

csharp// Model with concurrency token
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    [ConcurrencyCheck]
    public int Version { get; set; }
}

// Or use row version (SQL Server)
// public byte[] RowVersion { get; set; }

// On conflict
try
{
    await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // Reload and retry, or merge changes
    var entry = ex.Entries.Single();
    await entry.ReloadAsync();
}

Interceptors (EF Core 6+)

csharppublic class SlowQueryInterceptor : ISaveChangesInterceptor
{
    public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken ct = default)
    {
        var db = eventData.Context!;
        foreach (var entry in db.ChangeTracker.Entries())
        {
            Console.WriteLine($"{entry.Entity.GetType().Name}: {entry.State}");
        }
        return result;
    }
}

// Register in DbContext or Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .AddInterceptors(new SlowQueryInterceptor()));

Performance Tips

Key Takeaways

  • No-tracking queries are faster for read-only scenarios.
  • Change tracker detects entity state changes automatically.
  • Concurrency tokens prevent lost updates in concurrent scenarios.
  • Batch operations in EF Core 7+ reduce round-trips.

Chapter 22

Dapper and Micro-ORMs

Dapper is a lightweight, high-performance micro-ORM by the Stack Overflow team. It extends IDbConnection with extension methods for executing queries and mapping results to objects, giving you SQL control with object convenience.

Getting Started

csharpusing Dapper;
using System.Data.SqlClient;

string connectionString = /* ... */;
using var connection = new SqlConnection(connectionString);

// Query and map to typed objects
var sql = "SELECT Id, Name, Email FROM Users WHERE IsActive = @active";
var users = await connection.QueryAsync<User>(sql, new { active = true });

foreach (var user in users)
    Console.WriteLine($"{user.Name} <{user.Email}>");

Key Methods

csharp// QueryAsync — returns IEnumerable<T>
var users = await conn.QueryAsync<User>("SELECT * FROM Users");

// QueryFirstAsync — single result or exception
var user = await conn.QueryFirstAsync<User>(
    "SELECT * FROM Users WHERE Id = @id", new { id = 1 });

// QueryFirstOrDefaultAsync — single result or null
var user = await conn.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Id = @id", new { id = 1 });

// QuerySingleAsync — exactly one result
var user = await conn.QuerySingleAsync<User>(
    "SELECT * FROM Users WHERE Email = @email",
    new { email = "alice@example.com" });

// ExecuteAsync — for INSERT, UPDATE, DELETE
int rows = await conn.ExecuteAsync(
    "UPDATE Users SET Name = @name WHERE Id = @id",
    new { name = "NewName", id = 1 });

// ExecuteScalarAsync — single value
int count = await conn.ExecuteScalarAsync<int>(
    "SELECT COUNT(*) FROM Users");

Multiple Results

csharpvar sql = @"SELECT * FROM Users;
              SELECT * FROM Orders WHERE UserId = @userId;";

using var multi = await conn.QueryMultipleAsync(sql, new { userId = 1 });
var users = await multi.ReadAsync<User>();
var orders = await multi.ReadAsync<Order>();

Transactions

csharpusing var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var tx = await connection.BeginTransactionAsync();

try
{
    await connection.ExecuteAsync(
        "INSERT INTO Orders (Product, Total) VALUES (@product, @total)",
        new { product = "Widget", total = 9.99m }, transaction: tx);

    await connection.ExecuteAsync(
        "UPDATE Inventory SET Stock = Stock - 1 WHERE Product = @product",
        new { product = "Widget" }, transaction: tx);

    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}

EF Core vs Dapper

AspectEF CoreDapper
ORM TypeFull, with change trackingMicro, SQL-first
PerformanceSlower (overhead)Near-raw ADO.NET
SQL ControlGenerated automaticallyFull manual control
Learning CurveSteeper (magic)Shallow (just SQL)
Best ForCRUD apps, complex domain modelsHigh-performance, queries, reports

Key Takeaways

  • Dapper is fast — essentially raw ADO.NET with object mapping.
  • Write SQL manually — full control over queries.
  • Great for high-performance scenarios, reports, and read-heavy apps.
  • Use transactions explicitly when multiple operations must be atomic.

Part VII: Web Development

Chapter 23

ASP.NET Core Fundamentals

ASP.NET Core is a cross-platform, high-performance framework for building modern, cloud-enabled web applications. It runs on .NET and is designed from the ground up for performance and flexibility.

ASP.NET Core Request Pipeline HTTP Request Kestrel / IIS Middleware Pipeline Endpoint / Controller Middleware Components (execute in order through pipeline) ExceptionHandler → HSTS → HttpsRedirect → StaticFiles → Routing → CORS → Auth → Endpoint Minimal API app.MapGet() — lightweight, no controllers MVC Controllers [ApiController], ControllerBase — full-featured Dependency Injection Container AddScoped / AddTransient / AddSingleton — constructor injection across pipeline Response: JSON / HTML / File / Redirect Middleware unwinds, writes headers + body back to client

WebApplication Builder

csharpvar builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Configuration

csharp// appsettings.json
// {
//   "Database": { "ConnectionString": "..." },
//   "Features": { "EnableLogging": true }
// }

// Reading configuration
string connStr = builder.Configuration
    .GetConnectionString("DefaultConnection");

bool logging = builder.Configuration
    .GetValue<bool>("Features:EnableLogging");

// Options pattern
public class DatabaseOptions
{
    public string ConnectionString { get; set; } = "";
}

builder.Services.Configure<DatabaseOptions>(
    builder.Configuration.GetSection("Database"));

// Environment-based config
// appsettings.Development.json, appsettings.Production.json
var env = builder.Environment.EnvironmentName;
Console.WriteLine($"Running in: {env}");

Middleware

csharp// Custom middleware component
app.Use(async (context, next) =>
{
    var sw = Stopwatch.StartNew();
    Console.WriteLine($">> {context.Request.Method} {context.Request.Path}");

    await next();  // call next middleware

    sw.Stop();
    Console.WriteLine($"<< {context.Response.StatusCode} ({sw.ElapsedMilliseconds}ms)");
});

// Built-in middleware (typical order)
// app.UseExceptionHandler();
// app.UseHsts();
// app.UseHttpsRedirection();
// app.UseStaticFiles();
// app.UseRouting();
// app.UseCors();
// app.UseAuthentication();
// app.UseAuthorization();
// app.MapControllers();

Environment Management

csharpif (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Custom environment
// DOTNET_ENVIRONMENT=Staging in launchSettings.json

Key Takeaways

  • ASP.NET Core is modular, cross-platform, and high-performance.
  • Configuration system supports JSON, env vars, CLI args, and more.
  • Middleware pipeline processes every request in configured order.
  • Use environment-based configuration for dev/staging/production.

Chapter 24

RESTful APIs with Minimal APIs

Minimal APIs (introduced in .NET 6) provide a streamlined way to build HTTP APIs with minimal ceremony. They are ideal for microservices, simple APIs, and learning.

Basic Endpoints

csharpvar app = WebApplication.Create(args);

// GET
app.MapGet("/api/items", async (AppDbContext db) =>
    await db.Items.ToListAsync());

// GET with ID
app.MapGet("/api/items/{id}", async (int id, AppDbContext db) =>
    await db.Items.FindAsync(id) is Item item
        ? Results.Ok(item)
        : Results.NotFound());

// POST
app.MapPost("/api/items", async (Item item, AppDbContext db) =>
{
    db.Items.Add(item);
    await db.SaveChangesAsync();
    return Results.Created($"/api/items/{item.Id}", item);
});

// PUT
app.MapPut("/api/items/{id}", async (int id, Item input, AppDbContext db) =>
{
    var item = await db.Items.FindAsync(id);
    if (item is null) return Results.NotFound();

    item.Name = input.Name;
    item.Price = input.Price;
    await db.SaveChangesAsync();
    return Results.NoContent();
});

// DELETE
app.MapDelete("/api/items/{id}", async (int id, AppDbContext db) =>
{
    var item = await db.Items.FindAsync(id);
    if (item is null) return Results.NotFound();

    db.Items.Remove(item);
    await db.SaveChangesAsync();
    return Results.NoContent();
});

Route Groups

csharpvar items = app.MapGroup("/api/items")
    .WithTags("Items")
    .RequireAuthorization();

items.MapGet("/", GetAllItems);
items.MapGet("/{id}", GetItemById);
items.MapPost("/", CreateItem);
items.MapPut("/{id}", UpdateItem);
items.MapDelete("/{id}", DeleteItem);

Parameter Binding

csharp// Route parameters
app.MapGet("/users/{id}", (int id) => ...);

// Query parameters
app.MapGet("/users",
    (int page, int pageSize) => ...);

// Request body (JSON)
app.MapPost("/users", (User user) => ...);

// Form values
app.MapPost("/upload",
    (IFormFile file) => ...);

// Header values
app.MapGet("/check",
    ([FromHeader] string authorization) => ...);

// Services (DI)
app.MapGet("/data", (AppDbContext db, ILogger<Program> log) => ...);
// log and db are resolved from DI container

Structured Response

csharpapp.MapGet("/api/items/{id}", async (int id, AppDbContext db) =>
{
    var item = await db.Items.FindAsync(id);
    return item switch
    {
        null => Results.Problem(
            statusCode: 404,
            title: "Item not found",
            detail: $"Item with ID {id} does not exist."),

        _ => Results.Ok(new ItemResponse
        {
            Id = item.Id,
            Name = item.Name,
            Price = item.Price,
            Links = new[]
            {
                new Link("self", $"/api/items/{item.Id}"),
                new Link("update", $"/api/items/{item.Id}")
            }
        })
    };
});

Key Takeaways

  • Minimal APIs are concise and perfect for simple endpoints.
  • Parameter binding automatically resolves from route, query, body, DI.
  • Results helpers (Ok, NotFound, Created, NoContent, Problem) standardize responses.
  • Route groups organize related endpoints with shared configuration.

Chapter 25

MVC Pattern

MVC (Model-View-Controller) separates an application into three interconnected components: Model (data), View (UI), Controller (handles requests). ASP.NET Core MVC provides full-featured support.

Controller

csharp[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _db;

    public ProductsController(AppDbContext db) => _db = db;

    [HttpGet]
    public async Task<ActionResult<List<Product>>> GetAll()
    {
        return await _db.Products.ToListAsync();
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetById(int id)
    {
        var product = await _db.Products.FindAsync(id);
        if (product == null) return NotFound();
        return product;
    }

    [HttpPost]
    public async Task<ActionResult<Product>> Create(Product product)
    {
        _db.Products.Add(product);
        await _db.SaveChangesAsync();
        return CreatedAtAction(nameof(GetById),
            new { id = product.Id }, product);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, Product input)
    {
        var product = await _db.Products.FindAsync(id);
        if (product == null) return NotFound();

        product.Name = input.Name;
        product.Price = input.Price;
        await _db.SaveChangesAsync();

        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        var product = await _db.Products.FindAsync(id);
        if (product == null) return NotFound();

        _db.Products.Remove(product);
        await _db.SaveChangesAsync();

        return NoContent();
    }

    [HttpGet("search")]
    public async Task<ActionResult<List<Product>>> Search(
        [FromQuery] string term)
    {
        return await _db.Products
            .Where(p => p.Name.Contains(term))
            .ToListAsync();
    }
}

Model Binding and Validation

csharppublic class CreateProductRequest
{
    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; } = "";

    [Range(0.01, 999999.99)]
    public decimal Price { get; set; }

    [EmailAddress]
    public string? ContactEmail { get; set; }

    [Url]
    public string? Website { get; set; }

    [RegularExpression(@"^[A-Z]{2}\d{4}$")]
    public string? Code { get; set; }
}

// In controller:
[HttpPost]
public async Task<ActionResult> Create(
    [FromBody] CreateProductRequest request)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    // Process valid request...
}

Filter Pipeline

csharp// Custom action filter
public class LogActionFilter : IActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger)
        => _logger = logger;

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation("Before action: {Action}",
            context.ActionDescriptor.DisplayName);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation("After action: {Status}",
            context.Result?.GetType().Name);
    }
}

// Global registration
builder.Services.AddControllers(options =>
{
    options.Filters.Add<LogActionFilter>();
    options.Filters.Add<ProducesResponseTypeAttribute>(304);
});

Key Takeaways

  • MVC separates concerns: controllers handle requests, models represent data.
  • Data annotations provide declarative validation on models.
  • Action filters enable cross-cutting concerns (logging, caching, validation).
  • Model binding automatically maps request data to action parameters.

Chapter 26

Authentication and Authorization

ASP.NET Core provides a rich security system with authentication (who you are) and authorization (what you can do). JWT (JSON Web Tokens) is the most common approach for API authentication.

JWT Authentication Setup

csharpvar builder = WebApplication.CreateBuilder(args);

// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

Generating Tokens

csharppublic class AuthController : ControllerBase
{
    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        // Validate credentials (against DB, etc.)
        if (request.Username != "admin" || request.Password != "pass")
            return Unauthorized();

        var claims = new[]
        {
            new Claim(ClaimTypes.Name, request.Username),
            new Claim(ClaimTypes.Role, "Admin"),
            new Claim("department", "Engineering")
        };

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes("YourSecretKeyHere!123"));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: "myapp",
            audience: "myapp",
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),
            signingCredentials: creds
        );

        return Ok(new
        {
            token = new JwtSecurityTokenHandler().WriteToken(token),
            expires = token.ValidTo
        });
    }
}

Authorization

csharp// Role-based
[Authorize(Roles = "Admin")]
[HttpGet("admin-only")]
public IActionResult AdminEndpoint() => Ok();

// Policy-based
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("RequireEngineering", policy =>
        policy.RequireClaim("department", "Engineering"));

    options.AddPolicy("Over21", policy =>
        policy.RequireAssertion(context =>
        {
            var ageClaim = context.User.FindFirst("age")?.Value;
            return int.TryParse(ageClaim, out int age) && age >= 21;
        }));
});

[Authorize(Policy = "Over21")]
[HttpGet("age-restricted")]
public IActionResult Restricted() => Ok();

// Multiple policies
[Authorize(Roles = "Admin")]
[Authorize(Policy = "RequireEngineering")]
public IActionResult SuperSecure() => Ok();

Identity and User Management

csharpbuilder.Services.AddDefaultIdentity<IdentityUser>()
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<AppDbContext>();

// With Identity, you get:
// UserManager, SignInManager, RoleManager
// Register, Login, Logout, Password management
// Two-factor authentication, Email confirmation

Key Takeaways

  • Authentication verifies identity; authorization checks permissions.
  • JWT is the standard for stateless API authentication.
  • Use claims and policies for fine-grained authorization.
  • ASP.NET Core Identity provides full user management out of the box.

Part VIII: Testing and Design Patterns

Chapter 27

Unit Testing with xUnit

Unit testing ensures individual components work correctly. xUnit is the most popular testing framework for .NET, known for its extensibility and clean design.

Testing Pyramid Unit Tests xUnit / NUnit / MSTest Fast, isolated, many Integration Tests WebApplicationFactory / TestContainers DB, API, external services E2E / UI Tests Playwright / Selenium Fakes vs Mocks: Stubs provide values · Mocks verify behavior (Moq / NSubstitute)

Setting Up xUnit

csharp// Create a test project
// dotnet new xunit -n MyApp.Tests

// Add project reference
// dotnet add reference ../MyApp/MyApp.csproj

Writing Tests

csharpusing Xunit;
using FluentAssertions;  // optional, for readable assertions

public class CalculatorTests
{
    private readonly Calculator _sut = new();  // system under test

    [Fact]
    public void Add_ShouldReturnSum_WhenGivenTwoNumbers()
    {
        // Arrange
        int a = 5, b = 3;

        // Act
        int result = _sut.Add(a, b);

        // Assert
        Assert.Equal(8, result);
        // Or with FluentAssertions:
        // result.Should().Be(8);
    }

    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(-1, -1, -2)]
    [InlineData(0, 0, 0)]
    public void Add_ShouldReturnCorrectSum_ForMultipleInputs(
        int a, int b, int expected)
    {
        int result = _sut.Add(a, b);
        Assert.Equal(expected, result);
    }
}

Mocking with Moq

csharpusing Moq;

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repoMock;
    private readonly UserService _service;

    public UserServiceTests()
    {
        _repoMock = new Mock<IUserRepository>();
        _service = new UserService(_repoMock.Object);
    }

    [Fact]
    public async Task GetUser_ShouldReturnUser_WhenExists()
    {
        // Arrange
        var expectedUser = new User { Id = 1, Name = "Alice" };
        _repoMock.Setup(r => r.GetByIdAsync(1))
                 .ReturnsAsync(expectedUser);

        // Act
        var result = await _service.GetUserAsync(1);

        // Assert
        Assert.Equal("Alice", result?.Name);
        _repoMock.Verify(r => r.GetByIdAsync(1), Times.Once);
    }

    [Fact]
    public async Task GetUser_ShouldThrow_WhenNotFound()
    {
        _repoMock.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
                 .ReturnsAsync((User?)null);

        await Assert.ThrowsAsync<NotFoundException>(() =>
            _service.GetUserAsync(999));
    }
}

Integration Testing with WebApplicationFactory

csharpusing Microsoft.AspNetCore.Mvc.Testing;

public class ApiIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
        => _factory = factory;

    [Fact]
    public async Task GetItems_ShouldReturnSuccess()
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync("/api/items");

        // Assert
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        Assert.Contains("items", json);
    }

    [Fact]
    public async Task CreateItem_ShouldReturnCreated()
    {
        var client = _factory.CreateClient();
        var newItem = new { Name = "Test", Price = 9.99 };
        var content = new StringContent(
            JsonSerializer.Serialize(newItem),
            Encoding.UTF8,
            "application/json");

        var response = await client.PostAsync("/api/items", content);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}

Code Coverage

csharp# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"

# Generate report (install tool first)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:*/coverage.cobertura.xml
    -targetdir:coverage-report -reporttypes:Html

Key Takeaways

  • xUnit is the standard .NET testing framework — use [Fact] and [Theory].
  • Moq creates mock dependencies for isolated unit tests.
  • WebApplicationFactory enables full integration tests without deployment.
  • Aim for high code coverage, but focus on critical paths.

Chapter 28

Design Patterns in C#

Design patterns are reusable solutions to common software design problems. Here are the most important patterns in C# with modern implementations.

Singleton

csharppublic sealed class Logger
{
    private static readonly Logger _instance = new();
    private Logger() { }
    public static Logger Instance => _instance;

    public void Log(string message) =>
        Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {message}");
}

// Usage
Logger.Instance.Log("Application started");
// Or with DI: services.AddSingleton<Logger>();

Factory Method

csharppublic interface IPaymentProcessor
{
    Task<bool> ProcessPayment(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor { /* ... */ }
public class PayPalProcessor : IPaymentProcessor { /* ... */ }

public class PaymentProcessorFactory
{
    public IPaymentProcessor Create(string method) => method switch
    {
        "CreditCard" => new CreditCardProcessor(),
        "PayPal" => new PayPalProcessor(),
        _ => throw new ArgumentException($"Unknown method: {method}")
    };
}

Repository Pattern

csharppublic interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task<int> SaveChangesAsync();
}

public class EfRepository<T> : IRepository<T> where T : class
{
    private readonly AppDbContext _db;

    public EfRepository(AppDbContext db) => _db = db;

    public async Task<T?> GetByIdAsync(int id)
        => await _db.Set<T>().FindAsync(id);

    public async Task<List<T>> GetAllAsync()
        => await _db.Set<T>().ToListAsync();

    public async Task AddAsync(T entity)
        => await _db.Set<T>().AddAsync(entity);

    public void Update(T entity) => _db.Set<T>().Update(entity);
    public void Delete(T entity) => _db.Set<T>().Remove(entity);
    public async Task<int> SaveChangesAsync()
        => await _db.SaveChangesAsync();
}

Strategy Pattern

csharppublic interface IShippingStrategy
{
    decimal CalculateCost(double weight, string destination);
}

public class StandardShipping : IShippingStrategy
{
    public decimal CalculateCost(double weight, string dest)
        => (decimal)(weight * 0.5 + 5);
}

public class ExpressShipping : IShippingStrategy
{
    public decimal CalculateCost(double weight, string dest)
        => (decimal)(weight * 1.5 + 15);
}

public class OrderService
{
    private readonly IShippingStrategy _shipping;

    public OrderService(IShippingStrategy shipping)
        => _shipping = shipping;

    public decimal GetShippingCost(double weight, string dest)
        => _shipping.CalculateCost(weight, dest);
}

Observer Pattern (Events)

csharppublic class Stock
{
    private decimal _price;
    public event EventHandler<decimal>? PriceChanged;

    public decimal Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                _price = value;
                OnPriceChanged();
            }
        }
    }

    protected virtual void OnPriceChanged()
        => PriceChanged?.Invoke(this, _price);
}

// Usage
var stock = new Stock();
stock.PriceChanged += (_, price) =>
    Console.WriteLine($"New price: {price:C}");
stock.Price = 100.50m;  // triggers event

Pattern Comparison

PatternTypePurpose
SingletonCreationalSingle instance globally
FactoryCreationalEncapsulate object creation
RepositoryStructuralAbstract data access
StrategyBehavioralSwap algorithms at runtime
ObserverBehavioralNotify subscribers of changes
DIStructuralInject dependencies

Key Takeaways

  • Design patterns provide proven solutions to recurring problems.
  • Use creational patterns for object creation, structural for composition.
  • Behavioral patterns define communication between objects.
  • Don't force patterns — apply them where they solve a real problem.

Chapter 29

Best Practices and Performance

Coding Conventions

Performance Tips

Security Best Practices

Project Structure

textMyApp/
|-- src/
|   |-- MyApp.Api/          # Web API / Controllers
|   |-- MyApp.Application/  # Business logic, DTOs, interfaces
|   |-- MyApp.Domain/       # Entities, value objects, domain events
|   |-- MyApp.Infrastructure/ # Data access, external services
|-- tests/
|   |-- MyApp.UnitTests/    # Unit tests (xUnit)
|   |-- MyApp.IntegrationTests/ # Integration tests
|-- MyApp.sln

CI/CD Pipeline

csharp# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0'
      - run: dotnet restore
      - run: dotnet build --no-restore --configuration Release
      - run: dotnet test --no-build --configuration Release
        --collect:"XPlat Code Coverage"
      - run: dotnet publish --configuration Release
      - name: Deploy
        run: # deploy to Azure / Cloudflare / etc.

Recommended Tools

CategoryTools
IDEVisual Studio, VS Code, JetBrains Rider
TestingxUnit, FluentAssertions, Moq, TestContainers
PerformanceBenchmarkDotNet, dotnet-trace, PerfView
Static AnalysisRoslyn analyzers, SonarCloud, StyleCop
LoggingSerilog, NLog, Application Insights
CI/CDGitHub Actions, Azure DevOps, GitLab CI
ContainersDocker, Kubernetes, AKS

Key Takeaways

  • Write clean, readable code that follows team conventions.
  • Profile before optimizing — measure, then improve.
  • Security is everyone's responsibility, not just the security team.
  • Automate everything: build, test, deploy, monitor.

Appendices

Chapter A

C# Version History

VersionReleasedKey Features
C# 1.02002Managed code, classes, structs, interfaces, events, properties, delegates
C# 2.02005Generics, nullable types, anonymous methods, iterators (yield), partial classes
C# 3.02007LINQ, lambda expressions, extension methods, anonymous types, expression trees
C# 4.02010Dynamic binding (dynamic), named/optional parameters, co/contravariance
C# 5.02012Async/await, caller info attributes (CallerMemberName, CallerFilePath, CallerLineNumber)
C# 6.02015String interpolation, null-conditional operators (?.), expression-bodied members, using static
C# 7.02017Tuples, pattern matching, out variables, local functions, ref locals/returns
C# 7.12017Async Main, default literal expressions, inferred tuple element names
C# 7.22017in parameters, readonly struct, private protected, Span<T> support
C# 7.32018Enum, delegate, and unmanaged constraints; ref local reassignment; stackalloc initializers
C# 8.02019Nullable reference types, async streams (IAsyncEnumerable), default interface methods, indices/ranges
C# 9.02020Records, init-only setters, top-level statements, pattern matching enhancements, function pointers
C# 102021Global usings, file-scoped namespaces, record structs, extended property patterns
C# 112022Raw string literals, generic math (INumber<T>), required members, list patterns, file-local types
C# 122023Primary constructors, collection expressions ([x, y, z]), aliases for any type, inline arrays

Chapter B

.NET CLI Reference

Project Management

shelldotnet new console -n MyApp           # Create console app
dotnet new webapi -n MyApi             # Create web API
dotnet new classlib -n MyLib           # Create class library
dotnet new xunit -n MyTests            # Create test project
dotnet new sln -n MyApp                # Create solution file
dotnet sln add src/MyApp/MyApp.csproj  # Add project to solution

Build and Run

shelldotnet build                           # Build the project
dotnet run                              # Build and run
dotnet publish -c Release               # Publish for deployment
dotnet clean                            # Clean build outputs

Testing

shelldotnet test                             # Run all tests
dotnet test --filter "Category=Integration"
dotnet test --collect:"XPlat Code Coverage"  # With coverage

Packages

shelldotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Newtonsoft.Json --version 13.0.3
dotnet remove package PackageName
dotnet list package                      # List all packages
dotnet list package --outdated           # Check for updates

Entity Framework

shelldotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations remove
dotnet ef migrations list
dotnet ef dbcontext info
dotnet ef migrations script -o script.sql

Tool Management

shelldotnet tool install -g dotnet-ef        # Install global tool
dotnet tool list -g                      # List global tools
dotnet tool update -g dotnet-ef          # Update tool
dotnet tool uninstall -g dotnet-ef       # Remove tool

SDK and Runtime

shelldotnet --version                        # SDK version
dotnet --list-sdks                       # List installed SDKs
dotnet --list-runtimes                   # List installed runtimes
dotnet --info                            # Full .NET info

.NET Full Course with C# — Generated with opencode.ai — July 05, 2026

This work is licensed under CC BY 4.0. Free to share and adapt with attribution.