MODULE 2: ADVANCED C# FEATURES - Complete Guide

24. Exception Handling (Error Management)

using System;
using System.IO;

public class ExceptionHandlingDemo
{
// BASIC TRY-CATCH
public void BasicException()
{
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // This will throw exception
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
// SPECIFIC EXCEPTION HANDLING
public void SpecificExceptions()
{
try
{
Console.Write("Enter a number: ");
int number = int.Parse(Console.ReadLine());
int result = 100 / number;
Console.WriteLine($"Result: {result}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Cannot divide by zero!");
Console.WriteLine($"Technical: {ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine("Invalid number format!");
Console.WriteLine($"Technical: {ex.Message}");
}
catch (OverflowException ex)
{
Console.WriteLine("Number too large or too small!");
}
catch (Exception ex)
{
// Catch any other exceptions
Console.WriteLine($"Unexpected error: {ex.Message}");
throw; // Re-throw to preserve stack trace
}
}
// TRY-CATCH-FINALLY
public void WithFinally()
{
FileStream file = null;
try
{
file = File.Open("data.txt", FileMode.Open);
// Read file content
byte[] data = new byte[file.Length];
file.Read(data, 0, data.Length);
Console.WriteLine("File read successfully");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"IO Error: {ex.Message}");
}
finally
{
// ALWAYS executes, even if exception occurs
if (file != null)
{
file.Close();
Console.WriteLine("File closed");
}
}
}
// USING STATEMENT (Automatic disposal) - Better than try-finally
public void UsingStatement()
{
// Automatically calls Dispose() even if exception occurs
using (FileStream file = File.Open("data.txt", FileMode.Open))
{
// Use file
byte[] data = new byte[file.Length];
file.Read(data, 0, data.Length);
} // File automatically closed here
}
// THROWING EXCEPTIONS
public void ValidateAge(int age)
{
if (age < 0)
throw new ArgumentException("Age cannot be negative", nameof(age));
if (age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "Age too high");
if (age < 18)
throw new UnderageException($"User is {age}, minimum age is 18");
}
// CUSTOM EXCEPTION
public class UnderageException : Exception
{
public UnderageException() : base() { }
public UnderageException(string message) : base(message) { }
public UnderageException(string message, Exception inner) : base(message, inner) { }
// Recommended for serialization
protected UnderageException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
// FILTERING EXCEPTIONS (C# 6+)
public void ExceptionFiltering()
{
try
{
ProcessData();
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
// Only catch if message contains "timeout"
Console.WriteLine("Timeout occurred, retrying...");
RetryOperation();
}
catch (Exception ex) when (LogException(ex))
{
// Filter that always returns false (logs but doesn't catch)
}
}
private bool LogException(Exception ex)
{
Console.WriteLine($"Logged: {ex.Message}");
return false; // Don't catch, let it bubble up
}
private void ProcessData() { }
private void RetryOperation() { }
// AGGREGATE EXCEPTIONS (for parallel processing)
public void AggregateExceptionExample()
{
try
{
Parallel.For(0, 10, i =>
{
if (i == 5)
throw new InvalidOperationException($"Error at {i}");
});
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"Inner exception: {innerEx.Message}");
}
// Handle and flatten
var flattened = ex.Flatten();
// Re-throw as single exception
throw ex.InnerException ?? ex;
}
}
// BEST PRACTICES
public void BestPractices()
{
// ✅ DO: Use specific exceptions
try { /* code */ }
catch (FileNotFoundException) { } // Specific
catch (IOException) { } // Still specific
catch (Exception) { } // Last resort
// ❌ DON'T: Catch and swallow
try { /* code */ }
catch (Exception) { } // BAD! Hides errors
// ✅ DO: Use finally for cleanup
// ✅ DO: Use using for IDisposable
// ✅ DO: Throw original exception with 'throw;'
// ❌ DON'T: Use 'throw ex;' (loses stack trace)
// ✅ DO: Create custom exceptions for domain errors
// ✅ DO: Document which exceptions a method throws
}
}

// Exception Handling Patterns
public class ExceptionPatterns
{
// PATTERN 1: Try-Parse Pattern (No exception)
public bool TryParseAge(string input, out int age)
{
if (int.TryParse(input, out age))
{
if (age >= 0 && age <= 150)
return true;
}
age = 0;
return false;
}
// PATTERN 2: Exception for exceptional cases only
public int Divide(int a, int b)
{
if (b == 0)
throw new DivideByZeroException("Cannot divide by zero");
return a / b;
}
// PATTERN 3: Global exception handling (in Main/Program)
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Console.WriteLine($"Unhandled exception: {e.ExceptionObject}");
// Log and cleanup
};
try
{
// Application code
}
catch (Exception ex)
{
Console.WriteLine($"Application error: {ex.Message}");
// Log exception
}
}
}

25. Delegates (Type-Safe Function Pointers)

using System;

public class DelegatesDemo
{
// DELEGATE DECLARATION
// Syntax: delegate returnType Name(parameters);
public delegate int MathOperation(int a, int b);
public delegate void LogDelegate(string message);
public delegate bool FilterDelegate(int number);
// BASIC DELEGATE USAGE
public void BasicDelegate()
{
// Create delegate instance
MathOperation add = new MathOperation(Add);
// Or simply (syntax sugar)
MathOperation subtract = Subtract;
// Invoke delegate
int result1 = add(10, 5); // 15
int result2 = subtract(10, 5); // 5
Console.WriteLine($"Add: {result1}, Subtract: {result2}");
}
private int Add(int a, int b) => a + b;
private int Subtract(int a, int b) => a - b;
// DELEGATE AS PARAMETER
public void ProcessNumbers(int[] numbers, FilterDelegate filter)
{
foreach (int num in numbers)
{
if (filter(num))
{
Console.WriteLine($"Number {num} passed filter");
}
}
}
public void TestFilter()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Pass different filtering logic
ProcessNumbers(numbers, IsEven);
ProcessNumbers(numbers, IsGreaterThanFive);
ProcessNumbers(numbers, IsPrime);
}
private bool IsEven(int number) => number % 2 == 0;
private bool IsGreaterThanFive(int number) => number > 5;
private bool IsPrime(int number)
{
if (number < 2) return false;
for (int i = 2; i <= Math.Sqrt(number); i++)
if (number % i == 0) return false;
return true;
}
// MULTICAST DELEGATES (Multiple methods)
public void MulticastDelegate()
{
LogDelegate logger = null;
// Add methods to delegate
logger += LogToConsole;
logger += LogToFile;
logger += LogToDatabase;
// Invoke all methods
logger("Application started");
// Remove a method
logger -= LogToDatabase;
// Invoke remaining
logger("Processing data");
}
private void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
private void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
private void LogToDatabase(string msg) => Console.WriteLine($"Database: {msg}");
// GETTING INVOCATION LIST
public void GetInvocationList()
{
LogDelegate logger = LogToConsole;
logger += LogToFile;
foreach (LogDelegate del in logger.GetInvocationList())
{
del("Testing");
}
}
// BUILT-IN DELEGATES
public void BuiltInDelegates()
{
// Action: No return value, up to 16 parameters
Action<string> print = msg => Console.WriteLine(msg);
Action<int, int> addAndPrint = (a, b) => Console.WriteLine(a + b);
// Func: Has return value, up to 16 parameters (last is return)
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> getLength = s => s.Length;
Func<int, bool> isEven = n => n % 2 == 0;
// Predicate: Returns bool, one parameter
Predicate<int> isPositive = n => n > 0;
Predicate<string> isEmpty = s => string.IsNullOrEmpty(s);
// Usage
print("Hello");
int sum = add(5, 3);
bool even = isEven(10);
bool positive = isPositive(-5);
}
// DELEGATE COVARIANCE AND CONTRAVARIANCE
public void Variance()
{
// Covariance: Return type can be derived type
Func<object> returnsObject = GetString; // string derives from object
// Contravariance: Parameter can be base type
Action<string> takesString = ProcessObject; // object is base of string
}
private string GetString() => "Hello";
private void ProcessObject(object obj) => Console.WriteLine(obj);
// REAL-WORLD EXAMPLE: Callback Pattern
public class DataProcessor
{
public delegate void ProgressCallback(int percent);
public void ProcessData(ProgressCallback callback)
{
for (int i = 0; i <= 100; i += 10)
{
Thread.Sleep(100); // Simulate work
callback?.Invoke(i);
}
}
}
public void UseCallback()
{
var processor = new DataProcessor();
processor.ProcessData(percent => Console.WriteLine($"Progress: {percent}%"));
}
}

26. Events (Publisher-Subscriber Pattern)

using System;

public class EventsDemo
{
// EVENT DECLARATION
public class Button
{
// Standard event pattern
public event EventHandler Clicked;
public event EventHandler<EventArgs> Hovered;
// Custom event with custom EventArgs
public event EventHandler<ButtonClickedEventArgs> ClickedWithData;
// Property-changed event (common in UI)
public event EventHandler<PropertyChangedEventArgs> PropertyChanged;
private string _text;
public string Text
{
get => _text;
set
{
_text = value;
OnPropertyChanged(nameof(Text));
}
}
// Protected virtual method to raise event
protected virtual void OnClicked()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnClickedWithData(int x, int y)
{
ClickedWithData?.Invoke(this, new ButtonClickedEventArgs(x, y));
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// Public method to simulate click
public void Click()
{
Console.WriteLine("Button clicked!");
OnClicked();
OnClickedWithData(100, 200);
}
public void Hover()
{
Hovered?.Invoke(this, EventArgs.Empty);
}
}
// Custom EventArgs class
public class ButtonClickedEventArgs : EventArgs
{
public int X { get; }
public int Y { get; }
public DateTime ClickTime { get; }
public ButtonClickedEventArgs(int x, int y)
{
X = x;
Y = y;
ClickTime = DateTime.Now;
}
}
public class PropertyChangedEventArgs : EventArgs
{
public string PropertyName { get; }
public PropertyChangedEventArgs(string propertyName)
{
PropertyName = propertyName;
}
}
// SUBSCRIBER CLASS
public class Logger
{
public void OnButtonClicked(object sender, EventArgs e)
{
Console.WriteLine($"[LOG] Button clicked at {DateTime.Now}");
}
public void OnButtonClickedWithData(object sender, ButtonClickedEventArgs e)
{
Console.WriteLine($"[LOG] Click at ({e.X}, {e.Y}) at {e.ClickTime}");
}
public void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine($"[LOG] Property '{e.PropertyName}' changed");
}
}
// ANOTHER SUBSCRIBER
public class Analytics
{
public void TrackClick(object sender, EventArgs e)
{
Console.WriteLine("[ANALYTICS] Click event tracked");
}
public void TrackPropertyChange(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine($"[ANALYTICS] User changed {e.PropertyName}");
}
}
// UI CLASS
public class UserInterface
{
public void UpdateDisplay(object sender, ButtonClickedEventArgs e)
{
Console.WriteLine($"[UI] Displaying click at position ({e.X}, {e.Y})");
}
}
// DEMO USAGE
public void RunEventDemo()
{
// Create publisher
Button button = new Button();
// Create subscribers
Logger logger = new Logger();
Analytics analytics = new Analytics();
UserInterface ui = new UserInterface();
// Subscribe to events
button.Clicked += logger.OnButtonClicked;
button.Clicked += analytics.TrackClick;
button.ClickedWithData += logger.OnButtonClickedWithData;
button.ClickedWithData += ui.UpdateDisplay;
button.PropertyChanged += logger.OnPropertyChanged;
button.PropertyChanged += analytics.TrackPropertyChange;
// Trigger events
button.Text = "Submit"; // Triggers PropertyChanged
button.Click(); // Triggers Click and ClickedWithData
// Unsubscribe (important to prevent memory leaks)
button.Clicked -= logger.OnButtonClicked;
button.Clicked -= analytics.TrackClick;
}
// GENERIC EVENT HANDLER
public class EventAggregator
{
private Dictionary<Type, List<Delegate>> _handlers = new();
public void Subscribe<T>(EventHandler<T> handler) where T : EventArgs
{
if (!_handlers.ContainsKey(typeof(T)))
_handlers[typeof(T)] = new List<Delegate>();
_handlers[typeof(T)].Add(handler);
}
public void Publish<T>(object sender, T eventArgs) where T : EventArgs
{
if (_handlers.ContainsKey(typeof(T)))
{
foreach (EventHandler<T> handler in _handlers[typeof(T)])
{
handler?.Invoke(sender, eventArgs);
}
}
}
}
// WEAK EVENT PATTERN (to avoid memory leaks)
public class WeakEventManager
{
private List<WeakReference<EventHandler>> _handlers = new();
public void AddHandler(EventHandler handler)
{
_handlers.Add(new WeakReference<EventHandler>(handler));
}
public void RaiseEvent(object sender, EventArgs e)
{
foreach (var weakRef in _handlers.ToList())
{
if (weakRef.TryGetTarget(out var handler))
{
handler?.Invoke(sender, e);
}
else
{
_handlers.Remove(weakRef); // Clean up dead references
}
}
}
}
}

// Common Event Patterns in .NET
public class CommonEventPatterns
{
// 1. Property Changed (INotifyPropertyChanged)
public class ViewModel : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
}
// 2. Cancellable Event
public class CancellableEventArgs : EventArgs
{
public bool Cancel { get; set; }
}
public class Process
{
public event EventHandler<CancellableEventArgs> Starting;
public void Start()
{
var args = new CancellableEventArgs();
Starting?.Invoke(this, args);
if (args.Cancel)
{
Console.WriteLine("Process start cancelled");
return;
}
Console.WriteLine("Process started");
}
}
// 3. Event with Data
public class DataReceivedEventArgs : EventArgs
{
public byte[] Data { get; }
public DataReceivedEventArgs(byte[] data) => Data = data;
}
public class DataSource
{
public event EventHandler<DataReceivedEventArgs> DataReceived;
public void ReceiveData(byte[] data)
{
DataReceived?.Invoke(this, new DataReceivedEventArgs(data));
}
}
}

27. Lambda Expressions (=> Operator Deep Dive)

using System;
using System.Linq.Expressions;

public class LambdaExpressionsDemo
{
// LAMBDA BASICS
public void BasicLambda()
{
// Expression lambda (single line, no braces)
Func<int, int> square = x => x * x;
// Statement lambda (multiple lines, braces, return)
Func<int, int> factorial = n =>
{
int result = 1;
for (int i = 2; i <= n; i++)
result *= i;
return result;
};
// No parameters
Action sayHello = () => Console.WriteLine("Hello!");
// One parameter (parentheses optional)
Func<int, bool> isEven = x => x % 2 == 0;
// Multiple parameters
Func<int, int, int> add = (a, b) => a + b;
// Usage
Console.WriteLine($"Square of 5: {square(5)}");
Console.WriteLine($"Factorial of 5: {factorial(5)}");
sayHello();
Console.WriteLine($"Is 10 even: {isEven(10)}");
Console.WriteLine($"5 + 3 = {add(5, 3)}");
}
// LAMBDA WITH LINQ (Most Common)
public void LambdaWithLinq()
{
var numbers = Enumerable.Range(1, 20);
// Filter
var evens = numbers.Where(n => n % 2 == 0);
// Transform
var squares = numbers.Select(n => n * n);
// Combine
var result = numbers
.Where(n => n > 10)
.Select(n => new { Original = n, Squared = n * n })
.OrderByDescending(x => x.Squared);
// Complex condition
var special = numbers.Where(n =>
n % 2 == 0 &&
n > 5 &&
n < 15 &&
Math.Sqrt(n) % 1 == 0); // Perfect squares that are even
}
// LAMBDA WITH MULTIPLE STATEMENTS
public void MultiStatementLambda()
{
Func<int, string> formatNumber = n =>
{
if (n < 0) return "Negative";
if (n == 0) return "Zero";
if (n == 1) return "One";
return n % 2 == 0 ? "Even" : "Odd";
};
Console.WriteLine(formatNumber(5)); // Odd
Console.WriteLine(formatNumber(2)); // Even
}
// LAMBDA WITH CLOSURES (Capturing variables)
public void LambdaClosures()
{
int multiplier = 10;
// Lambda captures 'multiplier' from outer scope
Func<int, int> multiply = x => x * multiplier;
Console.WriteLine(multiply(5)); // 50
multiplier = 20;
Console.WriteLine(multiply(5)); // 100 (captured variable changed!)
// Be careful with captured variables in loops
var actions = new List<Action>();
// PROBLEM: All actions capture the same variable
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
{
action(); // Prints 5,5,5,5,5 (not 0,1,2,3,4)
}
// SOLUTION: Create local copy
actions.Clear();
for (int i = 0; i < 5; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
{
action(); // Prints 0,1,2,3,4
}
}
// LAMBDA WITH EXPRESSION TREES
public void ExpressionTrees()
{
// Expression tree (code as data)
Expression<Func<int, int, int>> expression = (a, b) => a + b;
// Examine the expression tree
Console.WriteLine($"Expression type: {expression.NodeType}");
Console.WriteLine($"Return type: {expression.ReturnType}");
// Compile and run
Func<int, int, int> compiled = expression.Compile();
Console.WriteLine($"Result: {compiled(5, 3)}");
// Build expression manually
ParameterExpression paramA = Expression.Parameter(typeof(int), "a");
ParameterExpression paramB = Expression.Parameter(typeof(int), "b");
BinaryExpression body = Expression.Add(paramA, paramB);
Expression<Func<int, int, int>> manualExpr =
Expression.Lambda<Func<int, int, int>>(body, paramA, paramB);
var func = manualExpr.Compile();
Console.WriteLine($"Manual expression: {func(10, 20)}");
}
// LAMBDA IN ASYNC CODE
public async Task LambdaAsync()
{
// Async lambda
Func<Task> asyncLambda = async () =>
{
await Task.Delay(1000);
Console.WriteLine("Async lambda executed");
};
await asyncLambda();
// Async lambda with parameters
Func<int, Task<int>> doubleAsync = async x =>
{
await Task.Delay(100);
return x * 2;
};
int result = await doubleAsync(5);
Console.WriteLine($"Double: {result}");
// In LINQ with async (need to enumerate)
var ids = new[] { 1, 2, 3, 4, 5 };
var tasks = ids.Select(async id => await ProcessIdAsync(id));
var results = await Task.WhenAll(tasks);
}
private async Task<int> ProcessIdAsync(int id)
{
await Task.Delay(100);
return id * 10;
}
// LAMBDA WITH DEFAULT PARAMETERS (C# 12+)
public void LambdaDefaultParameters()
{
// C# 12 feature
var greet = (string name, string greeting = "Hello") =>
$"{greeting}, {name}!";
Console.WriteLine(greet("John")); // Hello, John!
Console.WriteLine(greet("Jane", "Hi")); // Hi, Jane!
}
// LAMBDA WITH ATTRIBUTES (C# 12+)
public void LambdaAttributes()
{
// var func = [Description("Adds two numbers")] (int a, int b) => a + b;
// Attributes on lambdas are coming in future versions
}
// PRACTICAL EXAMPLES
public void PracticalLambdaExamples()
{
// 1. Sorting with custom comparison
var people = new[]
{
new { Name = "John", Age = 30 },
new { Name = "Alice", Age = 25 },
new { Name = "Bob", Age = 35 }
};
var sortedByAge = people.OrderBy(p => p.Age);
var sortedByName = people.OrderBy(p => p.Name);
// 2. Conditional transformation
var numbers = Enumerable.Range(1, 10);
var transformed = numbers.Select(n => n > 5 ? n * 2 : n * 3);
// 3. Aggregation
int sum = numbers.Aggregate((a, b) => a + b);
int product = numbers.Aggregate(1, (acc, n) => acc * n);
// 4. Predicate composition
Func<int, bool> isEven = n => n % 2 == 0;
Func<int, bool> isPositive = n => n > 0;
Func<int, bool> isEvenAndPositive = n => isEven(n) && isPositive(n);
// 5. Event handler with lambda
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");
}
// Helper class for event demo
public class Button
{
public event EventHandler Clicked;
public void Click() => Clicked?.Invoke(this, EventArgs.Empty);
}
}

28. Generics (Type-Safe Code)

using System;
using System.Collections.Generic;

public class GenericsDemo
{
// BASIC GENERIC CLASS
public class GenericList<T>
{
private T[] _items = new T[10];
private int _count = 0;
public void Add(T item)
{
if (_count >= _items.Length)
Array.Resize(ref _items, _items.Length * 2);
_items[_count++] = item;
}
public T Get(int index)
{
if (index < 0 || index >= _count)
throw new IndexOutOfRangeException();
return _items[index];
}
public int Count => _count;
public T this[int index]
{
get => Get(index);
set => _items[index] = value;
}
}
// GENERIC WITH MULTIPLE TYPE PARAMETERS
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public void Deconstruct(out TFirst first, out TSecond second)
{
first = First;
second = Second;
}
}
// GENERIC CONSTRAINTS
public class ConstrainedGeneric<T> where T : class, new()
{
// T must be a reference type (class) and have parameterless constructor
public T CreateInstance()
{
return new T(); // Allowed because of 'new()' constraint
}
public void ProcessItem(T item)
{
if (item == null) throw new ArgumentNullException();
// T is guaranteed to be reference type
}
}
// DIFFERENT CONSTRAINTS
public class ConstraintExamples
{
// where T : struct - Value type only
public class ValueTypeOnly<T> where T : struct { }
// where T : class - Reference type only
public class ReferenceTypeOnly<T> where T : class { }
// where T : new() - Must have parameterless constructor
public class MustBeCreatable<T> where T : new() { }
// where T : BaseClass - Must inherit from BaseClass
public class DerivedFromBase<T> where T : BaseClass { }
// where T : IInterface - Must implement interface
public class ImplementsInterface<T> where T : IDisposable { }
// where T : U - Type parameter constraint
public class TypeParameterConstraint<T, U> where T : U { }
// Multiple constraints
public class MultipleConstraints<T>
where T : class, IDisposable, new() { }
// Constraint for unmanaged types (C# 7.3+)
public class UnmanagedType<T> where T : unmanaged { }
// Not null constraint (C# 8+)
public class NotNullType<T> where T : notnull { }
// Base class for demonstration
public class BaseClass { }
}
// GENERIC METHODS
public class GenericMethods
{
// Basic generic method
public T GetDefault<T>()
{
return default(T);
}
// Generic method with constraint
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// Generic method with multiple parameters
public TResult Convert<TInput, TResult>(TInput input, Func<TInput, TResult> converter)
{
return converter(input);
}
// Generic method with type inference
public void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// Usage examples
public void TestMethods()
{
int defaultInt = GetDefault<int>(); // 0
string defaultString = GetDefault<string>(); // null
int maxInt = Max(10, 20); // Type inferred
string maxString = Max("Apple", "Banana"); // "Banana"
int result = Convert("123", s => int.Parse(s));
int x = 5, y = 10;
Swap(ref x, ref y); // x=10, y=5
}
}
// GENERIC INTERFACES
public interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class UserRepository : IRepository<User>
{
private List<User> _users = new List<User>();
public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
public IEnumerable<User> GetAll() => _users;
public void Add(User entity) => _users.Add(entity);
public void Update(User entity) { /* Implementation */ }
public void Delete(int id) => _users.RemoveAll(u => u.Id == id);
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// GENERIC DELEGATES
public class GenericDelegates
{
// Custom generic delegate
public delegate T Transformer<T>(T input);
public void Example()
{
Transformer<int> square = x => x * x;
Transformer<string> toUpper = s => s.ToUpper();
Console.WriteLine(square(5)); // 25
Console.WriteLine(toUpper("hello")); // HELLO
}
}
// COVARIANCE AND CONTRAVARIANCE
public class VarianceExample
{
// Covariance (out T) - T can be more derived
public interface ICovariant<out T>
{
T GetItem(); // Can only return T, not accept
}
// Contravariance (in T) - T can be less derived
public interface IContravariant<in T>
{
void Process(T item); // Can only accept T, not return
}
// Real examples
public void CovarianceDemo()
{
// IEnumerable<out T> is covariant
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings; // Allowed! string -> object
// Action<in T> is contravariant
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // Allowed! object -> string
}
}
// GENERIC TYPE INFERENCE
public class TypeInference
{
public void Demo()
{
// Compiler can infer generic types
var list = new List<int>(); // Can't infer, need explicit
var pair = new Pair<string, int>("Age", 30); // Can't infer
// But methods can infer
Swap(ref int a, ref int b); // T inferred as int
// Sometimes need explicit
var empty = Enumerable.Empty<string>(); // Explicit needed
}
private void Swap<T>(ref T a, ref T b) { }
}
// PRACTICAL GENERIC PATTERNS
public class GenericPatterns
{
// 1. Singleton pattern with generics
public class Singleton<T> where T : class, new()
{
private static T _instance;
private static readonly object _lock = new object();
public static T Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new T();
}
}
return _instance;
}
}
}
// 2. Factory pattern
public class Factory<T> where T : new()
{
public T Create() => new T();
}
// 3. Repository pattern (already shown)
// 4. Result 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> Success(T value) => new Result<T>(value);
public static Result<T> Failure(string error) => new Result<T>(error);
}
// 5. Lazy initialization
public class Lazy<T>
{
private T _value;
private Func<T> _initializer;
private bool _initialized;
public Lazy(Func<T> initializer)
{
_initializer = initializer;
}
public T Value
{
get
{
if (!_initialized)
{
_value = _initializer();
_initialized = true;
}
return _value;
}
}
}
}
}

29. Collections Library (Complete Guide)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;

public class CollectionsDemo
{
// 1. LIST<T> - Most common, dynamic array
public void ListExamples()
{
// Creation
List<int> numbers = new List<int>();
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
var initialized = new List<int> { 1, 2, 3, 4, 5 };
// Adding
numbers.Add(10);
numbers.AddRange(new[] { 20, 30, 40 });
numbers.Insert(1, 15); // Insert at index 1
// Removing
numbers.Remove(20); // Remove first occurrence
numbers.RemoveAt(0); // Remove by index
numbers.RemoveRange(1, 2); // Remove 2 elements starting at index 1
numbers.Clear(); // Remove all
// Access
int first = numbers[0];
int last = numbers[^1]; // C# 8+ index from end
// Search
bool exists = numbers.Contains(10);
int index = numbers.IndexOf(20);
int findIndex = numbers.FindIndex(x => x > 15);
// Sort
numbers.Sort(); // Ascending
numbers.Sort((a, b) => b.CompareTo(a)); // Descending
numbers.Reverse();
// Convert
int[] array = numbers.ToArray();
List<int> copy = new List<int>(numbers);
// Performance
// - O(1) access by index
// - O(n) insertion/deletion (except at end)
// - O(n) search (unless sorted)
}
// 2. DICTIONARY<TKey, TValue> - Key-value pairs
public void DictionaryExamples()
{
// Creation
Dictionary<string, int> ages = new Dictionary<string, int>();
var dict = new Dictionary<string, string>
{
["key1"] = "value1",
["key2"] = "value2"
};
// Adding
ages.Add("Alice", 30);
ages["Bob"] = 25; // Add or update
// Access (safe)
if (ages.ContainsKey("Alice"))
{
int age = ages["Alice"];
}
// TryGetValue (most efficient)
if (ages.TryGetValue("Charlie", out int charlieAge))
{
Console.WriteLine($"Charlie is {charlieAge}");
}
else
{
Console.WriteLine("Charlie not found");
}
// Remove
ages.Remove("Bob");
// Iteration
foreach (KeyValuePair<string, int> kvp in ages)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
foreach (string key in ages.Keys) { }
foreach (int value in ages.Values) { }
// Performance
// - O(1) average access
// - O(n) worst case (hash collisions)
// - Keys must be unique and immutable (after insertion)
}
// 3. HASHSET<T> - Unique elements
public void HashSetExamples()
{
// Creation
HashSet<int> numbers = new HashSet<int>();
var unique = new HashSet<string> { "apple", "banana", "orange" };
// Adding (returns bool if added)
bool added = numbers.Add(5); // true
bool duplicate = numbers.Add(5); // false
// Set operations
var set1 = new HashSet<int> { 1, 2, 3, 4, 5 };
var set2 = new HashSet<int> { 4, 5, 6, 7, 8 };
set1.UnionWith(set2); // {1,2,3,4,5,6,7,8}
set1.IntersectWith(set2); // {4,5}
set1.ExceptWith(set2); // {1,2,3}
set1.SymmetricExceptWith(set2); // {1,2,3,6,7,8}
// Check containment
bool contains = set1.Contains(3);
// Subset/superset
var subset = new HashSet<int> { 1, 2 };
bool isSubset = subset.IsSubsetOf(set1);
bool isSuperset = set1.IsSupersetOf(subset);
// Performance
// - O(1) Add, Remove, Contains
// - Great for unique collections
}
// 4. STACK<T> - LIFO (Last In, First Out)
public void StackExamples()
{
Stack<string> stack = new Stack<string>();
// Push (add to top)
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
// Pop (remove and return top)
string top = stack.Pop(); // "Third"
// Peek (view top without removing)
string peek = stack.Peek(); // "Second"
// Check if empty
bool isEmpty = stack.Count == 0;
// Iteration (doesn't remove)
foreach (string item in stack)
{
Console.WriteLine(item);
}
// Use cases: Undo/Redo, Browser history, Expression evaluation
}
// 5. QUEUE<T> - FIFO (First In, First Out)
public void QueueExamples()
{
Queue<string> queue = new Queue<string>();
// Enqueue (add to back)
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");
// Dequeue (remove and return front)
string front = queue.Dequeue(); // "First"
// Peek (view front without removing)
string peek = queue.Peek(); // "Second"
// Use cases: Print queue, Message queue, Task scheduler
}
// 6. LINKEDLIST<T> - Doubly linked list
public void LinkedListExamples()
{
LinkedList<string> list = new LinkedList<string>();
// Adding
LinkedListNode<string> first = list.AddFirst("First");
LinkedListNode<string> last = list.AddLast("Last");
LinkedListNode<string> middle = list.AddAfter(first, "Middle");
list.AddBefore(last, "Before Last");
// Access
LinkedListNode<string> head = list.First;
LinkedListNode<string> tail = list.Last;
// Iteration
foreach (string item in list)
{
Console.WriteLine(item);
}
// Forward iteration
for (var node = list.First; node != null; node = node.Next)
{
Console.WriteLine(node.Value);
}
// Backward iteration
for (var node = list.Last; node != null; node = node.Previous)
{
Console.WriteLine(node.Value);
}
// Remove
list.RemoveFirst();
list.RemoveLast();
list.Remove(middle);
// Use cases: When frequent insertions/deletions needed
// Performance: O(1) insert/delete, O(n) access
}
// 7. SORTED COLLECTIONS
public void SortedCollections()
{
// SortedDictionary - Sorted by key
SortedDictionary<string, int> sortedDict = new SortedDictionary<string, int>();
sortedDict.Add("Zebra", 3);
sortedDict.Add("Apple", 1);
sortedDict.Add("Monkey", 2);
// Order: Apple, Monkey, Zebra
// SortedList - Sorted by key (uses less memory than SortedDictionary)
SortedList<string, int> sortedList = new SortedList<string, int>();
sortedList.Add("C", 3);
sortedList.Add("A", 1);
sortedList.Add("B", 2);
// SortedSet - Unique sorted elements
SortedSet<int> sortedSet = new SortedSet<int> { 3, 1, 4, 2, 5 };
// Order: 1, 2, 3, 4, 5
}
// 8. ARRAY (Fixed size)
public void ArrayExamples()
{
// Fixed size
int[] fixedSize = new int[10];
// Multidimensional
int[,] matrix = new int[3, 4];
int[,,] cube = new int[2, 3, 4];
// Jagged array
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
// Array methods
int[] arr = { 5, 2, 8, 1, 9 };
Array.Sort(arr);
Array.Reverse(arr);
Array.IndexOf(arr, 8);
Array.Clear(arr, 0, 2);
Array.Copy(arr, new int[5], 3);
// Use array when:
// - Size is fixed and known
// - Performance critical (slightly faster than List)
// - Multidimensional needed
}
// 9. CONCURRENT COLLECTIONS (Thread-safe)
public void ConcurrentCollections()
{
// ConcurrentDictionary
ConcurrentDictionary<string, int> concurrentDict = new ConcurrentDictionary<string, int>();
concurrentDict.TryAdd("key", 1);
concurrentDict.AddOrUpdate("key", 2, (k, v) => v + 1);
concurrentDict.GetOrAdd("key2", 0);
// ConcurrentBag (unordered)
ConcurrentBag<int> bag = new ConcurrentBag<int>();
bag.Add(1);
bag.TryTake(out int item);
// ConcurrentQueue (thread-safe queue)
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
concurrentQueue.Enqueue(1);
concurrentQueue.TryDequeue(out int dequeued);
// ConcurrentStack
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
concurrentStack.Push(1);
concurrentStack.TryPop(out int popped);
// BlockingCollection (producer-consumer)
BlockingCollection<int> blocking = new BlockingCollection<int>();
blocking.Add(1);
int taken = blocking.Take();
}
// 10. IMMUTABLE COLLECTIONS (System.Collections.Immutable)
public void ImmutableCollections()
{
// Need to add NuGet: System.Collections.Immutable
// ImmutableArray<int> immutable = ImmutableArray.Create(1, 2, 3);
// ImmutableList<int> immutableList = ImmutableList.Create(1, 2, 3);
// ImmutableDictionary<string, int> immutableDict = ImmutableDictionary.Create<string, int>();
// Immutable collections never change - operations create new ones
// Great for functional programming and thread safety
}
// PERFORMANCE COMPARISON
public void PerformanceGuide()
{
// Use Dictionary when: Key-value lookups, fast access by key
// Use List when: Ordered collection, indexed access, dynamic size
// Use HashSet when: Unique items, set operations
// Use Stack/Queue when: LIFO/FIFO behavior
// Use LinkedList when: Many insertions/deletions in middle
// Use Array when: Fixed size, performance critical, multidimensional
// Complexity Summary:
// Collection | Access | Search | Insert | Delete
// --------------------|---------|---------|---------|----------
// List<T> | O(1) | O(n) | O(n) | O(n)
// Dictionary<K,V> | O(1) | O(1) | O(1) | O(1)
// HashSet<T> | O(1) | O(1) | O(1) | O(1)
// Stack<T> | O(1) | O(n) | O(1) | O(1)
// Queue<T> | O(1) | O(n) | O(1) | O(1)
// LinkedList<T> | O(n) | O(n) | O(1) | O(1)
// SortedDictionary | O(log n)| O(log n)| O(log n)| O(log n)
}
}

// COLLECTION INITIALIZERS
public class CollectionInitializers
{
public void Examples()
{
// List initializer
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Dictionary initializer (old syntax)
var dict1 = new Dictionary<string, int>
{
{ "one", 1 },
{ "two", 2 },
{ "three", 3 }
};
// Dictionary initializer (new syntax C# 6+)
var dict2 = new Dictionary<string, int>
{
["one"] = 1,
["two"] = 2,
["three"] = 3
};
// Array initializer
int[] array = { 1, 2, 3, 4, 5 };
// Collection expression (C# 12+)
// int[] arr = [1, 2, 3, 4, 5];
// List<int> list = [1, 2, 3];
// Span<int> span = [1, 2, 3];
}
}

30. LINQ (Language Integrated Query)

using System;
using System.Collections.Generic;
using System.Linq;

public class LINQDemo
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public int Stock { get; set; }
public bool IsActive { get; set; }
}
public class Order
{
public int Id { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public DateTime OrderDate { get; set; }
}
private List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m, Category = "Electronics", Stock = 10, IsActive = true },
new Product { Id = 2, Name = "Mouse", Price = 19.99m, Category = "Electronics", Stock = 50, IsActive = true },
new Product { Id = 3, Name = "Keyboard", Price = 49.99m, Category = "Electronics", Stock = 30, IsActive = true },
new Product { Id = 4, Name = "Desk", Price = 299.99m, Category = "Furniture", Stock = 5, IsActive = false },
new Product { Id = 5, Name = "Chair", Price = 149.99m, Category = "Furniture", Stock = 15, IsActive = true },
new Product { Id = 6, Name = "Monitor", Price = 249.99m, Category = "Electronics", Stock = 8, IsActive = true }
};
private List<Order> orders = new List<Order>
{
new Order { Id = 1, ProductId = 1, Quantity = 2, OrderDate = DateTime.Now.AddDays(-5) },
new Order { Id = 2, ProductId = 2, Quantity = 10, OrderDate = DateTime.Now.AddDays(-3) },
new Order { Id = 3, ProductId = 1, Quantity = 1, OrderDate = DateTime.Now.AddDays(-1) }
};
// QUERY SYNTAX vs METHOD SYNTAX
public void QueryVsMethod()
{
// Query Syntax (SQL-like)
var querySyntax = from p in products
where p.Price > 100
orderby p.Name
select p;
// Method Syntax (Fluent)
var methodSyntax = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name);
// Both produce same result, choose based on preference
}
// FILTERING (WHERE)
public void Filtering()
{
// Basic filtering
var expensive = products.Where(p => p.Price > 200);
// Multiple conditions
var electronics = products.Where(p => p.Category == "Electronics" && p.Stock > 0);
// OfType - filter by type
var mixed = new object[] { 1, "hello", 2, "world", 3.5 };
var strings = mixed.OfType<string>(); // "hello", "world"
// Where with index
var everyOther = products.Where((p, index) => index % 2 == 0);
}
// PROJECTION (SELECT)
public void Projection()
{
// Select single property
var names = products.Select(p => p.Name);
// Select anonymous type
var cheapProducts = products
.Where(p => p.Price < 100)
.Select(p => new { p.Name, p.Price, Discount = p.Price * 0.9m });
// Select with index
var indexed = products.Select((p, index) => new { Index = index, Product = p });
// SelectMany - flatten collections
var ordersPerProduct = products.SelectMany(p => orders.Where(o => o.ProductId == p.Id));
// SelectMany with result selector
var orderDetails = products.SelectMany(
p => orders.Where(o => o.ProductId == p.Id),
(p, o) => new { Product = p.Name, o.Quantity, Total = p.Price * o.Quantity }
);
}
// ORDERING
public void Ordering()
{
// Basic sort
var byName = products.OrderBy(p => p.Name);
var byPriceDesc = products.OrderByDescending(p => p.Price);
// Multiple sort criteria
var sorted = products
.OrderBy(p => p.Category)
.ThenBy(p => p.Price)
.ThenByDescending(p => p.Stock);
// Reverse order
var reversed = products.Reverse();
}
// GROUPING
public void Grouping()
{
// Group by category
var byCategory = products.GroupBy(p => p.Category);
foreach (var group in byCategory)
{
Console.WriteLine($"Category: {group.Key}");
foreach (var product in group)
{
Console.WriteLine($" {product.Name} - {product.Price:C}");
}
}
// Group with aggregation
var categoryStats = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AveragePrice = g.Average(p => p.Price),
TotalStock = g.Sum(p => p.Stock),
MostExpensive = g.Max(p => p.Price),
Cheapest = g.Min(p => p.Price)
});
// Group by multiple keys
var byCategoryAndStock = products
.GroupBy(p => new { p.Category, InStock = p.Stock > 0 });
}
// AGGREGATION
public void Aggregation()
{
// Count
int total = products.Count();
int inStock = products.Count(p => p.Stock > 0);
// Sum
decimal totalValue = products.Sum(p => p.Price * p.Stock);
// Average
double avgPrice = products.Average(p => (double)p.Price);
// Min/Max
decimal minPrice = products.Min(p => p.Price);
decimal maxPrice = products.Max(p => p.Price);
// Aggregate (custom accumulation)
string allNames = products.Aggregate("", (current, next) => current + next.Name + ", ");
// Multiple aggregations at once
var stats = new
{
Count = products.Count(),
Min = products.Min(p => p.Price),
Max = products.Max(p => p.Price),
Avg = products.Average(p => p.Price)
};
}
// JOINING
public void Joining()
{
// Inner Join
var productOrders = from p in products
join o in orders on p.Id equals o.ProductId
select new
{
Product = p.Name,
o.Quantity,
Total = p.Price * o.Quantity
};
// Method syntax join
var productOrders2 = products.Join(
orders,
p => p.Id,
o => o.ProductId,
(p, o) => new { p.Name, o.Quantity, Total = p.Price * o.Quantity }
);
// Group Join (left join)
var productsWithOrders = from p in products
join o in orders on p.Id equals o.ProductId into orderGroup
select new
{
Product = p.Name,
OrderCount = orderGroup.Count(),
TotalQuantity = orderGroup.Sum(o => o.Quantity)
};
// Cross Join (Cartesian product)
var crossJoin = from p in products
from o in orders
select new { p.Name, o.Id };
// Zip (combine two sequences)
var zipped = products.Zip(orders, (p, o) => new { p.Name, o.Quantity });
}
// SET OPERATIONS
public void SetOperations()
{
var electronics = products.Where(p => p.Category == "Electronics").Select(p => p.Name);
var furniture = products.Where(p => p.Category == "Furniture").Select(p => p.Name);
// Union - combines unique elements from both
var allProducts = electronics.Union(furniture);
// Intersect - elements in both
var common = electronics.Intersect(furniture);
// Except - elements in first but not second
var onlyElectronics = electronics.Except(furniture);
// Distinct - unique elements
var categories = products.Select(p => p.Category).Distinct();
// Concat - combines all (including duplicates)
var all = electronics.Concat(furniture);
}
// QUANTIFIERS (Any, All, Contains)
public void Quantifiers()
{
// Any - at least one matches
bool hasAny = products.Any(p => p.Price > 1000);
bool hasItems = products.Any(); // true if not empty
// All - all match condition
bool allActive = products.All(p => p.IsActive);
bool allPositiveStock = products.All(p => p.Stock >= 0);
// Contains - specific element exists
var laptop = new Product { Id = 1, Name = "Laptop" };
bool contains = products.Contains(laptop); // Reference equality
bool containsByName = products.Any(p => p.Name == "Laptop"); // Value equality
}
// ELEMENT OPERATORS
public void ElementOperators()
{
// First/FirstOrDefault
var first = products.First(); // Throws if empty
var firstOrDefault = products.FirstOrDefault(); // Returns null if empty
var firstExpensive = products.First(p => p.Price > 500);
// Last/LastOrDefault
var last = products.Last();
// Single/SingleOrDefault (expects exactly one)
var single = products.Single(p => p.Id == 1); // Throws if none or multiple
var singleOrDefault = products.SingleOrDefault(p => p.Id == 999); // Returns null
// ElementAt
var third = products.ElementAt(2);
var maybe = products.ElementAtOrDefault(100);
// DefaultIfEmpty
var withDefault = products.Where(p => false).DefaultIfEmpty(new Product { Name = "None" });
}
// CONVERSION
public void Conversion()
{
// ToList, ToArray, ToDictionary, ToHashSet
List<Product> productList = products.ToList();
Product[] productArray = products.ToArray();
Dictionary<int, Product> productDict = products.ToDictionary(p => p.Id);
HashSet<string> productNames = products.Select(p => p.Name).ToHashSet();
// ToLookup (similar to group, but with key access)
ILookup<string, Product> categoryLookup = products.ToLookup(p => p.Category);
var electronics = categoryLookup["Electronics"]; // All electronics products
// Cast and OfType
ArrayList mixed = new ArrayList { 1, "hello", 2, "world" };
var ints = mixed.OfType<int>(); // 1, 2
var strings = mixed.Cast<string>(); // Throws for ints
}
// DEFERRED EXECUTION vs IMMEDIATE EXECUTION
public void Execution()
{
// DEFERRED - Executed when enumerated
var filtered = products.Where(p => p.Price > 100);
products.Add(new Product { Name = "New", Price = 200 }); // Will be included!
foreach (var p in filtered) { } // Query executes here
// IMMEDIATE - Executed immediately
var filteredList = products.Where(p => p.Price > 100).ToList();
products.Add(new Product { Name = "New2", Price = 300 }); // Not included in list
// Force execution with ToList(), ToArray(), Count(), First(), etc.
}
// PAGINATION
public void Pagination()
{
int pageNumber = 2;
int pageSize = 10;
var page = products
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
// COMPLEX QUERY EXAMPLES
public void ComplexQueries()
{
// 1. Get top 3 most expensive active products
var topExpensive = products
.Where(p => p.IsActive)
.OrderByDescending(p => p.Price)
.Take(3);
// 2. Get products with low stock (less than 10)
var lowStock = products
.Where(p => p.Stock < 10 && p.IsActive)
.Select(p => new { p.Name, p.Stock, Action = "Reorder" });
// 3. Category summary with conditional logic
var summary = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
TotalValue = g.Sum(p => p.Price * p.Stock),
Status = g.Average(p => p.Stock) > 20 ? "Healthy" : "Low Stock"
});
// 4. Find products that have been ordered
var orderedProducts = products
.Where(p => orders.Any(o => o.ProductId == p.Id));
// 5. Multi-step transformation
var result = products
.Where(p => p.IsActive)
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price)
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Products = g.Take(3),
AveragePrice = g.Average(p => p.Price)
})
.Where(g => g.AveragePrice > 100);
}
// CUSTOM LINQ OPERATORS (Extension Methods)
public static class LinqExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T> source)
{
return source.Where(x => x != null);
}
public static IEnumerable<T> TakePercent<T>(this IEnumerable<T> source, int percent)
{
int count = source.Count();
int take = (int)(count * percent / 100.0);
return source.Take(take);
}
// Custom operator: running total
public static IEnumerable<(T Item, decimal RunningTotal)> WithRunningTotal<T>(
this IEnumerable<T> source,
Func<T, decimal> selector)
{
decimal total = 0;
foreach (var item in source)
{
total += selector(item);
yield return (item, total);
}
}
}
public void CustomOperators()
{
var withTotal = products
.OrderBy(p => p.Price)
.WithRunningTotal(p => p.Price);
foreach (var (product, runningTotal) in withTotal)
{
Console.WriteLine($"{product.Name}: {product.Price:C} (Running: {runningTotal:C})");
}
}
// PERFORMANCE TIPS
public void PerformanceTips()
{
// ✅ DO: Use specific types
var list = products.ToList(); // Immediate execution
// ❌ DON'T: Multiple enumeration
var query = products.Where(p => p.Price > 100);
int count = query.Count(); // Enumerates once
var first = query.First(); // Enumerates again!
// ✅ DO: Materialize if enumerating multiple times
var materialized = products.Where(p => p.Price > 100).ToList();
count = materialized.Count();
first = materialized.First();
// ✅ DO: Use Any() instead of Count() > 0
bool hasAny = products.Any(); // Fast
bool hasAnySlow = products.Count() > 0; // Slow - enumerates all
// ✅ DO: Use Contains for simple lookup
// ❌ DON'T: Use Where + FirstOrDefault for single item
var product = products.FirstOrDefault(p => p.Id == 5); // OK
var product2 = products.Where(p => p.Id == 5).FirstOrDefault(); // Extra step
}
}

// REAL-WORLD LINQ EXAMPLES
public class RealWorldLINQ
{
// Data processing pipeline
public List<ReportItem> GenerateReport(List<Product> products, List<Order> orders)
{
return products
.GroupJoin(orders, p => p.Id, o => o.ProductId, (p, orderGroup) => new { p, orderGroup })
.Select(x => new ReportItem
{
ProductName = x.p.Name,
TotalSold = x.orderGroup.Sum(o => o.Quantity),
Revenue = x.orderGroup.Sum(o => o.Quantity * x.p.Price),
AveragePrice = x.p.Price
})
.Where(r => r.TotalSold > 0)
.OrderByDescending(r => r.Revenue)
.ToList();
}
// Search functionality
public IEnumerable<Product> SearchProducts(
List<Product> products,
string searchTerm,
decimal? minPrice,
decimal? maxPrice,
string category)
{
var query = products.AsEnumerable();
if (!string.IsNullOrWhiteSpace(searchTerm))
query = query.Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
if (minPrice.HasValue)
query = query.Where(p => p.Price >= minPrice.Value);
if (maxPrice.HasValue)
query = query.Where(p => p.Price <= maxPrice.Value);
if (!string.IsNullOrWhiteSpace(category))
query = query.Where(p => p.Category == category);
return query.OrderBy(p => p.Name);
}
public class ReportItem
{
public string ProductName { get; set; }
public int TotalSold { get; set; }
public decimal Revenue { get; set; }
public decimal AveragePrice { get; set; }
}
}

Module 2 Summary

You've learned advanced C# features:

  1. Exception Handling - Try-catch-finally, custom exceptions
  2. Delegates - Type-safe function pointers, multicast
  3. Events - Publisher-subscriber pattern
  4. Lambda Expressions - => operator, closures, expression trees
  5. Generics - Type-safe code, constraints, covariance
  6. Collections - List, Dictionary, HashSet, Stack, Queue, etc.
  7. LINQ - Query syntax, method syntax, filtering, grouping, joining

Practice Exercises for Module 2

Exercise 1: Generic Repository

// Implement a generic repository with:
// - Add, Get, Update, Delete methods
// - Find method with predicate
// - Use appropriate collections internally
// - Handle exceptions appropriately

public interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> Find(Func<T, bool> predicate);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}

// Your implementation here

Exercise 2: Event System

// Create a chat room system with events:
// - User joins/leaves events
// - Message received event
// - Typing notification event
// Implement proper event raising and subscription

public class ChatRoom
{
public event EventHandler<UserJoinedEventArgs> UserJoined;
// Add other events
public void Join(string userName) { }
public void SendMessage(string userName, string message) { }
}

Exercise 3: LINQ Query Challenge

// Given this data:
var employees = new[]
{
new { Name = "John", Department = "IT", Salary = 75000, JoinDate = DateTime.Parse("2020-01-15") },
new { Name = "Jane", Department = "HR", Salary = 65000, JoinDate = DateTime.Parse("2019-03-20") },
// Add more data...
};

// Write LINQ queries to:
// 1. Get all employees with salary > 70000, sorted by name
// 2. Group employees by department, show average salary per department
// 3. Find top 3 highest paid employees
// 4. Get employees who joined in the last year
// 5. Create a report with Name, Department, YearsOfService