Lecture 1 / 19
Lecture 01 · Fundamentals

Introduction to C# & .NET Setup

No prerequisites

What is C#?

C# (pronounced "C-sharp") is a modern, object-oriented, and type-safe programming language developed by Microsoft. It is designed to be simple, powerful, and versatile. While it shares roots with C and C++, it incorporates modern features that make it more productive and less prone to errors.

C# is the primary language for the .NET ecosystem, meaning it can be used to build almost anything:

  • Enterprise Software: Robust backend systems for banks and corporations.
  • Web Development: Using ASP.NET Core to build high-performance APIs and websites.
  • Game Development: The industry standard for the Unity Engine.
  • Cloud Services: Deep integration with Microsoft Azure for scalable cloud apps.
  • Mobile Apps: Cross-platform development via .NET MAUI.

The .NET Ecosystem: How it Works

To understand C#, you must understand .NET. C# is the language, but .NET is the runtime and library that executes the code.

Professional Insight: The Compilation Process

C# does not compile directly into machine code (binary). Instead, it follows a two-step process:

  1. C# Source Code -> Compiled into CIL (Common Intermediate Language).
  2. CIL -> Translated by the CLR (Common Language Runtime) using JIT (Just-In-Time) Compilation into machine code that your specific CPU understands.

This is why C# is cross-platform; the CIL is the same regardless of whether you are on Windows, Linux, or macOS.

Setting Up Your Environment

To start developing, you need two main components: the SDK and an IDE.

1. The .NET SDK (Software Development Kit)

The SDK contains the compilers, libraries, and tools (like the dotnet command) needed to build applications. Download the latest version from dotnet.microsoft.com.

terminal / cmd
dotnet --version
# Expected Output: 8.0.x (or the latest stable version)

2. Choosing Your Editor (IDE)

  • Visual Studio 2022: The "Full Power" IDE. Best for large enterprise projects and Windows desktop apps.
  • VS Code: A lightweight, fast editor. Preferred by many professionals for web development and cross-platform work (requires the "C# Dev Kit" extension).
  • JetBrains Rider: A premium, high-performance cross-platform IDE.

Anatomy of Your First Program

Let's look at a standard C# program and break down exactly what each line is doing.

Program.cs
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello, World!");
        Console.WriteLine("Welcome to C# Mastery!");
    }
}

Code Breakdown:

  • using System;: This tells the program to use the System namespace, which contains fundamental classes like Console.
  • class Program: C# is an Object-Oriented language. Every piece of code must live inside a class.
  • static void Main(): This is the Entry Point. When you run your app, the .NET runtime looks for this exact method to start execution.

Interacting with the User

Professional apps aren't just one-way; they interact with users. We use Console.ReadLine() to capture input.

Program.cs
using System;

class Program
{
    static void Main()
    {
        Console.Write("Enter your name: ");
        string userName = Console.ReadLine();
        Console.WriteLine("Hello, " + userName + "! Welcome to the course.");
    }
}
🎯 Professional Lab 1.1

Objective: Create a "User Profile Generator".

Build a console application that asks the user for the following four pieces of information:

  • Full Name
  • Age
  • Country of Residence
  • Favorite Programming Language

Once the input is collected, the program should print a professional summary: "User Profile: [Name], [Age] years old, from [Country], specializing in [Language]."

Lecture 02 · Fundamentals

Variables & Data Types

Prerequisite: Lecture 1

Understanding Variables

In programming, a variable is a named storage location in the computer's memory. Think of it as a labeled box where you can store a value, and then retrieve or change that value later in your program.

Professional Concept: Strong Typing

C# is a Strongly Typed language. This means that every variable must have a defined type. Once a variable is declared as an integer, you cannot store a string in it. This prevents a huge category of bugs that occur in "loosely typed" languages like JavaScript.

Primitive Data Types

C# provides a variety of built-in types to handle different kinds of data. Choosing the right type is critical for performance and memory efficiency.

Type Storage Size Description Example
int 4 bytes Whole numbers (Integers) int age = 25;
double 8 bytes Decimal numbers (Floating point) double price = 19.99;
decimal 16 bytes High-precision decimals (Financial) decimal tax = 15.50m;
bool 1 byte True or False values bool isComplete = true;
char 2 bytes Single character (Unicode) char grade = 'A';
string Variable Sequence of characters (Text) string name = "Alice";
⚠️ Critical Note: Double vs. Decimal

Never use double or float for financial calculations. They use binary floating-point math which can cause tiny rounding errors (e.g., 0.1 + 0.2 = 0.30000000000000004). Always use decimal for money to ensure absolute precision.

The var Keyword (Implicit Typing)

C# allows you to use the var keyword. This tells the compiler to infer (guess) the type based on the value assigned to it.

Example.cs
var message = "Hello World"; // Compiler knows this is a string
var count = 10;            // Compiler knows this is an int

Professional Tip: Use var when the type is obvious (e.g., var list = new List<string>();). Avoid using var if it makes the code harder for other developers to read.

Type Casting

Sometimes you need to convert a value from one type to another. This is called Casting.

1. Implicit Casting (Automatic)

Happens when you move a smaller type to a larger type. No data loss occurs.

int myInt = 9;
double myDouble = myInt; // Automatic: int becomes double

2. Explicit Casting (Manual)

Required when moving a larger type to a smaller type. This can lead to data loss (truncation).

double myDouble = 9.78;
int myInt = (int)myDouble; // Manual: 9.78 becomes 9 (decimals are dropped)

Constants and Read-Only Fields

If a value should never change during the execution of the program, use the const keyword. This improves performance and prevents accidental bugs.

Constants.cs
const double Pi = 3.14159;
const string CompanyName = "TechCorp Inc";

// Pi = 3.15; // ❌ This will cause a compile-time error

Professional Naming Conventions

In the professional world, following the Microsoft C# Coding Conventions is mandatory for teamwork.

  • camelCase: Used for local variables and method parameters.
    Example: int userAge = 25;
  • PascalCase: Used for Classes, Methods, and Constants.
    Example: public class UserProfile { }
  • Avoid: Single letter names like a, b, x. Use descriptive names like totalInvoiceAmount.
🎯 Professional Lab 2.1: The Financial Calculator

Scenario: You are building a simple billing system for a client.

Requirements: Create a program that does the following:

  1. Declare a const decimal for a Tax Rate (e.g., 0.15 for 15%).
  2. Ask the user to input the Product Name (string).
  3. Ask the user to input the Price of the product (decimal).
  4. Ask the user for the Quantity (int).
  5. Calculate the Subtotal (Price × Quantity).
  6. Calculate the Tax Amount (Subtotal × Tax Rate).
  7. Calculate the Final Total (Subtotal + Tax Amount).
  8. Print a professional receipt including all the above values.

Hint: Use decimal.Parse(Console.ReadLine()) to convert user input to a decimal.

Lecture 03 · Fundamentals

Operators & Expressions

Prerequisite: Lecture 2

What are Operators & Expressions?

An expression is a combination of operators, variables, and values that are evaluated to produce a single result. An operator is a symbol that tells the compiler to perform a specific mathematical or logical manipulation.

Professional Concept: Evaluation

Every expression in C# results in a value. For example, 5 + 2 is an expression that evaluates to the integer 7. In professional software, we aim for "clean" expressions—avoiding overly complex one-liners that are hard for other developers to read.

1. Arithmetic Operators

Used to perform standard mathematical operations.

Operator Name Description Example (int a=10, b=3)
+ Addition Adds two values a + b // 13
- Subtraction Subtracts one value from another a - b // 7
* Multiplication Multiplies two values a * b // 30
/ Division Divides one value by another a / b // 3
% Modulus Returns the division remainder a % b // 1
⚠️ The "Integer Division" Trap

In C#, if you divide two integers, the result is always an integer. Any remainder is simply discarded (truncated).

Example: int result = 5 / 2; will give you 2, not 2.5. To get a decimal result, at least one number must be a double or decimal: double result = 5.0 / 2; // 2.5.

2. Comparison (Relational) Operators

These operators compare two values and always return a bool (true or false). They are the foundation of logic in programming.

  • == Equal to
  • != Not equal to
  • > Greater than
  • < Less than
  • >= Greater than or equal to
  • <= Less than or equal to

3. Logical Operators

Logical operators are used to combine multiple comparison expressions.

  • AND (&&): Returns true only if both statements are true.
  • OR (||): Returns true if at least one statement is true.
  • NOT (!): Reverses the result (true becomes false, false becomes true).
Professional Insight: Short-Circuit Evaluation

C# uses "short-circuiting." In an && operation, if the first condition is false, C# doesn't even check the second condition because the whole thing is already guaranteed to be false. This is used by professionals to prevent crashes (e.g., checking if an object is not null before accessing its properties).

4. Assignment & Compound Operators

Instead of writing x = x + 5, professionals use compound assignments for brevity and clarity.

Shorthand.cs
int score = 100;

score += 10; // Same as: score = score + 10; (110)
score -= 5;   // Same as: score = score - 5;   (105)
score *= 2;   // Same as: score = score * 2;   (210)
score /= 10;  // Same as: score = score / 10;  (21)

5. Professional "Syntactic Sugar"

Modern C# provides shorthand operators that make code cleaner and more readable.

The Ternary Operator (?:)

A compact way to write a simple If-Else statement.

int age = 20;
string status = (age >= 18) ? "Adult" : "Minor";
// If age >= 18 is true, result is "Adult". Otherwise, "Minor".

The Null-Coalescing Operator (??)

Used to provide a default value if a variable is null.

string input = null;
string displayName = input ?? "Guest User";
// If input is null, displayName becomes "Guest User".
🎯 Professional Lab 3.1: E-Commerce Logic Engine

Scenario: You are coding the checkout logic for an online store.

Requirements: Build a program that calculates the final price based on these rules:

  1. Ask user for the Order Total (decimal).
  2. Ask user if they have a Discount Coupon (bool/string).
  3. Rule 1: If the Order Total is over $100, they get a 10% discount.
  4. Rule 2: If they have a coupon AND the order is over $50, they get an additional $5 off.
  5. Rule 3: If the final total is under $20, add a $5 shipping fee. If it's $20 or more, shipping is free.
  6. Final Output: Display the Original Price, Total Discounts applied, and the Final Amount to pay.

Challenge: Try using a Ternary operator to determine the shipping fee message (e.g., "Shipping: Free" or "Shipping: $5").

Lecture 04 · Fundamentals

Control Flow: Decision Making

Prerequisite: Lecture 3

What is Control Flow?

By default, code executes line-by-line from top to bottom. Control Flow allows us to break this linear path, skipping sections of code or executing specific blocks based on whether a condition is true or false.

Professional Concept: Cyclomatic Complexity

In the industry, we track "Cyclomatic Complexity"—a measure of how many independent paths there are through your code. The more nested if statements you have, the higher the complexity, and the harder the code is to test and maintain. Professional code aims for low complexity.

1. Conditional Branching (If, Else If, Else)

The if statement is the most basic form of decision-making. It evaluates a boolean expression and executes a block of code if the result is true.

Logic.cs
int userAge = 20;

if (userAge >= 21) 
{
    Console.WriteLine("Access Granted: Full Access.");
} 
else if (userAge >= 18) 
{
    Console.WriteLine("Access Granted: Limited Access.");
} 
else 
{
    Console.WriteLine("Access Denied: Underage.");
}

Professional Tip: Always use curly braces { } even for single-line statements. It prevents bugs during future code edits.

2. The Switch Statement

When you have a single variable that could be one of many distinct values, a switch statement is cleaner and more performant than multiple else if blocks.

string role = "Admin";

switch (role)
{
    case "Admin":
        Console.WriteLine("Full System Access.");
        break; 
    case "Editor":
        Console.WriteLine("Content Management Access.");
        break;
    case "Guest":
        Console.WriteLine("Read-Only Access.");
        break;
    default:
        Console.WriteLine("Unknown Role: Access Denied.");
        break;
}

The break keyword is mandatory. Without it, the code would "fall through" to the next case, which usually causes a compiler error in C#.

3. Modern C#: Switch Expressions (C# 8.0+)

Professional developers rarely use the old switch statement for simple assignments. They use Switch Expressions, which are more concise and return a value directly.

ModernSwitch.cs
int dayNumber = 3;

string dayName = dayNumber switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    _ => "Weekend" // The underscore (_) is the default case
};

Console.WriteLine($"Today is {dayName}");

4. Professional Pattern: The Guard Clause

Beginners often write "nested" code that drifts to the right (Arrow Code). Professionals use Guard Clauses to handle errors early and "flatten" the code.

❌ The "Beginner" Way (Nested)
if (user != null) {
    if (user.IsActive) {
        if (user.HasPermission) {
            // Do the actual work here...
        }
    }
}

✅ The "Professional" Way (Guard Clauses)
if (user == null) return; 
if (!user.IsActive) return;
if (!user.HasPermission) return;

// Do the actual work here... (The code stays flat!)
🎯 Professional Lab 4.1: Advanced Access Control System

Scenario: You are building a security module for a corporate building.

Requirements: Create a program that asks for the user's Employee ID and Security Clearance Level (1 to 5).

  1. Requirement 1: If the Employee ID is empty, print "Error: ID Required" and stop. (Use a Guard Clause).
  2. Requirement 2: Use a Switch Expression to determine the Access Level based on the Clearance Level:
    • 1 -> "Public Area Only"
    • 2 -> "Office Area"
    • 3 -> "Server Room"
    • 4 -> "Executive Suite"
    • 5 -> "Top Secret Vault"
    • Any other number -> "Invalid Clearance Level"
  3. Requirement 3: If the Clearance Level is 5 AND the Employee ID is "ADMIN-001", print "Welcome, Director." Otherwise, print the assigned Access Level.
Lecture 05 · Fundamentals

Loops: Mastering Iteration

Prerequisite: Lecture 4

What is Iteration?

Iteration is the process of executing a block of code repeatedly until a specific condition is met. In professional software, loops are used for processing data sets, polling for status changes, or repeating user prompts until valid input is received.

Professional Concept: Time Complexity (Big O)

When you write a loop, you are consuming CPU cycles. A single loop through a list of $n$ items has a time complexity of O(n). However, if you put a loop inside another loop (nested loops), the complexity becomes O(n²). Professionals always strive to minimize nested loops to prevent "performance bottlenecks" as data grows.

1. The for Loop

The for loop is best used when you know exactly how many times the code should run. It is a "counter-driven" loop.

ForLoop.cs
// Structure: (Initialization; Condition; Iterator)
for (int i = 0; i < 5; i++) 
{
    Console.WriteLine($"Processing item number: {i}");
}

Breakdown:
1. int i = 0: Starts the counter.
2. i < 5: The loop runs as long as this is true.
3. i++: Increases the counter by 1 after each loop.

2. The while and do-while Loops

These are "condition-driven" loops. They are used when you don't know how many times the loop will run, but you know when it should stop.

The while Loop (Check then Act)

Checks the condition before executing the block. If the condition is false at the start, the code inside never runs.

bool isRunning = true;
while (isRunning) 
{
    Console.WriteLine("System is scanning...");
    // Logic to eventually set isRunning = false;
}

The do-while Loop (Act then Check)

Executes the block once first, then checks the condition. This guarantees the code runs at least once.

string input;
do 
{
    Console.WriteLine("Please enter 'exit' to stop:");
    input = Console.ReadLine();
} while (input != "exit");

3. The foreach Loop (Industry Standard)

The foreach loop is the most common loop in professional C# development. It is used to iterate through collections (Arrays, Lists, Dictionaries) without needing an index counter.

ForeachExample.cs
string[] employees = { "Alice", "Bob", "Charlie" };

foreach (var name in employees) 
{
    Console.WriteLine($"Sending email to: {name}");
}
⚠️ Professional Warning: The Read-Only Nature

You cannot modify a collection while iterating through it with a foreach loop. If you try to add or remove an item from the list inside a foreach, C# will throw a InvalidOperationException. To modify a list while looping, use a standard for loop and iterate backwards.

4. Loop Control: break and continue

Professionals use these keywords to optimize loops and handle edge cases.

  • break: Immediately terminates the loop and jumps to the code following the loop.
  • continue: Skips the current iteration and jumps immediately to the next cycle of the loop.
for (int i = 1; i <= 10; i++) 
{
    if (i == 3) continue; // Skip 3
    if (i == 7) break;    // Stop entirely at 7
    
    Console.WriteLine(i); // Output: 1, 2, 4, 5, 6
}
🎯 Professional Lab 5.1: Smart Data Processor

Scenario: You are processing a list of server response times (in milliseconds). You need to analyze the data and filter out "noise".

Requirements:

  1. Create an array of integers representing response times: { 120, 450, 80, -10, 1100, 90, 300, -5, 150 }.
  2. Use a foreach loop to iterate through the times.
  3. Rule 1 (Data Cleaning): If a response time is negative, it's an error. Use continue to skip it.
  4. Rule 2 (Critical Failure): If a response time exceeds 1000ms, the server is considered "Crashed". Use break to stop processing immediately.
  5. Rule 3 (Analysis): Calculate the total sum of all valid response times processed.
  6. Final Output: Print the total sum and the number of valid servers processed before the crash (or end of list).
Lecture 06 · Core Concepts

Methods & Parameters

What Are Methods?

Methods are reusable blocks of code that perform a specific task. Instead of writing the same logic multiple times, we place that logic inside a method and call it whenever needed.

Methods help make programs:

  • Cleaner and easier to read
  • Reusable and modular
  • Easier to debug and maintain
  • More organized in large applications
void SayHello() {
    Console.WriteLine("Hello World");
}

Once defined, the method can be called anywhere in the program:

SayHello();
SayHello();
SayHello();

Defining Methods

A method definition usually contains:

  • Access modifier (optional)
  • Return type
  • Method name
  • Parameters
  • Method body
static int Add(int x, int y) => x + y;

void Greet(string name, string title = "Dr.") {
    Console.WriteLine($"Hello {title} {name}");
}

Understanding Return Types

The return type tells C# what kind of value the method sends back.

  • int → returns a whole number
  • double → returns decimal values
  • string → returns text
  • bool → returns true or false
  • void → returns nothing
int Square(int number) {
    return number * number;
}

Calling the method:

int result = Square(5);

Console.WriteLine(result);

Method Parameters

Parameters allow methods to receive data from outside. They act like variables that belong to the method.

void DisplayUser(string username, int age) {
    Console.WriteLine(username);
    Console.WriteLine(age);
}

Passing arguments into the method:

DisplayUser("Alex", 22);

Default Parameters

Default parameters provide fallback values when an argument is not supplied. This makes methods more flexible.

void Welcome(string name = "Guest") {
    Console.WriteLine($"Welcome {name}");
}
Welcome();
Welcome("Emma");

Expression-Bodied Methods

Small methods can be shortened using the lambda-style arrow syntax. This creates cleaner and shorter code.

int Multiply(int a, int b) => a * b;

Method Overloading

C# allows multiple methods with the same name as long as their parameters are different.

int Add(int a, int b) {
    return a + b;
}

double Add(double a, double b) {
    return a + b;
}

The compiler automatically selects the correct version.

Pass By Value

By default, C# passes arguments by value. This means the method receives a copy of the data.

void ChangeNumber(int num) {
    num = 100;
}

int value = 10;

ChangeNumber(value);

Console.WriteLine(value);

Output:

10

The original variable remains unchanged because only a copy was modified.

Using ref Parameters

The ref keyword passes variables by reference, allowing the method to directly modify the original variable.

void Increase(ref int number) {
    number++;
}

int score = 5;

Increase(ref score);

Console.WriteLine(score);

Using out Parameters

The out keyword is used when a method needs to return multiple values.

void GetValues(out int x, out int y) {
    x = 10;
    y = 20;
}

GetValues(out int a, out int b);

Console.WriteLine(a);
Console.WriteLine(b);

Local Methods

Methods can also exist inside another method. These are called local methods.

void MainMethod() {

    void PrintMessage() {
        Console.WriteLine("Inside Local Method");
    }

    PrintMessage();
}

Best Practices

  • Keep methods short and focused
  • Use meaningful method names
  • Avoid methods that do too many tasks
  • Reuse methods whenever possible
  • Use parameters instead of hardcoded values

Common Mistakes

  • Forgetting the return statement
  • Using the wrong return type
  • Passing incorrect argument types
  • Creating methods with too many parameters
  • Not understanding value vs reference passing

Mini Practice

Try creating the following methods yourself:

  • A method that calculates area of a rectangle
  • A method that checks if a number is even
  • A method that prints a full name
  • A method with a default parameter
  • A method using ref or out
Lecture 07 · Core Concepts

Collections & LINQ

What Are Collections?

Collections are data structures used to store and manage groups of objects. Instead of creating multiple individual variables, collections allow us to organize related data efficiently.

C# provides several built-in collection types that are powerful, flexible, and easy to use.

  • Arrays
  • Lists
  • Dictionaries
  • Queues
  • Stacks
  • HashSets

Why Use Collections?

Collections help developers work with large amounts of data dynamically. They make programs cleaner and more scalable.

  • Store multiple values together
  • Add or remove data dynamically
  • Search and sort efficiently
  • Loop through items easily
  • Organize related information

Working with Lists

A List<T> is one of the most commonly used collections in C#. Unlike arrays, lists can grow and shrink dynamically.

var numbers = new List<int> { 10, 20, 30, 40 };

numbers.Add(50);

numbers.RemoveAt(0);

After removing the first element, the list becomes:

20, 30, 40, 50

Adding Items to a List

Use the Add() method to insert new items into a list.

var fruits = new List<string>();

fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Orange");

Accessing List Elements

List items are accessed using indexes. Indexes start from 0.

Console.WriteLine(fruits[0]);

Console.WriteLine(fruits[1]);

Looping Through Lists

Lists are commonly used with loops to process every item.

foreach (var item in fruits) {
    Console.WriteLine(item);
}

Useful List Methods

Method Description
Add() Adds a new item
Remove() Removes a specific item
RemoveAt() Removes item by index
Contains() Checks if item exists
Clear() Removes all items
Count Returns total number of items

Using Dictionaries

Dictionaries store data as key-value pairs. They are useful when each item has a unique identifier.

var students = new Dictionary<int, string>();

students.Add(1, "John");
students.Add(2, "Emma");

Console.WriteLine(students[1]);

Understanding LINQ

LINQ stands for Language Integrated Query. It allows developers to query collections using simple and readable syntax.

LINQ works with:

  • Lists
  • Arrays
  • Dictionaries
  • Databases
  • XML data

Introduction to LINQ

LINQ (Language Integrated Query) allows you to query collections like SQL.

var highScores = numbers
    .Where(n => n > 25)
    .OrderByDescending(n => n);

This query:

  • Filters numbers greater than 25
  • Sorts them in descending order

The Where() Method

Where() filters data based on a condition.

var evenNumbers = numbers.Where(n => n % 2 == 0);

Only even numbers will be selected.

The Select() Method

Select() transforms data into another form.

var squared = numbers.Select(n => n * n);

This creates a new collection with squared values.

Sorting Data with LINQ

LINQ provides easy methods for sorting collections.

var ascending = numbers.OrderBy(n => n);

var descending = numbers.OrderByDescending(n => n);

Finding Data

LINQ can quickly search for matching values.

var firstLargeNumber = numbers.First(n => n > 25);

The first number greater than 25 will be returned.

Counting Items

The Count() method returns the total number of matching items.

int total = numbers.Count();

int evenCount = numbers.Count(n => n % 2 == 0);

Combining LINQ Methods

Multiple LINQ methods can be chained together for powerful queries.

var result = numbers
    .Where(n => n > 10)
    .OrderBy(n => n)
    .Select(n => n * 2);

Deferred Execution

Most LINQ queries use deferred execution. This means the query runs only when the data is actually used.

var query = numbers.Where(n => n > 20);

Console.WriteLine(query.Count());

Common LINQ Methods

Method Purpose
Where() Filter data
Select() Transform data
OrderBy() Sort ascending
OrderByDescending() Sort descending
First() Get first matching item
Count() Count items
Any() Check if data exists

Best Practices

  • Use collections instead of many separate variables
  • Choose the correct collection type for the task
  • Keep LINQ queries readable
  • Avoid overly complex chained queries
  • Use meaningful variable names

Common Mistakes

  • Using the wrong collection type
  • Forgetting indexes start at 0
  • Modifying collections during iteration
  • Writing unreadable LINQ chains
  • Ignoring null values in queries

Mini Practice

Try creating the following:

  • A list of student names
  • A dictionary of product prices
  • A LINQ query that finds even numbers
  • A query that sorts numbers descending
  • A query using both Where() and Select()
Lecture 08 · Core Concepts

Strings & Formatting

What Are Strings?

A string is a sequence of characters used to store text. Strings are one of the most commonly used data types in C#.

Examples of strings:

  • Names
  • Email addresses
  • Messages
  • User input
  • File paths
string name = "John";

string message = "Welcome to C#";

Creating Strings

Strings are written inside double quotation marks.

string city = "London";

string language = "C#";

String Manipulation

C# provides many built-in methods for modifying and working with strings.

string text = "  C# Programming  ";

Console.WriteLine(text.Trim().ToUpper());

Console.WriteLine(text.Contains("C#"));

This example:

  • Removes extra spaces using Trim()
  • Converts text to uppercase using ToUpper()
  • Checks if text contains a value using Contains()

Common String Methods

Method Description
ToUpper() Converts text to uppercase
ToLower() Converts text to lowercase
Trim() Removes extra spaces
Contains() Checks for matching text
Replace() Replaces text
Substring() Extracts part of a string
StartsWith() Checks starting characters
EndsWith() Checks ending characters

Converting Case

Strings can easily be converted between uppercase and lowercase.

string word = "Programming";

Console.WriteLine(word.ToUpper());

Console.WriteLine(word.ToLower());

Replacing Text

The Replace() method changes parts of a string.

string sentence = "I like Java";

sentence = sentence.Replace("Java", "C#");

Console.WriteLine(sentence);

Extracting Text

The Substring() method extracts a portion of a string.

string value = "Programming";

Console.WriteLine(value.Substring(0, 4));

Output:

Prog

Escaping Characters

Escape characters allow special characters inside strings.

Console.WriteLine("She said \"Hello\"");

Console.WriteLine("C:\\Projects\\App");

Verbatim Strings

Verbatim strings use the @ symbol to preserve formatting.

string path = @"
C:\Projects\MyApp\data.txt";

Console.WriteLine(path);

String Concatenation

Concatenation means joining strings together.

string firstName = "John";

string lastName = "Doe";

string fullName = firstName + " " + lastName;

Console.WriteLine(fullName);

String Interpolation

String interpolation is a cleaner way to insert variables into strings.

Console.WriteLine($"Date: {DateTime.Now:d}");

Console.WriteLine($"Price: {19.99:C}"); // Currency

The $ symbol allows variables and expressions inside curly braces.

Formatting Values

C# supports formatting numbers, dates, and currency values.

double price = 99.95;

Console.WriteLine($"{price:C}");

Console.WriteLine($"{price:N2}");

Console.WriteLine($"{price:P}");
Format Meaning
C Currency
N2 Number with 2 decimals
P Percentage
d Short date format

Working with String Length

The Length property returns the number of characters.

string username = "Admin";

Console.WriteLine(username.Length);

Splitting Strings

The Split() method separates text into multiple parts.

string data = "HTML,CSS,JavaScript";

var skills = data.Split(',');

foreach (var skill in skills) {
    Console.WriteLine(skill);
}

Comparing Strings

Strings can be compared using comparison operators or helper methods.

string a = "C#";

string b = "c#";

Console.WriteLine(a == b);

Console.WriteLine(a.Equals(b, StringComparison.OrdinalIgnoreCase));

String Immutability

Strings in C# are immutable. This means their contents cannot be changed after creation.

Methods like Replace() or ToUpper() create new strings instead of modifying the original one.

string textValue = "hello";

string upper = textValue.ToUpper();

Console.WriteLine(textValue);

Console.WriteLine(upper);

Best Practices

  • Use meaningful variable names
  • Prefer string interpolation over concatenation
  • Trim user input when necessary
  • Use formatting for readable output
  • Keep string operations simple and readable

Common Mistakes

  • Forgetting escape characters
  • Using invalid indexes in Substring()
  • Ignoring whitespace in user input
  • Comparing strings with incorrect casing
  • Confusing immutable behavior

Mini Practice

Try creating programs that:

  • Convert text to uppercase
  • Replace words in a sentence
  • Display formatted prices
  • Split a CSV string into values
  • Create a formatted welcome message using interpolation
Lecture 09 · Advanced

Object-Oriented Programming

Welcome, class. Today we deep-dive into the foundational architecture of scalable software design: Object-Oriented Programming (OOP). As software systems grow in complexity, linear, procedural code inevitably collapses under its own weight. OOP provides us with a framework to manage this complexity by partitioning our programs into self-contained, reusable modules modeled after real-world entities.

The Conceptual Paradigm: Blueprints vs. Artifacts

Before looking at the syntax, you must master the fundamental distinction between a Class and an Object:

  • The Class (The Blueprint): This is an abstract data type. It defines the structure, capabilities, and rules of engagement, but it consumes no operational memory for data storage. It is merely a design.
  • The Object (The Instance): This is the concrete artifact generated from that blueprint. It lives dynamically within your system's RAM, maintaining its own unique state.

Consider the architectural example below written in C#. Here, we define a baseline archetype (Person) and a specialized extension of that archetype (Developer).

public class Person {
    public string Name { get; set; }
    public virtual void Work() => Console.WriteLine("Working...");
}

public class Developer : Person {
    public override void Work() => Console.WriteLine("Coding...");
}

Anatomy of the Mechanics: Inheritance & Polymorphism

Let us break down the underlying mechanics of this code slice. Notice the use of the : operator in Developer : Person. This establishes an IS-A relationship (Inheritance). A Developer is a Person, meaning it automatically inherits the Name property without explicit redeclaration.

However, the real magic happens within the execution behavior, driven by two critical keywords:

  1. virtual: Declared in the base class, this signals to the compiler that this method's behavior is fluid and can be intercepted by derived child classes.
  2. override: Declared in the subclass, this actively intercepts the runtime execution path, substituting the base behavior with a specialized implementation.

Advanced Execution: Dynamic Binding in Action

To fully appreciate why this matters to a software architect, look at how the runtime engine processes these objects when grouped together. Because of polymorphism, a collection typed to the base class can hold any derived class instance seamlessly.

List<Person> team = new List<Person>() {
    new Person { Name = "Professor Smith" },
    new Developer { Name = "Grace Hopper" }
};

foreach (var member in team) {
    Console.Write(member.Name + " execution path: ");
    member.Work(); 
}

When this loop runs, the output demonstrates Dynamic Binding (or late binding). The system determines which method to call at runtime based on the actual object type, not the variable type:

Professor Smith execution path: Working...
Grace Hopper execution path: Coding...

Professor's Lab Task: Expanding the Ecosystem

To reinforce today's lecture, you must complete a practical coding task. Your goal is to introduce a third tier to our organizational hierarchy that introduces unique tracking and alters behavior appropriately.

Your Instructions:

  • Create a new class named Manager that inherits directly from Person.
  • Add an exclusive property to the Manager class: TeamSize (an integer type).
  • Override the Work() method so that it outputs: "Managing a team of [TeamSize] people..." instead of the generic response.
public class Manager : Person {
    // 1. Task: Define your TeamSize property here
    
    // 2. Task: Override the Work() method using string interpolation
}

Verify your solution by creating an instance of your Manager, assigning it a TeamSize of 5, adding it to the team list created above, and confirming that the output shifts dynamically at runtime.

Lecture 10 · Advanced

Exceptions & File I/O

Handling Errors

try {
    int x = int.Parse(Console.ReadLine());
} catch (FormatException ex) {
    Console.WriteLine("Invalid number format.");
} finally {
    Console.WriteLine("Done.");
}

Reading/Writing Files

File.WriteAllText("log.txt", "Hello C#");
string content = File.ReadAllText("log.txt");
Lecture 11 · Advanced

The .NET Ecosystem

NuGet & Libraries

Use dotnet add package to install community libraries like Newtonsoft.Json.

Asynchronous Programming

public async Task<string> GetDataAsync() {
    var client = new HttpClient();
    return await client.GetStringAsync("https://api.example.com");
}
Lecture 12 · Capstone

Capstone Project: Task Manager

Build a console-based Task Manager that uses everything we've learned: Lists, LINQ, OOP, and File I/O.

// Project requirements:
// 1. Add/Remove tasks
// 2. Mark tasks as complete
// 3. Save tasks to tasks.json
// 4. Filter tasks using LINQ
Lecture 13 · Advanced

Asynchronous Programming with async & await

⏱ ~35 minAdvancedRequires: Lecture 12

Asynchronous programming is fundamental to scalable .NET backends and responsive UI applications. By leveraging the Task-based Asynchronous Pattern (TAP), async, and await, your threads remain unblocked during I/O-bound database queries, HTTP network calls, and file operations.

1. The Task-based Asynchronous Pattern (TAP)

In synchronous execution, a thread waits idle while network packets travel across the wire or a disk head seeks sectors. In asynchronous C#, the thread is yielded back to the thread pool to process other web requests while the I/O completion port awaits completion:

WeatherService.cs
using System.Net.Http.Json;

public class WeatherService
{
    private readonly HttpClient _http = new();

    // Task<T> represents an ongoing operation that will yield a T in the future
    public async Task<WeatherForecast?> FetchForecastAsync(string city, CancellationToken ct = default)
    {
        string url = $"https://api.weather.com/v1/{Uri.EscapeDataString(city)}";
        
        // Thread is NOT blocked during GetFromJsonAsync!
        return await _http.GetFromJsonAsync<WeatherForecast>(url, ct);
    }
}

public record WeatherForecast(string City, double TemperatureC, string Summary);

2. CancellationTokens & Resource Management

Every asynchronous API in production .NET should accept a CancellationToken. When a user navigates away from a page or closes an HTTP connection, the token triggers cancellation to immediately stop wasteful backend work.

Program.cs
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Timeout after 5s

try
{
    var service = new WeatherService();
    var forecast = await service.FetchForecastAsync("London", cts.Token);
    Console.WriteLine($"Forecast: {forecast?.Summary} at {forecast?.TemperatureC}°C");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Request timed out or was aborted by the client.");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Network error: {ex.Message}");
}
⚠️ Common Mistakes
  • Avoid async void: Never write async void except for UI event handlers. Exceptions thrown inside an async void method cannot be caught by callers and will immediately crash the process. Always return Task or Task<T>.
  • Do not use .Result or .Wait(): Calling task.Result or task.Wait() synchronously blocks the calling thread, risking thread pool starvation and deadlocks in ASP.NET SynchronizationContexts. Always use await.

3. Concurrency with Task.WhenAll & ValueTask

When you have multiple independent I/O tasks, run them concurrently instead of sequentially with Task.WhenAll:

DashboardService.cs
public async Task<DashboardData> LoadDashboardAsync(int userId, CancellationToken ct)
{
    // Initiate both queries concurrently without awaiting yet
    Task<User> userTask = _userRepo.GetByIdAsync(userId, ct);
    Task<List<Order>> ordersTask = _orderRepo.GetRecentOrdersAsync(userId, ct);

    // Wait for both to complete simultaneously
    await Task.WhenAll(userTask, ordersTask);

    return new DashboardData(userTask.Result, ordersTask.Result);
}
💡 Pro Tips
  • When to use ValueTask<T>: Use ValueTask<T> when a method frequently completes synchronously (for instance, reading from an in-memory cache). It avoids heap allocations for Task objects when results are already available.
  • ConfigureAwait(false): In general class libraries that do not depend on UI contexts, appending .ConfigureAwait(false) prevents capturing synchronization context and improves throughput.

What is the primary danger of using `async void` instead of `async Task` in C#?

  1. It causes a compilation error in modern C#
  2. Uncaught exceptions cannot be observed or caught by the calling code, causing unhandled process crashes
  3. It forces the method to run synchronously on the UI thread
  4. It prevents the method from accepting parameters

Because `async void` methods do not return a Task, the caller cannot await them or catch exceptions thrown inside them. Any unhandled exception is posted directly to the synchronization context and terminates the application.

What happens when calling `.Result` on a running Task instead of awaiting it?

  1. The Task is automatically canceled
  2. The calling thread blocks synchronously until the Task completes, causing potential thread pool starvation and deadlocks
  3. The runtime spawns a secondary background process
  4. The Task yields control back to the operating system

`.Result` blocks the calling thread synchronously. This ties up thread pool threads and, if run in an environment with a SynchronizationContext (like ASP.NET classic or WPF/WinForms), frequently causes circular deadlocks.

Which utility method allows executing multiple independent Tasks concurrently and waiting for all of them to finish?

  1. Task.RunSynchronously()
  2. Task.WhenAll()
  3. Task.WaitAny()
  4. ThreadPool.QueueUserWorkItem()

`Task.WhenAll()` returns a Task that completes only when all supplied tasks have completed, enabling parallel asynchronous execution of multiple I/O operations.

Lecture 14 · Advanced

Delegates, Lambdas & Events

⏱ ~35 minAdvancedRequires: Lecture 13

Delegates are type-safe function pointers that form the bedrock of event-driven architectures, LINQ expressions, and callback pipelines in .NET. Master custom delegates, standard library delegates (Action, Func, Predicate), and thread-safe event publishing.

1. What is a Delegate?

A delegate is a reference type that defines a strict method signature (return type and parameters). Any method matching that signature can be assigned to the delegate:

DelegatesBasics.cs
// Custom delegate declaration
public delegate int MathOperation(int a, int b);

public class Calculator
{
    public static int Add(int x, int y) => x + y;
    public static int Multiply(int x, int y) => x * y;
}

public class Program
{
    public static void Main()
    {
        MathOperation op = Calculator.Add;
        Console.WriteLine(op(4, 5)); // 9

        op = Calculator.Multiply;
        Console.WriteLine(op(4, 5)); // 20
    }
}

2. Built-in Delegates: Action, Func, and Predicate

In modern C#, you rarely need to declare custom delegate types. The .NET BCL provides generic delegates that cover virtually every scenario:

  • Action<T1, T2...>: Represents a method returning void with up to 16 parameters.
  • Func<T1, T2..., TResult>: Represents a method returning TResult with up to 16 parameters.
  • Predicate<T>: Equivalent to Func<T, bool>, used for filtering criteria.
StandardDelegates.cs
// Action: returns void
Action<string> logger = message => Console.WriteLine($"[LOG {DateTime.UtcNow:s}] {message}");
logger("Application booted");

// Func: last generic type argument is always the return type
Func<string, int, bool> isLongerThan = (text, threshold) => text.Length > threshold;
bool check = isLongerThan("Enterprise .NET", 10); // true

// Predicate: filtering lists
Predicate<int> isEven = n => n % 2 == 0;
List<int> numbers = [1, 2, 3, 4, 5, 6];
List<int> evens = numbers.FindAll(isEven); // [2, 4, 6]

3. The Event Pattern & Memory Leaks

Events encapsulate delegates using the publish-subscribe pattern, ensuring that external classes can only subscribe (+=) or unsubscribe (-=), preventing external callers from clearing or triggering the delegate directly.

EventsDemo.cs
// Custom event arguments inheriting from EventArgs
public class OrderEventArgs : EventArgs
{
    public required string OrderId { get; init; }
    public decimal TotalAmount { get; init; }
}

public class OrderProcessor
{
    // Standard .NET event declaration
    public event EventHandler<OrderEventArgs>? OrderCompleted;

    public void ProcessOrder(string id, decimal amount)
    {
        Console.WriteLine($"Processing order {id} for ${amount:F2}...");
        
        // Thread-safe invocation using null-conditional operator
        OrderCompleted?.Invoke(this, new OrderEventArgs 
        { 
            OrderId = id, 
            TotalAmount = amount 
        });
    }
}

// Subscriber
public class InvoiceService
{
    public void OnOrderCompleted(object? sender, OrderEventArgs e)
    {
        Console.WriteLine($"Generating invoice for order {e.OrderId}: ${e.TotalAmount:F2}");
    }
}
⚠️ Common Mistakes
  • Event Subscription Memory Leaks: If a long-lived publisher holds an event subscription to a short-lived subscriber without unsubscribing (publisher.Event -= subscriber.Handler), the garbage collector cannot collect the subscriber. Always unsubscribe or implement IDisposable.
  • Calling delegate without null check: In multithreaded apps, if the last subscriber unregisters between your null check and invocation, a NullReferenceException will occur. Always use EventName?.Invoke(this, args);.

In the delegate `Func`, which type is the return value?

  1. int
  2. string
  3. bool
  4. void

In the generic `Func` delegate family, the last generic type parameter is always the return type of the method. The preceding types represent the parameter types.

Why should you always invoke events using `MyEvent?.Invoke(this, e)` instead of `MyEvent(this, e)`?

  1. Because `Invoke` executes asynchronously on a background thread
  2. Because if no subscribers have registered, calling `MyEvent(...)` directly throws a `NullReferenceException`
  3. Because `Invoke` prevents garbage collection of the publisher
  4. Because C# 12 deprecated direct parentheses invocation

If an event has no registered subscribers, its internal multicast delegate field is `null`. Using `?.Invoke()` safely short-circuits and prevents a `NullReferenceException`.

How does an event subscription cause a memory leak if not unsubscribed?

  1. The operating system allocates non-paged memory for each delegate
  2. The publisher retains a reference to the subscriber via the delegate target, preventing the garbage collector from reclaiming the subscriber
  3. The CLR compiler leaks memory whenever using the `+=` operator
  4. It corrupts the process heap segment

A delegate instance stores a reference to its target object (the subscriber). As long as the publisher remains alive, that reference prevents the GC from freeing the subscriber.

Lecture 15 · Advanced

Generics, Constraints & Interface Design

⏱ ~40 minAdvancedRequires: Lecture 14

Generics provide type safety, code reusability, and high runtime performance by eliminating boxing/unboxing overhead. Paired with robust interface design and generic constraints, they form the cornerstone of enterprise software architecture and Dependency Injection.

1. Generic Classes & Methods

Generics decouple algorithms and data structures from specific concrete data types. In C#, generics are reified at runtime by the CLR, generating specialized native code for value types without boxing overhead:

GenericsBasics.cs
// Generic Result container pattern
public class Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(T value) { IsSuccess = true; Value = value; }
    private Result(string error) { IsSuccess = false; Error = error; }

    public static Result<T> Ok(T value) => new(value);
    public static Result<T> Fail(string error) => new(error);
}

// Generic Utility Method
public static class EnumerableExtensions
{
    public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> source, int count)
    {
        var rng = new Random();
        return source.OrderBy(_ => rng.Next()).Take(count);
    }
}

2. Generic Constraints (where T : ...)

Generic constraints inform the compiler what capabilities the type argument must possess. Without constraints, you can only invoke methods defined on object:

Repository.cs
public interface IEntity
{
    Guid Id { get; }
    DateTime CreatedAtUtc { get; }
}

// Repository constrained to reference types implementing IEntity with a parameterless constructor
public class Repository<T> where T : class, IEntity, new()
{
    private readonly Dictionary<Guid, T> _storage = new();

    public void Add(T entity)
    {
        _storage[entity.Id] = entity;
    }

    public T? GetById(Guid id) => _storage.GetValueOrDefault(id);

    public T CreateDefault()
    {
        // Allowed because of 'new()' constraint!
        return new T();
    }
}
💡 Pro Tips
  • Common Generic Constraints:
  • where T : class: Must be a reference type.
  • where T : struct: Must be a non-nullable value type.
  • where T : notnull: Must be non-nullable (C# 8+).
  • where T : new(): Must have a public parameterless constructor.
  • where T : unmanaged: Must be a blittable struct with no reference fields.

3. Interface Segregation & DI Contracts

Follow the Interface Segregation Principle (ISP): prefer small, focused interfaces over monolithic ones. This simplifies unit test mocking and dependency injection registration:

IRepositories.cs
public interface IReadOnlyRepository<T> where T : class
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<T>> ListAllAsync(CancellationToken ct = default);
}

public interface IWriteRepository<T> where T : class
{
    Task AddAsync(T entity, CancellationToken ct = default);
    Task DeleteAsync(Guid id, CancellationToken ct = default);
}

// Combined interface when both capabilities are required
public interface IRepository<T> : IReadOnlyRepository<T>, IWriteRepository<T> where T : class
{
}

What is the performance benefit of C# generics over non-generic collections like ArrayList?

  1. Generics run in a separate operating system thread
  2. Generics eliminate boxing and unboxing of value types, avoiding heap allocations and type-cast overhead
  3. Generics compress data in memory automatically
  4. Generics disable runtime exception checking

When value types (like int, struct) are stored in generic collections (List), the CLR produces specialized native instructions that store values inline without boxing them into heap objects.

What does the constraint `where T : new()` enable you to do inside a generic class?

  1. Inherit from another generic class
  2. Instantiate a new instance of T using `new T()`
  3. Convert T to an unmanaged pointer
  4. Ensure T is allocated on the stack

The `new()` constraint specifies that any type argument supplied for T must have a public parameterless constructor, allowing the compiler to permit expressions like `new T()`.

Which generic variance keyword allows an interface to return derived instances when a base type is expected (covariance)?

  1. in (e.g. IConsumer)
  2. out (e.g. IEnumerable)
  3. ref
  4. readonly

The `out` keyword marks a generic type parameter as covariant, meaning the type parameter can only be used as an output/return type (e.g., `IEnumerable`), enabling `IEnumerable` to be treated as `IEnumerable`.

Lecture 16 · Professional

Entity Framework Core & Relational Databases

⏱ ~45 minProfessionalRequires: Lecture 15

Entity Framework Core (EF Core) is the modern object-database mapper for .NET. It lets you query and persist domain models to relational databases (PostgreSQL, SQL Server, SQLite) using strongly-typed LINQ queries, automatic change tracking, and code-first database migrations.

1. Modeling Entities & DbContext

An entity is a domain model mapped to a database table. The DbContext coordinates querying, relationship configuration, and saving changes:

Models.cs
using Microsoft.EntityFrameworkCore;

// Domain Entities
public class Customer
{
    public int Id { get; set; }
    public required string FullName { get; set; }
    public required string Email { get; set; }
    
    // Navigation property: One-to-Many
    public List<Order> Orders { get; set; } = new();
}

public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; } = DateTime.UtcNow;
    public decimal TotalAmount { get; set; }
    
    // Foreign key and navigation back to Customer
    public int CustomerId { get; set; }
    public Customer Customer { get; set; } = null!;
}

// Application DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(entity =>
        {
            entity.HasKey(c => c.Id);
            entity.HasIndex(c => c.Email).IsUnique();
            entity.Property(c => c.FullName).HasMaxLength(150).IsRequired();
        });
    }
}

2. Efficient LINQ Queries & AsNoTracking

EF Core translates LINQ expressions directly into optimized SQL queries executed by the database server:

Queries.cs
public async Task<List<CustomerDto>> GetTopSpendersAsync(AppDbContext db, CancellationToken ct)
{
    // AsNoTracking(): Skips change-tracking snapshots; 2-3x faster for read-only queries!
    return await db.Customers
        .AsNoTracking()
        .Where(c => c.Orders.Any())
        .Select(c => new CustomerDto(
            c.Id,
            c.FullName,
            c.Email,
            c.Orders.Sum(o => o.TotalAmount)
        ))
        .OrderByDescending(dto => dto.TotalSpent)
        .Take(10)
        .ToListAsync(ct);
}

public record CustomerDto(int Id, string FullName, string Email, decimal TotalSpent);
⚠️ Common Mistakes
  • The N+1 Query Problem: Never access navigation properties in a loop without eager loading (.Include(c => c.Orders)) or projecting with .Select(). Otherwise, EF executes 1 query for the parent and N separate queries for each child.
  • Client-Side Evaluation: Avoid invoking arbitrary C# methods inside Where() or Select() clauses that the EF provider cannot translate into SQL. This forces EF to pull entire tables into memory before filtering.

3. Transactions & Unit of Work (SaveChangesAsync)

SaveChangesAsync collects all dirty, added, and deleted entities and executes them inside an implicit database transaction:

OrderService.cs
public async Task<Order> CreateOrderAsync(AppDbContext db, int customerId, decimal amount, CancellationToken ct)
{
    var customer = await db.Customers.FindAsync([customerId], ct)
        ?? throw new KeyNotFoundException($"Customer {customerId} not found");

    var order = new Order
    {
        CustomerId = customerId,
        TotalAmount = amount,
        OrderDate = DateTime.UtcNow
    };

    db.Orders.Add(order);
    
    // Executes inside an atomic transaction
    await db.SaveChangesAsync(ct);
    return order;
}

Why should you include `.AsNoTracking()` on read-only queries in EF Core?

  1. It encrypts database responses across the network
  2. It tells EF Core not to store snapshots in the change tracker, significantly improving memory and CPU performance
  3. It prevents other database users from updating the rows
  4. It forces EF to execute raw ADO.NET SQL commands directly

When EF Core queries entities, it normally attaches them to the change tracker and saves a state snapshot. For read-only operations, `.AsNoTracking()` bypasses this tracking overhead, dramatically cutting memory allocations and query execution time.

How do you prevent the N+1 query problem when loading related child entities?

  1. Using `.Include()` for eager loading or projecting directly into DTOs with `.Select()`
  2. Calling `.ToList()` immediately after `.Where()`
  3. Disabling database indexes on the foreign key column
  4. Using synchronous `ToList()` instead of asynchronous `ToListAsync()`

Eager loading with `.Include()` instructs EF Core to generate SQL JOINs to fetch parent and child rows in a single query, eliminating separate roundtrips for each individual child record.

What is the role of EF Core Migrations in a production project?

  1. Translating C# code into WebAssembly
  2. Version-controlling and incrementally evolving the database schema to match domain models
  3. Exporting database tables into JSON backup files
  4. Managing user authentication sessions

EF Core Migrations provide a structured, version-controlled mechanism to apply incremental schema updates (creating tables, modifying columns, adding indexes) to database instances across development, staging, and production.

Lecture 17 · Professional

Unit Testing & Mocking with xUnit & Moq

⏱ ~40 minProfessionalRequires: Lecture 16

High-quality software demands automated tests that verify correctness, protect against regressions, and document design behavior. Learn test-driven development (TDD) using xUnit (the de facto standard in .NET), parameterized testing with [Theory], and test doubles with Moq.

1. Test Anatomy: Arrange, Act, Assert

xUnit identifies test methods using attributes. Every clean unit test follows the AAA pattern:

BankAccountTests.cs
using Xunit;

public class BankAccountTests
{
    [Fact] // [Fact] denotes a test that is always true under invariant conditions
    public void Deposit_ValidAmount_IncreasesBalance()
    {
        // 1. Arrange: setup initial state and dependencies
        var account = new BankAccount(initialBalance: 100m);

        // 2. Act: invoke the method under test
        account.Deposit(50m);

        // 3. Assert: verify expected outcome
        Assert.Equal(150m, account.Balance);
    }

    [Fact]
    public void Withdraw_AmountGreaterThanBalance_ThrowsInvalidOperationException()
    {
        // Arrange
        var account = new BankAccount(initialBalance: 50m);

        // Act & Assert
        var ex = Assert.Throws<InvalidOperationException>(() => account.Withdraw(100m));
        Assert.Contains("Insufficient funds", ex.Message);
    }
}

2. Parameterized Tests with [Theory] & [InlineData]

Instead of duplicating test methods for different test cases, use [Theory] to run the same test logic against a matrix of inputs:

EmailValidatorTests.cs
public class EmailValidatorTests
{
    [Theory]
    [InlineData("[email protected]", true)]
    [InlineData("[email protected]", true)]
    [InlineData("missing-at-sign.com", false)]
    [InlineData("@missing-local.com", false)]
    [InlineData("", false)]
    [InlineData(null, false)]
    public void IsValidEmail_EvaluatesCorrectly(string? input, bool expected)
    {
        var validator = new EmailValidator();
        bool result = validator.IsValid(input);
        Assert.Equal(expected, result);
    }
}

3. Mocking Dependencies with Moq

Unit tests must test logic in isolation without talking to real databases or sending real emails. We substitute interfaces with mock objects:

OrderServiceTests.cs
using Moq;
using Xunit;

public class OrderServiceTests
{
    [Fact]
    public async Task PlaceOrderAsync_ValidOrder_SendsNotificationAndSaves()
    {
        // Arrange: Mock dependencies
        var mockRepo = new Mock<IOrderRepository>();
        var mockNotifier = new Mock<INotificationService>();

        var service = new OrderService(mockRepo.Object, mockNotifier.Object);
        var order = new Order { Id = 101, CustomerId = 1, TotalAmount = 99.99m };

        // Act
        await service.PlaceOrderAsync(order);

        // Assert: Verify mock interactions
        mockRepo.Verify(r => r.SaveAsync(order, It.IsAny<CancellationToken>()), Times.Once);
        mockNotifier.Verify(n => n.NotifyCustomerAsync(order.CustomerId, It.IsAny<string>()), Times.Once);
    }
}
💡 Pro Tips
  • FluentAssertions: For expressive assertions that read like natural language, install FluentAssertions. Replace Assert.Equal(5, list.Count); with list.Should().HaveCount(5).And.Contain(x);.
  • Fast and Deterministic: Unit tests should execute in milliseconds and never touch disk, network, or external clocks. Use TimeProvider (introduced in .NET 8) to mock time without calling DateTime.UtcNow.

In xUnit, what is the difference between `[Fact]` and `[Theory]` attributes?

  1. `[Fact]` tests async methods, while `[Theory]` tests synchronous methods
  2. `[Fact]` is a test with no parameters that asserts invariant behavior; `[Theory]` executes with parameterized test datasets via attributes like `[InlineData]`
  3. `[Fact]` is executed only in Release builds
  4. `[Theory]` tests run in a separate process container

`[Fact]` marks an invariant unit test requiring no parameters. `[Theory]` marks a parameterized data-driven test that xUnit runs repeatedly against each supplied dataset (from `[InlineData]`, `[MemberData]`, or `[ClassData]`).

What does `mock.Verify(x => x.SaveAsync(...), Times.Once)` check in a unit test?

  1. It checks whether the method threw an unhandled exception
  2. It asserts that the specified method on the mock was called exactly one time during the test run
  3. It measures the execution time of the method
  4. It cancels the mock if invoked more than once

Moq's .Verify() method asserts behavioral expectations on mocked interfaces, confirming that the code under test interacted with the dependency exactly the expected number of times.

Which phase of the Arrange-Act-Assert (AAA) pattern executes the method being evaluated?

  1. Arrange
  2. Act
  3. Assert
  4. Teardown

The "Act" phase is where you execute the specific method or business logic under test, capturing any returned values or thrown exceptions for subsequent assertion.

Lecture 18 · Professional

Final Project — Production ASP.NET Core Web API

⏱ ~60 minProfessionalRequires: All Previous Lectures

In this capstone project, you will build a production-grade RESTful Web API using ASP.NET Core Minimal APIs, Entity Framework Core, structured validation, dependency injection, and OpenAPI documentation.

1. Project Architecture & Minimal API Endpoints

Modern ASP.NET Core uses Minimal APIs to achieve industry-leading throughput with concise, readable syntax:

Program.cs
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Register Services in Dependency Injection Container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(opts => 
    opts.UseSqlite(builder.Configuration.GetConnectionString("Default") ?? "Data Source=store.db"));

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Route Group: /api/products
var products = app.MapGroup("/api/products").WithTags("Products");

products.MapGet("/", async (AppDbContext db, CancellationToken ct) =>
    Results.Ok(await db.Products.AsNoTracking().ToListAsync(ct)));

products.MapGet("/{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>
    await db.Products.FindAsync([id], ct) is Product p 
        ? Results.Ok(p) 
        : Results.NotFound(new { Message = $"Product {id} not found" }));

products.MapPost("/", async (CreateProductDto dto, AppDbContext db, CancellationToken ct) =>
{
    if (string.IsNullOrWhiteSpace(dto.Name) || dto.Price <= 0)
        return Results.BadRequest(new { Error = "Invalid name or price." });

    var product = new Product { Name = dto.Name, Price = dto.Price, Stock = dto.Stock };
    db.Products.Add(product);
    await db.SaveChangesAsync(ct);

    return Results.Created($"/api/products/{product.Id}", product);
});

products.MapDelete("/{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>
{
    var product = await db.Products.FindAsync([id], ct);
    if (product is null) return Results.NotFound();

    db.Products.Remove(product);
    await db.SaveChangesAsync(ct);
    return Results.NoContent();
});

app.Run();

public record CreateProductDto(string Name, decimal Price, int Stock);
public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

2. Global Error Handling Middleware

Never let unhandled exceptions leak stack traces to public API consumers. Use custom exception handling or app.UseExceptionHandler() with ProblemDetails:

MiddlewareConfig.cs
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/problem+json";

        var problem = new
        {
            Status = 500,
            Title = "An unexpected server error occurred.",
            TraceId = context.TraceIdentifier
        };

        await context.Response.WriteAsJsonAsync(problem);
    });
});
💡 Pro Tips
  • DTOs (Data Transfer Objects): Never accept or return EF Core entity classes directly in API endpoints. Exposing entities leaks internal schema details and makes your API vulnerable to mass-assignment over-posting attacks.
  • CancellationToken Propagation: Always accept CancellationToken ct in your endpoint delegates and pass it to async EF Core methods. If the client drops the connection, EF aborts the SQL query immediately.
🎯 Capstone Web API Deliverables

To finalize your C# & .NET Certification, implement these extensions:

  1. Add a PUT /api/products/{id} endpoint with concurrency token checks.
  2. Introduce an authentication endpoint using JWT Bearer tokens (app.UseAuthentication() and app.UseAuthorization()).
  3. Write xUnit integration tests using Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory to verify endpoints return correct HTTP status codes.

Why should you use DTOs instead of binding directly to EF Core entity classes in API endpoints?

  1. EF Core entities cannot be serialized to JSON
  2. It prevents over-posting attacks and decouples your public API contract from internal database schema
  3. DTOs automatically run in SQLite memory caches
  4. Minimal APIs require DTOs to compile

Accepting EF entities directly allows malicious callers to inject fields (like IsAdmin = true or Balance = 999999) that they should not modify. DTOs define strict input contracts and prevent over-posting vulnerabilities.

What is the standard HTTP status code returned upon successfully creating a resource via POST?

  1. 200 OK
  2. 201 Created
  3. 204 NoContent
  4. 301 MovedPermanently

HTTP 201 Created is the standard REST status code for a successful resource creation, typically accompanied by a `Location` header pointing to the URI of the newly created resource.

What does `Results.NoContent()` translate to in HTTP response terms?

  1. 404 Not Found
  2. 204 No Content (operation succeeded, empty response body)
  3. 400 Bad Request
  4. 500 Server Error

HTTP 204 No Content indicates that the server successfully processed the request (such as a DELETE or PUT) and there is no entity-body to return to the client.

Topic 19 - Real-World Practical Projects

Project 1: Enterprise Inventory Manager (LINQ & File I/O)

Hands-on Project .NET & LINQ
🎯 Project Goal

Build a robust C# Inventory Management application using LINQ queries, JSON serialization, and async tasks.

Project Overview

Master LINQ filtering, aggregation, file storage, and object-oriented C# design.

Full Code Implementation

InventoryApp.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.IO;

public class Product {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

public class InventoryManager {
    private List _products = new List();
    public void AddProduct(Product p) => _products.Add(p);

    public void DisplayLowStock(int threshold) {
        var lowStock = _products.Where(p => p.Stock < threshold).OrderBy(p => p.Stock);
        foreach (var p in lowStock) {
            Console.WriteLine($"{p.Name} (Category: {p.Category}) - Stock: {p.Stock}");
        }
    }
}

class Program {
    static void Main() {
        var manager = new InventoryManager();
        manager.AddProduct(new Product { Id = 1, Name = "Laptop", Category = "Tech", Price = 999.99m, Stock = 5 });
        manager.DisplayLowStock(10);
    }
}
Practice Challenge

Write a LINQ query that calculates total inventory valuation (SUM(Price * Stock)) grouped by Category!