Skip to main content

C# Interview Questions

Language Fundamentals

Q1: What is the difference between readonly and const?

Both create immutable values, but they differ in when the value is set and what types they accept:

Featureconstreadonly
When assignedCompile timeRuntime (constructor or declaration)
Implicitly staticYesNo (can be instance or static)
Allowed typesPrimitives, strings, enums, nullAny type
Can use newNoYes
Changing valueBinary-breaking change (inlined)Safe across assemblies
public class Settings
{
// const — value baked in at compile time, implicitly static
public const int MaxRetries = 3;
public const string AppName = "MyApp";

// readonly — value set at runtime
public readonly DateTime CreatedAt;
public readonly string ConnectionString;

public Settings(string connectionString)
{
ConnectionString = connectionString;
CreatedAt = DateTime.UtcNow;
}
}
Prefer readonly for public values

const values are inlined at compile time — every referencing assembly gets the literal value baked in. If you change a const, all referencing assemblies must be recompiled. Use public static readonly for values that might change between versions.


Q2: What is the sealed keyword used for?

sealed prevents further inheritance or overriding:

// Sealed class — cannot be inherited
public sealed class ConnectionString
{
public string Value { get; }
public ConnectionString(string value) => Value = value;
}
// public class CustomConnStr : ConnectionString { } // Compile error

// Sealed method — cannot be overridden further down the chain
public class BaseRenderer
{
protected virtual void RenderHeader() { }
}

public class HtmlRenderer : BaseRenderer
{
protected sealed override void RenderHeader()
{
// This override is final — no further override allowed
}
}
Seal by default

Mark classes sealed unless you intentionally design for inheritance. The JIT compiler can optimize virtual calls on sealed types (devirtualization). Common sealed types: string, Exception.


Q3: Name all the access modifiers for types

ModifierSame ClassDerived ClassSame AssemblyExternal Assembly
publicYesYesYesYes
privateYesNoNoNo
protectedYesYesNoNo
internalYesNoYesNo
protected internalYesYesYesNo
private protectedYesYes (same assembly)NoNo

Top-level types (not nested) can only be public or internal. The default is internal.


Q4: What is the difference between an interface and an abstract class?

FeatureAbstract ClassInterface
ConstructorsYesNo
Fields / stateYesNo (static fields only, C# 8+)
Default implementationYesYes (C# 8+, via DIM)
Multiple inheritanceNo (single class inheritance)Yes (multiple interfaces)
Access modifiersAnypublic (implicitly)
When to use"Is-a" with shared logic"Can-do" capability contract
// Abstract class — shared state and logic
public abstract class Vehicle
{
public string Model { get; set; } // state
public void Start() => Console.WriteLine("Engine started"); // concrete method
public abstract double CalculateFuelEfficiency(); // must override
}

// Interface — pure contract
public interface IWriter
{
void WriteFile();
// C# 8+ default implementation
void Log() => Console.WriteLine("Writing...");
}

Since C# 8, interfaces can have default implementations, but they still cannot hold instance state (no instance fields).


Q5: When is a static constructor called?

A static constructor runs once per AppDomain, lazily — before the first instance is created or any static member is accessed. It is never called directly.

public class Configuration
{
public static int InstanceCount { get; private set; }

static Configuration()
{
InstanceCount = 0;
Console.WriteLine("Static ctor ran");
}

public Configuration() => InstanceCount++;
}

var c1 = new Configuration(); // "Static ctor ran" printed here
var c2 = new Configuration(); // No output — static ctor already ran

Rules:

  • No access modifiers, no parameters
  • Runs only once per AppDomain
  • Called lazily (before first use)
  • Thread-safe — the CLR guarantees only one thread executes it

Q6: How to create an extension method?

Extension methods add new methods to existing types without modifying them. They must be defined in a static class and the first parameter uses the this keyword:

public static class StringExtensions
{
public static bool IsNullOrEmpty(this string str)
=> string.IsNullOrEmpty(str);

public static string Truncate(this string str, int maxLength)
=> str.Length <= maxLength ? str : str[..maxLength];
}

// Usage — appears as instance method
string name = "CommitToMemory";
var shortName = name.Truncate(6); // "Commit"
bool empty = "".IsNullOrEmpty(); // true

Rules:

  • Must be in a static class at namespace level
  • First parameter must have this modifier
  • Cannot access private members of the extended type
  • Instance methods always win over extension methods with the same signature

Q7: Does C# support multiple class inheritance?

No. C# supports only single class inheritance — a class can have exactly one direct base class. However, a class can implement multiple interfaces:

public class Base { }
public interface IReader { void Read(); }
public interface IWriter { void Write(); }

// Single class + multiple interfaces
public class Document : Base, IReader, IWriter
{
public void Read() { }
public void Write() { }
}

The reason: multiple class inheritance leads to the diamond problem — ambiguity when two base classes define the same member. Interfaces avoid this because they carry no implementation state (prior to C# 8 default methods, which are resolved differently).


Q8: Explain boxing and unboxing

Boxing converts a value type to a reference type (stored on the heap). Unboxing extracts the value type from the object.

int number = 42;

// Boxing — value type → reference type (heap allocation)
object boxed = number;

// Unboxing — reference type → value type (explicit cast)
int unboxed = (int)boxed;
// Hidden boxing in everyday code
ArrayList list = new(); // stores object — boxes value types
list.Add(42); // boxing!
int val = (int)list[0]; // unboxing

// Generic collections avoid boxing entirely
List<int> genericList = new();
genericList.Add(42); // no boxing — stored as int
Performance impact

Boxing allocates on the heap and causes GC pressure. Avoid it by using generic collections (List<T> over ArrayList) and generic interfaces (IEnumerable<T> over IEnumerable).


Q9: What is the heap and the stack?

AspectStackHeap
StoresValue types, method frames, referencesObjects (reference type instances)
SpeedVery fast (CPU stack pointer)Slower (indirected access)
LifetimeAutomatic — freed when method returnsManaged by GC
SizeSmall (~1 MB per thread)Large (GBs available)
Thread safetyPer-threadShared across threads
void Example()
{
int x = 10; // x lives on the stack
string s = "hello"; // reference 's' on stack, "hello" object on heap
var p = new Person(); // reference 'p' on stack, Person object on heap
} // stack frame popped — x, s, p references gone; Person object lives until GC collects

Value types (int, double, bool, struct, enum) are typically stored on the stack (unless captured by a closure, part of a class, or boxed). Reference types (class, string, array, delegate) are always stored on the heap — the variable holds a reference (pointer) to the heap object.


Q10: What is the difference between string and StringBuilder?

string is immutable — every modification creates a new string object. StringBuilder is mutable — it modifies a single buffer, making it efficient for repeated concatenations.

// string — each + creates a new allocation
string result = "";
for (int i = 0; i < 1000; i++)
result += i.ToString(); // 1000 new string allocations!

// StringBuilder — modifies one buffer
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i.ToString()); // no intermediate allocations
string final = sb.ToString();
FeaturestringStringBuilder
MutabilityImmutableMutable
ConcatenationCreates new object each timeAppends to same buffer
PerformanceSlow for many operationsFast for many operations
Thread safetyNaturally thread-safeNot thread-safe
Use caseFew operations, comparisonsLoops, many concatenations
Use StringBuilder when you have more than ~3-4 concatenations in a loop. For simple one-off concatenations, string is fine and the compiler optimizes + into string.Concat.

Q11: What is the difference between is and as operators?

Both deal with runtime type checking, but they serve different purposes:

Featureisas
Returnsbool (type check result)Reference (casted object or null)
ThrowsNeverNever
Value typesSupported (C# 7+ pattern matching)Reference/boxing/unboxing only
Pattern matchingYes (is int x, is string s)No
object obj = "Hello";

// is — checks type, returns bool
if (obj is string) { /* true */ }

// is with pattern matching (C# 7+) — check AND extract
if (obj is string str)
Console.WriteLine(str.Length); // 5

// as — attempts cast, returns null on failure
string? s = obj as string; // "Hello"
int? n = obj as int?; // null — int is a value type, must use nullable

// typeof — gets the Type object at compile time
Type t = typeof(string);
Prefer is with pattern matching

Use if (obj is string s) over var s = obj as string; if (s != null) — it's more concise and works with value types.


Q12: What is the difference between early binding and late binding?

Early binding resolves method calls at compile time. The compiler knows the exact type and validates method existence. Late binding resolves at runtime — the actual method is determined when the code executes.

// Early binding — compiler knows the type and method
string name = "Alice";
int len = name.Length; // resolved at compile time

// Late binding via dynamic — resolved at runtime
dynamic value = "Hello";
int length = value.Length; // resolved at runtime, no compile-time check

// Late binding via reflection — resolved at runtime
var type = obj.GetType();
var method = type.GetMethod("DoWork");
method?.Invoke(obj, null);

// Late binding via virtual methods — runtime dispatch
Animal animal = new Dog();
animal.Speak(); // Dog.Speak() resolved at runtime (but type-checked at compile time)
FeatureEarly BindingLate Binding
ResolutionCompile timeRuntime
PerformanceFaster (direct call)Slower (lookup overhead)
Type safetyCaught at compile timeRuntime errors possible
IDE supportFull IntelliSenseNone
ExamplesNormal method calls, overloaded methodsdynamic, reflection, virtual dispatch
Prefer early binding

Use early binding whenever possible for type safety, performance, and IDE support. Only use late binding (dynamic, reflection) when the type is genuinely unknown at compile time (e.g., COM interop, plugin systems).

Collections & LINQ

Q13: How to create a date with a specific timezone?

Use TimeZoneInfo to convert a DateTime to a specific timezone, or use DateTimeOffset for unambiguous point-in-time values:

// Using TimeZoneInfo
var tz = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time"); // Windows ID
var utcNow = DateTime.UtcNow;
var localDate = TimeZoneInfo.ConvertTimeFromUtc(utcNow, tz);

// Using DateTimeOffset — stores date + time + offset
var dto = new DateTimeOffset(2025, 6, 15, 10, 30, 0, TimeSpan.FromHours(7)); // UTC+7
Console.WriteLine(dto); // 6/15/2025 10:30:00 AM +07:00
Console.WriteLine(dto.UtcDateTime); // 6/15/2025 3:30:00 AM

// Cross-platform timezone IDs (IANA) — use TimeZoneInfo on .NET 6+
var ianaTz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Ho_Chi_Minh");
Prefer DateTimeOffset over DateTime

DateTime alone is ambiguous — you don't know if it's local, UTC, or unspecified. DateTimeOffset always carries the offset, making it safe for serialization, APIs, and cross-timezone scenarios.


Q14: How to change the current culture?

Use CultureInfo.CurrentCulture for formatting/parsing and CultureInfo.CurrentUICulture for resource lookup:

using System.Globalization;

// Change culture for the current thread
CultureInfo.CurrentCulture = new CultureInfo("vi-VN");
CultureInfo.CurrentUICulture = new CultureInfo("vi-VN");

Console.WriteLine(1234.56.ToString("N2")); // "1 234,56" (Vietnamese formatting)

// Culture-scoped change (restores after use)
using var _ = new CultureScope(new CultureInfo("en-US"));
Console.WriteLine(1234.56.ToString("N2")); // "1,234.56"

// Simple helper
public class CultureScope : IDisposable
{
private readonly CultureInfo _original = CultureInfo.CurrentCulture;
public CultureScope(CultureInfo culture) => CultureInfo.CurrentCulture = culture;
public void Dispose() => CultureInfo.CurrentCulture = _original;
}

Q15: What is the difference between HashSet<T> and Dictionary<TKey, TValue>?

FeatureHashSet<T>Dictionary<TKey, TValue>
StoresUnique values onlyKey-value pairs
LookupBy value (O(1) average)By key (O(1) average)
DuplicatesSilently ignoredThrows on duplicate key
Use caseMembership testing, set operationsMapping keys to values
// HashSet — unique items, set operations
var tags = new HashSet<string> { "csharp", "dotnet", "linq" };
tags.Add("csharp"); // ignored — already exists
tags.Add("async"); // added
bool has = tags.Contains("dotnet"); // true

// Dictionary — key → value mapping
var ages = new Dictionary<string, int>
{
["Alice"] = 30,
["Bob"] = 25
};
ages["Alice"] = 31; // update existing
ages["Charlie"] = 28; // add new

Q16: What is the purpose of the ToLookup method?

ToLookup creates a one-to-many dictionary (ILookup<TKey, TElement>) where each key can map to multiple values. Unlike GroupBy (deferred), ToLookup executes immediately:

var students = new[]
{
new { Name = "Alice", Grade = "A" },
new { Name = "Bob", Grade = "B" },
new { Name = "Charlie", Grade = "A" },
new { Name = "Diana", Grade = "B" },
};

// ToLookup — immediate execution, one-to-many
ILookup<string, string> byGrade = students.ToLookup(s => s.Grade, s => s.Name);

foreach (var group in byGrade)
{
Console.WriteLine($"Grade {group.Key}: {string.Join(", ", group)}");
}
// Grade A: Alice, Charlie
// Grade B: Bob, Diana

// Lookup by key — returns empty sequence if key not found (no KeyNotFoundException)
var gradeC = byGrade["C"]; // empty sequence, not an error
FeatureGroupByToLookup
ExecutionDeferredImmediate
Return typeIEnumerable<IGrouping>ILookup<TKey, TElement>
Re-enumerationRe-executes queryNo (cached)
Missing keyN/AReturns empty sequence

Q17: Does LINQ Cast<T> method create a new object?

No. Cast<T> performs reference conversion only — it casts each element to T without creating new objects. If the cast fails at runtime, it throws InvalidCastException.

var list = new ArrayList { "a", "b", "c" };

// Cast<string> — no new objects, just reference casts
IEnumerable<string> strings = list.Cast<string>();

// This throws — can't cast int to string
var mixed = new ArrayList { "a", 1, "b" };
var result = mixed.Cast<string>().ToList(); // InvalidCastException at 1
Use OfType<T> to filter instead of throw

OfType<T> filters out elements that can't be cast, instead of throwing:

var mixed = new ArrayList { "a", 1, "b", 2 };
var strings = mixed.OfType<string>().ToList(); // ["a", "b"] — no exception

Q18: Explain deferred execution in LINQ

Deferred execution means the query is not evaluated until you actually enumerate over it. Most LINQ operators (Where, Select, OrderBy, GroupBy) are deferred. Operators like ToList(), ToArray(), Count(), First() force immediate execution.

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Deferred — query defined but NOT executed
var query = numbers.Where(n =>
{
Console.WriteLine($"Filtering {n}");
return n > 2;
});

// Nothing printed yet — no execution

// Immediate — ToList() forces enumeration
var result = query.ToList();
// Filtering 1, Filtering 2, Filtering 3, Filtering 4, Filtering 5

// Each enumeration re-executes the query!
numbers.Add(6);
var result2 = query.ToList(); // Filters all 6 elements again
Re-execution trap

Deferred queries re-evaluate on every enumeration. If the source changes between enumerations, you get different results. Cache with ToList() or ToArray() if you need stable results.


Q19: How does ImmutableList work?

ImmutableList<T> is a collection that never modifies itself — every "mutation" returns a new instance while sharing unchanged data internally (structural sharing):

var list = ImmutableList.Create(1, 2, 3);

// Each operation returns a NEW list — original is unchanged
var list2 = list.Add(4); // list2 = [1, 2, 3, 4], list = [1, 2, 3]
var list3 = list2.Remove(2); // list3 = [1, 3, 4], list2 = [1, 2, 3, 4]
var list4 = list3.SetItem(0, 99); // list4 = [99, 3, 4]

// Builder — efficient for bulk mutations
var builder = list.ToBuilder();
builder.Add(4);
builder.Add(5);
var result = builder.ToImmutable(); // [1, 2, 3, 4, 5]
FeatureList<T>ImmutableList<T>
MutabilityMutableImmutable (returns new)
Add/RemoveO(1) amortizedO(log n) — tree structure
Thread safetyNoNaturally thread-safe
Use caseGeneral purposeFunctional pipelines, shared state
Use ImmutableArray<T> for read-heavy, small collections — it has O(1) read access (contiguous memory). Use ImmutableList<T> when you need efficient "mutation" via structural sharing.

Q20: What is the difference between Array.CopyTo() and Array.Clone()?

Both copy array elements, but differ in destination and depth:

FeatureCopyTo()Clone()
DestinationCopies into an existing arrayCreates a new array
Starting indexCan specify start indexAlways starts at 0
Return typevoidobject (must cast)
Array must existYes (must be pre-allocated)No (creates new)
int[] source = [1, 2, 3, 4, 5];

// Clone() — creates a new array, returns object (shallow copy)
int[] cloned = (int[])source.Clone();
// cloned = [1, 2, 3, 4, 5] — new array

// CopyTo() — copies into an existing array
int[] target = new int[5];
source.CopyTo(target, 0);
// target = [1, 2, 3, 4, 5]

// CopyTo with offset
int[] target2 = new int[7];
source.CopyTo(target2, 2);
// target2 = [0, 0, 1, 2, 3, 4, 5] — starts at index 2
Both perform shallow copies — for reference types, the references are copied, not the objects themselves. Use LINQ Select(x => ...) or manual deep-copy logic when you need a true deep copy.

Q21: What is the difference between Array and ArrayList?

FeatureArrayArrayList
Type safetyStrongly typed (T[])Stores object (no type safety)
BoxingNoYes (value types boxed to object)
SizeFixed at creationDynamic (auto-resizes)
NamespaceSystemSystem.Collections
PerformanceFaster (no casting/boxing)Slower (casting + boxing)
Modern alternativeT[]List<T>
// Array — type-safe, fixed size
int[] arr = new int[3];
arr[0] = 1;
// arr[0] = "text"; // Compile error — type safe

// ArrayList — stores object, dynamic size
ArrayList list = new ArrayList();
list.Add(1); // boxing: int → object
list.Add("text"); // mixed types allowed (no type safety)
int val = (int)list[0]; // unboxing: object → int

// Modern replacement — List<T>
List<int> generic = new List<int>();
generic.Add(1);
// generic.Add("text"); // Compile error — type safe
Always use List<T> over ArrayList. ArrayList is a legacy type from before generics (C# 1.x). List<T> provides type safety, avoids boxing, and has better performance.

Q22: What is the difference between SortedList and SortedDictionary?

Both maintain key-value pairs sorted by key, but with different internal structures and performance characteristics:

FeatureSortedList<TKey, TValue>SortedDictionary<TKey, TValue>
Internal structureSorted array (contiguous)Red-black tree
MemoryLess memory (compact array)More memory (tree nodes)
Lookup by keyO(log n) — binary searchO(log n) — tree traversal
Insert (unsorted data)O(n) — array shiftO(log n) — tree rebalance
Insert (already sorted)O(1) amortized — appendO(log n)
Retrieve min/maxO(1) — first/last elementO(log n)
EnumerationFast (contiguous memory)Slower (tree traversal)
// SortedList — compact, fast for lookups on static data
var sortedList = new SortedList<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Keys are always sorted: Alice, Bob, Charlie

// SortedDictionary — efficient inserts for dynamic data
var sortedDict = new SortedDictionary<string, int>
{
["Charlie"] = 3,
["Alice"] = 1,
["Bob"] = 2
};
// Same result, but faster for frequent insertions
Use SortedList when you populate once and query often (e.g., lookup tables). Use SortedDictionary when data is frequently inserted/removed. For most cases, a regular Dictionary with OrderBy at query time is simpler and sufficient.

Modern C# Types

Q23: Explain Inheritance vs Composition

Inheritance ("is-a") creates a subtype relationship. Composition ("has-a") builds types by combining smaller, focused components.

// Inheritance — "Worker is a Person"
public class Person { public string Name { get; set; } }
public class Worker : Person { public string Company { get; set; } }

// Composition — "Car has an Engine"
public class Engine { public int HorsePower { get; set; } }
public class Car
{
private readonly Engine _engine; // Car HAS an Engine
public Car(Engine engine) => _engine = engine;
}

Prefer composition because:

  • Avoids deep, fragile inheritance hierarchies
  • Supports runtime behavior changes (swap components)
  • Follows Single Responsibility Principle
  • Enables multiple "capabilities" via composition (vs single inheritance)
Composition over Inheritance

Inheritance should model true "is-a" relationships. If the relationship is "has-a" or "can-do", use composition with interfaces.


Q24: Difference between class, record, and struct

Featureclassrecordstruct
TypeReference (heap)Reference (heap)Value (stack)
MutabilityMutable by defaultImmutable by defaultMutable by default
EqualityReference equalityValue-based equalityValue-based equality
InheritanceYes (single)Yes (single)No (can implement interfaces)
with expressionNoYesYes (C# 10+)
Auto-generatesNothingEquals, GetHashCode, ToString, withNothing
// class — reference type, reference equality
public class PointClass { public int X { get; set; } public int Y { get; set; } }

// record — reference type, value-based equality, immutable-friendly
public record PointRecord(int X, int Y);

// struct — value type, stack-allocated
public struct PointStruct { public int X { get; set; } public int Y { get; set; } }

var r1 = new PointRecord(1, 2);
var r2 = new PointRecord(1, 2);
Console.WriteLine(r1 == r2); // True — value-based equality
Console.WriteLine(ReferenceEquals(r1, r2)); // False — different references

var c1 = new PointClass { X = 1, Y = 2 };
var c2 = new PointClass { X = 1, Y = 2 };
Console.WriteLine(c1 == c2); // False — reference equality

Q25: What are ref struct used for?

ref struct is a struct that is guaranteed to live only on the stack — it can never escape to the heap. This enables safe, zero-allocation operations with pointers and spans:

public ref struct SpanReader<T>
{
private readonly ReadOnlySpan<T> _span;
private int _position;

public SpanReader(ReadOnlySpan<T> span)
{
_span = span;
_position = 0;
}

public bool TryRead(out T value)
{
if (_position >= _span.Length) { value = default; return false; }
value = _span[_position++];
return true;
}
}

Restrictions — a ref struct cannot:

  • Be boxed (no casting to object, dynamic, or interface)
  • Be a field of a regular class
  • Implement interfaces
  • Be used in async methods or lambdas (unless ref-safe)
  • Be captured by closures

The primary use case is Span<T> and ReadOnlySpan<T> — high-performance, zero-allocation slicing over arrays, native memory, or stackalloc buffers.


Q26: Name the two forms of records

  1. Positional record — primary constructor defines properties:
public record Person(string Name, int Age);

// Compiler auto-generates: init-only properties, constructor, Deconstruct()
var p = new Person("Alice", 30);
var (name, age) = p; // Deconstruct
  1. Record with standard syntax — you define properties manually:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}

Both forms produce the same compiler-generated Equals, GetHashCode, ToString, and with support. The positional form is more concise; the standard form gives you more control over property definitions.


Q27: What is the with keyword used for?

with creates a copy of a record (or struct, C# 10+) with one or more properties changed — non-destructive mutation:

public record Person(string Name, int Age, string City);

var alice = new Person("Alice", 30, "Hanoi");
var bob = alice with { Name = "Bob", Age = 25 };

Console.WriteLine(alice); // Person { Name = Alice, Age = 30, City = Hanoi }
Console.WriteLine(bob); // Person { Name = Bob, Age = 25, City = Hanoi }

Under the hood, the compiler generates a copy constructor and then applies the with assignments. The original object is never modified.


Q28: What is the purpose of Primary Constructors?

Primary constructors (C# 12) let you declare constructor parameters directly on the class/struct declaration. Parameters are available throughout the class body:

// Before C# 12 — explicit constructor + field
public class Service
{
private readonly IRepository _repo;
private readonly ILogger _logger;

public Service(IRepository repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
}

// C# 12 — primary constructor
public class Service(IRepository repo, ILogger logger)
{
public void DoWork()
{
repo.GetAll(); // available everywhere
logger.Log("Working");
}
}
Primary constructor parameters are not stored as fields

If you need to capture them (e.g., for lazy initialization or mutable operations), assign them to explicit fields or properties. Parameters that are only used during initialization are fine as-is.


Q29: Explain how Nullable Reference Types work

Nullable Reference Types (NRT), enabled in C# 8+, make reference type nullability explicit at compile time. The compiler warns when a non-nullable reference might be null:

#nullable enable

string name = "Alice"; // non-nullable — cannot be null
string? nickname = null; // nullable — can be null

// Compiler warnings
name = null; // Warning: Converting null to non-nullable type
int length = nickname.Length; // Warning: Possible null reference

// Defensive checks
if (nickname is not null)
length = nickname.Length; // OK — compiler knows it's not null

// Null-forgiving operator (!)
length = nickname!.Length; // "Trust me, it's not null" — suppresses warning

Key points:

  • NRT is a compile-time feature — no runtime behavior changes
  • Enabled per-project via <Nullable>enable</Nullable> in .csproj
  • Method signatures communicate intent: string Get() vs string? Find()

Q30: Do switch expressions have any return type limitations?

No. Switch expressions can return any type, but all arms must return the same type (or a type that all arms can be implicitly converted to):

// Returns string
string category = age switch
{
< 13 => "Child",
< 20 => "Teenager",
< 65 => "Adult",
_ => "Senior"
};

// Returns double
double discount = customerType switch
{
"VIP" => 0.3,
"Regular" => 0.1,
_ => 0.0
};

// Returns object (common base type)
object result = input switch
{
int i => i,
string s => s,
bool b => b,
_ => "unknown"
};

The compiler infers the return type from all arms. If arms return incompatible types, it's a compile error.


Q31: What is yield return used for?

yield return produces a value lazily — items are generated one at a time as the caller enumerates, without creating a full collection upfront:

// Eager — builds entire list in memory
public static List<int> GetEvens(int max)
{
var result = new List<int>();
for (int i = 0; i <= max; i++)
if (i % 2 == 0) result.Add(i);
return result;
}

// Lazy — yields one at a time
public static IEnumerable<int> GetEvensLazy(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0) yield return i;
}

// Usage — only computes values as needed
foreach (var n in GetEvensLazy(1_000_000).Take(5))
Console.WriteLine(n); // 0, 2, 4, 6, 8 — never computes all 1M

yield break exits the iterator early (equivalent to return from a normal method). The compiler transforms yield return methods into state machines.

Advanced C#

Q32: How many generations does the Garbage Collector have?

The .NET GC has 3 generations (0, 1, 2) plus the Large Object Heap (LOH):

GenerationWhat it holdsCollection frequencyCost
Gen 0Short-lived objectsMost frequentCheapest
Gen 1Survived Gen 0ModerateModerate
Gen 2Long-lived objectsLeast frequentMost expensive
LOHObjects >= 85,000 bytesWith Gen 2Very expensive
// Short-lived → Gen 0, collected quickly
void Process()
{
var temp = new byte[100]; // Gen 0
} // eligible for Gen 0 collection

// Long-lived → survives to Gen 2
static byte[] cache = new byte[100]; // promoted to Gen 2

// Large → LOH (85KB+)
var large = new byte[100_000]; // goes directly to LOH

How promotion works: Objects that survive a Gen 0 collection are promoted to Gen 1. Gen 1 survivors move to Gen 2. The GC uses a generational hypothesis — new objects die young, so collecting Gen 0 is fast and reclaims the most memory.


Q33: What is the Interlocked class used for?

Interlocked provides atomic, thread-safe operations on variables — without explicit locks. It's faster than lock for simple numeric operations:

private static int _counter = 0;

// Thread-safe increment (atomic)
Interlocked.Increment(ref _counter); // _counter++
Interlocked.Decrement(ref _counter); // _counter--

// Atomic add
Interlocked.Add(ref _counter, 5);

// Atomic exchange (set and return old value)
int old = Interlocked.Exchange(ref _counter, 0); // reset and get old value

// Compare-and-swap (CAS)
int expected = 10;
Interlocked.CompareExchange(ref _counter, 20, expected); // if _counter==10, set to 20
Use Interlocked for simple atomic operations (increment, exchange). For complex multi-step critical sections, use lock or SemaphoreSlim.

Q34: What is the code generated by compiler for auto properties?

An auto-property like public string Name { get; set; } is expanded by the compiler into a property with a hidden backing field:

// What you write
public class Person
{
public string Name { get; set; }
public int Age { get; init; }
}

// What the compiler generates (simplified IL → C# equivalent)
public class Person
{
// Hidden backing fields — names are compiler-generated
[CompilerGenerated]
private string <Name>k__BackingField;
[CompilerGenerated]
private int <Age>k__BackingField;

public string Name
{
get => <Name>k__BackingField;
set => <Name>k__BackingField = value;
}

public int Age
{
get => <Age>k__BackingField;
init => <Age>k__BackingField = value; // init-only setter
}
}

The backing field names include angle brackets to prevent naming conflicts with user code.


Q35: How is Polymorphism implemented in C#?

Polymorphism in C# is achieved through:

  1. Subtype polymorphismvirtual/override (runtime dispatch):
public class Animal
{
public virtual string Speak() => "...";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
Animal a = new Dog();
a.Speak(); // "Woof!" — runtime dispatches to Dog's implementation
  1. Interface polymorphism — multiple types implement the same interface:
public interface IDrawable { void Draw(); }
public class Circle : IDrawable { public void Draw() => Console.WriteLine("Circle"); }
public class Square : IDrawable { public void Draw() => Console.WriteLine("Square"); }

void Render(IDrawable shape) => shape.Draw(); // works for any IDrawable
  1. Ad-hoc polymorphism — method overloading (compile-time):
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;

Q36: How is Encapsulation implemented in C#?

Encapsulation is achieved by hiding internal state with access modifiers and exposing controlled access through properties and methods:

public class BankAccount
{
private decimal _balance; // hidden internal state

public decimal Balance => _balance; // read-only exposure

public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Must be positive");
_balance += amount;
}

public bool Withdraw(decimal amount)
{
if (amount > _balance) return false;
_balance -= amount;
return true;
}
}

Mechanisms: private fields, public properties, private set, init setters, readonly fields, and file-scoped types.


Q37: What is the difference between ref and out parameters?

Both pass arguments by reference, but differ in initialization requirements:

Featurerefout
Caller must initializeYes — before callingNo
Method must assignNo (can read existing value)Yes — must assign before returning
Use casePass existing value for modificationReturn multiple values
// ref — caller initializes, method can modify
void Double(ref int x) => x *= 2;

int num = 5;
Double(ref num);
Console.WriteLine(num); // 10

// out — method must assign
bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}

if (TryParse("42", out int value))
Console.WriteLine(value); // 42

Q38: How does the using statement work?

using ensures Dispose() is called on an IDisposable object, even if an exception occurs:

// Classic using statement
using (var stream = new FileStream("data.txt", FileMode.Open))
{
// use stream
} // stream.Dispose() called here — guaranteed

// Using declaration (C# 8+) — Dispose at end of scope
using var stream = new FileStream("data.txt", FileMode.Open);
// use stream...
// Dispose called automatically at end of method/scope

// What the compiler generates:
var stream = new FileStream("data.txt", FileMode.Open);
try
{
// use stream
}
finally
{
stream?.Dispose();
}

await using works the same way for IAsyncDisposable:

await using var resource = new AsyncResource();
// DisposeAsync called at end of scope

Q39: What is a delegate and how is it used?

A delegate is a type-safe function pointer — it holds a reference to a method with a matching signature:

// Define delegate type
public delegate int MathOp(int a, int b);

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

// Usage
MathOp op = Add;
Console.WriteLine(op(3, 4)); // 7

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

// Multicast — combine multiple delegates
MathOp combined = (a, b) => a + b;
combined += (a, b) => a * b;
// Note: multicast returns the LAST result

Built-in delegates:

  • Action — void return, up to 16 parameters
  • Func<T> — typed return, up to 16 parameters
  • Predicate<T> — returns bool
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

Q40: Explain method overloading and overriding

Overloading — same method name, different parameters (compile-time polymorphism):

public int Sum(int a, int b) => a + b;
public double Sum(double a, double b) => a + b;
public int Sum(int a, int b, int c) => a + b + c;

Overriding — replacing a virtual/abstract base method in a derived class (runtime polymorphism):

public class Shape
{
public virtual double Area() => 0;
}

public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}

// Runtime polymorphism
Shape shape = new Circle { Radius = 5 };
Console.WriteLine(shape.Area()); // 78.54 — calls Circle.Area()
FeatureOverloadingOverriding
BindingCompile-timeRuntime
Same nameYesYes
ParametersMust differMust match exactly
KeywordNonevirtual + override
RelationshipSame classBase → Derived

Q41: Difference between IEnumerable<T> and IQueryable<T>

FeatureIEnumerable<T>IQueryable<T>
NamespaceSystem.Collections.GenericSystem.Linq
ExecutionIn-memory (client-side)Translatable to remote (e.g., SQL)
FilteringDone in C# after loading dataTranslated to SQL — filtered at database
Best forIn-memory collections (Lists, Arrays)Database queries (EF Core)
// IEnumerable — loads ALL products, then filters in memory
IEnumerable<Product> enumerable = context.Products;
var result = enumerable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products (loads everything!)

// IQueryable — filter is translated to SQL
IQueryable<Product> queryable = context.Products;
var result = queryable.Where(p => p.Price > 100).ToList();
// SQL: SELECT * FROM Products WHERE Price > 100 (filtered at DB)
Always use IQueryable for database queries to avoid loading unnecessary data into memory.

Q42: What are expression trees in LINQ?

Expression trees represent code as data structures (trees) that can be inspected, modified, and translated at runtime. They power IQueryable<T> — LINQ providers translate expression trees into SQL, REST API calls, etc.

// Lambda as a delegate — executable code
Func<int, bool> isLarge = x => x > 100;

// Lambda as an expression tree — data structure describing the code
Expression<Func<int, bool>> isLargeExpr = x => x > 100;

// Inspect the tree
Console.WriteLine(isLargeExpr.Body); // "x > 100"
Console.WriteLine(isLargeExpr.Parameters[0]); // "x"

// Compile and execute
Func<int, bool> compiled = isLargeExpr.Compile();
Console.WriteLine(compiled(150)); // True

EF Core uses expression trees to translate Where(p => p.Price > 100) into WHERE Price > 100 in SQL — the lambda is never executed as C# code.


Q43: How does exception handling work in C#?

C# uses a structured exception handling model with try, catch, finally, and throw:

try
{
var result = int.Parse("abc"); // throws FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
{
// Exception filter — catches only if condition is true
Console.WriteLine("Specific error");
}
finally
{
// Always runs — with or without exception
Console.WriteLine("Cleanup");
}

Key points:

  • Exceptions bubble up the call stack until caught
  • finally always executes (even with return in try/catch)
  • Exception filters (when) let you inspect without catching
  • Most specific exceptions should be caught first

Q44: Name all the ways to rethrow an exception

  1. throw; — preserves the original stack trace (preferred):
catch (Exception)
{
Log("Something failed");
throw; // preserves original stack trace
}
  1. throw ex; — resets the stack trace to this point (avoid):
catch (Exception ex)
{
Log("Something failed");
throw ex; // WARNING: stack trace starts here — original location lost
}
  1. throw new Exception("message", innerEx); — wrap with context:
catch (SqlException ex)
{
throw new DataAccessException("Failed to load user", ex);
// Original exception preserved as InnerException
}
Always use throw; (bare throw) to preserve the original stack trace. Use throw ex only when you intentionally want to hide the original source. Always wrap with inner exception when adding context.

Q45: Explain generics

Generics allow you to write type-parameterized classes, methods, and interfaces — one implementation works for any type while maintaining type safety:

// Generic class
public class Repository<T> where T : class, new()
{
private readonly List<T> _items = new();

public void Add(T item) => _items.Add(item);
public T FindById(int id) => _items[id];
}

// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;

// Generic interface
public interface IService<T>
{
T GetById(int id);
void Save(T entity);
}

// Usage — type-safe, no boxing
var userRepo = new Repository<User>();
userRepo.Add(new User { Name = "Alice" });

int max = Max(10, 20); // inferred type: int
string maxStr = Max("a", "z"); // inferred type: string

Common constraints:

ConstraintMeaning
where T : classT must be a reference type
where T : structT must be a value type
where T : new()T must have a parameterless constructor
where T : BaseClassT must derive from BaseClass
where T : IInterfaceT must implement IInterface
where T : unmanagedT must be an unmanaged type

Q46: What is the difference between Dispose() and Finalize()?

Both clean up resources, but differ fundamentally in when and how they are called:

FeatureDispose()Finalize() (Destructor)
Called byExplicitly by developer (using, direct call)Automatically by GC before collection
TimingDeterministic — you control whenNon-deterministic — whenever GC runs
InterfaceIDisposableLanguage syntax (~ClassName())
PerformanceNo GC overheadPromotes object to older generation
Unmanaged resourcesAlways clean up hereSafety net only (if Dispose wasn't called)
Multiple callsMust be idempotent (safe to call multiple times)Called only once by GC
public class DatabaseConnection : IDisposable
{
private IntPtr _handle;
private bool _disposed;

public DatabaseConnection(string connStr)
{
_handle = NativeOpenConnection(connStr);
}

// Dispose — called by developer (deterministic cleanup)
public void Dispose()
{
if (_disposed) return;
NativeCloseConnection(_handle);
_handle = IntPtr.Zero;
_disposed = true;
GC.SuppressFinalize(this); // Prevent finalizer from running
}

// Finalizer — safety net, called by GC (non-deterministic)
~DatabaseConnection()
{
Dispose(); // Delegate to Dispose
}
}

// Usage with using — Dispose called at scope end
using var conn = new DatabaseConnection("Server=...");
// conn.Dispose() called automatically
Always use Dispose() via using statements. Implement a finalizer only as a safety net for unmanaged resources. Call GC.SuppressFinalize(this) in Dispose to prevent the GC from calling the finalizer unnecessarily.

Q47: What is the Singleton design pattern?

Singleton ensures a class has only one instance and provides a global access point to it. Common use cases: configuration, logging, caching.

// Thread-safe Singleton with Lazy<T> (recommended)
public sealed class DatabaseConfig
{
// Lazy<T> ensures thread-safe, lazy initialization
private static readonly Lazy<DatabaseConfig> _instance =
new(() => new DatabaseConfig());

public static DatabaseConfig Instance => _instance.Value;

private DatabaseConfig() // Private — prevents external instantiation
{
ConnectionString = "Server=localhost;Database=MyApp;";
}

public string ConnectionString { get; }
}

// Usage
var config = DatabaseConfig.Instance;
Console.WriteLine(config.ConnectionString);

Other implementation approaches:

ApproachThread-SafeLazyNotes
No lockNoYesSimple, but not thread-safe
lock everywhereYesYesPerformance overhead on every access
Double-check lockingYesYesComplex, error-prone
Lazy<T>YesYesRecommended — simple and correct
Static initializerYesNoInstance created at program start
// Alternative: Static initializer (simplest thread-safe approach)
public sealed class Singleton
{
public static readonly Singleton Instance = new();

// Static constructor ensures laziness (runs before first access)
static Singleton() { }

private Singleton() { }
}
Consider whether you truly need a Singleton. Often, dependency injection with a singleton lifetime (services.AddSingleton<T>()) is a better approach — it's testable, explicit, and follows IoC principles.

Q48: Why can't a private virtual method be overridden?

A private virtual method is a contradiction because:

  1. private means the member is only accessible within the declaring class — derived classes cannot see it.
  2. virtual means the member is designed to be overridden in derived classes — but derived classes can't access what's private.
public class Base
{
// Compile error: virtual or abstract members cannot be private
// private virtual void DoWork(); // ERROR!

// Correct: protected virtual — accessible to derived classes
protected virtual void DoWork() { }
}

public class Derived : Base
{
protected override void DoWork() { } // OK
}

The compiler enforces this — a private virtual method is inherently contradictory and produces a compile-time error.

Concurrency

Q49: What are the benefits of using Frozen collections?

FrozenSet<T> and FrozenDictionary<TKey, TValue> (.NET 8+) are immutable collections optimized for fast reads after an initial freeze phase. Once created, they never change, allowing the runtime to optimize lookups:

using System.Collections.Frozen;

var data = new Dictionary<string, int>
{
["apple"] = 1, ["banana"] = 2, ["cherry"] = 3
};

// Freeze — one-time cost, optimizes for read performance
FrozenDictionary<string, int> frozen = data.ToFrozenDictionary();

// Ultra-fast lookups — no locks, no version checks
if (frozen.TryGetValue("apple", out int value))
Console.WriteLine(value); // 1

// FrozenSet
var tags = new[] { "csharp", "dotnet", "linq" }.ToFrozenSet();
bool has = tags.Contains("csharp"); // optimized path
BenefitDetails
Zero allocation on readNo defensive copies, no version checks
Optimized internalsMay use perfect hashing, compact arrays
Thread-safeImmutable — safe to share across threads
Best forConfiguration data, lookup tables, constants
Use frozen collections when you build a collection once and query it many times (especially in hot paths). The freeze cost pays off through faster lookups.

Q50: Name thread-safe collections

System.Collections.Concurrent provides thread-safe collections that use lock-free or fine-grained locking:

CollectionDescriptionUse Case
ConcurrentDictionary<TKey, TValue>Thread-safe dictionaryShared cache, counters
ConcurrentQueue<T>Thread-safe FIFOProducer-consumer
ConcurrentStack<T>Thread-safe LIFOWork stealing
ConcurrentBag<T>Unordered, optimized for same threadThread-local work pools
BlockingCollection<T>Bounded, blocking wrapperProducer-consumer with limits
var cache = new ConcurrentDictionary<string, string>();
cache.TryAdd("key1", "value1");
cache.AddOrUpdate("key1", "new", (k, old) => $"{old}_updated");

var queue = new ConcurrentQueue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
if (queue.TryDequeue(out int item))
Console.WriteLine(item); // 1

Q51: How to perform a lock for asynchronous code?

You cannot use lock with await (lock doesn't support async). Use SemaphoreSlim instead:

// WRONG — lock statement cannot be used with await
// lock (_sync) { await DoWorkAsync(); } // Compile error!

// CORRECT — SemaphoreSlim
private readonly SemaphoreSlim _semaphore = new(1, 1);

public async Task DoWorkAsync()
{
await _semaphore.WaitAsync();
try
{
await SomeAsyncOperation();
}
finally
{
_semaphore.Release();
}
}

// C# pattern — use await using with a custom async lock
public sealed class AsyncLock
{
private readonly SemaphoreSlim _sem = new(1, 1);
public Task<IDisposable> LockAsync() =>
_sem.WaitAsync().ContinueWith(_ => (IDisposable)new Releaser(_sem));
private sealed class Releaser(SemaphoreSlim sem) : IDisposable
{
public void Dispose() => sem.Release();
}
}

Q52: Name all the ways for creating a new thread

// 1. Thread class — OS thread ( heavyweight)
var thread = new Thread(() => Console.WriteLine("New thread"));
thread.Start();

// 2. Thread pool — reuses threads (preferred)
ThreadPool.QueueUserWorkItem(_ => Console.WriteLine("Thread pool work"));

// 3. Task.Run — thread pool via TPL (most common)
Task.Run(() => Console.WriteLine("Task on thread pool"));

// 4. Task.Factory.StartNew — more options (long-running, cancellation)
Task.Factory.StartNew(() => Console.WriteLine("Custom task"),
TaskCreationOptions.LongRunning);

// 5. async/await — not a new thread, but offloads to thread pool
await Task.Run(() => Compute());

// 6. BackgroundWorker — legacy (WinForms/WPF)
var worker = new BackgroundWorker();
worker.DoWork += (s, e) => { /* background work */ };
worker.RunWorkerAsync();
Prefer Task.Run over new Thread. Tasks use the thread pool (efficient reuse), support cancellation, continuation, and exception handling. Use new Thread only when you need a dedicated OS thread (e.g., STA thread for COM interop).

Q53: How to execute multiple async tasks at once?

Use Task.WhenAll to run tasks concurrently and wait for all to complete:

var urls = new[] { "api1", "api2", "api3" };

// Concurrent execution — all tasks start at once
var tasks = urls.Select(url => FetchAsync(url)).ToArray();
string[] results = await Task.WhenAll(tasks);

// With result aggregation
var userIds = new[] { 1, 2, 3, 4, 5 };
var users = await Task.WhenAll(userIds.Select(id => GetUserAsync(id)));

// Task.WhenAll vs Task.WhenAny
await Task.WhenAll(tasks); // Wait for ALL to complete
Task<string> first = await Task.WhenAny(tasks); // Wait for FIRST to complete

Error handling:

try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Only the FIRST exception is caught
// To see all exceptions:
}

// Access all exceptions
var allResults = await Task.WhenAll(tasks.Select(t => TaskSafe(t)));

async Task<TResult> TaskSafe<TResult>(Task<TResult> task)
{
try { return await task; }
catch { return default; }
}

Q54: What is the difference between AutoResetEvent and ManualResetEvent?

Both are thread synchronization primitives that block threads until a signal is received:

FeatureAutoResetEventManualResetEvent
After Set()Automatically resets to unsignaledStays signaled until manually reset
Releases threadsOne thread per Set()All waiting threads
Use caseProducer-consumer (one at a time)Gate/door (release all at once)
// AutoResetEvent — releases ONE waiting thread, then auto-resets
var are = new AutoResetEvent(false);

// Thread 1
are.WaitOne(); // blocks until Set() is called
are.Set(); // releases ONE thread, then resets to unsignaled

// ManualResetEvent — releases ALL waiting threads, stays open
var mre = new ManualResetEvent(false);

mre.Set(); // releases ALL waiting threads — stays signaled
mre.Reset(); // manually close the gate

Q55: What are Channels in C#?

Channel<T> (System.Threading.Channels) is a high-performance, async-friendly producer-consumer queue — a modern alternative to BlockingCollection<T>:

using System.Threading.Channels;

// Unbounded — no capacity limit
var channel = Channel.CreateUnbounded<string>();

// Bounded — capacity limit with backpressure strategy
var bounded = Channel.CreateBounded<string>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait // block when full
});

// Producer
await channel.Writer.WriteAsync("message1");
channel.Writer.TryComplete(); // signal no more writes

// Consumer
await foreach (var msg in channel.Reader.ReadAllAsync())
{
Console.WriteLine(msg);
}
FeatureChannel<T>BlockingCollection<T>
Async supportNative (ReadAllAsync)Blocking only
PerformanceLock-free, optimizedLock-based
BackpressureYes (bounded + FullMode)Yes (bounded)
.NET version.NET Core 3.0+.NET Framework

Q56: Difference between volatile and Interlocked

Both address thread-safety issues, but at different levels:

FeaturevolatileInterlocked
What it doesPrevents CPU cache/read reorderProvides atomic operations
OperationsRead/Write onlyIncrement, Add, Exchange, CompareExchange
Use caseFlag checkingCounters, atomic swaps
// volatile — ensures reads/writes are not reordered by CPU/compiler
private volatile bool _running = true;

// Thread 1
while (_running) { DoWork(); }

// Thread 2
_running = false; // guaranteed to be visible immediately

// Interlocked — atomic increment (thread-safe counter)
private int _count = 0;
Interlocked.Increment(ref _count); // atomic _count++
volatile does NOT make ++ or += atomic. Those are read-modify-write operations — use Interlocked.Increment instead.

Q57: Explain how Async Streams work

Async streams (IAsyncEnumerable<T>, C# 8) let you asynchronously enumerate items one at a time — ideal when each item requires async I/O:

// Producer — yields items asynchronously
public async IAsyncEnumerable<User> GetUsersAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
int page = 0;
while (!ct.IsCancellationRequested)
{
var users = await _api.GetUsersPageAsync(page, ct);
if (users.Count == 0) yield break;

foreach (var user in users)
yield return user;

page++;
}
}

// Consumer — awaits each item
await foreach (var user in GetUsersAsync())
{
Console.WriteLine(user.Name);
}

// With cancellation
await foreach (var user in GetUsersAsync().WithCancellation(ct))
{
Console.WriteLine(user.Name);
}

When to use: Paginated API calls, database cursor streaming, real-time event feeds — anywhere you need to pull items asynchronously.


Q58: Difference between Task.Run and TaskFactory.StartNew

FeatureTask.RunTaskFactory.StartNew
SimplicitySimple, common caseMore options and control
Default schedulerTaskScheduler.Default (thread pool)Current TaskScheduler
Long-runningNo direct optionTaskCreationOptions.LongRunning
Return typeTask / Task<T>Task (not generic — must cast)
CancellationVia overloadVia overload
// Task.Run — simple, preferred for most cases
var task = Task.Run(() => Compute());

// TaskFactory.StartNew — more control
var task = Task.Factory.StartNew(() =>
{
Compute();
}, CancellationToken.None,
TaskCreationOptions.LongRunning, // dedicated thread
TaskScheduler.Default);
Use Task.Run by default. Only use Task.Factory.StartNew when you need LongRunning, a custom TaskScheduler, or other advanced creation options. Be careful — StartNew returns Task (not Task<T>), so use StartNew<T> for generic tasks.