100 C# Interview Questions and Answers for Developers
Interview preparation · Technical guide
C# Interview Questions and Answers
C# language, runtime, collections, concurrency, and design patterns, with corrections to memory-allocation explanations and legacy serialization guidance.
Examples are independent teaching snippets and may require application types, imports, packages, schema, and configuration. Framework behavior is version-dependent. Corrections address identified issues; the complete source code collection has not been compiled or integration-tested.
1. Value Types vs Reference Types in C
Value types have value-copy semantics; reference types copy an object reference on assignment. This does not mean every value type lives on the stack. A struct can be embedded in a heap object or array, boxed, stored in a register, or otherwise optimized. Copying a struct is shallow: reference-type fields still point to shared objects. Allocation location is a runtime detail, not the definition of the type category. Reference: Value types.
public struct Point { public int X; public int Y; }
// Inside a method:
Point a = new() { X = 1, Y = 2 };
Point b = a;
b.X = 9; // a.X remains 1
2. Struct vs Class Differences
A struct is a value type and a class is a reference type. Structs cannot inherit from another class or struct, but can implement interfaces. Classes support class inheritance. Use small, preferably immutable structs for values; copying a large struct can be expensive. Modern C# permits explicitly declared public parameterless struct constructors and field initializers under the language rules. default(T) still produces the zero-initialized value. Value types have value-copy semantics; reference types copy an object reference on assignment. This does not mean every value type lives on the stack. A struct can be embedded in a heap object or array, boxed, stored in a register, or otherwise optimized. Copying a struct is shallow: reference-type fields still point to shared objects. Allocation location is a runtime detail, not the definition of the type category. Reference: Value types.
3. Boxing and Unboxing
Boxing: Converting value type to reference type (object) Unboxing: Converting reference type back to value type
// Boxing
int number = 123;
object boxedNumber = number; // Boxing occurs here
// Unboxing
int unboxedNumber = (int)boxedNumber; // Unboxing occurs here
// Boxing with collections
List<object> numbers = new List<object>();
numbers.Add(42); // Boxing occurs
numbers.Add(3.14); // Boxing occurs
// Performance impact
int[] valueArray = new int[1000];
object[] referenceArray = new object[1000];
for (int i = 0; i < 1000; i++)
{
valueArray[i] = i; // No boxing
referenceArray[i] = i; // Boxing occurs - performance penalty
}
4. Access Modifiers and Scope
public class Example
{
public string PublicField; // Accessible from anywhere
private string PrivateField; // Only within this class
protected string ProtectedField; // Within this class and derived classes
internal string InternalField; // Within same assembly
protected internal string ProtectedInternalField; // Within same assembly OR derived classes
private protected string PrivateProtectedField; // Within same assembly AND derived classes
}
// Usage examples
public class DerivedClass : Example
{
public void AccessTest()
{
// PublicField - accessible
// PrivateField - NOT accessible
// ProtectedField - accessible (derived class)
// InternalField - accessible (same assembly)
// ProtectedInternalField - accessible
// PrivateProtectedField - accessible
}
}
5. readonly vs const
const: - Compile-time constant - Must be initialized at declaration - Cannot be changed after compilation
readonly: - Runtime constant - Can be initialized at declaration or in constructor - Cannot be changed after construction
public class ConstantsExample
{
// const - compile-time constant
public const double PI = 3.14159;
public const string AppName = "MyApp";
// readonly - runtime constant
public readonly DateTime CreatedDate;
public readonly string ConnectionString;
public ConstantsExample(string connectionString)
{
CreatedDate = DateTime.Now; // Can be set in constructor
ConnectionString = connectionString; // Can be set in constructor
}
public void Example()
{
// PI = 3.14; // Compilation error - cannot change const
// CreatedDate = DateTime.Now; // Compilation error - cannot change readonly
}
}
6. var vs dynamic
var: - Compile-time type inference - Type is determined at compile time - Provides IntelliSense and compile-time checking
dynamic: - Runtime type resolution - Type checking happens at runtime - No IntelliSense, no compile-time checking
// var - compile-time type inference
var number = 42; // Compiler knows it's int
var text = "Hello"; // Compiler knows it's string
var list = new List<int>(); // Compiler knows it's List<int>
// number = "string"; // Compilation error - type mismatch
// dynamic - runtime type resolution
dynamic dynamicValue = 42;
dynamicValue = "Hello"; // No compilation error
dynamicValue = new List<int>(); // No compilation error
// Runtime errors possible
try
{
dynamicValue.NonExistentMethod(); // Runtime error
}
catch (RuntimeBinderException ex)
{
Console.WriteLine("Runtime error: " + ex.Message);
}
7. Nullable Reference Types (C# 8.0+)
#nullable enable
public class NullableReferenceExample
{
// Non-nullable reference type
public string RequiredName { get; set; } = string.Empty;
// Nullable reference type
public string? OptionalName { get; set; }
public void ProcessName(string name) // name is non-nullable
{
Console.WriteLine(name.Length); // No warning - compiler knows name is not null
}
public void ProcessOptionalName(string? name) // name is nullable
{
if (name != null)
{
Console.WriteLine(name.Length); // No warning after null check
}
// Console.WriteLine(name.Length); // Warning: possible null reference
}
}
// Usage
var example = new NullableReferenceExample();
example.RequiredName = "John"; // Required
example.OptionalName = null; // Allowed
8. string vs StringBuilder
string: - Immutable - Thread-safe - Good for small operations
StringBuilder: - Mutable - Better performance for multiple concatenations - Not thread-safe
// String concatenation (creates new objects)
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString(); // Creates new string each time
}
// StringBuilder (mutable, better performance)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i.ToString()); // Modifies existing object
}
string finalResult = sb.ToString();
// StringBuilder with capacity
StringBuilder sbWithCapacity = new StringBuilder(10000);
sbWithCapacity.Append("Initial text");
sbWithCapacity.AppendLine("New line");
sbWithCapacity.AppendFormat("Formatted: {0}", 42);
9. == vs Equals()
== (Equality Operator): - Reference equality for reference types - Value equality for value types - Can be overloaded
Equals(): - Virtual method for reference equality - Can be overridden for custom equality logic
public class Person
{
public string Name { get; set; }
public override bool Equals(object? obj)
{
if (obj is Person other)
{
return Name == other.Name;
}
return false;
}
public override int GetHashCode()
{
return Name?.GetHashCode() ?? 0;
}
}
// Usage
Person person1 = new Person { Name = "John" };
Person person2 = new Person { Name = "John" };
Person person3 = person1;
Console.WriteLine(person1 == person2); // False (reference equality)
Console.WriteLine(person1.Equals(person2)); // True (custom equality)
Console.WriteLine(person1 == person3); // True (same reference)
// String comparison
string str1 = "Hello";
string str2 = "Hello";
Console.WriteLine(str1 == str2); // True (string interning)
Console.WriteLine(str1.Equals(str2)); // True
10. ref vs out Parameters
ref: - Parameter must be initialized before method call - Method can read and write the parameter - Parameter retains its value if method doesn't modify it
out: - Parameter doesn't need to be initialized - Method must assign a value to the parameter - Parameter is considered unassigned until method assigns it
public class ParameterExample
{
// ref parameter
public void ModifyWithRef(ref int number)
{
number = number * 2; // Can read and modify
}
// out parameter
public void GetValue(out int number)
{
number = 42; // Must assign a value
// Console.WriteLine(number); // Error - can't read before assignment
}
// Multiple out parameters
public void GetMultipleValues(out int x, out int y, out string message)
{
x = 10;
y = 20;
message = "Success";
}
}
// Usage
var example = new ParameterExample();
// ref usage
int number = 5;
example.ModifyWithRef(ref number);
Console.WriteLine(number); // 10
// out usage
int result;
example.GetValue(out result);
Console.WriteLine(result); // 42
// Multiple out parameters (C# 7.0+)
example.GetMultipleValues(out int x, out int y, out string message);
Console.WriteLine($"x={x}, y={y}, message={message}");
// Discard pattern (C# 7.0+)
example.GetMultipleValues(out int a, out int b, out _); // Ignore message
These concepts are fundamental to understanding C# and .NET development. Each has specific use cases and understanding when to use which approach is crucial for writing efficient and maintainable code.
11. Inheritance, Encapsulation, and Polymorphism in C
Inheritance
Inheritance allows a class to inherit properties and methods from another class.
// Base class
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("Some animal sound");
}
}
// Derived class
public class Dog : Animal
{
public string Breed { get; set; }
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
// Usage
Dog myDog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
myDog.MakeSound(); // Output: Woof!
Encapsulation
Encapsulation bundles data and methods that operate on that data within a single unit and restricts access to some of the object's components.
public class BankAccount
{
// Private fields - data hiding
private decimal _balance;
private readonly string _accountNumber;
// Public properties - controlled access
public decimal Balance
{
get { return _balance; }
private set { _balance = value; } // Only internal methods can modify
}
public string AccountNumber => _accountNumber;
// Constructor
public BankAccount(string accountNumber, decimal initialBalance)
{
_accountNumber = accountNumber;
_balance = initialBalance;
}
// Public methods - controlled behavior
public bool Withdraw(decimal amount)
{
if (amount > 0 && amount <= _balance)
{
_balance -= amount;
return true;
}
return false;
}
public void Deposit(decimal amount)
{
if (amount > 0)
{
_balance += amount;
}
}
}
Polymorphism
Polymorphism allows objects of different classes to be treated as objects of a common base class.
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
}
// Polymorphic usage
List<Shape> shapes = new List<Shape>
{
new Circle { Radius = 5 },
new Rectangle { Width = 4, Height = 6 }
};
foreach (var shape in shapes)
{
Console.WriteLine($"Area: {shape.CalculateArea()}"); // Different implementations
}
12. Method Overriding vs Method Hiding
Method Overriding
Uses the virtual keyword in base class and override in derived class.
public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("Base class display");
}
}
public class DerivedClass : BaseClass
{
public override void Display()
{
Console.WriteLine("Derived class display");
}
}
// Usage
BaseClass obj = new DerivedClass();
obj.Display(); // Output: "Derived class display" (runtime polymorphism)
Method Hiding
Uses the new keyword to hide the base class method.
public class BaseClass
{
public void Display()
{
Console.WriteLine("Base class display");
}
}
public class DerivedClass : BaseClass
{
public new void Display()
{
Console.WriteLine("Derived class display");
}
}
// Usage
BaseClass obj = new DerivedClass();
obj.Display(); // Output: "Base class display" (no runtime polymorphism)
13. Virtual, Abstract, and Sealed Keywords
Virtual
Allows a method to be overridden in derived classes.
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
Abstract
Used for classes and methods that must be implemented by derived classes.
public abstract class Shape
{
public abstract double CalculateArea(); // Must be implemented
public virtual void DisplayInfo()
{
Console.WriteLine("This is a shape");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
Sealed
Prevents further inheritance or method overriding.
public class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("Base Method1");
}
}
public class DerivedClass : BaseClass
{
public sealed override void Method1() // Cannot be overridden further
{
Console.WriteLine("Derived Method1");
}
}
public sealed class FinalClass : DerivedClass // Cannot be inherited
{
// This class cannot be inherited
}
14. Interfaces and When to Use Them
Interfaces define a contract that implementing classes must follow.
public interface ILogger
{
void Log(string message);
void LogError(string error);
}
public interface IRepository<T>
{
T GetById(int id);
void Save(T entity);
void Delete(int id);
}
// Implementation
public class FileLogger : ILogger
{
public void Log(string message)
{
File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
}
public void LogError(string error)
{
File.AppendAllText("error.txt", $"{DateTime.Now}: ERROR - {error}\n");
}
}
public class UserRepository : IRepository<User>
{
private List<User> _users = new List<User>();
public User GetById(int id)
{
return _users.FirstOrDefault(u => u.Id == id);
}
public void Save(User entity)
{
var existing = _users.FirstOrDefault(u => u.Id == entity.Id);
if (existing != null)
_users.Remove(existing);
_users.Add(entity);
}
public void Delete(int id)
{
var user = _users.FirstOrDefault(u => u.Id == id);
if (user != null)
_users.Remove(user);
}
}
When to use interfaces: - Multiple inheritance (C# doesn't support multiple class inheritance) - Defining contracts for different implementations - Dependency injection - Unit testing with mocks
15. Abstract Class vs Interface
Abstract Class
public abstract class Animal
{
// Can have fields
protected string _name;
// Can have constructors
protected Animal(string name)
{
_name = name;
}
// Can have concrete methods
public void Sleep()
{
Console.WriteLine($"{_name} is sleeping");
}
// Can have abstract methods
public abstract void MakeSound();
// Can have virtual methods
public virtual void Move()
{
Console.WriteLine("Moving...");
}
}
Interface
public interface IAnimal
{
// Can only have properties and methods (no fields)
string Name { get; set; }
// Cannot have constructors
// Cannot have concrete implementations (before C# 8.0)
void MakeSound();
void Move();
}
// C# 8.0+ default interface methods
public interface ILogger
{
void Log(string message);
// Default implementation
void LogError(string error) => Log($"ERROR: {error}");
}
Key Differences: - Abstract classes can have fields, constructors, and concrete methods - Interfaces can only have properties and methods (before C# 8.0) - A class can inherit only one abstract class but implement multiple interfaces - Abstract classes can have access modifiers, interfaces are public by default
16. New vs Override Keywords
Override (Runtime Polymorphism)
public class BaseClass
{
public virtual void Method()
{
Console.WriteLine("Base Method");
}
}
public class DerivedClass : BaseClass
{
public override void Method()
{
Console.WriteLine("Derived Method");
}
}
// Usage
BaseClass obj = new DerivedClass();
obj.Method(); // Output: "Derived Method"
New (Method Hiding)
public class BaseClass
{
public void Method()
{
Console.WriteLine("Base Method");
}
}
public class DerivedClass : BaseClass
{
public new void Method()
{
Console.WriteLine("Derived Method");
}
}
// Usage
BaseClass obj = new DerivedClass();
obj.Method(); // Output: "Base Method"
DerivedClass derived = new DerivedClass();
derived.Method(); // Output: "Derived Method"
17. Covariance and Contravariance
Covariance (out keyword)
public interface ICovariant<out T>
{
T GetItem();
}
public class CovariantList<T> : ICovariant<T>
{
private List<T> _items = new List<T>();
public void Add(T item) => _items.Add(item);
public T GetItem() => _items.FirstOrDefault();
}
// Usage
ICovariant<string> stringList = new CovariantList<string>();
ICovariant<object> objectList = stringList; // Covariant assignment
Contravariance (in keyword)
public interface IContravariant<in T>
{
void SetItem(T item);
}
public class ContravariantList<T> : IContravariant<T>
{
private List<T> _items = new List<T>();
public void SetItem(T item) => _items.Add(item);
}
// Usage
IContravariant<object> objectList = new ContravariantList<object>();
IContravariant<string> stringList = objectList; // Contravariant assignment
18. Extension Methods
Extension methods allow you to add methods to existing types without modifying them.
public static class StringExtensions
{
public static bool IsValidEmail(this string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
public static string Reverse(this string str)
{
return new string(str.Reverse().ToArray());
}
}
public static class EnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
}
// Usage
string email = "test@example.com";
bool isValid = email.IsValidEmail(); // Extension method call
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.ForEach(n => Console.WriteLine(n)); // Extension method call
19. Partial Classes and Partial Methods
Partial Classes
Allow you to split a class definition across multiple files.
File 1: Person.cs
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
File 2: Person.Generated.cs
public partial class Person
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; }
public void Save()
{
// Database save logic
Console.WriteLine("Saving person to database");
}
}
Partial Methods
Allow you to define a method signature in one partial class and implement it in another.
public partial class DataProcessor
{
public void ProcessData()
{
Console.WriteLine("Starting data processing");
OnDataProcessingStarted(); // Partial method call
// Processing logic
Console.WriteLine("Data processing completed");
OnDataProcessingCompleted(); // Partial method call
}
// Partial method declarations
partial void OnDataProcessingStarted();
partial void OnDataProcessingCompleted();
}
public partial class DataProcessor
{
// Partial method implementations
partial void OnDataProcessingStarted()
{
Console.WriteLine("Data processing started at: " + DateTime.Now);
}
partial void OnDataProcessingCompleted()
{
Console.WriteLine("Data processing completed at: " + DateTime.Now);
}
}
20. Static vs Instance Members
Static Members
Belong to the type itself, not to any specific instance.
public class MathHelper
{
// Static field
public static int TotalCalculations = 0;
// Static property
public static double PI => 3.14159;
// Static method
public static int Add(int a, int b)
{
TotalCalculations++;
return a + b;
}
// Static constructor
static MathHelper()
{
Console.WriteLine("MathHelper class initialized");
}
}
// Usage
int result = MathHelper.Add(5, 3); // No instance needed
Console.WriteLine(MathHelper.PI);
Console.WriteLine(MathHelper.TotalCalculations);
Instance Members
Belong to specific instances of the class.
public class BankAccount
{
// Instance fields
private decimal _balance;
private string _accountNumber;
// Instance properties
public decimal Balance => _balance;
public string AccountNumber => _accountNumber;
// Instance constructor
public BankAccount(string accountNumber, decimal initialBalance)
{
_accountNumber = accountNumber;
_balance = initialBalance;
}
// Instance methods
public void Deposit(decimal amount)
{
if (amount > 0)
{
_balance += amount;
}
}
public bool Withdraw(decimal amount)
{
if (amount > 0 && amount <= _balance)
{
_balance -= amount;
return true;
}
return false;
}
}
// Usage
BankAccount account1 = new BankAccount("12345", 1000);
BankAccount account2 = new BankAccount("67890", 500);
account1.Deposit(200); // Instance method call
account2.Withdraw(100); // Instance method call
Key Differences: - Static members are accessed via the class name, instance members via object reference - Static members are shared across all instances, instance members are unique per instance - Static methods cannot access instance members, instance methods can access both static and instance members - Static constructors run once when the class is first accessed, instance constructors run for each new object
.NET Architecture Concepts: Comprehensive Guide
21. Async/Await Pattern and Task-based Programming
Overview
The async/await pattern is a modern approach to asynchronous programming in C# that makes it easier to write and maintain asynchronous code without blocking threads.
Key Concepts
Task-based Asynchronous Pattern (TAP)
- Task: Represents an asynchronous operation that can return a value
- Task<T>: Represents an asynchronous operation that returns a value of type T
- async: Keyword that enables the use of await in a method
- await: Keyword that suspends execution until the awaited task completes
Basic Examples
// Simple async method
public async Task<string> GetDataAsync()
{
// Simulate async work
await Task.Delay(1000);
return "Data retrieved successfully";
}
// Async method with exception handling
public async Task<string> GetDataWithErrorHandlingAsync()
{
try
{
await Task.Delay(1000);
return "Success";
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
// Async void (use sparingly - only for event handlers)
public async void Button_Click(object sender, EventArgs e)
{
try
{
var result = await GetDataAsync();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
Advanced Patterns
// Parallel execution
public async Task<List<string>> GetMultipleDataAsync()
{
var tasks = new List<Task<string>>
{
GetDataAsync(),
GetDataAsync(),
GetDataAsync()
};
// Wait for all tasks to complete
var results = await Task.WhenAll(tasks);
return results.ToList();
}
// Race condition - first to complete
public async Task<string> GetFirstResultAsync()
{
var tasks = new List<Task<string>>
{
GetDataAsync(),
GetDataAsync(),
GetDataAsync()
};
// Return the first completed task
var firstResult = await Task.WhenAny(tasks);
return await firstResult;
}
// Cancellation support
public async Task<string> GetDataWithCancellationAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(5000, cancellationToken);
return "Data retrieved";
}
catch (OperationCanceledException)
{
return "Operation cancelled";
}
}
// ConfigureAwait for performance
public async Task<string> GetDataOptimizedAsync()
{
// Use ConfigureAwait(false) when you don't need to return to the original context
var result = await GetDataAsync().ConfigureAwait(false);
return result;
}
Best Practices
// Good: Proper exception handling
public async Task<string> GoodAsyncMethod()
{
try
{
return await GetDataAsync();
}
catch (HttpRequestException ex)
{
// Log the exception
_logger.LogError(ex, "Network error occurred");
throw;
}
}
// Good: Avoid async void (except for event handlers)
public async Task ProcessDataAsync()
{
await GetDataAsync();
}
// Bad: Don't block on async code
public string BadMethod()
{
// This blocks the thread - avoid this pattern
return GetDataAsync().Result; // Don't do this!
}
22. Delegates and How They Work
Overview
Delegates are type-safe function pointers that allow you to pass methods as parameters, store them in variables, and invoke them later.
Basic Delegates
// Declaring a delegate
public delegate int MathOperation(int x, int y);
// Using the delegate
public class Calculator
{
public int Add(int x, int y) => x + y;
public int Multiply(int x, int y) => x * y;
public void PerformOperation(MathOperation operation, int a, int b)
{
int result = operation(a, b);
Console.WriteLine($"Result: {result}");
}
}
// Usage
var calc = new Calculator();
MathOperation addOp = calc.Add;
MathOperation multiplyOp = calc.Multiply;
calc.PerformOperation(addOp, 5, 3); // Output: Result: 8
calc.PerformOperation(multiplyOp, 5, 3); // Output: Result: 15
Built-in Delegates
// Func<T> - for methods that return a value
public class FuncExamples
{
public void ProcessNumbers(Func<int, int, int> operation)
{
int result = operation(10, 5);
Console.WriteLine($"Result: {result}");
}
public void ProcessWithMultipleInputs(Func<int, int, int, string> operation)
{
string result = operation(1, 2, 3);
Console.WriteLine($"Result: {result}");
}
}
// Action<T> - for methods that don't return a value
public class ActionExamples
{
public void ProcessData(Action<string> processor)
{
processor("Hello World");
}
public void ProcessMultiple(Action<int, string> processor)
{
processor(42, "Answer");
}
}
// Predicate<T> - for methods that return bool
public class PredicateExamples
{
public List<int> FilterNumbers(List<int> numbers, Predicate<int> filter)
{
return numbers.FindAll(filter);
}
}
Multicast Delegates
public class MulticastExample
{
public delegate void LogHandler(string message);
public void LogToConsole(string message)
{
Console.WriteLine($"Console: {message}");
}
public void LogToFile(string message)
{
// Simulate file logging
Console.WriteLine($"File: {message}");
}
public void LogToDatabase(string message)
{
// Simulate database logging
Console.WriteLine($"Database: {message}");
}
public void TestMulticast()
{
LogHandler logger = LogToConsole;
logger += LogToFile; // Add another method
logger += LogToDatabase; // Add another method
logger("Test message"); // All three methods will be called
logger -= LogToFile; // Remove a method
logger("Another message"); // Only Console and Database will be called
}
}
23. Events and Event Handlers in C
Overview
Events are a way for a class to notify other classes when something interesting happens. They are built on top of delegates and provide a standardized way to handle notifications.
Basic Event Pattern
public class OrderProcessor
{
// Define the event using EventHandler delegate
public event EventHandler<OrderProcessedEventArgs> OrderProcessed;
// Define custom event args
public class OrderProcessedEventArgs : EventArgs
{
public int OrderId { get; set; }
public DateTime ProcessedAt { get; set; }
public bool Success { get; set; }
}
public void ProcessOrder(int orderId)
{
// Simulate processing
Thread.Sleep(1000);
var args = new OrderProcessedEventArgs
{
OrderId = orderId,
ProcessedAt = DateTime.Now,
Success = true
};
// Raise the event
OnOrderProcessed(args);
}
// Protected method to raise the event
protected virtual void OnOrderProcessed(OrderProcessedEventArgs e)
{
OrderProcessed?.Invoke(this, e);
}
}
// Event subscriber
public class OrderLogger
{
public void SubscribeToEvents(OrderProcessor processor)
{
processor.OrderProcessed += OnOrderProcessed;
}
private void OnOrderProcessed(object sender, OrderProcessor.OrderProcessedEventArgs e)
{
Console.WriteLine($"Order {e.OrderId} processed at {e.ProcessedAt}");
}
}
Custom Event Pattern
public class StockMarket
{
// Custom delegate for price change events
public delegate void PriceChangedHandler(object sender, PriceChangedEventArgs e);
// Custom event args
public class PriceChangedEventArgs : EventArgs
{
public string Symbol { get; set; }
public decimal OldPrice { get; set; }
public decimal NewPrice { get; set; }
public decimal Change => NewPrice - OldPrice;
public decimal ChangePercent => (Change / OldPrice) * 100;
}
// Event declaration
public event PriceChangedHandler PriceChanged;
private Dictionary<string, decimal> _prices = new();
public void UpdatePrice(string symbol, decimal newPrice)
{
if (_prices.TryGetValue(symbol, out decimal oldPrice))
{
var args = new PriceChangedEventArgs
{
Symbol = symbol,
OldPrice = oldPrice,
NewPrice = newPrice
};
_prices[symbol] = newPrice;
OnPriceChanged(args);
}
else
{
_prices[symbol] = newPrice;
}
}
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
// Multiple subscribers
public class StockAlert
{
public void Subscribe(StockMarket market)
{
market.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object sender, StockMarket.PriceChangedEventArgs e)
{
if (Math.Abs(e.ChangePercent) > 5)
{
Console.WriteLine($"ALERT: {e.Symbol} changed by {e.ChangePercent:F2}%");
}
}
}
public class StockLogger
{
public void Subscribe(StockMarket market)
{
market.PriceChanged += OnPriceChanged;
}
private void OnPriceChanged(object sender, StockMarket.PriceChangedEventArgs e)
{
Console.WriteLine($"{e.Symbol}: {e.OldPrice:C} -> {e.NewPrice:C} ({e.Change:C})");
}
}
Event Best Practices
public class EventBestPractices
{
// Use EventHandler<T> for standard events
public event EventHandler<CustomEventArgs> StandardEvent;
// Use custom delegates only when needed
public event Action<string> SimpleEvent;
// Always provide protected virtual method for raising events
protected virtual void OnStandardEvent(CustomEventArgs e)
{
StandardEvent?.Invoke(this, e);
}
// Use null-conditional operator to check for subscribers
protected virtual void OnSimpleEvent(string message)
{
SimpleEvent?.Invoke(message);
}
// Thread-safe event raising
private readonly object _eventLock = new object();
private EventHandler<CustomEventArgs> _threadSafeEvent;
public event EventHandler<CustomEventArgs> ThreadSafeEvent
{
add
{
lock (_eventLock)
{
_threadSafeEvent += value;
}
}
remove
{
lock (_eventLock)
{
_threadSafeEvent -= value;
}
}
}
protected virtual void OnThreadSafeEvent(CustomEventArgs e)
{
EventHandler<CustomEventArgs> handler;
lock (_eventLock)
{
handler = _threadSafeEvent;
}
handler?.Invoke(this, e);
}
}
24. Lambda Expressions and Anonymous Methods
Overview
Lambda expressions are a concise way to write anonymous functions. They can be used wherever a delegate type is expected.
Basic Lambda Expressions
public class LambdaExamples
{
public void BasicLambdas()
{
// Lambda expression with one parameter
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // Output: 25
// Lambda expression with multiple parameters
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(3, 4)); // Output: 7
// Lambda expression with no parameters
Func<string> getMessage = () => "Hello World";
Console.WriteLine(getMessage()); // Output: Hello World
// Lambda expression with multiple statements
Func<int, int> complexOperation = x =>
{
int result = x * 2;
result += 10;
return result;
};
Console.WriteLine(complexOperation(5)); // Output: 20
}
}
Lambda Expressions with LINQ
public class LambdaWithLinq
{
public void FilterAndTransform()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Filter even numbers
var evenNumbers = numbers.Where(n => n % 2 == 0);
// Transform numbers
var squaredNumbers = numbers.Select(n => n * n);
// Filter and transform in one operation
var evenSquared = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n);
// Using lambda with multiple conditions
var filteredNumbers = numbers.Where(n => n > 5 && n < 9);
// Lambda with custom objects
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25 },
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Charlie", Age = 35 }
};
var adults = people.Where(p => p.Age >= 18);
var names = people.Select(p => p.Name);
var oldPeople = people.Where(p => p.Age > 30).Select(p => p.Name);
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Anonymous Methods (Legacy Syntax)
public class AnonymousMethodExamples
{
public void LegacySyntax()
{
// Anonymous method syntax (pre-C# 3.0)
Func<int, int> square = delegate(int x) { return x * x; };
// Multiple statements in anonymous method
Func<int, int> complex = delegate(int x)
{
int result = x * 2;
result += 10;
return result;
};
// Anonymous method with no parameters
Action printMessage = delegate { Console.WriteLine("Hello"); };
// Anonymous method with multiple parameters
Func<int, int, int> add = delegate(int x, int y) { return x + y; };
}
}
Lambda Expressions with Events
public class LambdaWithEvents
{
public void SubscribeWithLambda()
{
var button = new Button();
// Subscribe using lambda expression
button.Click += (sender, e) => Console.WriteLine("Button clicked!");
// Lambda with multiple statements
button.Click += (sender, e) =>
{
Console.WriteLine("Button clicked!");
Console.WriteLine($"Sender: {sender}");
Console.WriteLine($"EventArgs: {e}");
};
// Lambda with closure
int clickCount = 0;
button.Click += (sender, e) =>
{
clickCount++;
Console.WriteLine($"Button clicked {clickCount} times");
};
}
}
public class Button
{
public event EventHandler Click;
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty);
}
}
Expression Trees
public class ExpressionTreeExamples
{
public void ExpressionTrees()
{
// Lambda expression that can be converted to expression tree
Expression<Func<int, int>> squareExpression = x => x * x;
// Compile the expression tree to a delegate
Func<int, int> square = squareExpression.Compile();
Console.WriteLine(square(5)); // Output: 25
// Expression trees are useful for LINQ to SQL, Entity Framework, etc.
Expression<Func<Person, bool>> ageFilter = p => p.Age > 25;
// This can be translated to SQL in LINQ to SQL
var people = new List<Person>();
var filteredPeople = people.AsQueryable().Where(ageFilter);
}
}
25. LINQ and Query Syntax vs Method Syntax
Overview
LINQ (Language Integrated Query) provides a unified way to query data from different sources. It supports two syntaxes: query syntax and method syntax.
Query Syntax
public class QuerySyntaxExamples
{
public void BasicQueries()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Basic filtering
var evenNumbers = from n in numbers
where n % 2 == 0
select n;
// Projection (transformation)
var squaredNumbers = from n in numbers
select n * n;
// Filtering and projection
var evenSquared = from n in numbers
where n % 2 == 0
select n * n;
// Ordering
var orderedNumbers = from n in numbers
orderby n descending
select n;
// Multiple ordering criteria
var complexOrdering = from n in numbers
orderby n % 2, n descending
select n;
}
public void ComplexQueries()
{
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25, City = "New York" },
new Person { Name = "Bob", Age = 30, City = "Los Angeles" },
new Person { Name = "Charlie", Age = 35, City = "New York" },
new Person { Name = "Diana", Age = 28, City = "Chicago" }
};
// Grouping
var groupedByCity = from p in people
group p by p.City into cityGroup
select new
{
City = cityGroup.Key,
Count = cityGroup.Count(),
People = cityGroup
};
// Joining
var cities = new List<City>
{
new City { Name = "New York", Population = 8000000 },
new City { Name = "Los Angeles", Population = 4000000 },
new City { Name = "Chicago", Population = 2700000 }
};
var peopleWithCityInfo = from p in people
join c in cities on p.City equals c.Name
select new
{
p.Name,
p.Age,
City = c.Name,
CityPopulation = c.Population
};
// Let clause for intermediate calculations
var peopleWithAgeGroup = from p in people
let ageGroup = p.Age < 30 ? "Young" : "Adult"
select new
{
p.Name,
p.Age,
AgeGroup = ageGroup
};
}
}
public class City
{
public string Name { get; set; }
public int Population { get; set; }
}
Method Syntax
public class MethodSyntaxExamples
{
public void BasicMethods()
{
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Basic filtering
var evenNumbers = numbers.Where(n => n % 2 == 0);
// Projection
var squaredNumbers = numbers.Select(n => n * n);
// Filtering and projection
var evenSquared = numbers.Where(n => n % 2 == 0).Select(n => n * n);
// Ordering
var orderedNumbers = numbers.OrderByDescending(n => n);
// Multiple ordering criteria
var complexOrdering = numbers.OrderBy(n => n % 2).ThenByDescending(n => n);
}
public void ComplexMethods()
{
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25, City = "New York" },
new Person { Name = "Bob", Age = 30, City = "Los Angeles" },
new Person { Name = "Charlie", Age = 35, City = "New York" },
new Person { Name = "Diana", Age = 28, City = "Chicago" }
};
// Grouping
var groupedByCity = people.GroupBy(p => p.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
People = g
});
// Joining
var cities = new List<City>
{
new City { Name = "New York", Population = 8000000 },
new City { Name = "Los Angeles", Population = 4000000 },
new City { Name = "Chicago", Population = 2700000 }
};
var peopleWithCityInfo = people.Join(cities,
p => p.City,
c => c.Name,
(p, c) => new
{
p.Name,
p.Age,
City = c.Name,
CityPopulation = c.Population
});
// Aggregation
var averageAge = people.Average(p => p.Age);
var maxAge = people.Max(p => p.Age);
var minAge = people.Min(p => p.Age);
var totalAge = people.Sum(p => p.Age);
}
}
Comparison and Best Practices
public class LinqComparison
{
public void CompareSyntaxes()
{
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25, City = "New York" },
new Person { Name = "Bob", Age = 30, City = "Los Angeles" },
new Person { Name = "Charlie", Age = 35, City = "New York" }
};
// Query syntax - good for complex queries
var querySyntax = from p in people
where p.Age > 25
orderby p.Name
select new { p.Name, p.Age };
// Method syntax - equivalent and often more readable
var methodSyntax = people.Where(p => p.Age > 25)
.OrderBy(p => p.Name)
.Select(p => new { p.Name, p.Age });
// Method syntax is often preferred for simple operations
var simpleFilter = people.Where(p => p.Age > 25);
// Query syntax is better for complex joins and grouping
var complexQuery = from p in people
group p by p.City into cityGroup
select new
{
City = cityGroup.Key,
Count = cityGroup.Count(),
AverageAge = cityGroup.Average(p => p.Age)
};
}
public void PerformanceConsiderations()
{
var numbers = Enumerable.Range(1, 1000000);
// Deferred execution - query is not executed until enumerated
var query = numbers.Where(n => n % 2 == 0).Select(n => n * n);
// Query is executed here
var results = query.ToList();
// Immediate execution with ToList(), ToArray(), etc.
var immediateResults = numbers.Where(n => n % 2 == 0).ToList();
// Chaining operations efficiently
var efficientQuery = numbers.Where(n => n % 2 == 0) // Filter first
.Take(100) // Limit results
.Select(n => n * n); // Transform last
// Avoid multiple enumerations
var list = numbers.ToList(); // Materialize once
var even = list.Where(n => n % 2 == 0);
var odd = list.Where(n => n % 2 != 0);
}
}
Advanced LINQ Features
public class AdvancedLinqFeatures
{
public void AdvancedOperations()
{
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25 },
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Charlie", Age = 35 }
};
// First, FirstOrDefault
var firstPerson = people.First();
var firstOrDefault = people.FirstOrDefault(p => p.Age > 40); // null if not found
// Single, SingleOrDefault
var singlePerson = people.Single(p => p.Name == "Alice");
var singleOrDefault = people.SingleOrDefault(p => p.Age == 25);
// Any, All
var hasAdults = people.Any(p => p.Age >= 18);
var allAdults = people.All(p => p.Age >= 18);
// Contains
var containsAlice = people.Any(p => p.Name == "Alice");
// Distinct
var uniqueAges = people.Select(p => p.Age).Distinct();
// Skip, Take
var skipFirst = people.Skip(1);
var takeFirstTwo = people.Take(2);
// Concat, Union, Intersect, Except
var morePeople = new List<Person>
{
new Person { Name = "Diana", Age = 28 }
};
var allPeople = people.Concat(morePeople);
var uniquePeople = people.Union(morePeople);
// Zip
var names = new[] { "Alice", "Bob", "Charlie" };
var ages = new[] { 25, 30, 35 };
var zipped = names.Zip(ages, (name, age) => new Person { Name = name, Age = age });
}
}
26. What are generics and their constraints?
Generics allow you to write classes, interfaces, and methods that work with any data type while maintaining type safety at compile time. They enable code reuse without sacrificing type safety.
Basic Generics Example:
public class GenericList<T>
{
private List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public T Get(int index)
{
return _items[index];
}
}
// Usage
var stringList = new GenericList<string>();
stringList.Add("Hello");
var numberList = new GenericList<int>();
numberList.Add(42);
Generic Constraints:
Constraints limit the types that can be used with generics:
// 1. Class constraint - T must be a reference type
public class Repository<T> where T : class
{
public T GetById(int id) { /* implementation */ }
}
// 2. Struct constraint - T must be a value type
public class ValueContainer<T> where T : struct
{
public T Value { get; set; }
}
// 3. Interface constraint - T must implement IComparable
public class Sorter<T> where T : IComparable<T>
{
public void Sort(T[] items)
{
Array.Sort(items);
}
}
// 4. Constructor constraint - T must have parameterless constructor
public class Factory<T> where T : new()
{
public T CreateInstance()
{
return new T();
}
}
// 5. Multiple constraints
public class DataProcessor<T> where T : class, IComparable<T>, new()
{
public void Process(T data) { /* implementation */ }
}
// 6. Base class constraint
public class EntityRepository<T> where T : Entity
{
public T GetById(int id) { /* implementation */ }
}
Generic Methods:
public static class Utilities
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
}
27. Explain reflection and its use cases
Reflection is the ability of a program to examine, introspect, and modify its own structure and behavior at runtime.
Basic Reflection Examples:
using System.Reflection;
// Get type information
Type type = typeof(string);
Console.WriteLine($"Type: {type.Name}");
Console.WriteLine($"Assembly: {type.Assembly.FullName}");
// Get all public methods
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"Method: {method.Name}");
}
// Get properties
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"Property: {prop.Name} - Type: {prop.PropertyType}");
}
Dynamic Object Creation and Method Invocation:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, I'm {Name} and I'm {Age} years old.");
}
}
// Using reflection to create and use objects
Type personType = typeof(Person);
object person = Activator.CreateInstance(personType);
// Set properties dynamically
PropertyInfo nameProperty = personType.GetProperty("Name");
PropertyInfo ageProperty = personType.GetProperty("Age");
nameProperty.SetValue(person, "John");
ageProperty.SetValue(person, 30);
// Invoke method dynamically
MethodInfo sayHelloMethod = personType.GetMethod("SayHello");
sayHelloMethod.Invoke(person, null);
Use Cases:
- Dependency Injection Containers:
public class SimpleContainer
{
private Dictionary<Type, Type> _registrations = new();
public void Register<TInterface, TImplementation>()
{
_registrations[typeof(TInterface)] = typeof(TImplementation);
}
public T Resolve<T>()
{
Type type = _registrations[typeof(T)];
ConstructorInfo constructor = type.GetConstructors().First();
ParameterInfo[] parameters = constructor.GetParameters();
object[] parameterInstances = parameters
.Select(p => Resolve(p.ParameterType))
.ToArray();
return (T)constructor.Invoke(parameterInstances);
}
private object Resolve(Type type)
{
// Recursive resolution for dependencies
return Activator.CreateInstance(type);
}
}
- Configuration Binding:
public static class ConfigurationBinder
{
public static T Bind<T>(IConfiguration configuration) where T : new()
{
T instance = new T();
Type type = typeof(T);
foreach (var property in type.GetProperties())
{
string value = configuration[property.Name];
if (value != null)
{
object convertedValue = Convert.ChangeType(value, property.PropertyType);
property.SetValue(instance, convertedValue);
}
}
return instance;
}
}
- Validation Framework:
public class ValidationAttribute : Attribute
{
public string ErrorMessage { get; set; }
public abstract bool IsValid(object value);
}
public class RequiredAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
return value != null && !string.IsNullOrEmpty(value.ToString());
}
}
public static class Validator
{
public static List<string> Validate(object obj)
{
var errors = new List<string>();
Type type = obj.GetType();
foreach (var property in type.GetProperties())
{
var attributes = property.GetCustomAttributes<ValidationAttribute>();
object value = property.GetValue(obj);
foreach (var attribute in attributes)
{
if (!attribute.IsValid(value))
{
errors.Add($"{property.Name}: {attribute.ErrorMessage}");
}
}
}
return errors;
}
}
28. What are attributes and how to create custom attributes?
Attributes are declarative tags that provide metadata about program elements like classes, methods, properties, etc.
Built-in Attributes:
[Serializable]
public class Person
{
[Obsolete("Use FullName instead")]
public string Name { get; set; }
[Required]
public string FullName { get; set; }
[Range(0, 120)]
public int Age { get; set; }
[TestMethod]
public void TestMethod()
{
// Test implementation
}
}
Creating Custom Attributes:
// Basic custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; set; }
public string Version { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}
// Usage
[Author("John Doe", Version = "1.0")]
public class Calculator
{
[Author("Jane Smith")]
public int Add(int a, int b)
{
return a + b;
}
}
Advanced Custom Attribute with Validation:
[AttributeUsage(AttributeTargets.Property)]
public class StringLengthAttribute : Attribute
{
public int MinLength { get; set; }
public int MaxLength { get; set; }
public StringLengthAttribute(int maxLength)
{
MaxLength = maxLength;
}
public StringLengthAttribute(int minLength, int maxLength)
{
MinLength = minLength;
MaxLength = maxLength;
}
public bool IsValid(string value)
{
if (value == null) return MinLength == 0;
int length = value.Length;
return length >= MinLength && length <= MaxLength;
}
}
// Usage
public class User
{
[StringLength(3, 50)]
public string Username { get; set; }
[StringLength(8, 100)]
public string Password { get; set; }
}
Reading Attributes with Reflection:
public static class AttributeReader
{
public static void ReadAttributes<T>()
{
Type type = typeof(T);
// Read class-level attributes
var classAttributes = type.GetCustomAttributes<AuthorAttribute>();
foreach (var attr in classAttributes)
{
Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}");
}
// Read method-level attributes
foreach (var method in type.GetMethods())
{
var methodAttributes = method.GetCustomAttributes<AuthorAttribute>();
foreach (var attr in methodAttributes)
{
Console.WriteLine($"Method {method.Name} - Author: {attr.Name}");
}
}
}
}
29. Explain the using statement and IDisposable pattern
The using statement ensures proper disposal of resources that implement IDisposable. It automatically calls the Dispose() method when the object goes out of scope.
Basic Using Statement:
// Automatic disposal
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// Use the stream
byte[] buffer = new byte[1024];
stream.Read(buffer, 0, buffer.Length);
} // Dispose() is called automatically here
// C# 8.0+ using declaration
using var stream = new FileStream("file.txt", FileMode.Open);
// Use the stream
// Dispose() is called when the method ends
IDisposable Pattern Implementation:
public class DatabaseConnection : IDisposable
{
private SqlConnection _connection;
private bool _disposed = false;
public DatabaseConnection(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
}
public void ExecuteQuery(string query)
{
if (_disposed)
throw new ObjectDisposedException(nameof(DatabaseConnection));
using var command = new SqlCommand(query, _connection);
command.ExecuteNonQuery();
}
// IDisposable implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_connection?.Dispose();
}
// Dispose unmanaged resources (if any)
_disposed = true;
}
}
// Finalizer (destructor)
~DatabaseConnection()
{
Dispose(false);
}
}
// Usage
using (var db = new DatabaseConnection("connectionString"))
{
db.ExecuteQuery("SELECT * FROM Users");
} // Dispose() is called automatically
Custom Resource Manager:
public class ResourceManager : IDisposable
{
private List<IDisposable> _resources = new List<IDisposable>();
private bool _disposed = false;
public T AddResource<T>(T resource) where T : IDisposable
{
_resources.Add(resource);
return resource;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
foreach (var resource in _resources)
{
resource?.Dispose();
}
_resources.Clear();
_disposed = true;
}
}
}
// Usage
using var manager = new ResourceManager();
var fileStream = manager.AddResource(new FileStream("file.txt", FileMode.Open));
var memoryStream = manager.AddResource(new MemoryStream());
// All resources are disposed when manager goes out of scope
30. What are yield return and iterator methods?
Yield return enables lazy evaluation by creating iterator methods that return elements one at a time, improving memory efficiency.
Basic Iterator Method:
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}
// Usage
foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
Conditional Yield:
public static IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i;
}
}
}
// Usage
foreach (var even in GetEvenNumbers(10))
{
Console.WriteLine(even); // 0, 2, 4, 6, 8, 10
}
Custom Iterator with Complex Logic:
public class TreeNode
{
public int Value { get; set; }
public List<TreeNode> Children { get; set; } = new List<TreeNode>();
}
public static class TreeTraversal
{
public static IEnumerable<TreeNode> TraverseBreadthFirst(TreeNode root)
{
if (root == null) yield break;
var queue = new Queue<TreeNode>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
yield return current;
foreach (var child in current.Children)
{
queue.Enqueue(child);
}
}
}
public static IEnumerable<TreeNode> TraverseDepthFirst(TreeNode root)
{
if (root == null) yield break;
yield return root;
foreach (var child in root.Children)
{
foreach (var descendant in TraverseDepthFirst(child))
{
yield return descendant;
}
}
}
}
Infinite Sequence Generator:
public static class SequenceGenerator
{
public static IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a;
int temp = a;
a = b;
b = temp + b;
}
}
public static IEnumerable<int> PrimeNumbers()
{
yield return 2;
for (int i = 3; ; i += 2)
{
if (IsPrime(i))
{
yield return i;
}
}
}
private static bool IsPrime(int number)
{
if (number < 2) return false;
if (number == 2) return true;
if (number % 2 == 0) return false;
for (int i = 3; i * i <= number; i += 2)
{
if (number % i == 0) return false;
}
return true;
}
}
// Usage
var fibonacci = SequenceGenerator.Fibonacci().Take(10);
foreach (var num in fibonacci)
{
Console.WriteLine(num);
}
Custom Iterator with State:
public class PaginatedResult<T>
{
public IEnumerable<T> Items { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
}
public static class PaginationHelper
{
public static IEnumerable<PaginatedResult<T>> Paginate<T>(
IEnumerable<T> source, int pageSize)
{
var items = source.ToList();
int totalCount = items.Count;
int pageNumber = 1;
for (int i = 0; i < totalCount; i += pageSize)
{
var pageItems = items.Skip(i).Take(pageSize);
yield return new PaginatedResult<T>
{
Items = pageItems,
PageNumber = pageNumber,
PageSize = pageSize,
TotalCount = totalCount
};
pageNumber++;
}
}
}
// Usage
var numbers = Enumerable.Range(1, 25);
var pages = PaginationHelper.Paginate(numbers, 5);
foreach (var page in pages)
{
Console.WriteLine($"Page {page.PageNumber}: {string.Join(", ", page.Items)}");
}
31. Garbage Collection in .NET
Garbage Collection (GC) is an automatic memory management system in .NET that reclaims memory from objects that are no longer in use.
Key Concepts: - Generational GC: Objects are divided into generations (0, 1, 2) - Mark and Sweep: GC marks reachable objects, then sweeps unreachable ones - Compaction: Moves live objects to reduce fragmentation
// Example showing GC behavior
public class GarbageCollectionExample
{
public static void DemonstrateGC()
{
// Create objects that will be collected
for (int i = 0; i < 1000; i++)
{
var obj = new byte[1000]; // Large objects
}
// Force garbage collection (generally not recommended)
GC.Collect();
GC.WaitForPendingFinalizers();
// Get memory info
var totalMemory = GC.GetTotalMemory(false);
Console.WriteLine($"Total memory: {totalMemory} bytes");
// Check generation of an object
var obj = new object();
var generation = GC.GetGeneration(obj);
Console.WriteLine($"Object generation: {generation}");
}
}
32. Stack vs Heap Memory
The execution stack holds call frames and some temporary/local storage. Managed heap objects are tracked by the garbage collector. The JIT may put values in registers, eliminate allocations, or use different storage depending on escape/lifetime analysis. A local reference and its target object are distinct. Do not infer stack versus heap solely from whether a variable uses struct or class. Reference: Value types.
33. Weak References
Weak references allow objects to be garbage collected even when referenced, useful for caching scenarios.
public class WeakReferenceExample
{
public static void DemonstrateWeakReferences()
{
// Strong reference
var strongRef = new byte[1000000];
// Weak reference
var weakRef = new WeakReference(strongRef);
// Check if object is alive
Console.WriteLine($"Is alive: {weakRef.IsAlive}");
// Get target (may be null if collected)
if (weakRef.Target is byte[] data)
{
Console.WriteLine($"Data length: {data.Length}");
}
// Clear strong reference
strongRef = null;
// Force GC
GC.Collect();
// Check if weak reference is still alive
Console.WriteLine($"Is alive after GC: {weakRef.IsAlive}");
}
// WeakReference<T> for type safety
public static void DemonstrateGenericWeakReference()
{
var obj = new List<string>();
var weakRef = new WeakReference<List<string>>(obj);
obj = null;
GC.Collect();
if (weakRef.TryGetTarget(out var target))
{
Console.WriteLine("Object still alive");
}
else
{
Console.WriteLine("Object collected");
}
}
}
34. Object Pooling
Object pooling reuses objects to reduce allocation overhead and GC pressure.
public class ObjectPool<T> where T : class, new()
{
private readonly ConcurrentQueue<T> _pool = new();
private readonly int _maxSize;
public ObjectPool(int maxSize = 100)
{
_maxSize = maxSize;
}
public T Get()
{
if (_pool.TryDequeue(out var item))
{
return item;
}
return new T();
}
public void Return(T item)
{
if (_pool.Count < _maxSize)
{
_pool.Enqueue(item);
}
}
}
// Usage example
public class ObjectPoolingExample
{
private static readonly ObjectPool<StringBuilder> StringBuilderPool = new();
public static void DemonstrateObjectPooling()
{
// Get from pool
var sb = StringBuilderPool.Get();
sb.Append("Hello World");
string result = sb.ToString();
// Clear and return to pool
sb.Clear();
StringBuilderPool.Return(sb);
// Microsoft's built-in ObjectPool
var pool = ObjectPool.Create<StringBuilder>();
var pooledSb = pool.Get();
// Use pooledSb...
pool.Return(pooledSb);
}
}
35. Memory Leaks in C
Memory leaks occur when objects remain referenced but are no longer needed.
Common Causes: - Event handlers not unsubscribed - Static collections holding references - Circular references - Unmanaged resources not disposed
public class MemoryLeakExample
{
// BAD: Static collection holding references
private static readonly List<object> _staticList = new();
// GOOD: Use WeakReference for caching
private static readonly Dictionary<string, WeakReference<object>> _cache = new();
public static void DemonstrateMemoryLeak()
{
var obj = new LargeObject();
// Memory leak - object will never be collected
_staticList.Add(obj);
// Proper caching with weak references
_cache["key"] = new WeakReference<object>(obj);
}
}
// Event handler memory leak
public class EventHandlerLeakExample
{
public event EventHandler MyEvent;
public void Subscribe(EventHandler handler)
{
MyEvent += handler; // Subscribe
}
public void Unsubscribe(EventHandler handler)
{
MyEvent -= handler; // Unsubscribe to prevent leaks
}
}
// Circular reference example
public class CircularReferenceExample
{
public class Parent
{
public Child Child { get; set; }
}
public class Child
{
public Parent Parent { get; set; } // Circular reference
}
// Solution: Use weak references
public class ChildFixed
{
private WeakReference<Parent> _parent;
public Parent Parent
{
get => _parent.TryGetTarget(out var parent) ? parent : null;
set => _parent = new WeakReference<Parent>(value);
}
}
}
36. Dispose() vs Finalize()
Dispose(): - Explicit cleanup method - Called by developer - Immediate resource release - Part of IDisposable pattern
Finalize(): - Automatic cleanup method - Called by GC - Non-deterministic timing - Last resort cleanup
public class DisposeFinalizeExample : IDisposable
{
private bool _disposed = false;
private IntPtr _unmanagedResource;
// Finalizer (destructor)
~DisposeFinalizeExample()
{
Dispose(false);
}
// Dispose method
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
// e.g., _managedResource?.Dispose();
}
// Dispose unmanaged resources
if (_unmanagedResource != IntPtr.Zero)
{
// Release unmanaged resource
_unmanagedResource = IntPtr.Zero;
}
_disposed = true;
}
}
public void DoWork()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DisposeFinalizeExample));
// Work with resources
}
}
37. Using Pattern and Resource Management
The using statement ensures proper disposal of resources.
public class UsingPatternExample
{
public static void DemonstrateUsingPattern()
{
// Automatic disposal
using (var stream = new FileStream("test.txt", FileMode.Create))
{
var bytes = Encoding.UTF8.GetBytes("Hello World");
stream.Write(bytes, 0, bytes.Length);
} // stream.Dispose() called automatically
// Using declaration (C# 8.0+)
using var reader = new StreamReader("test.txt");
string content = reader.ReadToEnd();
// reader.Dispose() called at end of scope
// Multiple resources
using (var stream1 = new FileStream("file1.txt", FileMode.Open))
using (var stream2 = new FileStream("file2.txt", FileMode.Open))
{
// Work with both streams
}
// Custom using pattern
using var customResource = new CustomResource();
customResource.DoWork();
}
}
public class CustomResource : IDisposable
{
public void DoWork()
{
Console.WriteLine("Working with resource");
}
public void Dispose()
{
Console.WriteLine("Resource disposed");
}
}
38. Value Types vs Reference Types Performance
Performance depends on size, copying, boxing, cache locality, allocation rate, and access patterns. Structs can avoid separate object allocation when embedded in another object or array, but passing large structs by value is costly. Classes share identity and avoid copying object contents at each assignment. Benchmark representative workloads; “value types are always faster” is false.
39. String Interning and String Pool
String interning stores unique string literals in a global pool to save memory.
public class StringInterningExample
{
public static void DemonstrateStringInterning()
{
// String literals are automatically interned
string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(ReferenceEquals(s1, s2)); // True
// Runtime strings are not automatically interned
string s3 = new string("Hello".ToCharArray());
Console.WriteLine(ReferenceEquals(s1, s3)); // False
// Manual interning
string s4 = string.Intern(s3);
Console.WriteLine(ReferenceEquals(s1, s4)); // True
// Check if string is interned
Console.WriteLine(string.IsInterned(s3) != null); // True after interning
// String pool in action
var strings = new List<string>();
for (int i = 0; i < 1000; i++)
{
strings.Add("SameString"); // All reference the same interned string
}
// Memory efficient
var memoryBefore = GC.GetTotalMemory(false);
var internedStrings = new List<string>();
for (int i = 0; i < 1000; i++)
{
internedStrings.Add(string.Intern($"String{i}"));
}
var memoryAfter = GC.GetTotalMemory(false);
Console.WriteLine($"Memory used: {memoryAfter - memoryBefore} bytes");
}
}
40. StringComparison Options
StringComparison provides different comparison behaviors for strings.
public class StringComparisonExample
{
public static void DemonstrateStringComparisons()
{
string s1 = "Hello";
string s2 = "hello";
string s3 = "HELLO";
// Ordinal comparison (fastest, case-sensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.Ordinal)); // False
Console.WriteLine(s1.CompareTo(s2, StringComparison.Ordinal)); // Negative
// OrdinalIgnoreCase (fast, case-insensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.OrdinalIgnoreCase)); // True
Console.WriteLine(s1.Equals(s3, StringComparison.OrdinalIgnoreCase)); // True
// CurrentCulture (culture-aware, case-sensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.CurrentCulture)); // False
// CurrentCultureIgnoreCase (culture-aware, case-insensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.CurrentCultureIgnoreCase)); // True
// InvariantCulture (culture-invariant, case-sensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.InvariantCulture)); // False
// InvariantCultureIgnoreCase (culture-invariant, case-insensitive)
Console.WriteLine(s1.Equals(s2, StringComparison.InvariantCultureIgnoreCase)); // True
// Performance comparison
var stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
s1.Equals(s2, StringComparison.Ordinal);
}
stopwatch.Stop();
Console.WriteLine($"Ordinal: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
for (int i = 0; i < 1000000; i++)
{
s1.Equals(s2, StringComparison.CurrentCulture);
}
stopwatch.Stop();
Console.WriteLine($"CurrentCulture: {stopwatch.ElapsedMilliseconds}ms");
// Best practices
var list = new List<string> { "Apple", "banana", "Cherry" };
// For sorting, use OrdinalIgnoreCase for consistency
list.Sort(StringComparer.OrdinalIgnoreCase);
// For user input comparison, use CurrentCultureIgnoreCase
var userInput = "apple";
var found = list.Contains(userInput, StringComparer.CurrentCultureIgnoreCase);
// For file paths, use Ordinal
var file1 = "file.txt";
var file2 = "FILE.TXT";
var isSameFile = file1.Equals(file2, StringComparison.Ordinal); // False on case-sensitive systems
}
}
41. Differences between List, Array, and ArrayList
Array: - Fixed-size, strongly-typed collection - Zero-based indexing - Best performance for read operations - Cannot grow or shrink after creation
List<T>: - Dynamic-size, strongly-typed generic collection - Built on top of arrays internally - Automatic resizing when capacity is exceeded - Better performance than ArrayList due to type safety
ArrayList: - Legacy collection from .NET Framework 1.0 - Stores objects (boxing/unboxing overhead) - Not type-safe - Avoid in new code - use List<T> instead
// Array - fixed size, strongly typed
int[] numbers = new int[5] { 1, 2, 3, 4, 5 };
// numbers[5] = 6; // Runtime error - index out of bounds
// List<T> - dynamic size, strongly typed
List<int> numberList = new List<int> { 1, 2, 3, 4, 5 };
numberList.Add(6); // Works fine
numberList.Remove(3); // Can remove elements
// ArrayList - legacy, not type-safe
ArrayList arrayList = new ArrayList();
arrayList.Add(1); // Boxing occurs
arrayList.Add("string"); // No type safety
int value = (int)arrayList[0]; // Unboxing required
42. Differences between Dictionary, Hashtable, and ConcurrentDictionary
Dictionary<TKey, TValue>: - Generic, strongly-typed key-value collection - Not thread-safe - Better performance than Hashtable - Uses hash table internally
Hashtable: - Legacy collection storing object keys and values - Not thread-safe - Boxing/unboxing overhead - Avoid in new code
ConcurrentDictionary<TKey, TValue>: - Thread-safe version of Dictionary - Designed for concurrent access - Slightly slower than Dictionary due to synchronization
// Dictionary<TKey, TValue> - modern, type-safe
Dictionary<string, int> scores = new Dictionary<string, int>();
scores.Add("Alice", 95);
scores["Bob"] = 87;
// scores["Charlie"]; // KeyNotFoundException if key doesn't exist
// Hashtable - legacy, not type-safe
Hashtable legacyScores = new Hashtable();
legacyScores.Add("Alice", 95);
legacyScores["Bob"] = 87;
int score = (int)legacyScores["Alice"]; // Unboxing required
// ConcurrentDictionary - thread-safe
ConcurrentDictionary<string, int> concurrentScores = new ConcurrentDictionary<string, int>();
concurrentScores.TryAdd("Alice", 95);
concurrentScores.AddOrUpdate("Bob", 87, (key, oldValue) => oldValue + 1);
43. IEnumerable, ICollection, and IList Interfaces
IEnumerable<T>: - Most basic interface for collections - Supports only forward iteration - Read-only access
ICollection<T>: - Extends IEnumerable<T> - Adds Count property and modification methods - Basic collection operations
IList<T>: - Extends ICollection<T> - Adds index-based access - Most complete collection interface
// IEnumerable - basic iteration
IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int num in numbers)
{
Console.WriteLine(num);
}
// ICollection - basic collection operations
ICollection<int> numberCollection = new List<int> { 1, 2, 3 };
Console.WriteLine($"Count: {numberCollection.Count}");
numberCollection.Add(4);
numberCollection.Remove(2);
// IList - index-based access
IList<int> numberList = new List<int> { 1, 2, 3, 4, 5 };
int first = numberList[0];
numberList[1] = 10;
numberList.Insert(2, 15);
numberList.RemoveAt(3);
44. Differences between Queue, Stack, and LinkedList
Queue<T>: - First-In-First-Out (FIFO) collection - Enqueue/Dequeue operations - Used for breadth-first processing
Stack<T>: - Last-In-First-Out (LIFO) collection - Push/Pop operations - Used for depth-first processing, undo operations
LinkedList<T>: - Doubly-linked list - Efficient insertions/deletions at any position - O(1) operations at ends, O(n) in middle
// Queue - FIFO
Queue<string> printQueue = new Queue<string>();
printQueue.Enqueue("Document1");
printQueue.Enqueue("Document2");
printQueue.Enqueue("Document3");
string nextDoc = printQueue.Dequeue(); // "Document1"
string peekDoc = printQueue.Peek(); // "Document2" (without removing)
// Stack - LIFO
Stack<string> undoStack = new Stack<string>();
undoStack.Push("Action1");
undoStack.Push("Action2");
undoStack.Push("Action3");
string lastAction = undoStack.Pop(); // "Action3"
string peekAction = undoStack.Peek(); // "Action2"
// LinkedList - flexible insertion/deletion
LinkedList<int> linkedList = new LinkedList<int>();
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
LinkedListNode<int> node = linkedList.Find(2);
linkedList.AddAfter(node, 2.5); // Insert after node with value 2
linkedList.Remove(node); // Remove node with value 2
45. HashSet and SortedSet Collections
HashSet<T>: - Unordered collection of unique elements - O(1) average case for add/remove/contains - Uses hash table internally - No duplicate elements allowed
SortedSet<T>: - Ordered collection of unique elements - O(log n) for add/remove/contains - Uses red-black tree internally - Elements are always sorted
// HashSet - unordered, unique elements
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(3);
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(3); // Ignored - already exists
Console.WriteLine(string.Join(", ", uniqueNumbers)); // Order not guaranteed
// Set operations
HashSet<int> set1 = new HashSet<int> { 1, 2, 3, 4 };
HashSet<int> set2 = new HashSet<int> { 3, 4, 5, 6 };
set1.UnionWith(set2); // { 1, 2, 3, 4, 5, 6 }
set1.IntersectWith(set2); // { 3, 4 }
set1.ExceptWith(set2); // { 1, 2 }
// SortedSet - ordered, unique elements
SortedSet<int> sortedNumbers = new SortedSet<int>();
sortedNumbers.Add(3);
sortedNumbers.Add(1);
sortedNumbers.Add(2);
Console.WriteLine(string.Join(", ", sortedNumbers)); // Always: 1, 2, 3
// Range operations
var range = sortedNumbers.GetViewBetween(1, 3); // Get subset
int min = sortedNumbers.Min; // 1
int max = sortedNumbers.Max; // 3
46. Differences between List and LinkedList
List<T>: - Array-based implementation - Fast random access (O(1)) - Slow insertions/deletions in middle (O(n)) - Better memory locality - Good for frequent random access
LinkedList<T>: - Node-based implementation - Slow random access (O(n)) - Fast insertions/deletions at any position (O(1) at ends) - Poor memory locality - Good for frequent insertions/deletions
// List - array-based, good for random access
List<int> numberList = new List<int> { 1, 2, 3, 4, 5 };
int third = numberList[2]; // O(1) - fast random access
numberList.Insert(2, 2.5); // O(n) - slow insertion in middle
numberList.RemoveAt(2); // O(n) - slow removal from middle
// LinkedList - node-based, good for insertions/deletions
LinkedList<int> linkedList = new LinkedList<int>();
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);
LinkedListNode<int> node = linkedList.Find(2); // O(n) - slow search
linkedList.AddAfter(node, 2.5); // O(1) - fast insertion
linkedList.Remove(node); // O(1) - fast removal
// Performance comparison
var list = new List<int>();
var linkedList = new LinkedList<int>();
// Adding elements
for (int i = 0; i < 100000; i++)
{
list.Add(i); // O(1) amortized
linkedList.AddLast(i); // O(1)
}
// Random access
int listValue = list[50000]; // O(1) - very fast
var node = linkedList.First;
for (int i = 0; i < 50000; i++) // O(n) - slow
node = node.Next;
47. Differences between Dictionary and SortedDictionary
Dictionary<TKey, TValue>: - Unordered key-value collection - O(1) average case for add/remove/contains - Uses hash table internally - Keys are not sorted
SortedDictionary<TKey, TValue>: - Ordered key-value collection - O(log n) for add/remove/contains - Uses red-black tree internally - Keys are always sorted
// Dictionary - unordered, fast access
Dictionary<string, int> scores = new Dictionary<string, int>();
scores.Add("Charlie", 85);
scores.Add("Alice", 95);
scores.Add("Bob", 87);
foreach (var kvp in scores)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Order not guaranteed
// SortedDictionary - ordered by keys
SortedDictionary<string, int> sortedScores = new SortedDictionary<string, int>();
sortedScores.Add("Charlie", 85);
sortedScores.Add("Alice", 95);
sortedScores.Add("Bob", 87);
foreach (var kvp in sortedScores)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Always: Alice: 95, Bob: 87, Charlie: 85
// Range operations with SortedDictionary
var range = sortedScores.Keys
.Where(k => k.CompareTo("B") >= 0 && k.CompareTo("C") < 0)
.ToDictionary(k => k, k => sortedScores[k]);
48. Concurrent Collections and When to Use Them
Concurrent Collections: - Thread-safe collections for multi-threaded scenarios - Designed for high-performance concurrent access - Avoid explicit locking
Types:
- ConcurrentDictionary<TKey, TValue>
- ConcurrentQueue<T>
- ConcurrentStack<T>
- ConcurrentBag<T>
- BlockingCollection<T>
// ConcurrentDictionary - thread-safe dictionary
ConcurrentDictionary<string, int> sharedScores = new ConcurrentDictionary<string, int>();
// Multiple threads can safely add/update
Parallel.For(0, 1000, i =>
{
string key = $"User{i}";
sharedScores.TryAdd(key, i);
sharedScores.AddOrUpdate(key, i, (k, oldValue) => oldValue + 1);
});
// ConcurrentQueue - thread-safe FIFO
ConcurrentQueue<string> workQueue = new ConcurrentQueue<string>();
workQueue.Enqueue("Task1");
workQueue.Enqueue("Task2");
string task;
if (workQueue.TryDequeue(out task))
{
Console.WriteLine($"Processing: {task}");
}
// BlockingCollection - producer-consumer pattern
BlockingCollection<int> dataQueue = new BlockingCollection<int>(boundedCapacity: 10);
// Producer
Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
dataQueue.Add(i); // Blocks if queue is full
Thread.Sleep(100);
}
dataQueue.CompleteAdding();
});
// Consumer
Task.Run(() =>
{
foreach (int item in dataQueue.GetConsumingEnumerable())
{
Console.WriteLine($"Processing: {item}");
Thread.Sleep(200);
}
});
49. Custom Collections and Implementing IEnumerable
Custom Collections: - Create specialized collections for specific needs - Implement collection interfaces for consistency - Provide custom behavior and constraints
// Custom collection implementing IEnumerable
public class CircularBuffer<T> : IEnumerable<T>
{
private readonly T[] _buffer;
private int _head;
private int _tail;
private int _count;
public CircularBuffer(int capacity)
{
_buffer = new T[capacity];
_head = 0;
_tail = 0;
_count = 0;
}
public void Add(T item)
{
_buffer[_tail] = item;
_tail = (_tail + 1) % _buffer.Length;
if (_count < _buffer.Length)
_count++;
else
_head = (_head + 1) % _buffer.Length;
}
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < _count; i++)
{
int index = (_head + i) % _buffer.Length;
yield return _buffer[index];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// Usage
var buffer = new CircularBuffer<int>(3);
buffer.Add(1);
buffer.Add(2);
buffer.Add(3);
buffer.Add(4); // Overwrites 1
foreach (int item in buffer)
{
Console.WriteLine(item); // 2, 3, 4
}
// Custom collection with constraints
public class UniqueList<T> : ICollection<T>
{
private readonly List<T> _items = new List<T>();
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Add(T item)
{
if (!_items.Contains(item))
_items.Add(item);
}
public void Clear() => _items.Clear();
public bool Contains(T item) => _items.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public bool Remove(T item) => _items.Remove(item);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
50. Differences between Array.Copy and Array.Clone
Array.Copy: - Static method - Copies elements from source to destination - Can copy between different arrays - More flexible and efficient - Shallow copy
Array.Clone: - Instance method - Creates a new array with same elements - Returns object (requires casting) - Less efficient - Shallow copy
// Array.Copy - static method, more flexible
int[] source = { 1, 2, 3, 4, 5 };
int[] destination = new int[3];
Array.Copy(source, 1, destination, 0, 3);
// destination = { 2, 3, 4 }
// Copy between different array types
object[] objectArray = { "Hello", 42, DateTime.Now };
string[] stringArray = new string[2];
Array.Copy(objectArray, 0, stringArray, 0, 2);
// stringArray = { "Hello", null } (42 cannot be copied to string)
// Array.Clone - instance method, creates new array
int[] original = { 1, 2, 3, 4, 5 };
int[] cloned = (int[])original.Clone();
// cloned is a new array with same elements
// Performance comparison
int[] largeArray = new int[1000000];
for (int i = 0; i < largeArray.Length; i++)
largeArray[i] = i;
// Array.Copy - more efficient
int[] copy1 = new int[largeArray.Length];
Array.Copy(largeArray, copy1, largeArray.Length);
// Array.Clone - less efficient, requires casting
int[] copy2 = (int[])largeArray.Clone();
// Both perform shallow copy
Person[] people = { new Person("Alice"), new Person("Bob") };
Person[] copiedPeople = (Person[])people.Clone();
copiedPeople[0].Name = "Charlie";
Console.WriteLine(people[0].Name); // "Charlie" - same object reference
51. Explain try-catch-finally blocks and their usage
Try-catch-finally blocks are the fundamental mechanism for exception handling in .NET. They allow you to handle exceptions gracefully and ensure cleanup code executes.
public class FileProcessor
{
public string ReadFileContent(string filePath)
{
FileStream fileStream = null;
try
{
fileStream = File.OpenRead(filePath);
using (var reader = new StreamReader(fileStream))
{
return reader.ReadToEnd();
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
return null;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
return null;
}
finally
{
// This always executes, regardless of exception
if (fileStream != null)
{
fileStream.Dispose();
}
}
}
}
Key points:
- try: Contains code that might throw exceptions
- catch: Handles specific exceptions (order matters - most specific first)
- finally: Always executes, used for cleanup (disposing resources, closing connections)
52. What is the difference between throw and throw ex?
throw preserves the original stack trace, while throw ex resets the stack trace to the current location.
public class ExceptionExample
{
public void MethodWithThrow()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Preserves original stack trace
throw;
}
}
public void MethodWithThrowEx()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Resets stack trace to this location
throw ex;
}
}
private void SomeMethod()
{
throw new InvalidOperationException("Something went wrong");
}
}
Best practice: Use throw; to re-throw exceptions while preserving the original stack trace.
53. Explain exception filters in C# 6.0+
Exception filters allow you to catch exceptions based on conditions, not just exception type.
public class ExceptionFilterExample
{
public void ProcessData(int data)
{
try
{
if (data < 0)
throw new ArgumentException("Data cannot be negative");
if (data > 1000)
throw new ArgumentException("Data too large");
}
catch (ArgumentException ex) when (ex.Message.Contains("negative"))
{
Console.WriteLine("Handling negative data error");
}
catch (ArgumentException ex) when (ex.Message.Contains("large"))
{
Console.WriteLine("Handling large data error");
}
catch (ArgumentException ex) when (data == 0)
{
Console.WriteLine("Handling zero data error");
}
}
// Exception filters with logging
public void ProcessWithLogging(int data)
{
try
{
ProcessData(data);
}
catch (Exception ex) when (LogException(ex))
{
// This catch block will only execute if LogException returns true
}
}
private bool LogException(Exception ex)
{
Console.WriteLine($"Logging exception: {ex.Message}");
return ex is ArgumentException; // Only catch ArgumentException
}
}
54. What are custom exceptions and when to create them?
Custom exceptions are user-defined exception classes that provide specific error information for your application domain.
// Custom exception hierarchy
public abstract class BusinessException : Exception
{
public string ErrorCode { get; }
public DateTime Timestamp { get; }
protected BusinessException(string message, string errorCode)
: base(message)
{
ErrorCode = errorCode;
Timestamp = DateTime.UtcNow;
}
protected BusinessException(string message, string errorCode, Exception innerException)
: base(message, innerException)
{
ErrorCode = errorCode;
Timestamp = DateTime.UtcNow;
}
}
public class InsufficientFundsException : BusinessException
{
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
public InsufficientFundsException(decimal currentBalance, decimal requiredAmount)
: base($"Insufficient funds. Current: {currentBalance}, Required: {requiredAmount}", "INSUFFICIENT_FUNDS")
{
CurrentBalance = currentBalance;
RequiredAmount = requiredAmount;
}
}
public class AccountNotFoundException : BusinessException
{
public string AccountNumber { get; }
public AccountNotFoundException(string accountNumber)
: base($"Account {accountNumber} not found", "ACCOUNT_NOT_FOUND")
{
AccountNumber = accountNumber;
}
}
// Usage
public class BankService
{
public void TransferMoney(string fromAccount, string toAccount, decimal amount)
{
var account = GetAccount(fromAccount);
if (account == null)
throw new AccountNotFoundException(fromAccount);
if (account.Balance < amount)
throw new InsufficientFundsException(account.Balance, amount);
// Process transfer
}
}
When to create custom exceptions: - Domain-specific errors - Need for additional error context - Consistent error handling across application - API design requirements
55. Explain the difference between checked and unchecked contexts
In C#, integer arithmetic overflow is unchecked by default, but you can control this behavior.
public class CheckedUncheckedExample
{
public void DemonstrateOverflow()
{
int maxInt = int.MaxValue;
// Unchecked context (default) - overflow wraps around
unchecked
{
int result = maxInt + 1; // Results in int.MinValue
Console.WriteLine($"Unchecked overflow: {result}");
}
// Checked context - throws OverflowException
checked
{
try
{
int result = maxInt + 1; // Throws OverflowException
}
catch (OverflowException ex)
{
Console.WriteLine($"Checked overflow caught: {ex.Message}");
}
}
// Global checked context
// #pragma warning disable 675
// #pragma warning disable 675
}
// Compile-time checked context
public void CompileTimeChecked()
{
const int a = int.MaxValue;
const int b = 1;
// This will cause compile-time error if checked context is enabled
// const int result = a + b; // Compile error in checked context
}
}
56. What is the difference between Exception and ApplicationException?
Exception is the base class for all exceptions, while ApplicationException was intended for application-specific exceptions but is now deprecated.
// DON'T inherit from ApplicationException (deprecated)
public class BadCustomException : ApplicationException // ❌ Avoid this
{
public BadCustomException(string message) : base(message) { }
}
// DO inherit from Exception
public class GoodCustomException : Exception // ✅ Recommended
{
public GoodCustomException(string message) : base(message) { }
}
// For business logic exceptions, inherit from Exception
public class BusinessRuleException : Exception
{
public string RuleCode { get; }
public BusinessRuleException(string message, string ruleCode)
: base(message)
{
RuleCode = ruleCode;
}
}
Microsoft's guidance: Don't inherit from ApplicationException. Use Exception as the base class for custom exceptions.
57. Explain exception handling best practices
public class ExceptionHandlingBestPractices
{
// ✅ DO: Catch specific exceptions
public void GoodExceptionHandling()
{
try
{
ProcessFile("data.txt");
}
catch (FileNotFoundException ex)
{
Logger.LogError($"File not found: {ex.FileName}");
// Handle specific case
}
catch (UnauthorizedAccessException ex)
{
Logger.LogError($"Access denied: {ex.Message}");
// Handle specific case
}
}
// ❌ DON'T: Catch all exceptions without handling
public void BadExceptionHandling()
{
try
{
ProcessFile("data.txt");
}
catch (Exception ex) // Too broad
{
// Swallowing exception - bad practice
}
}
// ✅ DO: Use using statements for resource management
public string ReadFileContent(string path)
{
using (var reader = new StreamReader(path))
{
return reader.ReadToEnd();
} // Automatically disposes reader
}
// ✅ DO: Provide meaningful error messages
public void ValidateUserInput(string input)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentException("User input cannot be null or empty", nameof(input));
}
}
// ✅ DO: Log exceptions appropriately
public void ProcessWithLogging()
{
try
{
RiskyOperation();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error occurred during risky operation");
throw; // Re-throw to preserve stack trace
}
}
// ✅ DO: Use exception filters for conditional handling
public void ConditionalExceptionHandling(int retryCount)
{
try
{
NetworkOperation();
}
catch (TimeoutException ex) when (retryCount < 3)
{
// Retry logic
retryCount++;
// Retry the operation
}
catch (TimeoutException ex) when (retryCount >= 3)
{
// Give up after 3 retries
throw new OperationCanceledException("Operation timed out after 3 retries", ex);
}
}
}
58. What are exception filters and when to use them?
Exception filters (C# 6.0+) allow conditional exception handling based on expressions.
public class ExceptionFilterExamples
{
// ✅ Use for retry logic
public async Task<string> GetDataWithRetryAsync(int retryCount = 0)
{
try
{
return await FetchDataFromApiAsync();
}
catch (HttpRequestException ex) when (retryCount < 3)
{
await Task.Delay(1000 * (retryCount + 1)); // Exponential backoff
return await GetDataWithRetryAsync(retryCount + 1);
}
}
// ✅ Use for logging without catching
public void ProcessWithLogging()
{
try
{
RiskyOperation();
}
catch (Exception ex) when (LogAndReturnFalse(ex))
{
// This block will never execute because LogAndReturnFalse returns false
}
}
private bool LogAndReturnFalse(Exception ex)
{
Logger.LogError(ex, "Exception occurred but not handled");
return false; // Don't catch the exception
}
// ✅ Use for performance monitoring
public void ProcessWithPerformanceMonitoring()
{
var stopwatch = Stopwatch.StartNew();
try
{
ExpensiveOperation();
}
catch (Exception ex) when (IsSlowOperation(stopwatch.ElapsedMilliseconds))
{
Logger.LogWarning($"Slow operation failed after {stopwatch.ElapsedMilliseconds}ms");
throw;
}
}
private bool IsSlowOperation(long elapsedMs) => elapsedMs > 5000;
// ✅ Use for debugging
public void DebugModeExceptionHandling()
{
try
{
ProductionCode();
}
catch (Exception ex) when (Debugger.IsAttached)
{
// Only break in debug mode
Debugger.Break();
throw;
}
}
}
59. Explain the difference between throw and throw new
throw re-throws the current exception, while throw new creates a new exception instance.
public class ThrowExamples
{
public void MethodWithThrow()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Re-throws the original exception with preserved stack trace
throw;
}
}
public void MethodWithThrowNew()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Creates new exception, losing original stack trace
throw new InvalidOperationException("Custom message", ex);
}
}
public void MethodWithThrowNewWithoutInner()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Creates new exception, completely losing original context
throw new InvalidOperationException("Custom message");
}
}
// ✅ Best practice: Use throw new with inner exception
public void BestPracticeExample()
{
try
{
SomeMethod();
}
catch (Exception ex)
{
// Preserves original exception as inner exception
throw new CustomBusinessException("Business logic failed", ex);
}
}
}
60. What is the difference between finally and using?
finally is a block that always executes, while using is syntactic sugar for try-finally with automatic disposal.
public class FinallyVsUsingExample
{
// Using finally block
public string ReadFileWithFinally(string path)
{
FileStream fileStream = null;
try
{
fileStream = File.OpenRead(path);
using (var reader = new StreamReader(fileStream))
{
return reader.ReadToEnd();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error reading file: {ex.Message}");
return null;
}
finally
{
// Always executes, even if exception occurs
if (fileStream != null)
{
fileStream.Dispose();
}
}
}
// Using statement (equivalent to try-finally)
public string ReadFileWithUsing(string path)
{
using (var fileStream = File.OpenRead(path))
using (var reader = new StreamReader(fileStream))
{
return reader.ReadToEnd();
} // Automatically calls Dispose() on both objects
}
// Using declaration (C# 8.0+)
public string ReadFileWithUsingDeclaration(string path)
{
using var fileStream = File.OpenRead(path);
using var reader = new StreamReader(fileStream);
return reader.ReadToEnd();
} // Dispose called at end of method scope
// Custom disposable class
public class CustomResource : IDisposable
{
public void DoWork()
{
Console.WriteLine("Working with resource...");
}
public void Dispose()
{
Console.WriteLine("Disposing custom resource...");
}
}
// Using with custom resource
public void UseCustomResource()
{
using (var resource = new CustomResource())
{
resource.DoWork();
} // Dispose called automatically
}
// Finally for non-disposable cleanup
public void CleanupExample()
{
var tempFile = Path.GetTempFileName();
try
{
// Work with temp file
File.WriteAllText(tempFile, "data");
}
finally
{
// Clean up temp file (not disposal, just cleanup)
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
}
Key differences:
- finally: Always executes, used for any cleanup
- using: Automatically calls Dispose() on IDisposable objects
- using is more concise and less error-prone for resource management
- Use finally for non-disposal cleanup (file deletion, logging, etc.)
61. Differences between File, FileInfo, Directory, and DirectoryInfo
File and Directory (Static Classes): - Provide static methods for file/directory operations - Don't maintain state - Good for one-time operations
FileInfo and DirectoryInfo (Instance Classes): - Represent specific file/directory instances - Maintain state and properties - Better for repeated operations on the same file/directory
// Static classes - one-time operations
string content = File.ReadAllText("data.txt");
File.WriteAllText("output.txt", "Hello World");
Directory.CreateDirectory("newFolder");
// Instance classes - repeated operations
FileInfo fileInfo = new FileInfo("data.txt");
Console.WriteLine($"Size: {fileInfo.Length}");
Console.WriteLine($"Created: {fileInfo.CreationTime}");
Console.WriteLine($"Last Modified: {fileInfo.LastWriteTime}");
DirectoryInfo dirInfo = new DirectoryInfo("C:\\Projects");
FileInfo[] files = dirInfo.GetFiles("*.txt");
foreach (FileInfo file in files)
{
Console.WriteLine($"File: {file.Name}, Size: {file.Length}");
}
62. Differences between Stream, FileStream, and MemoryStream
Stream (Abstract Base Class): - Base class for all stream operations - Defines common interface (Read, Write, Seek, etc.)
FileStream: - Reads/writes files on disk - Supports random access - Can be buffered or unbuffered
MemoryStream: - Works with data in memory - Faster than file operations - Good for temporary data processing
// FileStream - working with files
using (FileStream fileStream = new FileStream("data.bin", FileMode.Create))
{
byte[] data = Encoding.UTF8.GetBytes("Hello World");
fileStream.Write(data, 0, data.Length);
}
// MemoryStream - working with memory
using (MemoryStream memoryStream = new MemoryStream())
{
byte[] data = Encoding.UTF8.GetBytes("Hello World");
memoryStream.Write(data, 0, data.Length);
memoryStream.Position = 0;
byte[] readData = new byte[memoryStream.Length];
memoryStream.Read(readData, 0, (int)memoryStream.Length);
string result = Encoding.UTF8.GetString(readData);
Console.WriteLine(result); // "Hello World"
}
63. Serialization and Deserialization in C
Serialization converts data to a representation such as JSON; deserialization reconstructs a declared data model. Use explicit contracts and validate untrusted data. System.Text.Json is a modern JSON option; XML and binary formats have separate serializers. Avoid BinaryFormatter: its in-box implementation was removed in .NET 9 and now throws. Reference: Migration guidance.
public sealed record Person(string Name, int Age);
// Inside a method:
string json = System.Text.Json.JsonSerializer.Serialize(new Person("Ada", 36));
Person? person = System.Text.Json.JsonSerializer.Deserialize<Person>(json);
64. Differences between BinaryFormatter, XmlSerializer, and JsonSerializer
BinaryFormatter is obsolete and unsafe for untrusted data; the .NET 9+ in-box implementation always throws. XmlSerializer maps supported public data members to XML. System.Text.Json serializes supported .NET contracts to JSON. Select a format for interoperability, schema, performance, and required features; no format is inherently safe without bounds and validation. Reference: BinaryFormatter migration.
public sealed record Person(string Name, int Age);
// Inside a method:
string json = System.Text.Json.JsonSerializer.Serialize(new Person("Ada", 36));
Person? person = System.Text.Json.JsonSerializer.Deserialize<Person>(json);
65. Async File Operations and Their Benefits
Benefits: - Non-blocking I/O operations - Better application responsiveness - Improved scalability - Better resource utilization
public class AsyncFileOperations
{
public static async Task<string> ReadFileAsync(string path)
{
using (StreamReader reader = new StreamReader(path))
{
return await reader.ReadToEndAsync();
}
}
public static async Task WriteFileAsync(string path, string content)
{
using (StreamWriter writer = new StreamWriter(path))
{
await writer.WriteAsync(content);
}
}
public static async Task CopyFileAsync(string source, string destination)
{
using (FileStream sourceStream = File.OpenRead(source))
using (FileStream destStream = File.Create(destination))
{
await sourceStream.CopyToAsync(destStream);
}
}
public static async Task ProcessMultipleFilesAsync(string[] filePaths)
{
var tasks = filePaths.Select(async path =>
{
string content = await ReadFileAsync(path);
return new { Path = path, Length = content.Length };
});
var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
Console.WriteLine($"File: {result.Path}, Size: {result.Length}");
}
}
}
// Usage
await AsyncFileOperations.WriteFileAsync("test.txt", "Hello Async World");
string content = await AsyncFileOperations.ReadFileAsync("test.txt");
66. Difference between Synchronous and Asynchronous I/O
Synchronous I/O: - Blocks the calling thread until operation completes - Simple to understand and debug - Can cause UI freezing
Asynchronous I/O: - Non-blocking, returns immediately - Uses callbacks or async/await - Better for UI applications and scalability
public class IoComparison
{
// Synchronous - blocks thread
public static string ReadFileSync(string path)
{
// This blocks the current thread
return File.ReadAllText(path);
}
// Asynchronous - non-blocking
public static async Task<string> ReadFileAsync(string path)
{
// This doesn't block the thread
return await File.ReadAllTextAsync(path);
}
// Synchronous with progress reporting (not possible)
public static void ProcessFileSync(string path)
{
string content = File.ReadAllText(path);
// Can't report progress during read
ProcessContent(content);
}
// Asynchronous with progress reporting
public static async Task ProcessFileAsync(string path, IProgress<int> progress)
{
using (StreamReader reader = new StreamReader(path))
{
var buffer = new char[1024];
var content = new StringBuilder();
int totalRead = 0;
while (!reader.EndOfStream)
{
int read = await reader.ReadAsync(buffer, 0, buffer.Length);
content.Append(buffer, 0, read);
totalRead += read;
// Report progress
progress?.Report(totalRead);
}
ProcessContent(content.ToString());
}
}
}
67. File Compression and Decompression in C
public class CompressionExample
{
// Compress file using GZip
public static async Task CompressFileAsync(string sourceFile, string compressedFile)
{
using (FileStream sourceStream = File.OpenRead(sourceFile))
using (FileStream compressedStream = File.Create(compressedFile))
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
await sourceStream.CopyToAsync(gzipStream);
}
}
// Decompress file using GZip
public static async Task DecompressFileAsync(string compressedFile, string decompressedFile)
{
using (FileStream compressedStream = File.OpenRead(compressedFile))
using (FileStream decompressedStream = File.Create(decompressedFile))
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
{
await gzipStream.CopyToAsync(decompressedStream);
}
}
// Compress string to byte array
public static byte[] CompressString(string input)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
using (MemoryStream outputStream = new MemoryStream())
{
using (GZipStream gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
{
gzipStream.Write(inputBytes, 0, inputBytes.Length);
}
return outputStream.ToArray();
}
}
// Decompress byte array to string
public static string DecompressString(byte[] compressedData)
{
using (MemoryStream inputStream = new MemoryStream(compressedData))
using (GZipStream gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
using (MemoryStream outputStream = new MemoryStream())
{
gzipStream.CopyTo(outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
}
}
}
// Usage
string originalText = "This is a very long text that will be compressed...";
byte[] compressed = CompressionExample.CompressString(originalText);
string decompressed = CompressionExample.DecompressString(compressed);
68. Differences between TextReader, StreamReader, and StringReader
TextReader (Abstract Base Class): - Abstract base for text reading - Defines common interface
StreamReader: - Reads text from streams (files, network, etc.) - Handles encoding automatically - Buffered reading
StringReader: - Reads text from strings - In-memory operation - Useful for parsing string data
public class ReaderExamples
{
// StreamReader - reading from file
public static async Task<string> ReadFileWithStreamReader(string path)
{
using (StreamReader reader = new StreamReader(path, Encoding.UTF8))
{
return await reader.ReadToEndAsync();
}
}
// StringReader - reading from string
public static void ProcessStringWithStringReader(string input)
{
using (StringReader reader = new StringReader(input))
{
string line;
int lineNumber = 1;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"Line {lineNumber}: {line}");
lineNumber++;
}
}
}
// Generic method using TextReader
public static async Task<string> ReadAllTextAsync(TextReader reader)
{
return await reader.ReadToEndAsync();
}
// Usage with different readers
public static async Task DemonstrateReaders()
{
// File reading
string fileContent = await ReadFileWithStreamReader("data.txt");
// String reading
ProcessStringWithStringReader("Line 1\nLine 2\nLine 3");
// Generic approach
using (StreamReader fileReader = new StreamReader("data.txt"))
{
string content = await ReadAllTextAsync(fileReader);
}
using (StringReader stringReader = new StringReader("Hello World"))
{
string content = await ReadAllTextAsync(stringReader);
}
}
}
69. Custom Serialization and ISerializable Interface
ISerializable belongs to the older formatter-based serialization model and is not the customization mechanism for System.Text.Json. For modern JSON, define a DTO or implement JsonConverter<T> when a special representation is required. Treat legacy serialization constructors and GetObjectData examples as historical; do not restore BinaryFormatter to make them run. Reference: Migration guidance.
70. Difference between Serialization and Marshaling
Serialization: - Converting objects to a format for storage/transmission - Focuses on data persistence - Usually involves converting to text or binary format
Marshaling: - Converting data between different execution contexts - Often involves memory layout and platform-specific concerns - Common in interop scenarios (COM, P/Invoke)
public class SerializationVsMarshaling
{
// Serialization - converting object to persistent format
public static string SerializeToJson(Person person)
{
return JsonSerializer.Serialize(person);
}
public static Person DeserializeFromJson(string json)
{
return JsonSerializer.Deserialize<Person>(json);
}
// Marshaling - converting between different contexts
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
public static void DemonstrateMarshaling()
{
// Marshaling string to unmanaged code
string message = "Hello from C#";
string caption = "Marshaling Example";
// Strings are automatically marshaled to unmanaged format
MessageBox(IntPtr.Zero, message, caption, 0);
}
// Custom marshaling with attributes
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CustomStruct
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string Name;
[MarshalAs(UnmanagedType.I4)]
public int Age;
}
// Marshaling complex types
public static byte[] MarshalStructToBytes(CustomStruct data)
{
int size = Marshal.SizeOf(data);
byte[] bytes = new byte[size];
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(data, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
return bytes;
}
public static CustomStruct UnmarshalBytesToStruct(byte[] bytes)
{
IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
try
{
Marshal.Copy(bytes, 0, ptr, bytes.Length);
return Marshal.PtrToStructure<CustomStruct>(ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
}
71. Differences between Thread, Task, and async/await
Thread
- Low-level construct for creating and managing threads directly
- Resource-intensive and requires manual management
- Blocking operations
// Direct thread usage
Thread thread = new Thread(() => {
Console.WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
Console.WriteLine("Thread completed");
});
thread.Start();
thread.Join(); // Wait for completion
Task
- Higher-level abstraction representing asynchronous work
- Built on top of ThreadPool
- Supports continuation, cancellation, and exception handling
// Task usage
Task task = Task.Run(() => {
Console.WriteLine($"Task running on thread: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
return "Task completed";
});
// Task with continuation
Task<string> taskWithResult = Task.Run(() => "Hello");
taskWithResult.ContinueWith(t => Console.WriteLine($"Result: {t.Result}"));
async/await
- Syntactic sugar for working with Tasks
- Makes asynchronous code look synchronous
- Automatically handles context switching
// async/await pattern
public async Task<string> GetDataAsync()
{
Console.WriteLine($"Starting on thread: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000); // Non-blocking delay
Console.WriteLine($"Continuing on thread: {Thread.CurrentThread.ManagedThreadId}");
return "Data retrieved";
}
// Usage
public async Task MainAsync()
{
string result = await GetDataAsync();
Console.WriteLine(result);
}
72. Differences between lock, Monitor, and Mutex
lock
- Compiler-generated Monitor.Enter/Monitor.Exit
- Process-local synchronization
- Simple syntax
private readonly object _lockObject = new object();
private int _counter = 0;
public void IncrementCounter()
{
lock (_lockObject)
{
_counter++;
Console.WriteLine($"Counter: {_counter}");
}
}
Monitor
- More control over locking behavior
- Supports timeout and condition variables
- Same process scope as lock
private readonly object _monitorObject = new object();
private bool _dataReady = false;
public void Producer()
{
lock (_monitorObject)
{
// Produce data
_dataReady = true;
Monitor.Pulse(_monitorObject); // Signal waiting threads
}
}
public void Consumer()
{
lock (_monitorObject)
{
while (!_dataReady)
{
Monitor.Wait(_monitorObject); // Wait for signal
}
// Consume data
}
}
Mutex
- Cross-process synchronization
- Named mutexes can be shared between processes
- More overhead than Monitor
// Process-local mutex
using (var mutex = new Mutex())
{
mutex.WaitOne(); // Acquire lock
try
{
// Critical section
Console.WriteLine("Critical section executed");
}
finally
{
mutex.ReleaseMutex(); // Release lock
}
}
// Named mutex (cross-process)
using (var namedMutex = new Mutex(false, "MyAppMutex"))
{
if (namedMutex.WaitOne(TimeSpan.FromSeconds(5)))
{
try
{
// Only one process can execute this
Console.WriteLine("Single instance operation");
}
finally
{
namedMutex.ReleaseMutex();
}
}
}
73. Thread Synchronization and Race Conditions
Race Condition Example
public class BankAccount
{
private decimal _balance = 1000m;
// Race condition - unsafe
public void WithdrawUnsafe(decimal amount)
{
if (_balance >= amount)
{
Thread.Sleep(10); // Simulate processing time
_balance -= amount;
Console.WriteLine($"Withdrawn: {amount}, Balance: {_balance}");
}
}
// Thread-safe version
private readonly object _lockObject = new object();
public void WithdrawSafe(decimal amount)
{
lock (_lockObject)
{
if (_balance >= amount)
{
Thread.Sleep(10);
_balance -= amount;
Console.WriteLine($"Withdrawn: {amount}, Balance: {_balance}");
}
}
}
}
// Demonstration
var account = new BankAccount();
var tasks = new List<Task>();
// This will cause race conditions
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(() => account.WithdrawUnsafe(100)));
}
Task.WaitAll(tasks.ToArray());
Synchronization Techniques
public class ThreadSafeCounter
{
private int _counter = 0;
private readonly object _lockObject = new object();
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
// Using lock
public void IncrementWithLock()
{
lock (_lockObject)
{
_counter++;
}
}
// Using Interlocked
public void IncrementWithInterlocked()
{
Interlocked.Increment(ref _counter);
}
// Using ReaderWriterLockSlim
public int GetValueWithRWLock()
{
_rwLock.EnterReadLock();
try
{
return _counter;
}
finally
{
_rwLock.ExitReadLock();
}
}
public void SetValueWithRWLock(int value)
{
_rwLock.EnterWriteLock();
try
{
_counter = value;
}
finally
{
_rwLock.ExitWriteLock();
}
}
}
74. Differences between Semaphore, SemaphoreSlim, and CountdownEvent
Semaphore
- Cross-process synchronization
- Named semaphores
- More overhead
// Process-local semaphore
using (var semaphore = new Semaphore(3, 3)) // Initial count: 3, Maximum: 3
{
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
int taskId = i;
tasks.Add(Task.Run(() => {
semaphore.WaitOne();
try
{
Console.WriteLine($"Task {taskId} executing");
Thread.Sleep(1000);
}
finally
{
semaphore.Release();
}
}));
}
Task.WaitAll(tasks.ToArray());
}
SemaphoreSlim
- Process-local only
- Lighter weight than Semaphore
- Supports async/await
using (var semaphoreSlim = new SemaphoreSlim(2, 2)) // Allow 2 concurrent
{
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
int taskId = i;
tasks.Add(Task.Run(async () => {
await semaphoreSlim.WaitAsync();
try
{
Console.WriteLine($"Task {taskId} started");
await Task.Delay(1000);
Console.WriteLine($"Task {taskId} completed");
}
finally
{
semaphoreSlim.Release();
}
}));
}
await Task.WhenAll(tasks);
}
CountdownEvent
- Signals when count reaches zero
- Useful for waiting for multiple operations to complete
var countdownEvent = new CountdownEvent(3); // Wait for 3 signals
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
int taskId = i;
tasks.Add(Task.Run(() => {
Console.WriteLine($"Task {taskId} starting");
Thread.Sleep(1000);
Console.WriteLine($"Task {taskId} completed");
countdownEvent.Signal(); // Signal completion
}));
}
countdownEvent.Wait(); // Wait for all tasks to complete
Console.WriteLine("All tasks completed!");
75. Thread Pools and Their Benefits
Thread Pool Benefits
- Reuses threads instead of creating new ones
- Reduces overhead of thread creation/destruction
- Automatic load balancing
- Prevents thread explosion
public class ThreadPoolExample
{
public void DemonstrateThreadPool()
{
// ThreadPool automatically manages threads
ThreadPool.QueueUserWorkItem(_ => {
Console.WriteLine($"ThreadPool thread: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
});
// Task.Run uses ThreadPool internally
Task.Run(() => {
Console.WriteLine($"Task on ThreadPool: {Thread.CurrentThread.ManagedThreadId}");
});
// Custom ThreadPool settings
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(100, 100);
}
public async Task DemonstrateAsyncThreadPool()
{
// Async operations don't block ThreadPool threads
await Task.Delay(1000); // Non-blocking
// CPU-intensive work should use Task.Run
await Task.Run(() => {
// Heavy computation
for (int i = 0; i < 1000000; i++)
{
Math.Sqrt(i);
}
});
}
}
76. Differences between volatile and Interlocked
volatile
- Ensures visibility of changes across threads
- Prevents compiler optimizations
- Doesn't provide atomicity
public class VolatileExample
{
private volatile bool _flag = false;
private volatile int _value = 0;
public void SetFlag()
{
_flag = true; // Changes are immediately visible to other threads
}
public bool GetFlag()
{
return _flag; // Always reads the latest value
}
// WARNING: This is NOT atomic
public void IncrementValue()
{
_value++; // This is NOT thread-safe!
}
}
Interlocked
- Provides atomic operations
- Ensures both visibility and atomicity
- More expensive than volatile
public class InterlockedExample
{
private int _counter = 0;
private long _longValue = 0;
public void IncrementCounter()
{
Interlocked.Increment(ref _counter);
}
public void AddToCounter(int value)
{
Interlocked.Add(ref _counter, value);
}
public int CompareExchangeCounter(int newValue, int expectedValue)
{
return Interlocked.CompareExchange(ref _counter, newValue, expectedValue);
}
public void ExchangeLongValue(long newValue)
{
Interlocked.Exchange(ref _longValue, newValue);
}
}
77. async/await Best Practices and Pitfalls
Best Practices
public class AsyncBestPractices
{
// ✅ Good: Async all the way
public async Task<string> GetDataAsync()
{
return await FetchDataFromDatabaseAsync();
}
// ❌ Bad: Blocking in async method
public async Task<string> GetDataBadAsync()
{
return FetchDataFromDatabase(); // Blocking call
}
// ✅ Good: Use ConfigureAwait(false) for library code
public async Task<string> GetDataForLibraryAsync()
{
var data = await FetchDataAsync().ConfigureAwait(false);
return ProcessData(data);
}
// ✅ Good: Handle exceptions properly
public async Task<string> GetDataWithExceptionHandlingAsync()
{
try
{
return await FetchDataAsync();
}
catch (HttpRequestException ex)
{
// Log and handle specific exceptions
return "Fallback data";
}
}
// ✅ Good: Use cancellation tokens
public async Task<string> GetDataWithCancellationAsync(CancellationToken cancellationToken)
{
return await FetchDataAsync(cancellationToken);
}
}
Common Pitfalls
public class AsyncPitfalls
{
// ❌ Pitfall 1: async void (except for event handlers)
public async void BadAsyncVoid()
{
await Task.Delay(1000);
// Exceptions here can crash the application
}
// ✅ Good: async Task for methods that can be awaited
public async Task GoodAsyncTask()
{
await Task.Delay(1000);
}
// ❌ Pitfall 2: Blocking in async context
public async Task<string> BadBlockingAsync()
{
return Task.Run(() => "data").Result; // Deadlock risk
}
// ✅ Good: Proper async/await
public async Task<string> GoodAsync()
{
return await Task.Run(() => "data");
}
// ❌ Pitfall 3: Not awaiting tasks
public void BadFireAndForget()
{
Task.Run(() => Console.WriteLine("This might not complete"));
}
// ✅ Good: Proper task management
public async Task GoodTaskManagement()
{
await Task.Run(() => Console.WriteLine("This will complete"));
}
}
78. Differences between Task.Run and Task.Factory.StartNew
Task.Run
- Simplified API for ThreadPool work
- Automatically unwraps nested tasks
- Better default options
// Task.Run - simple and clean
var task1 = Task.Run(() => {
Console.WriteLine("Simple background work");
return 42;
});
// Task.Run with async lambda
var task2 = Task.Run(async () => {
await Task.Delay(1000);
return "Async result";
});
Task.Factory.StartNew
- More control over task creation
- Can specify custom TaskScheduler
- More complex but flexible
// Task.Factory.StartNew with options
var task1 = Task.Factory.StartNew(() => {
Console.WriteLine("Custom task");
return 42;
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
// Task.Factory.StartNew with custom scheduler
var customScheduler = new LimitedConcurrencyLevelTaskScheduler(2);
var task2 = Task.Factory.StartNew(() => {
Console.WriteLine("Task on custom scheduler");
}, CancellationToken.None, TaskCreationOptions.None, customScheduler);
// Custom TaskScheduler example
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
private readonly SemaphoreSlim _semaphore;
public LimitedConcurrencyLevelTaskScheduler(int maxConcurrencyLevel)
{
_semaphore = new SemaphoreSlim(maxConcurrencyLevel);
}
protected override void QueueTask(Task task)
{
_semaphore.WaitAsync().ContinueWith(_ => {
try
{
TryExecuteTask(task);
}
finally
{
_semaphore.Release();
}
});
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
public override int MaximumConcurrencyLevel => 2;
}
79. Cancellation Tokens and Their Usage
Basic Usage
public class CancellationExample
{
public async Task ProcessDataWithCancellationAsync(CancellationToken cancellationToken)
{
try
{
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
Console.WriteLine($"Processing item {i}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
public async Task DemonstrateCancellationAsync()
{
var cts = new CancellationTokenSource();
// Cancel after 5 seconds
cts.CancelAfter(TimeSpan.FromSeconds(5));
try
{
await ProcessDataWithCancellationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled after timeout");
}
}
}
Advanced Cancellation Patterns
public class AdvancedCancellation
{
public async Task ProcessWithMultipleTokensAsync(
CancellationToken userToken,
CancellationToken timeoutToken)
{
// Combine multiple cancellation tokens
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
userToken, timeoutToken);
try
{
await ProcessDataAsync(combinedCts.Token);
}
catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
{
Console.WriteLine("Operation timed out");
}
catch (OperationCanceledException) when (userToken.IsCancellationRequested)
{
Console.WriteLine("Operation cancelled by user");
}
}
public async Task ProcessWithProgressAsync(
IProgress<int> progress,
CancellationToken cancellationToken)
{
for (int i = 0; i <= 100; i += 10)
{
cancellationToken.ThrowIfCancellationRequested();
progress?.Report(i);
await Task.Delay(100, cancellationToken);
}
}
}
80. Differences between Parallel.For and Parallel.ForEach
Parallel.For
- For numeric ranges
- Simple iteration over indices
- Good for CPU-intensive work
public class ParallelForExample
{
public void ProcessRange()
{
var results = new int[1000];
Parallel.For(0, 1000, i => {
results[i] = ComputeValue(i);
});
// With options
var options = new ParallelOptions {
MaxDegreeOfParallelism = Environment.ProcessorCount,
CancellationToken = CancellationToken.None
};
Parallel.For(0, 1000, options, i => {
results[i] = ComputeValue(i);
});
}
private int ComputeValue(int index)
{
// Simulate CPU-intensive work
Thread.Sleep(10);
return index * index;
}
}
Parallel.ForEach
- For collections
- More flexible iteration
- Better for I/O-bound operations
public class ParallelForEachExample
{
public async Task ProcessCollectionAsync()
{
var items = Enumerable.Range(1, 100).ToList();
var results = new ConcurrentBag<string>();
// Basic usage
Parallel.ForEach(items, item => {
var result = ProcessItem(item);
results.Add(result);
});
// With async operations
await Parallel.ForEachAsync(items, async (item, token) => {
var result = await ProcessItemAsync(item, token);
results.Add(result);
});
// With custom options
var options = new ParallelOptions {
MaxDegreeOfParallelism = 4,
CancellationToken = CancellationToken.None
};
Parallel.ForEach(items, options, item => {
var result = ProcessItem(item);
results.Add(result);
});
}
private string ProcessItem(int item)
{
Thread.Sleep(100);
return $"Processed {item}";
}
private async Task<string> ProcessItemAsync(int item, CancellationToken token)
{
await Task.Delay(100, token);
return $"Async processed {item}";
}
}
Performance Comparison
public class PerformanceComparison
{
public void ComparePerformance()
{
var data = Enumerable.Range(1, 10000).ToList();
// Sequential
var stopwatch = Stopwatch.StartNew();
var sequentialResults = data.Select(x => ComputeValue(x)).ToList();
Console.WriteLine($"Sequential: {stopwatch.ElapsedMilliseconds}ms");
// Parallel.ForEach
stopwatch.Restart();
var parallelResults = new ConcurrentBag<int>();
Parallel.ForEach(data, item => {
parallelResults.Add(ComputeValue(item));
});
Console.WriteLine($"Parallel.ForEach: {stopwatch.ElapsedMilliseconds}ms");
// Parallel.For (if applicable)
stopwatch.Restart();
var results = new int[data.Count];
Parallel.For(0, data.Count, i => {
results[i] = ComputeValue(data[i]);
});
Console.WriteLine($"Parallel.For: {stopwatch.ElapsedMilliseconds}ms");
}
private int ComputeValue(int input)
{
// Simulate work
Thread.Sleep(1);
return input * input;
}
}
# .NET Architecture Q&A with Coding Examples
81. Expression Trees and Their Use Cases
Expression Trees are data structures that represent code in a tree-like format where each node is an expression. They allow you to treat code as data that can be analyzed, transformed, and executed at runtime.
Key Use Cases:
- LINQ to SQL/Entity Framework: Convert C# expressions to SQL queries
- Dynamic Method Generation: Create methods at runtime
- Validation Frameworks: Build complex validation rules
- API Query Building: Create dynamic query builders
Example:
using System;
using System.Linq.Expressions;
public class ExpressionTreeExamples
{
public void BasicExpressionTree()
{
// Create an expression tree: x => x * 2
ParameterExpression parameter = Expression.Parameter(typeof(int), "x");
ConstantExpression constant = Expression.Constant(2, typeof(int));
BinaryExpression multiply = Expression.Multiply(parameter, constant);
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(multiply, parameter);
// Compile and execute
Func<int, int> compiled = lambda.Compile();
int result = compiled(5); // Returns 10
Console.WriteLine($"Result: {result}");
}
public void DynamicQueryBuilder()
{
// Building dynamic queries
var users = new List<User>
{
new User { Name = "John", Age = 25 },
new User { Name = "Jane", Age = 30 }
};
// Create expression: user => user.Age > 25
ParameterExpression userParam = Expression.Parameter(typeof(User), "user");
MemberExpression ageProperty = Expression.Property(userParam, "Age");
ConstantExpression ageValue = Expression.Constant(25, typeof(int));
BinaryExpression ageComparison = Expression.GreaterThan(ageProperty, ageValue);
Expression<Func<User, bool>> filter = Expression.Lambda<Func<User, bool>>(ageComparison, userParam);
var filteredUsers = users.AsQueryable().Where(filter).ToList();
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
82. Differences Between Func, Action, and Predicate Delegates
Func Delegate
- Purpose: Represents a method that returns a value
- Signature:
Func<T1, T2, ..., TResult> - Return Type: Always returns a value (last generic parameter)
Action Delegate
- Purpose: Represents a method that performs an action (void method)
- Signature:
Action<T1, T2, ...> - Return Type: Always void
Predicate Delegate
- Purpose: Represents a method that tests a condition
- Signature:
Predicate<T> - Return Type: Always returns bool
Example:
public class DelegateExamples
{
public void DemonstrateDelegates()
{
// Func delegate - returns a value
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> getLength = str => str.Length;
int sum = add(5, 3); // 8
int length = getLength("Hello"); // 5
// Action delegate - performs an action
Action<string> printMessage = message => Console.WriteLine(message);
Action<int, int> printSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
printMessage("Hello World");
printSum(10, 20);
// Predicate delegate - tests a condition
Predicate<int> isEven = num => num % 2 == 0;
Predicate<string> isLongString = str => str.Length > 10;
bool even = isEven(4); // true
bool longString = isLongString("Short"); // false
// Practical usage with collections
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// Using Func with LINQ
var doubled = numbers.Select(x => x * 2).ToList();
// Using Action with ForEach
numbers.ForEach(x => Console.WriteLine($"Number: {x}"));
// Using Predicate with FindAll
var evenNumbers = numbers.FindAll(x => x % 2 == 0);
}
}
83. Differences Between IEnumerable and IQueryable
IEnumerable<T> exposes enumeration; it does not require loading an entire collection into memory. LINQ-to-Objects operators consume it locally and can stream. IQueryable<T> exposes an expression tree for a provider to translate or execute; the provider may target a database or another source. AsEnumerable changes which extension operators are selected for subsequent operations, while ToList materializes results. Put translatable filters before the local boundary.
84. Differences Between var and dynamic
var
- Type Inference: Compile-time type inference
- Type Safety: Strongly typed at compile time
- Performance: No runtime overhead
- IntelliSense: Full IntelliSense support
dynamic
- Type Inference: Runtime type resolution
- Type Safety: No compile-time type checking
- Performance: Runtime overhead due to dynamic dispatch
- IntelliSense: Limited IntelliSense support
Example:
public class VarVsDynamicExamples
{
public void DemonstrateVarVsDynamic()
{
// var - compile-time type inference
var stringValue = "Hello World"; // Type: string
var intValue = 42; // Type: int
var listValue = new List<string>(); // Type: List<string>
// Compile-time type safety
stringValue.Length; // Valid - string has Length property
// stringValue.InvalidProperty(); // Compile-time error
// dynamic - runtime type resolution
dynamic dynamicValue = "Hello World";
dynamic dynamicNumber = 42;
dynamic dynamicList = new List<string>();
// Runtime type resolution
var length = dynamicValue.Length; // Valid at runtime
// var invalid = dynamicValue.InvalidProperty(); // Runtime error
// Performance comparison
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// var - no runtime overhead
for (int i = 0; i < 1000000; i++)
{
var result = stringValue.Length;
}
stopwatch.Stop();
Console.WriteLine($"var time: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
// dynamic - runtime overhead
for (int i = 0; i < 1000000; i++)
{
var result = dynamicValue.Length;
}
stopwatch.Stop();
Console.WriteLine($"dynamic time: {stopwatch.ElapsedMilliseconds}ms");
}
public void DynamicUseCases()
{
// Useful for COM interop
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true;
excel.Workbooks.Add();
// Useful for JSON deserialization
dynamic jsonData = System.Text.Json.JsonSerializer.Deserialize<dynamic>(
"{\"name\": \"John\", \"age\": 30}");
string name = jsonData.name;
int age = jsonData.age;
}
}
85. Differences Between object and dynamic
object
- Type: Reference type, base class for all types
- Type Safety: Compile-time type checking
- Performance: Boxing/unboxing overhead for value types
- Method Calls: Requires casting for specific operations
dynamic
- Type: Special type for runtime binding
- Type Safety: No compile-time checking
- Performance: Runtime dispatch overhead
- Method Calls: Direct method calls without casting
Example:
public class ObjectVsDynamicExamples
{
public void DemonstrateObjectVsDynamic()
{
// object - requires casting
object objString = "Hello World";
object objNumber = 42;
// Need to cast to access specific methods
string str = (string)objString;
int length = str.Length; // Valid
// Runtime error if wrong type
// int number = (int)objString; // InvalidCastException
// dynamic - no casting needed
dynamic dynString = "Hello World";
dynamic dynNumber = 42;
// Direct method calls
int dynLength = dynString.Length; // Valid at runtime
string dynStringRep = dynNumber.ToString(); // Valid at runtime
// Performance comparison
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// object with casting
for (int i = 0; i < 1000000; i++)
{
string str2 = (string)objString;
var result = str2.Length;
}
stopwatch.Stop();
Console.WriteLine($"object time: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
// dynamic
for (int i = 0; i < 1000000; i++)
{
var result = dynString.Length;
}
stopwatch.Stop();
Console.WriteLine($"dynamic time: {stopwatch.ElapsedMilliseconds}ms");
}
public void ObjectUseCases()
{
// object is useful for generic collections
var objectList = new List<object> { "string", 42, true, 3.14 };
foreach (var item in objectList)
{
if (item is string str)
Console.WriteLine($"String: {str}");
else if (item is int num)
Console.WriteLine($"Number: {num}");
}
}
}
86. Differences Between string and String
string
- Type: C# keyword alias for System.String
- Usage: Preferred in C# code
- Case: Lowercase keyword
String
- Type: Full .NET Framework type name
- Usage: Same as string, but more verbose
- Case: Uppercase class name
Example:
public class StringExamples
{
public void DemonstrateStringVsString()
{
// string - C# keyword (preferred)
string str1 = "Hello World";
string str2 = String.Empty;
string str3 = null;
// String - full type name (same thing)
String str4 = "Hello World";
String str5 = String.Empty;
String str6 = null;
// They are identical
Console.WriteLine(str1.GetType() == str4.GetType()); // True
Console.WriteLine(str1 == str4); // True
// Best practices
string preferred = "Use string keyword"; // Preferred
String verbose = "Use String class name"; // More verbose
// Both support the same methods
string result1 = str1.ToUpper();
String result2 = str4.ToUpper();
}
}
87. Differences Between int and Int32
int
- Type: C# keyword alias for System.Int32
- Usage: Preferred in C# code
- Case: Lowercase keyword
Int32
- Type: Full .NET Framework type name
- Usage: Same as int, but more verbose
- Case: Uppercase class name
Example:
public class IntExamples
{
public void DemonstrateIntVsInt32()
{
// int - C# keyword (preferred)
int number1 = 42;
int maxValue = int.MaxValue;
int minValue = int.MinValue;
// Int32 - full type name (same thing)
Int32 number2 = 42;
Int32 maxValue2 = Int32.MaxValue;
Int32 minValue2 = Int32.MinValue;
// They are identical
Console.WriteLine(number1.GetType() == number2.GetType()); // True
Console.WriteLine(number1 == number2); // True
// Best practices
int preferred = 100; // Preferred
Int32 verbose = 100; // More verbose
// Both support the same methods
string result1 = number1.ToString();
string result2 = number2.ToString();
}
}
88. Differences Between decimal and double
decimal
- Precision: 28-29 significant digits
- Range: ±1.0 × 10^-28 to ±7.9228 × 10^28
- Use Case: Financial calculations, exact decimal arithmetic
- Performance: Slower than double
- Memory: 16 bytes
double
- Precision: 15-17 significant digits
- Range: ±5.0 × 10^-324 to ±1.7 × 10^308
- Use Case: Scientific calculations, general floating-point
- Performance: Faster than decimal
- Memory: 8 bytes
Example:
public class DecimalVsDoubleExamples
{
public void DemonstrateDecimalVsDouble()
{
// decimal - precise decimal arithmetic
decimal price = 19.99m;
decimal quantity = 3;
decimal total = price * quantity; // Exact: 59.97
// double - floating-point arithmetic
double priceDouble = 19.99;
double quantityDouble = 3;
double totalDouble = priceDouble * quantityDouble; // May have precision issues
// Precision comparison
decimal precise = 0.1m + 0.2m; // Exactly 0.3
double imprecise = 0.1 + 0.2; // May not be exactly 0.3
Console.WriteLine($"decimal: {precise}"); // 0.3
Console.WriteLine($"double: {imprecise}"); // 0.30000000000000004
// Financial calculations - use decimal
decimal accountBalance = 1000.00m;
decimal interestRate = 0.05m; // 5%
decimal interest = accountBalance * interestRate; // Exact calculation
// Scientific calculations - use double
double pi = Math.PI;
double radius = 5.0;
double area = pi * radius * radius; // Scientific precision sufficient
// Performance comparison
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
decimal sumDecimal = 0m;
for (int i = 0; i < 1000000; i++)
{
sumDecimal += 0.1m;
}
stopwatch.Stop();
Console.WriteLine($"decimal time: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
double sumDouble = 0.0;
for (int i = 0; i < 1000000; i++)
{
sumDouble += 0.1;
}
stopwatch.Stop();
Console.WriteLine($"double time: {stopwatch.ElapsedMilliseconds}ms");
}
}
89. Differences Between DateTime and DateTimeOffset
DateTime
- Represents: Date and time without timezone information
- Kind Property: Unspecified, Utc, or Local
- Use Case: When timezone doesn't matter or working with local time
- Storage: 8 bytes
DateTimeOffset
- Represents: Date and time with timezone offset
- Offset: Always includes UTC offset
- Use Case: When timezone information is important
- Storage: 12 bytes
Example:
public class DateTimeExamples
{
public void DemonstrateDateTimeVsDateTimeOffset()
{
// DateTime - no timezone information
DateTime localTime = DateTime.Now; // Local time
DateTime utcTime = DateTime.UtcNow; // UTC time
DateTime unspecified = new DateTime(2023, 12, 25, 10, 30, 0); // Unspecified
// DateTimeOffset - includes timezone offset
DateTimeOffset offsetTime = DateTimeOffset.Now; // Local time with offset
DateTimeOffset utcOffset = DateTimeOffset.UtcNow; // UTC time with +00:00 offset
// Creating DateTimeOffset for specific timezone
TimeSpan estOffset = TimeSpan.FromHours(-5); // EST offset
DateTimeOffset estTime = new DateTimeOffset(2023, 12, 25, 10, 30, 0, estOffset);
// Converting between timezones
DateTimeOffset pstTime = estTime.ToOffset(TimeSpan.FromHours(-8)); // Convert to PST
// Working with different timezones
var timeZones = new[]
{
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"),
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"),
TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time")
};
DateTimeOffset baseTime = DateTimeOffset.UtcNow;
foreach (var tz in timeZones)
{
DateTimeOffset converted = TimeZoneInfo.ConvertTime(baseTime, tz);
Console.WriteLine($"{tz.DisplayName}: {converted}");
}
// Database considerations
// DateTime - stored as is, no timezone info
// DateTimeOffset - stored with offset, preserves timezone context
// API considerations
// DateTime - may cause confusion about timezone
// DateTimeOffset - clear about timezone context
}
}
90. Differences Between Guid and string for IDs
Guid
- Type: 128-bit globally unique identifier
- Format: 32 hexadecimal digits with hyphens
- Uniqueness: Globally unique across all systems
- Size: 16 bytes
- Performance: Fast comparison and indexing
string
- Type: Variable-length character sequence
- Format: Any string format
- Uniqueness: Must be managed by application
- Size: Variable (typically 1-255 bytes)
- Performance: Slower comparison, variable indexing performance
Example:
public class IdExamples
{
public void DemonstrateGuidVsString()
{
// Guid - globally unique
Guid guid1 = Guid.NewGuid();
Guid guid2 = Guid.NewGuid();
Console.WriteLine($"Guid1: {guid1}");
Console.WriteLine($"Guid2: {guid2}");
Console.WriteLine($"Equal: {guid1 == guid2}"); // False
// String IDs - must manage uniqueness
string stringId1 = "USER_001";
string stringId2 = "USER_002";
// Performance comparison
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Guid comparison
for (int i = 0; i < 1000000; i++)
{
bool equal = guid1 == guid2;
}
stopwatch.Stop();
Console.WriteLine($"Guid comparison time: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
// String comparison
for (int i = 0; i < 1000000; i++)
{
bool equal = stringId1 == stringId2;
}
stopwatch.Stop();
Console.WriteLine($"String comparison time: {stopwatch.ElapsedMilliseconds}ms");
// Database considerations
// Guid - good for distributed systems, larger storage
// String - smaller storage, but must ensure uniqueness
// Use cases
UseGuidForDistributedSystems();
UseStringForSimpleIdentifiers();
}
private void UseGuidForDistributedSystems()
{
// Guid is perfect for distributed systems
var user = new User
{
Id = Guid.NewGuid(), // Globally unique
Name = "John Doe",
Email = "john@example.com"
};
// Can be generated on any system without coordination
var order = new Order
{
Id = Guid.NewGuid(),
UserId = user.Id,
Amount = 100.00m
};
}
private void UseStringForSimpleIdentifiers()
{
// String IDs for simple, human-readable identifiers
var category = new Category
{
Id = "CAT_001", // Human readable
Name = "Electronics",
Description = "Electronic products"
};
var product = new Product
{
Id = "PROD_001", // Human readable
CategoryId = category.Id,
Name = "Laptop",
Price = 999.99m
};
}
}
public class User
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class Order
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public decimal Amount { get; set; }
}
public class Category
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Product
{
public string Id { get; set; }
public string CategoryId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Design Patterns Guide for .NET Architects
91. Singleton Pattern and Thread-Safe Implementation
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Basic Singleton (Not Thread-Safe)
public class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
Thread-Safe Singleton (Double-Check Locking)
public class ThreadSafeSingleton
{
private static volatile ThreadSafeSingleton _instance;
private static readonly object _lock = new object();
private ThreadSafeSingleton() { }
public static ThreadSafeSingleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ThreadSafeSingleton();
}
}
}
return _instance;
}
}
}
Thread-Safe Singleton (Static Constructor)
public class StaticSingleton
{
private static readonly StaticSingleton _instance = new StaticSingleton();
static StaticSingleton() { }
private StaticSingleton() { }
public static StaticSingleton Instance => _instance;
}
Lazy<T> Implementation (Recommended)
public class LazySingleton
{
private static readonly Lazy<LazySingleton> _instance =
new Lazy<LazySingleton>(() => new LazySingleton());
private LazySingleton() { }
public static LazySingleton Instance => _instance.Value;
}
92. Factory Pattern and When to Use It
The Factory pattern provides an interface for creating objects without specifying their exact classes.
Simple Factory
public interface IAnimal
{
void MakeSound();
}
public class Dog : IAnimal
{
public void MakeSound() => Console.WriteLine("Woof!");
}
public class Cat : IAnimal
{
public void MakeSound() => Console.WriteLine("Meow!");
}
public class AnimalFactory
{
public IAnimal CreateAnimal(string animalType)
{
return animalType.ToLower() switch
{
"dog" => new Dog(),
"cat" => new Cat(),
_ => throw new ArgumentException("Unknown animal type")
};
}
}
Factory Method Pattern
public abstract class AnimalCreator
{
public abstract IAnimal CreateAnimal();
public void SomeOperation()
{
var animal = CreateAnimal();
animal.MakeSound();
}
}
public class DogCreator : AnimalCreator
{
public override IAnimal CreateAnimal() => new Dog();
}
public class CatCreator : AnimalCreator
{
public override IAnimal CreateAnimal() => new Cat();
}
Abstract Factory Pattern
public interface IAnimalFactory
{
IAnimal CreateAnimal();
IFood CreateFood();
}
public interface IFood
{
void Serve();
}
public class DogFood : IFood
{
public void Serve() => Console.WriteLine("Serving dog food");
}
public class CatFood : IFood
{
public void Serve() => Console.WriteLine("Serving cat food");
}
public class DogFactory : IAnimalFactory
{
public IAnimal CreateAnimal() => new Dog();
public IFood CreateFood() => new DogFood();
}
public class CatFactory : IAnimalFactory
{
public IAnimal CreateAnimal() => new Cat();
public IFood CreateFood() => new CatFood();
}
93. Observer Pattern and Events in C
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
Traditional Observer Pattern
public interface IObserver
{
void Update(string message);
}
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
public class NewsAgency : ISubject
{
private List<IObserver> _observers = new();
private string _news;
public void Attach(IObserver observer) => _observers.Add(observer);
public void Detach(IObserver observer) => _observers.Remove(observer);
public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(_news);
}
}
public void SetNews(string news)
{
_news = news;
Notify();
}
}
public class NewsChannel : IObserver
{
private string _name;
public NewsChannel(string name) => _name = name;
public void Update(string news)
{
Console.WriteLine($"{_name} received: {news}");
}
}
C# Events Implementation
public class NewsAgencyWithEvents
{
public event EventHandler<string> NewsPublished;
public void PublishNews(string news)
{
NewsPublished?.Invoke(this, news);
}
}
public class NewsChannelWithEvents
{
private string _name;
public NewsChannelWithEvents(string name) => _name = name;
public void OnNewsPublished(object sender, string news)
{
Console.WriteLine($"{_name} received: {news}");
}
}
// Usage
var agency = new NewsAgencyWithEvents();
var channel1 = new NewsChannelWithEvents("CNN");
var channel2 = new NewsChannelWithEvents("BBC");
agency.NewsPublished += channel1.OnNewsPublished;
agency.NewsPublished += channel2.OnNewsPublished;
agency.PublishNews("Breaking news!");
94. Strategy Pattern and Implementation
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using Credit Card");
}
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using PayPal");
}
}
public class CryptoPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using Cryptocurrency");
}
}
public class ShoppingCart
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void Checkout(decimal amount)
{
_paymentStrategy?.Pay(amount);
}
}
// Usage
var cart = new ShoppingCart();
cart.SetPaymentStrategy(new CreditCardPayment());
cart.Checkout(100.00m);
cart.SetPaymentStrategy(new PayPalPayment());
cart.Checkout(50.00m);
95. Repository Pattern and Benefits
The Repository pattern abstracts data persistence logic and provides a collection-like interface for accessing domain objects.
Repository Interface
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
}
Generic Repository Implementation
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
}
Specific Repository
public interface IUserRepository : IRepository<User>
{
Task<User> GetByEmailAsync(string email);
Task<IEnumerable<User>> GetActiveUsersAsync();
}
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(DbContext context) : base(context) { }
public async Task<User> GetByEmailAsync(string email)
{
return await _dbSet.FirstOrDefaultAsync(u => u.Email == email);
}
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
return await _dbSet.Where(u => u.IsActive).ToListAsync();
}
}
Benefits:
- Separation of Concerns: Data access logic is separated from business logic
- Testability: Easy to mock repositories for unit testing
- Flexibility: Can easily switch between different data sources
- Maintainability: Centralized data access logic
96. Unit of Work Pattern and When to Use It
The Unit of Work pattern manages transactions and coordinates the work of multiple repositories.
public interface IUnitOfWork : IDisposable
{
IUserRepository Users { get; }
IOrderRepository Orders { get; }
IProductRepository Products { get; }
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private IDbContextTransaction _transaction;
public UnitOfWork(DbContext context)
{
_context = context;
Users = new UserRepository(context);
Orders = new OrderRepository(context);
Products = new ProductRepository(context);
}
public IUserRepository Users { get; }
public IOrderRepository Orders { get; }
public IProductRepository Products { get; }
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
try
{
await _transaction?.CommitAsync();
}
catch
{
await _transaction?.RollbackAsync();
throw;
}
}
public async Task RollbackTransactionAsync()
{
await _transaction?.RollbackAsync();
}
public void Dispose()
{
_transaction?.Dispose();
_context?.Dispose();
}
}
Usage Example
public class OrderService
{
private readonly IUnitOfWork _unitOfWork;
public OrderService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateOrderAsync(int userId, List<int> productIds)
{
try
{
await _unitOfWork.BeginTransactionAsync();
var user = await _unitOfWork.Users.GetByIdAsync(userId);
if (user == null) throw new ArgumentException("User not found");
var order = new Order { UserId = userId, OrderDate = DateTime.UtcNow };
await _unitOfWork.Orders.AddAsync(order);
foreach (var productId in productIds)
{
var product = await _unitOfWork.Products.GetByIdAsync(productId);
if (product != null)
{
// Add order items logic
}
}
await _unitOfWork.SaveChangesAsync();
await _unitOfWork.CommitTransactionAsync();
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
}
97. Command Pattern and Use Cases
The Command pattern encapsulates a request as an object, allowing parameterization of clients with different requests.
public interface ICommand
{
void Execute();
void Undo();
}
public class Light
{
public void TurnOn() => Console.WriteLine("Light is ON");
public void TurnOff() => Console.WriteLine("Light is OFF");
}
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light) => _light = light;
public void Execute() => _light.TurnOn();
public void Undo() => _light.TurnOff();
}
public class LightOffCommand : ICommand
{
private readonly Light _light;
public LightOffCommand(Light light) => _light = light;
public void Execute() => _light.TurnOff();
public void Undo() => _light.TurnOn();
}
public class RemoteControl
{
private readonly ICommand[] _onCommands;
private readonly ICommand[] _offCommands;
private ICommand _undoCommand;
public RemoteControl()
{
_onCommands = new ICommand[7];
_offCommands = new ICommand[7];
}
public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
{
_onCommands[slot] = onCommand;
_offCommands[slot] = offCommand;
}
public void OnButtonPressed(int slot)
{
_onCommands[slot]?.Execute();
_undoCommand = _onCommands[slot];
}
public void OffButtonPressed(int slot)
{
_offCommands[slot]?.Execute();
_undoCommand = _offCommands[slot];
}
public void UndoButtonPressed()
{
_undoCommand?.Undo();
}
}
Macro Commands
public class MacroCommand : ICommand
{
private readonly ICommand[] _commands;
public MacroCommand(ICommand[] commands) => _commands = commands;
public void Execute()
{
foreach (var command in _commands)
{
command.Execute();
}
}
public void Undo()
{
for (int i = _commands.Length - 1; i >= 0; i--)
{
_commands[i].Undo();
}
}
}
98. Decorator Pattern and Implementation
The Decorator pattern attaches additional responsibilities to an object dynamically.
public abstract class Coffee
{
public abstract string GetDescription();
public abstract double GetCost();
}
public class SimpleCoffee : Coffee
{
public override string GetDescription() => "Simple Coffee";
public override double GetCost() => 1.0;
}
public abstract class CoffeeDecorator : Coffee
{
protected Coffee _coffee;
public CoffeeDecorator(Coffee coffee) => _coffee = coffee;
public override string GetDescription() => _coffee.GetDescription();
public override double GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(Coffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override double GetCost() => _coffee.GetCost() + 0.5;
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(Coffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Sugar";
public override double GetCost() => _coffee.GetCost() + 0.2;
}
public class WhipDecorator : CoffeeDecorator
{
public WhipDecorator(Coffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Whip";
public override double GetCost() => _coffee.GetCost() + 0.3;
}
Usage
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhipDecorator(coffee);
Console.WriteLine($"Description: {coffee.GetDescription()}");
Console.WriteLine($"Cost: ${coffee.GetCost():F2}");
99. Adapter Pattern and Benefits
The Adapter pattern allows incompatible interfaces to work together.
Object Adapter
// Target interface
public interface ITarget
{
void Request();
}
// Adaptee (existing class)
public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Specific request from Adaptee");
}
}
// Adapter
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee) => _adaptee = adaptee;
public void Request()
{
_adaptee.SpecificRequest();
}
}
Class Adapter
public class ClassAdapter : Adaptee, ITarget
{
public void Request()
{
SpecificRequest();
}
}
Real-world Example: Payment Gateway Adapter
public interface IPaymentProcessor
{
Task<bool> ProcessPaymentAsync(decimal amount, string cardNumber);
}
public class PayPalAPI
{
public async Task<bool> MakePaymentAsync(decimal amount, string email)
{
// PayPal specific implementation
await Task.Delay(100);
return true;
}
}
public class PayPalAdapter : IPaymentProcessor
{
private readonly PayPalAPI _paypalApi;
public PayPalAdapter(PayPalAPI paypalApi) => _paypalApi = paypalApi;
public async Task<bool> ProcessPaymentAsync(decimal amount, string cardNumber)
{
// Convert card number to email (simplified)
string email = $"user_{cardNumber}@example.com";
return await _paypalApi.MakePaymentAsync(amount, email);
}
}
Benefits:
- Compatibility: Enables integration of incompatible systems
- Reusability: Existing code can be reused with new interfaces
- Flexibility: Multiple adapters can be created for different scenarios
- Maintainability: Changes to adaptee don't affect client code
100. Template Method Pattern and When to Use It
The Template Method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps.
public abstract class DataProcessor
{
// Template method
public void ProcessData()
{
ReadData();
TransformData();
ValidateData();
SaveData();
LogResult();
}
// Abstract methods - must be implemented by subclasses
protected abstract void ReadData();
protected abstract void TransformData();
// Concrete methods - can be overridden
protected virtual void ValidateData()
{
Console.WriteLine("Default validation");
}
protected virtual void SaveData()
{
Console.WriteLine("Default save operation");
}
// Hook method - optional override
protected virtual void LogResult()
{
Console.WriteLine("Default logging");
}
}
public class CSVProcessor : DataProcessor
{
protected override void ReadData()
{
Console.WriteLine("Reading CSV data");
}
protected override void TransformData()
{
Console.WriteLine("Transforming CSV data");
}
protected override void ValidateData()
{
Console.WriteLine("Validating CSV data");
}
}
public class JSONProcessor : DataProcessor
{
protected override void ReadData()
{
Console.WriteLine("Reading JSON data");
}
protected override void TransformData()
{
Console.WriteLine("Transforming JSON data");
}
protected override void LogResult()
{
Console.WriteLine("Logging JSON processing result");
}
}
Advanced Example: Build Process
public abstract class BuildProcess
{
public void ExecuteBuild()
{
PrepareEnvironment();
CompileCode();
RunTests();
if (ShouldDeploy())
{
Deploy();
}
Cleanup();
}
protected abstract void PrepareEnvironment();
protected abstract void CompileCode();
protected abstract void RunTests();
protected abstract void Deploy();
protected virtual void Cleanup()
{
Console.WriteLine("Default cleanup");
}
protected virtual bool ShouldDeploy()
{
return true; // Default behavior
}
}
public class WebAppBuildProcess : BuildProcess
{
protected override void PrepareEnvironment()
{
Console.WriteLine("Preparing web app environment");
}
protected override void CompileCode()
{
Console.WriteLine("Compiling web app code");
}
protected override void RunTests()
{
Console.WriteLine("Running web app tests");
}
protected override void Deploy()
{
Console.WriteLine("Deploying web app");
}
protected override bool ShouldDeploy()
{
// Only deploy if tests pass
return true; // Simplified logic
}
}
When to Use Template Method:
- Common Algorithm Structure: When multiple classes share the same algorithm structure
- Code Reuse: To avoid code duplication across similar classes
- Framework Development: When building frameworks that need extensibility
- Workflow Definition: For defining workflows with customizable steps
Benefits:
- Code Reuse: Common algorithm structure is defined once
- Extensibility: Easy to add new implementations
- Consistency: Ensures consistent algorithm execution
- Maintainability: Changes to algorithm structure affect all implementations