Introduction

C# (pronounced "C-sharp") is a modern, object-oriented, and type-safe programming language developed by Microsoft. It's widely used for building a variety of applications, from web and mobile apps to games and enterprise software. The .NET framework (and its cross-platform successor, .NET) provides a robust platform for developing these applications.

1. Introduction to C# and .NET

What is C#?

C# is a versatile, high-level programming language that is part of the .NET ecosystem. It's known for its strong typing, object-oriented features, and garbage collection, which simplifies memory management. C# draws inspiration from C++, Java, and other languages, offering a balance of power and ease of use.

What is .NET?

.NET is a free, open-source developer platform for building many different types of applications. It includes:

Initially, .NET Framework was Windows-only. Now, with .NET (formerly .NET Core), you can build cross-platform applications that run on Windows, macOS, and Linux. This guide primarily focuses on modern C# and the cross-platform .NET.

Why Learn C# and .NET?

2. Setting Up Your Development Environment

The primary tool for C# and .NET development is Visual Studio.

2.1 Install Visual Studio (Community Edition)

  1. Go to the official Visual Studio website: https://visualstudio.microsoft.com/downloads/
  2. Download the Visual Studio Community edition, which is free for students, open-source contributors, and individual developers.
  3. Run the installer. During installation, select the following workloads:
    • .NET Desktop development (for console and desktop applications)
    • ASP.NET and web development (for web applications)
    • .NET Multi-platform App UI development (for cross-platform mobile/desktop apps, if interested)
    • Game development with Unity (if you plan to develop games)
  4. Click "Install".

2.2 Verify .NET SDK Installation

Visual Studio usually installs the necessary .NET SDKs. You can verify this by opening a command prompt or terminal and typing:

dotnet --version

This command should output the installed .NET SDK version (e.g., 8.0.x).

2.3 Creating Your First "Hello World" Application

Let's create a simple console application.

Using Visual Studio

  1. Open Visual Studio.
  2. Click "Create a new project".
  3. Search for "Console App" and select the template for "Console App" (make sure it's the C# version and uses .NET).
  4. Click "Next".
  5. Give your project a name (e.g., HelloWorldApp) and choose a location.
  6. Click "Next".
  7. Select the desired .NET version (e.g., .NET 8.0) and click "Create".

Visual Studio will create a project with a Program.cs file containing:

// Program.cs

Console.WriteLine("Hello, World!");
  1. Press F5 or click the "Run" button (green triangle) to run your application. A console window will appear, displaying "Hello, World!".

Using the .NET CLI (Command Line Interface)

  1. Open a command prompt or terminal.
  2. Navigate to the directory where you want to create your project.
  3. Create a new console project:
  4. dotnet new console -n HelloWorldCLI
  5. cd HelloWorldCLI

Run the application:

dotnet run

This will compile and execute your Program.cs file, showing "Hello, World!".

3. C# Fundamentals

3.1 Basic Syntax and Structure

A basic C# program consists of:

using System; // Imports the System namespace, providing basic functionalities like Console

namespace MyFirstProgram // Declares a namespace to organize your code
{
    class Program // Declares a class named Program
    {
        static void Main(string[] args) // The entry point of the application
        {
            // This is a single-line comment

            /*
             * This is a multi-line comment.
             * It can span multiple lines.
             */

            Console.WriteLine("Hello from MyFirstProgram!"); // A statement to print text
        }
    }
}

3.2 Data Types

C# is a strongly-typed language, meaning you must declare the type of a variable before using it. Data types are categorized into Value Types and Reference Types.

Value Types (store data directly)

Reference Types (store a reference to data in memory)

int age = 30;
double price = 19.99;
bool isActive = true;
char initial = 'J';
string name = "Alice";

Console.WriteLine($"Name: {name}, Age: {age}, Price: {price}, Active: {isActive}, Initial: {initial}");

3.3 Variables and Constants

// Variables
int score = 100;             // Declared and initialized
score = 120;                 // Value can be changed

string message;              // Declared
message = "Hello";           // Initialized later

var quantity = 50;           // Type (int) is inferred by the compiler
var greeting = "Hi there!";  // Type (string) is inferred

// Constants
const double PI = 3.14159;   // Value cannot be changed after this line
// PI = 3.14;                // This would cause a compile-time error

3.4 Operators

Operators perform operations on one or more operands.

int a = 10, b = 5;

// Arithmetic
Console.WriteLine($"a + b = {a + b}"); // 15
Console.WriteLine($"a / b = {a / b}"); // 2 (integer division)

// Assignment
a += 2; // a is now 12

// Comparison
Console.WriteLine($"a == b is {a == b}"); // False

// Logical
bool isAdult = true;
bool hasLicense = false;
Console.WriteLine($"Can drive: {isAdult && hasLicense}"); // False

// Increment/Decrement
int x = 5;
Console.WriteLine(x++); // Prints 5, then x becomes 6 (post-increment)
Console.WriteLine(++x); // x becomes 7, then prints 7 (pre-increment)

// Ternary
int age = 20;
string status = (age >= 18) ? "Adult" : "Minor";
Console.WriteLine(status); // Adult

3.5 Type Conversions

Converting one data type to another.

// Implicit
int myInt = 10;
double myDouble = myInt; // Implicit conversion from int to double

// Explicit (Casting)
double anotherDouble = 10.75;
int anotherInt = (int)anotherDouble; // Explicit conversion (casting), truncates to 10

// Using Convert class
string strNumber = "123";
int convertedInt = Convert.ToInt32(strNumber);

// Using Parse (throws exception if conversion fails)
string invalidNumber = "abc";

try
{
    int parsedInt = int.Parse(invalidNumber); // This will throw a FormatException
}
catch (FormatException)
{
    Console.WriteLine("Invalid number format for Parse.");
}

// Using TryParse (returns bool, safer)
string potentiallyValidNumber = "456";

if (int.TryParse(potentiallyValidNumber, out int result))
{
    Console.WriteLine($"Successfully parsed: {result}");
}
else
{
    Console.WriteLine("Could not parse the string.");
}

4. Control Structures

Control structures dictate the flow of execution in your program.

4.1 Conditional Statements

int temperature = 25;

// if-else if-else
if (temperature < 0)
{
    Console.WriteLine("Freezing weather.");
}
else if (temperature >= 0 && temperature <= 15)
{
    Console.WriteLine("Cold weather.");
}
else if (temperature > 15 && temperature <= 25)
{
    Console.WriteLine("Pleasant weather.");
}
else
{
    Console.WriteLine("Hot weather.");
}

// switch statement
string dayOfWeek = "Monday";

switch (dayOfWeek)
{
    case "Monday":
    case "Tuesday": // Fall-through is allowed if no code between cases
    case "Wednesday":
        Console.WriteLine("It's a weekday.");
        break;

    case "Saturday":
    case "Sunday":
        Console.WriteLine("It's the weekend!");
        break;

    default:
        Console.WriteLine("Invalid day.");
        break;
}

4.2 Looping Constructs

// for loop
for (int i = 0; i < 5; i++)
{
    Console.WriteLine($"For loop iteration: {i}");
}

// foreach loop (excellent for collections)
string[] fruits = { "Apple", "Banana", "Cherry" };
foreach (string fruit in fruits)
{
    Console.WriteLine($"Fruit: {fruit}");
}

// while loop
int count = 0;
while (count < 3)
{
    Console.WriteLine($"While loop count: {count}");
    count++;
}

// do-while loop
int num = 0;
do
{
    Console.WriteLine($"Do-While loop num: {num}");
    num++;
} while (num < 0); // Condition is false, but it runs once

4.3 Jump Statements

// break example
for (int i = 0; i < 10; i++)
{
    if (i == 5)
    {
        break; // Exit the loop when i is 5
    }
    Console.WriteLine($"Break example: {i}");
}
// Output: 0, 1, 2, 3, 4

// continue example
for (int i = 0; i < 5; i++)
{
    if (i == 2)
    {
        continue; // Skip printing when i is 2
    }
    Console.WriteLine($"Continue example: {i}");
}
// Output: 0, 1, 3, 4

// return example (see Methods section)
// This would typically be used inside a method to exit early and return a value.

5. Object-Oriented Programming (OOP) in C#

C# is fundamentally an object-oriented language. OOP principles help in designing modular, reusable, and maintainable code. The four pillars of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction.

5.1 Classes and Objects

// Define a class
public class Car
{
    // Properties (data)
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public string Color { get; set; }

    // Method (behavior)
    public void StartEngine()
    {
        Console.WriteLine($"{Make} {Model} engine started!");
    }

    public void Drive()
    {
        Console.WriteLine($"{Make} {Model} is driving.");
    }
}

// In Main method or another class:
Car myCar = new Car();
myCar.Make = "Toyota";
myCar.Model = "Camry";
myCar.Year = 2022;
myCar.Color = "Blue";

Car anotherCar = new Car();
anotherCar.Make = "Honda";
anotherCar.Model = "Civic";
anotherCar.Year = 2023;
anotherCar.Color = "Red";

myCar.StartEngine();
anotherCar.Drive();

5.2 Encapsulation

Encapsulation is the bundling of data (properties) and methods that operate on the data within a single unit (class), and restricting direct access to some of the object's components. This is achieved using access modifiers and properties.

Access Modifiers

Properties

Properties provide a flexible mechanism to read, write, or compute the value of a private field. They use get (accessor) and set (mutator) blocks.

public class BankAccount
{
    private decimal _balance; // private field

    // Public property with full access
    public string AccountNumber { get; set; } // Auto-implemented property

    // Public property with custom logic
    public decimal Balance
    {
        get { return _balance; }
        private set // Balance can only be set internally or by methods in this class
        {
            if (value < 0)
            {
                Console.WriteLine("Balance cannot be negative.");
                _balance = 0;
            }
            else
            {
                _balance = value;
            }
        }
    }

    public BankAccount(string accountNumber, decimal initialBalance)
    {
        AccountNumber = accountNumber;
        Balance = initialBalance; // This uses the 'set' accessor
    }

    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount; // This uses the 'set' accessor
            Console.WriteLine($"Deposited {amount}. New balance: {Balance}");
        }
    }

    public void Withdraw(decimal amount)
    {
        if (amount > 0 && Balance >= amount)
        {
            Balance -= amount; // This uses the 'set' accessor
            Console.WriteLine($"Withdrew {amount}. New balance: {Balance}");
        }
        else
        {
            Console.WriteLine("Insufficient funds or invalid amount.");
        }
    }
}

// Usage
BankAccount account = new BankAccount("123456789", 1000m);
Console.WriteLine($"Account: {account.AccountNumber}, Initial Balance: {account.Balance}");

account.Deposit(200);
account.Withdraw(500);
account.Withdraw(1000); // Insufficient funds

// account.Balance = -50; // This would cause a compile-time error due to private set

5.3 Inheritance

Inheritance allows a class (derived class or subclass) to inherit properties and methods from another class (base class or superclass). This promotes code reusability. C# supports single inheritance (a class can only inherit from one base class).

using System;

// Base class
class Employee
{
    public string Name { get; set; }
    public int Id { get; set; }

    public virtual void Work()
    {
        Console.WriteLine($"{Name} (ID: {Id}) is working.");
    }
}

// Derived class
class Manager : Employee
{
    public int TeamSize { get; set; }

    // Method unique to Manager
    public void ConductMeeting()
    {
        Console.WriteLine($"{Name} is conducting a meeting with {TeamSize} team members.");
    }

    // Override Work method
    public override void Work()
    {
        Console.WriteLine($"{Name} (ID: {Id}) is managing the team.");
    }
}

class Program
{
    static void Main()
    {
        Manager mgr = new Manager
        {
            Name = "Kiran",
            Id = 101,
            TeamSize = 8
        };

        mgr.Work();             // Overridden method in Manager
        mgr.ConductMeeting();   // Method specific to Manager

        // Output:
        // Kiran (ID: 101) is managing the team.
        // Kiran is conducting a meeting with 8 team members.
    }
}

5.4 Polymorphism

Polymorphism means "many forms." In C#, it allows objects of different classes to be treated as objects of a common base class. This is achieved through:

using System;

// ---------------------
// Method Overloading
// ---------------------
public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b) // Overloaded with different types
    {
        return a + b;
    }

    public int Add(int a, int b, int c) // Overloaded with more parameters
    {
        return a + b + c;
    }
}

// ---------------------
// Abstract Class
// ---------------------
public abstract class Shape
{
    public abstract double GetArea(); // Must be implemented by derived classes

    public void Display()
    {
        Console.WriteLine("This is a shape.");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

// ---------------------
// Interface
// ---------------------
public interface ILogger
{
    void LogMessage(string message);
    void LogError(string error);
}

public class ConsoleLogger : ILogger
{
    public void LogMessage(string message)
    {
        Console.WriteLine($"[MESSAGE]: {message}");
    }

    public void LogError(string error)
    {
        Console.Error.WriteLine($"[ERROR]: {error}");
    }
}

// ---------------------
// Usage Example
// ---------------------
class Program
{
    static void Main()
    {
        // Method Overloading
        Calculator calc = new Calculator();
        Console.WriteLine(calc.Add(5, 3));           // Calls Add(int, int)
        Console.WriteLine(calc.Add(5.5, 3.2));       // Calls Add(double, double)
        Console.WriteLine(calc.Add(1, 2, 3));        // Calls Add(int, int, int)

        // Abstract Class Polymorphism
        Shape circle = new Circle(5);                // Circle treated as Shape
        Console.WriteLine($"Circle Area: {circle.GetArea():F2}");
        circle.Display();

        // Interface Polymorphism
        ILogger logger = new ConsoleLogger();        // ConsoleLogger treated as ILogger
        logger.LogMessage("Application started.");
        logger.LogError("Something went wrong!");
    }
}

5.5 Constructors and Destructors

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    // Default constructor
    public Person()
    {
        FirstName = "Unknown";
        LastName = "Unknown";
        Age = 0;
        Console.WriteLine("Default Person created.");
    }

    // Parameterized constructor
    public Person(string firstName, string lastName) : this() // Calls default constructor first
    {
        FirstName = firstName;
        LastName = lastName;
        Console.WriteLine($"Person {FirstName} {LastName} created.");
    }

    // Another parameterized constructor
    public Person(string firstName, string lastName, int age) : this(firstName, lastName) // Calls the (string, string) constructor
    {
        Age = age;
        Console.WriteLine($"Person {FirstName} {LastName} (Age: {Age}) created.");
    }

    // Destructor (rarely used, for unmanaged resources)
    ~Person()
    {
        Console.WriteLine($"Person {FirstName} {LastName} is being finalized.");
    }
}

// Usage
Person p1 = new Person();
Person p2 = new Person("John", "Doe");
Person p3 = new Person("Jane", "Smith", 25);

5.6 Static Members

static members belong to the class itself, not to any specific instance of the class. You access them directly using the class name.

public static class MathOperations // Static class
{
    public static double PI = 3.14159; // Static field

    public static int Add(int a, int b) // Static method
    {
        return a + b;
    }

    public static int Subtract(int a, int b)
    {
        return a - b;
    }
}

public class Counter
{
    public static int InstanceCount = 0; // Static field, tracks instances

    public Counter()
    {
        InstanceCount++; // Incremented every time a new instance is created
    }
}

// Usage
Console.WriteLine($"PI: {MathOperations.PI}");
Console.WriteLine($"Sum: {MathOperations.Add(10, 5)}");

Counter c1 = new Counter();
Counter c2 = new Counter();

Console.WriteLine($"Number of Counter instances: {Counter.InstanceCount}"); // Output: 2

6. Methods and Functions

Methods (often called functions in other languages) are blocks of code that perform a specific task.

6.1 Defining and Calling Methods

public class Greeter
{
    // Method with no parameters and no return value (void)
    public void SayHello()
    {
        Console.WriteLine("Hello!");
    }

    // Method with one parameter and no return value
    public void SayHelloTo(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }

    // Method with parameters and a return value
    public int AddNumbers(int num1, int num2)
    {
        return num1 + num2;
    }

    // Method with optional parameters (must be at the end)
    public void Greet(string name, string greeting = "Hello")
    {
        Console.WriteLine($"{greeting}, {name}!");
    }
}

// Usage
Greeter greeter = new Greeter();

greeter.SayHello();
greeter.SayHelloTo("Alice");

int sum = greeter.AddNumbers(10, 20);
Console.WriteLine($"Sum: {sum}");

greeter.Greet("Bob");               // Uses default greeting "Hello"
greeter.Greet("Charlie", "Hi");     // Uses provided greeting "Hi"

6.2 Method Parameters (ref, out, params)

public class ParameterExamples
{
    public void Increment(int num) // Value parameter
    {
        num++;
        Console.WriteLine($"Inside Increment (value): {num}");
    }

    public void IncrementRef(ref int num) // ref parameter
    {
        num++;
        Console.WriteLine($"Inside IncrementRef (ref): {num}");
    }

    public void GetCoordinates(out int x, out int y) // out parameters
    {
        x = 10;
        y = 20;
        Console.WriteLine("Coordinates set.");
    }

    public int SumNumbers(params int[] numbers) // params parameter
    {
        int total = 0;
        foreach (int num in numbers)
        {
            total += num;
        }
        return total;
    }
}

// Usage
ParameterExamples pe = new ParameterExamples();

int val = 5;
pe.Increment(val); // val remains 5
Console.WriteLine($"Outside Increment (value): {val}"); // Output: 5

int refVal = 5;
pe.IncrementRef(ref refVal); // refVal becomes 6
Console.WriteLine($"Outside IncrementRef (ref): {refVal}"); // Output: 6

int coordX, coordY; // No need to initialize for out parameters
pe.GetCoordinates(out coordX, out coordY);
Console.WriteLine($"Coordinates: ({coordX}, {coordY})");

Console.WriteLine($"Sum of 1,2,3: {pe.SumNumbers(1, 2, 3)}");
Console.WriteLine($"Sum of 1,2,3,4,5: {pe.SumNumbers(1, 2, 3, 4, 5)}");

6.3 Return Values

Methods can return a value using the return keyword. The return type must match the method's declared return type. void means no value is returned.

6.4 Extension Methods

Extension methods allow you to add new methods to existing types without modifying the original type. They are static methods defined in a static class, where the first parameter is prefixed with this.

public static class StringExtensions // Must be a static class
{
    public static string CapitalizeFirstLetter(this string input) // 'this' keyword
    {
        if (string.IsNullOrEmpty(input))
        {
            return input;
        }
        return char.ToUpper(input[0]) + input.Substring(1);
    }

    public static int WordCount(this string text)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            return 0;
        }
        return text.Split(new char[] { ' ', '.', '?', '!' }, StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

// Usage
string sentence = "hello world. how are you?";
Console.WriteLine(sentence.CapitalizeFirstLetter()); // Output: Hello world. how are you?
Console.WriteLine($"Word count: {sentence.WordCount()}"); // Output: 5

7. Arrays, Collections, and Generics

7.1 Arrays

Arrays are fixed-size collections of elements of the same data type.

// Declare and initialize an array
int[] numbers = new int[5]; // Array of 5 integers, initialized to 0s
numbers[0] = 10;
numbers[1] = 20;
// ...

// Declare and initialize with values
string[] names = { "Alice", "Bob", "Charlie" };

// Accessing elements
Console.WriteLine($"First name: {names[0]}"); // Alice

// Iterating through an array using for loop
for (int i = 0; i < names.Length; i++)
{
    Console.WriteLine($"Name at index {i}: {names[i]}");
}

// Iterating through an array using foreach loop
foreach (string name in names)
{
    Console.WriteLine($"Name: {name}");
}

// Multi-dimensional arrays
int[,] matrix = new int[2, 3]
{
    { 1, 2, 3 },
    { 4, 5, 6 }
};

Console.WriteLine($"Element at (0,1): {matrix[0, 1]}"); // Output: 2

7.2 Common Collections

The System.Collections.Generic namespace provides flexible, type-safe collections.

using System.Collections.Generic; // Important for generic collections

// List<T>
List<string> cities = new List<string>();
cities.Add("New York");
cities.Add("London");
cities.Add("Paris");
cities.Insert(1, "Tokyo"); // Insert at specific index
cities.Remove("London");

Console.WriteLine("Cities in list:");
foreach (string city in cities)
{
    Console.WriteLine(city);
}
Console.WriteLine($"List contains Paris: {cities.Contains("Paris")}");

// Dictionary<TKey, TValue>
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 30);
ages.Add("Bob", 25);
ages["Charlie"] = 35; // Add or update

Console.WriteLine($"Alice's age: {ages["Alice"]}");

if (ages.TryGetValue("David", out int davidAge))
{
    Console.WriteLine($"David's age: {davidAge}");
}
else
{
    Console.WriteLine("David not found.");
}

Console.WriteLine("Ages in dictionary:");
foreach (var entry in ages)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}

// HashSet<T>
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(1); // Duplicate, won't be added

Console.WriteLine("Unique numbers:");
foreach (int num in uniqueNumbers)
{
    Console.WriteLine(num); // Output: 1, 2
}

// Queue<T>
Queue<string> tasks = new Queue<string>();
tasks.Enqueue("Task A");
tasks.Enqueue("Task B");

Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Output: Task A

// Stack<T>
Stack<string> history = new Stack<string>();
history.Push("Page 1");
history.Push("Page 2");

Console.WriteLine($"Last visited: {history.Pop()}"); // Output: Page 2

// Generics

// Generic class
public class Box<T> // T is a type parameter
{
    public T Content { get; set; }

    public Box(T content)
    {
        Content = content;
    }

    public void DisplayContent()
    {
        Console.WriteLine($"Box content: {Content}");
    }
}

// Generic method
public class Utilities
{
    public static void Swap<T>(ref T a, ref T b) // T is a type parameter for the method
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

// Usage
Box<int> intBox = new Box<int>(123);
intBox.DisplayContent();

Box<string> stringBox = new Box<string>("Hello Generics");
stringBox.DisplayContent();

int x = 10, y = 20;
Console.WriteLine($"Before swap: x={x}, y={y}");
Utilities.Swap(ref x, ref y); // Type inference for T (int)
Console.WriteLine($"After swap: x={x}, y={y}");

string s1 = "First", s2 = "Second";
Console.WriteLine($"Before swap: s1={s1}, s2={s2}");
Utilities.Swap(ref s1, ref s2); // Type inference for T (string)
Console.WriteLine($"After swap: s1={s1}, s2={s2}");

8. Exception Handling

Exceptions are errors that occur during the execution of a program. C# provides a structured way to handle these errors using try-catch-finally blocks.

public class ExceptionExample
{
    public void Divide(int numerator, int denominator)
    {
        try
        {
            if (denominator == 0)
            {
                // Explicitly throw an exception
                throw new DivideByZeroException("Denominator cannot be zero.");
            }

            int result = numerator / denominator;
            Console.WriteLine($"Result of division: {result}");
        }
        catch (DivideByZeroException ex) // Catch specific exception
        {
            Console.WriteLine($"Error: {ex.Message}");
            Console.WriteLine($"Stack Trace: {ex.StackTrace}");
        }
        catch (FormatException ex) // Catch another specific exception
        {
            Console.WriteLine($"Input format error: {ex.Message}");
        }
        catch (Exception ex) // Catch any other exception (general catch should be last)
        {
            Console.WriteLine($"An unexpected error occurred: {ex.Message}");
        }
        finally
        {
            Console.WriteLine("Division attempt finished."); // This always executes
        }
    }

    // Custom Exception
    public class InvalidAgeException : Exception
    {
        public InvalidAgeException() 
            : base("Age cannot be negative.") { }

        public InvalidAgeException(string message) 
            : base(message) { }

        public InvalidAgeException(string message, Exception innerException) 
            : base(message, innerException) { }
    }

    public void SetAge(int age)
    {
        if (age < 0)
        {
            throw new InvalidAgeException("Provided age is negative.");
        }

        Console.WriteLine($"Age set to: {age}");
    }
}

// Usage
ExceptionExample exHandler = new ExceptionExample();

exHandler.Divide(10, 2);   // Normal case
exHandler.Divide(10, 0);   // Will throw and catch DivideByZeroException

try
{
    exHandler.SetAge(-5);  // Will throw InvalidAgeException
}
catch (ExceptionExample.InvalidAgeException ex)
{
    Console.WriteLine($"Custom Exception Caught: {ex.Message}");
}

9. Delegates, Events, and Lambdas

These concepts are crucial for building flexible and extensible applications, especially for event-driven programming.

9.1 Delegates

A delegate is a type that represents references to methods with a particular parameter list and return type. It's like a type-safe function pointer.

// 1. Declare a delegate type
public delegate void MyDelegate(string message);

public class DelegateExample
{
    public void Method1(string msg)
    {
        Console.WriteLine($"Method1 called: {msg}");
    }

    public void Method2(string msg)
    {
        Console.WriteLine($"Method2 called: {msg.ToUpper()}");
    }

    public static void StaticMethod(string msg)
    {
        Console.WriteLine($"StaticMethod called: {msg}");
    }
}

// Usage
DelegateExample delEx = new DelegateExample();

// Create delegate instances
MyDelegate handler1 = new MyDelegate(delEx.Method1);
MyDelegate handler2 = delEx.Method2;              // Shorthand syntax
MyDelegate handler3 = DelegateExample.StaticMethod;

// Invoke delegates
handler1("Hello from handler1");
handler2("hello from handler2");
handler3("Hello from static method");

// Multicast delegate (combining multiple methods)
MyDelegate multiHandler = handler1 + handler2;
multiHandler("Hello from multi-handler");          // Calls both Method1 and Method2

multiHandler -= handler1;                          // Remove a method
multiHandler("Hello again from multi-handler");    // Only calls Method2 now

9.2 Events

Events are a way for a class to provide notifications to other classes when something interesting happens. They are built on top of delegates.

public class Button
{
    // 1. Declare a delegate for the event (often EventHandler or custom)
    public delegate void ClickEventHandler(object sender, EventArgs e);

    // 2. Declare the event using the 'event' keyword
    public event ClickEventHandler Click;

    public void SimulateClick()
    {
        Console.WriteLine("Button clicked!");
        // 3. Raise the event (check if there are any subscribers)
        Click?.Invoke(this, EventArgs.Empty); // Shorthand for null check and invocation
    }
}

public class Form
{
    public Form()
    {
        Button myButton = new Button();

        // 4. Subscribe to the event
        myButton.Click += MyButton_Click;       // += to add event handler
        myButton.Click += AnotherClickHandler;

        myButton.SimulateClick();               // Simulate the event
    }

    // Event handler method (matches the delegate signature)
    private void MyButton_Click(object sender, EventArgs e)
    {
        Console.WriteLine("MyButton_Click event handler executed!");
    }

    private void AnotherClickHandler(object sender, EventArgs e)
    {
        Console.WriteLine("AnotherClickHandler executed!");
    }
}

// To run this, you'd typically instantiate Form: new Form();

9.3 Lambda Expressions

Lambda expressions are a concise way to create anonymous methods (methods without a name). They are often used with LINQ and events.

// Lambda as a delegate

Func<int, int, int> add = (a, b) => a + b; // Func is a built-in delegate type
Console.WriteLine($"Lambda sum: {add(10, 5)}");

Action<string> printMessage = msg => Console.WriteLine($"Lambda message: {msg}"); // Action is for void methods
printMessage("Hello from lambda!");

// Lambda with event subscription (from Button example)
public class AnotherForm
{
    public AnotherForm()
    {
        Button myButton = new Button();

        myButton.Click += (sender, e) =>
        {
            // Lambda expression as event handler
            Console.WriteLine("Lambda event handler executed!");
            Console.WriteLine($"Sender type: {sender.GetType().Name}");
        };

        myButton.SimulateClick();
    }
}

// To run this, you'd typically instantiate AnotherForm: new AnotherForm();

10. Asynchronous Programming

Asynchronous programming allows your application to perform long-running operations (like network requests, file I/O) without blocking the main thread, keeping the UI responsive. C# uses the async and await keywords for this.

using System;
using System.Net.Http; // For HttpClient
using System.Threading.Tasks; // For Task and Task<T>

public class AsyncExample
{
    // Asynchronous method that returns a Task (no return value)
    public async Task SimulateLongRunningOperation()
    {
        Console.WriteLine("Starting long-running operation...");
        await Task.Delay(3000); // Simulate 3 seconds of work
        Console.WriteLine("Long-running operation finished.");
    }

    // Asynchronous method that returns a Task<T> (returns a value)
    public async Task<string> DownloadWebpageContent(string url)
    {
        Console.WriteLine($"Downloading content from {url}...");

        using (HttpClient client = new HttpClient())
        {
            string content = await client.GetStringAsync(url); // Await an I/O operation
            Console.WriteLine($"Finished downloading content from {url}. Content length: {content.Length}");

            return content.Substring(0, Math.Min(content.Length, 100)) + "..."; // Return first 100 chars
        }
    }

    public async Task RunAsyncOperations()
    {
        Console.WriteLine("Main thread: Before async calls.");

        // Start an async operation without waiting immediately
        Task operationTask = SimulateLongRunningOperation();

        // Start another async operation and await its result
        string googleContent = await DownloadWebpageContent("https://www.google.com");
        Console.WriteLine($"Main thread: Google content snippet: {googleContent}");

        // Now await the first operation if it hasn't finished yet
        await operationTask;

        Console.WriteLine("Main thread: After all async calls.");
    }
}

// To run this example, you'd typically call it from an async Main method (C# 7.1+)
// Example:
// static async Task Main(string[] args)
// {
//     AsyncExample asyncEx = new AsyncExample();
//     await asyncEx.RunAsyncOperations();
//     Console.WriteLine("Press any key to exit.");
//     Console.ReadKey();
// }

11. LINQ (Language Integrated Query)

LINQ provides a powerful and consistent way to query data from various sources (collections, databases, XML, etc.) directly within C#.

using System;
using System.Collections.Generic;
using System.Linq; // Essential for LINQ

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

public class LinqExample
{
    public void RunLinqQueries()
    {
        List<Product> products = new List<Product>
        {
            new Product { Id = 1, Name = "Laptop", Price = 1200m, Category = "Electronics" },
            new Product { Id = 2, Name = "Keyboard", Price = 75m, Category = "Electronics" },
            new Product { Id = 3, Name = "Mouse", Price = 25m, Category = "Electronics" },
            new Product { Id = 4, Name = "Desk Chair", Price = 300m, Category = "Furniture" },
            new Product { Id = 5, Name = "Monitor", Price = 400m, Category = "Electronics" },
            new Product { Id = 6, Name = "Table", Price = 150m, Category = "Furniture" }
        };

        // 1. Filtering (Where) - Query Syntax
        var electronicsProducts = from p in products
                                  where p.Category == "Electronics"
                                  select p;

        Console.WriteLine("Electronics Products (Query Syntax):");
        foreach (var p in electronicsProducts)
        {
            Console.WriteLine($"- {p.Name} (${p.Price})");
        }

        // 2. Filtering (Where) - Method Syntax
        var expensiveProducts = products.Where(p => p.Price > 100m);
        Console.WriteLine("\nExpensive Products (Method Syntax):");
        foreach (var p in expensiveProducts)
        {
            Console.WriteLine($"- {p.Name} (${p.Price})");
        }

        // 3. Ordering (OrderBy, OrderByDescending)
        var productsSortedByPrice = products.OrderBy(p => p.Price);
        Console.WriteLine("\nProducts Sorted by Price (Ascending):");
        foreach (var p in productsSortedByPrice)
        {
            Console.WriteLine($"- {p.Name} (${p.Price})");
        }

        // 4. Projection (Select)
        var productNames = products.Select(p => p.Name);
        Console.WriteLine("\nProduct Names:");
        foreach (var name in productNames)
        {
            Console.WriteLine($"- {name}");
        }

        // 5. Grouping (GroupBy)
        var productsByCategory = products.GroupBy(p => p.Category);
        Console.WriteLine("\nProducts Grouped by Category:");
        foreach (var group in productsByCategory)
        {
            Console.WriteLine($"- {group.Key} ({group.Count()} items)");
            foreach (var p in group)
            {
                Console.WriteLine($"  -- {p.Name}");
            }
        }

        // 6. Aggregation (Sum, Average, Count, Max, Min)
        decimal totalElectronicsPrice = products
            .Where(p => p.Category == "Electronics")
            .Sum(p => p.Price);
        Console.WriteLine($"\nTotal price of Electronics: ${totalElectronicsPrice}");

        // 7. Chaining LINQ methods
        var cheapElectronicsNames = products
            .Where(p => p.Category == "Electronics" && p.Price < 100)
            .OrderBy(p => p.Name)
            .Select(p => p.Name);
        Console.WriteLine("\nCheap Electronics Names (Chained):");
        foreach (var name in cheapElectronicsNames)
        {
            Console.WriteLine($"- {name}");
        }
    }
}

// To run this: new LinqExample().RunLinqQueries();

12. File I/O and Streams

C# provides classes in the System.IO namespace for reading from and writing to files and other data streams.

using System;
using System.IO; // For file operations

public class FileIOExample
{
    private readonly string filePath = "mydata.txt";

    public void WriteToFile(string content)
    {
        try
        {
            // Write all text to a file (overwrites if exists)
            File.WriteAllText(filePath, content);
            Console.WriteLine($"Content written to {filePath}");

            // Append text to a file
            File.AppendAllText(filePath, "\nAppended new line.");
            Console.WriteLine("Content appended.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error writing to file: {ex.Message}");
        }
    }

    public void ReadFromFile()
    {
        try
        {
            if (File.Exists(filePath))
            {
                // Read all text from a file
                string content = File.ReadAllText(filePath);
                Console.WriteLine($"\nContent read from {filePath}:\n{content}");

                // Read line by line
                Console.WriteLine("\nReading line by line:");
                using (StreamReader reader = new StreamReader(filePath))
                {
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        Console.WriteLine($"- {line}");
                    }
                }
            }
            else
            {
                Console.WriteLine($"\nFile {filePath} does not exist.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error reading from file: {ex.Message}");
        }
    }

    public void DeleteFile()
    {
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
            Console.WriteLine($"\nFile {filePath} deleted.");
        }
        else
        {
            Console.WriteLine($"\nFile {filePath} not found for deletion.");
        }
    }
}

// Usage:
// FileIOExample fileEx = new FileIOExample();
// fileEx.WriteToFile("This is the first line.\nThis is the second line.");
// fileEx.ReadFromFile();
// fileEx.DeleteFile();

13. Debugging Techniques

Debugging is the process of finding and fixing errors (bugs) in your code. Visual Studio provides powerful debugging tools.

How to Debug?

  1. Set a breakpoint on a line of code you want to inspect.
  2. Start debugging by pressing F5 or selecting "Debug > Start Debugging".
  3. When execution hits the breakpoint, use F10 (Step Over) or F11 (Step Into) to move through your code line by line.
  4. Hover over variables to see their current values, or use the Watch/Locals/Autos windows.
  5. Observe the Call Stack to understand the execution flow.
  6. If an exception occurs, the debugger will stop at the line where it happened, allowing you to inspect the state.

14. Best Practices and Design Principles

Writing good code goes beyond just making it work.

// Example of using statement for resource management
public void ProcessFile(string filePath)
{
    // The StreamReader will be automatically disposed of when exiting the 'using' block
    using (StreamReader reader = new StreamReader(filePath))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            Console.WriteLine(line);
        }
    } // reader.Dispose() is called here automatically
}

15. Key .NET Libraries and Tools

The .NET ecosystem is vast. Here are some essential components: