The C# Fundamentals I'd Not Skip Before a .NET Interview
25 Questions — Jump to a topic
On this page
- 1. What is Encapsulation in C#?
- 2. Expression-Bodied vs Traditional Property Syntax in C#
- 3. Why Doesn’t C# Support Multiple Inheritance?
- 4. Explain Inheritance with a Real-World Example
- 5. How Does protected set Work in C# Properties?
- 6. What Are Generics in C#?
- 7. What Are Boxing and Unboxing in C#?
- 8. What Are Delegates in C#?
- 9. Demonstrate Delegates with a Calculator Example
- 10. Multicast Delegates — Audit Log Pipeline
- 11. The Four Pillars of OOP in C#
- 12. SOLID Principles with C# Code Examples
- 1. Single Responsibility Principle (SRP)
- 2. Open/Closed Principle (OCP)
- 3. Liskov Substitution Principle (LSP)
- 4. Interface Segregation Principle (ISP)
- 5. Dependency Inversion Principle (DIP)
- 13. Value Types vs Reference Types in C#
- 14. string vs StringBuilder in C# — When to Use Which
- 15. How Does Garbage Collection Work in .NET?
- 16. Where Is IDisposable Used in .NET Core?
- 1. The using Statement / Declaration
- 2. Dependency Injection Container
- 3. Asynchronous Cleanup (IAsyncDisposable)
- 17. How Does the Garbage Collector Work Under the Hood?
- 18. IEnumerable vs IQueryable — Which One to Use?
- 19. Does IQueryable Inherit IEnumerable?
- 20. Which .NET Collection Interfaces Inherit IEnumerable?
- 21. ArrayList vs Generic Collections in C#
- 22. Hashtable vs Dictionary vs HashSet in C#
- 1. Hashtable (Legacy, Non-Generic)
- 2. Dictionary<TKey, TValue> (Modern, Generic)
- 3. HashSet<T> vs. Dictionary — The Key Difference
- 23. ref, out, and in Parameter Modifiers in C#
- ref — Pass for Modification
- out — Pass for Output
- in — Pass for High-Performance Read-Only
- 24. Is in Restricted Only to Struct Types?
- 25. Access Modifiers in .NET
Here are the C# fundamentals and OOP concepts I’d never skip before a .NET interview. This post distills a deep-dive chat with Gemini AI into clear question-and-answer pairs covering the topics that interviewers test most — from the four pillars of OOP to generics, delegates, GC internals, collections, and access modifiers. Each answer includes real-world analogies, code examples, and practical takeaways.
1. What is Encapsulation in C#?
Question: Explain encapsulation.
Answer: Encapsulation is the object-oriented programming principle of bundling data (fields) and the methods that operate on that data into a single unit (a class), while restricting direct access to some of the object’s internal components. Think of it as a protective shell around your object’s internal state.
Key Concepts
- Data Hiding: Internal state fields are marked
privateso outside code cannot modify them directly or corrupt the data. - Controlled Access: Access to private data is granted exclusively through controlled public mechanisms (like public getters/setters or methods).
- Validation & Invariants: Methods controlling access can validate input, ensuring an object never enters an invalid state.
Real-World Analogy: A Medical Capsule
Think of a medicine capsule. The active ingredients are enclosed inside the shell. You interact with the medicine as a single pill without touching the chemical powders inside.
Another example is an ATM / Bank Account:
- Without Encapsulation: Anyone could rewrite the balance directly (
account.Balance = 1000000;). - With Encapsulation: The
Balancefield isprivate— you must useDeposit()orWithdraw(), which enforce business rules.
Code Example (C#)
Violating Encapsulation (Unsafe):
public class BankAccount
{
public decimal Balance; // Exposed; anyone can set a negative balance!
}
Applying Encapsulation (Safe):
public class BankAccount
{
private decimal _balance;
public decimal Balance => _balance;
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive.");
_balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > _balance)
throw new InvalidOperationException("Insufficient funds.");
_balance -= amount;
}
}
Core Benefits
-
Maintainability: Internal implementation can change without breaking dependent code.
-
Security & Integrity: Prevents unauthorized manipulation of object states.
-
Flexibility: Allows fields to be read-only, write-only, or subject to business rules.
-
Maintainability: Internal implementation can change without breaking dependent code.
-
Security & Integrity: Prevents unauthorized manipulation of object states.
-
Flexibility: Allows fields to be read-only, write-only, or subject to business rules.
2. Expression-Bodied vs Traditional Property Syntax in C#
Question: Write this in traditional C# code (convert expression-bodied to traditional syntax)?
private decimal _balance;
public decimal Balance => _balance;
Answer: Here is how that expression-bodied property looks using traditional, verbose C# syntax:
private decimal _balance;
public decimal Balance
{
get { return _balance; }
}
What Changed?
- Expression-bodied syntax (
=> _balance) is shorthand introduced in C# 6.0 for read-only properties. - Traditional syntax (
get { return _balance; }) uses explicit braces and thereturnkeyword — standard in C# 1.0–5.0.
3. Why Doesn’t C# Support Multiple Inheritance?
Question: Why doesn’t C# support multiple inheritance?
Answer: C# avoids multiple class inheritance to sidestep the Diamond Problem — the ambiguity and complexity it introduces. Instead, C# enforces single class inheritance while allowing a class to implement multiple interfaces.
Key Reasons
- The Diamond Problem (Ambiguity): When two base classes inherit from a common parent, the derived class faces conflicting method implementations and state duplication.
- Compiler/Runtime Simplicity: Single inheritance gives every object a linear memory layout and simple vtable lookups.
- Maintenance Complexity: Multiple inheritance leads to brittle “spaghetti inheritance” hierarchies.
How C# Solves It — Interfaces:
public abstract class Animal { public abstract void Eat(); }
public interface ISwimmer { void Swim(); }
public interface IFlyer { void Fly(); }
public class Duck : Animal, ISwimmer, IFlyer
{
public override void Eat() => Console.WriteLine("Eating...");
public void Swim() => Console.WriteLine("Swimming...");
public void Fly() => Console.WriteLine("Flying...");
}
- No State Ambiguity: Interfaces declare contracts without instance fields.
- Flexible Composition: One base class + many interfaces = maximum flexibility.
4. Explain Inheritance with a Real-World Example
Question: Explain inheritance with a real-world example.
Answer: Inheritance lets a child class (derived class) automatically acquire the attributes and behaviors of a parent class (base class). It models an “is-a” relationship and enables code reuse.
Real-World Example: Banking Accounts
All accounts share basic characteristics (account number, balance, deposit/withdraw). Rather than writing separate classes from scratch, define one BankAccount parent class. Specialized types inherit from it and add their own rules.
[ BankAccount ] (Base Class)
- AccountNumber
- Balance
+ Deposit()
+ Withdraw()
/ \
/ \
[ SavingsAccount ] [ CheckingAccount ] (Derived Classes)
+ InterestRate + OverdraftLimit
+ AddInterest() + ChargeFee()
Code Implementation (C#)
// 1. The Parent Class — common properties and logic
public class BankAccount
{
public string AccountNumber { get; set; }
public decimal Balance { get; protected set; }
public void Deposit(decimal amount)
{
if (amount > 0) Balance += amount;
}
public virtual void Withdraw(decimal amount)
{
if (amount <= Balance) Balance -= amount;
}
}
// 2. The Child Class — inherits and extends
public class SavingsAccount : BankAccount
{
public decimal InterestRate { get; set; } = 0.05m;
public void AddInterest() => Deposit(Balance * InterestRate);
}
// 3. Another Child Class — overrides behavior
public class CheckingAccount : BankAccount
{
public decimal OverdraftLimit { get; set; } = 500m;
public override void Withdraw(decimal amount)
{
if (amount <= Balance + OverdraftLimit)
Balance -= amount;
else
throw new InvalidOperationException("Overdraft limit exceeded.");
}
}
5. How Does protected set Work in C# Properties?
Question: Explain the accessibility of protected set here: public decimal Balance { get; protected set; }
Answer: This uses asymmetric property accessibility — the get and set accessors have different access levels.
Access Levels Breakdown
public(forget): Anyone can readBalancefrom anywhere.protected set: Only the class itself and derived child classes can modifyBalance.
Who Can Do What?
| Code Location | Read (get) | Write (set) |
|---|---|---|
Inside BankAccount (same class) | ✅ Yes | ✅ Yes |
Inside SavingsAccount (derived child) | ✅ Yes | ✅ Yes |
| Outside code (Main, API controllers, tests) | ✅ Yes | ❌ No (compiler error) |
Real-World Example:
public class BankAccount
{
public decimal Balance { get; protected set; } // Read everywhere, write only in hierarchy
public void Deposit(decimal amount) => Balance += amount;
}
public class SavingsAccount : BankAccount
{
public void ApplyInterest(decimal rate) => Balance += Balance * rate; // Allowed: child can modify
}
public class Program
{
public static void Main()
{
var account = new SavingsAccount();
Console.WriteLine(account.Balance); // ✅ READING: Allowed (public)
// account.Balance = 5000m; // ❌ CS0272: Compiler Error (set is protected)
}
}
Why Use protected set?
- Enforces Encapsulation: Prevents external code from corrupting state (e.g., setting a negative balance).
- Allows Inheritance Flexibility: Unlike
private set,protected setlets child classes customize state changes (e.g., adding fees, applying interest).
6. What Are Generics in C#?
Question: What are Generics in C#?
Answer: Generics let you define classes, interfaces, structs, and methods with type parameters — placeholders for the actual data types they’ll operate on. Instead of hardcoding a specific type or using object (which causes performance and safety issues), generics let you write a single blueprint that works safely with any data type.
The Problem Generics Solve
Before C# 2.0, developers used object for reusable collections. This caused two major issues:
- Lack of Type Safety: Any object could be added, leading to runtime
InvalidCastException. - Performance Overhead (Boxing/Unboxing): Converting value types to
objectand back requires heap allocation.
// ❌ Without Generics (ArrayList)
ArrayList list = new ArrayList();
list.Add(10); // Boxing occurs
list.Add("Hello"); // Allowed, but dangerous!
int number = (int)list[1]; // Crashes at RUNTIME — InvalidCastException!
// ✅ With Generics (List<T>)
List<int> numbers = new List<int>();
numbers.Add(10); // No boxing, type-safe
// numbers.Add("Hello"); // ❌ Compiler Error at COMPILE TIME!
int number = numbers[0]; // No casting, no boxing, fast performance
Key Concepts & Syntax
- Generic Classes — use a placeholder
T(short for Type):
public class Response<T>
{
public bool Success { get; set; }
public string Message { get; set; }
public T Data { get; set; } // Type determined when instantiated
}
var userResponse = new Response<User>();
var stringResponse = new Response<string>();
- Generic Methods — declare their own type parameters:
public static void Swap<T>(ref T a, ref T b)
{
T temp = a; a = b; b = temp;
}
int x = 5, y = 10;
Utilities.Swap(ref x, ref y); // Works with integers
string first = "World", second = "Hello";
Utilities.Swap(ref first, ref second); // Works with strings
- Generic Constraints (
where T : ...) — restrict what types can be used:
// Ensures T must be a reference type with a parameterless constructor
public class Repository<T> where T : class, new()
{
public T CreateInstance() => new T();
}
// Common constraints:
// where T : struct — T must be a value type
// where T : class — T must be a reference type
// where T : BaseClass — T must inherit from BaseClass
// where T : ISomeInterface — T must implement ISomeInterface
Benefits of Generics
- Type Safety: Catches type mismatch errors at compile time rather than crashing at runtime.
- Performance: Eliminates boxing/unboxing overhead for value types.
- Code Reusability: Write data structures and algorithms once, use them everywhere.
7. What Are Boxing and Unboxing in C#?
Question: What are Boxing and Unboxing in C#, which is explicit and which is implicit, why?
Answer:
- Boxing = converting a value type (like
int,struct) into a reference type (object). - Unboxing = extracting the value type back from the boxed object.
Implicit vs. Explicit
- Boxing is Implicit: The compiler handles it automatically — no cast needed.
- Unboxing is Explicit: You must use an explicit cast
(int)to extract the value.
int num = 42; // Value type on the Stack
object boxed = num; // ✅ BOXING: Implicit (Value Type → Reference Type)
int unboxed = (int)boxed; // ✅ UNBOXING: Explicit cast required
// ❌ Runtime error — must unbox to exact type first
double wrong = (double)boxed; // InvalidCastException!
double correct = (double)(int)boxed; // OK: unbox to int, then convert to double
Why Boxing is Implicit
- Safety: Every type in C# derives from
System.Object, so converting any value type toobjectalways succeeds. - Type Hierarchy: Assigning a derived type to a base type reference is always implicit in C#.
Why Unboxing is Explicit
- Runtime Failure Risk: The compiler can’t verify what value type is inside the
object. Casting to the wrong type throwsInvalidCastException. - Null Reference Risk: An
objectcan benull, but a value type can’t. UnboxingnullthrowsNullReferenceException.
Behind the Scenes (Memory & Performance)
During Boxing:
- Memory is allocated on the managed heap for the value + runtime overhead.
- The value is copied from the stack to the heap.
- A reference to the heap location is returned.
During Unboxing:
- The CLR checks the object isn’t null and matches the requested type.
- The value is copied from the heap back onto the stack.
Because heap allocations trigger GC overhead, avoiding boxing/unboxing (by using Generics like List<int> instead of ArrayList) is a key performance optimization in C#.
8. What Are Delegates in C#?
Question: What are Delegates in C#?
Answer: A delegate in C# is a type-safe function pointer — a reference type that holds a reference to one or more methods with a specific signature and return type. Delegates let you pass methods as parameters, store them in variables, and invoke them dynamically.
Real-World Analogy: A Work Order
Think of a delegate as a job description or work order form:
- The delegate defines the required parameters and result (e.g., “Takes a message, returns void”).
- You can attach any method matching that signature (e.g.,
LogToConsoleorLogToFile). - When you execute the work order, it invokes whichever method was assigned — without the caller knowing the implementation.
Syntax & Basic Usage
public delegate void Notify(string message); // 1. Declare
public static void Main()
{
Notify notifier = LogToConsole; // 2. Instantiate
notifier("Hello!"); // 3. Invoke
}
public static void LogToConsole(string msg) => Console.WriteLine(msg);
Built-in Delegate Types
C# provides pre-defined delegate types, eliminating the need to declare your own:
| Delegate | Signature | Example |
|---|---|---|
Action<T1, T2> | Takes parameters, returns void | Action<string> log = msg => Console.WriteLine(msg); |
Func<T, TResult> | Takes parameters, returns a value | Func<int, int, int> add = (a, b) => a + b; |
Predicate<T> | Takes one parameter, returns bool | Predicate<int> isEven = num => num % 2 == 0; |
Multicast Delegates
A delegate can hold references to multiple methods using the += operator. When invoked, it calls all subscribers in order.
public class Logger
{
public static void LogToConsole(string msg) => Console.WriteLine($"Console: {msg}");
public static void LogToFile(string msg) => Console.WriteLine($"File: {msg}");
}
public static void Run()
{
Action<string> logPipeline = LogToConsole;
logPipeline += LogToFile; // Attach second method
logPipeline("Database connected.");
// Output:
// Console: Database connected.
// File: Database connected.
}
Key Multicast Mechanics
- Order of Execution: Methods run in FIFO (First-In, First-Out) order.
- Return Value: If a multicast delegate returns a value, only the last method’s return value is returned (which is why multicast delegates are typically
void). - Exception Handling: If one method throws, subsequent methods in the chain won’t be called.
9. Demonstrate Delegates with a Calculator Example
Question: Explain delegates using a simple calculator example.
Answer: A delegate lets you pass calculation behavior as a parameter, allowing a single generic calculator method to execute addition, subtraction, multiplication, or division on the fly.
using System;
public class Program
{
// 1. Declare a delegate matching the operation signature
public delegate int PerformMath(int a, int b);
public static void Main()
{
// 2. Point the delegate to specific math methods
PerformMath addOperation = Add;
PerformMath multiplyOperation = Multiply;
// 3. Pass the delegate (behavior) into our calculator
Calculate(10, 5, addOperation); // Output: Result: 15
Calculate(10, 5, multiplyOperation); // Output: Result: 50
// 4. Modern alternative: Pass an inline lambda expression directly
Calculate(10, 5, (a, b) => a - b); // Output: Result: 5
}
// High-level Calculator that accepts numbers AND the operation to run
public static void Calculate(int x, int y, PerformMath operation)
{
int result = operation(x, y); // Invokes whichever method was passed in
Console.WriteLine($"Result: {result}");
}
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
}
Why Use a Delegate Here?
- Without Delegates: You’d need a rigid
switch/if-elseblock insideCalculate()checking for"+"or"-"strings. Adding new operations requires modifying the core method. - With Delegates:
Calculate()doesn’t care how the math is done — it only requires a method matching the(int, int) → intsignature. New operations can be added without touching the calculator’s code.
10. Multicast Delegates — Audit Log Pipeline
Question: Show me the multicast side of delegates.
Answer: A multicast delegate holds references to multiple methods simultaneously. When invoked, it executes every attached method sequentially (FIFO order).
In a calculator context, a multicast delegate acts as an Audit Log / Output Pipeline — performing a calculation and sending the result to multiple destinations.
using System;
public class Program
{
// 1. Declare a delegate that accepts a calculation result
public delegate void ResultHandler(int result);
public static void Main()
{
// 2. Instantiate with the first method
ResultHandler pipeline = DisplayOnScreen;
// 3. Attach additional methods using +=
pipeline += SaveToLogFile;
pipeline += SendNotification;
// Execute calculation and pass the multicast pipeline
CalculateAndPublish(10, 5, pipeline);
Console.WriteLine("\n--- Detaching Log File Output ---");
// 4. Detach a method using -=
pipeline -= SaveToLogFile;
CalculateAndPublish(20, 4, pipeline);
}
public static void CalculateAndPublish(int x, int y, ResultHandler notify)
{
int result = x * y;
notify(result); // Invokes ALL subscribed methods in order
}
public static void DisplayOnScreen(int result) =>
Console.WriteLine($"[SCREEN] Calculation Result: {result}");
public static void SaveToLogFile(int result) =>
Console.WriteLine($"[LOG FILE] Writing '{result}' to database/disk...");
public static void SendNotification(int result) =>
Console.WriteLine($"[ALERT] Sending push notification with result {result}");
}
Console Output:
[SCREEN] Calculation Result: 50
[LOG FILE] Writing '50' to database/disk...
[ALERT] Sending push notification with result 50
--- Detaching Log File Output ---
[SCREEN] Calculation Result: 80
[ALERT] Sending push notification with result 80
11. The Four Pillars of OOP in C#
Question: What are the OOP principles?
Answer: The core foundation of Object-Oriented Programming (OOP) rests on four fundamental principles, remembered as APIE:
- Abstraction
- Polymorphism
- Inheritance
- Encapsulation
1. Encapsulation (Data Hiding & Protection)
Bundles data and methods into a single unit (class), hiding internal state and exposing controlled access through properties and methods. (See Q1 for full details.)
2. Inheritance (Code Reuse & Hierarchy)
A derived class acquires the attributes and behaviors of a base class, modeling an “is-a” relationship. (See Q4 for full details.)
3. Abstraction (Hiding Complexity)
Exposes only the necessary features of an object while hiding implementation details — via interface and abstract class.
public abstract class Shape { public abstract double Area(); }
public class Circle : Shape { ... }
4. Polymorphism (Many Forms)
Allows objects of different types to be treated through a common interface. The same method call behaves differently depending on the actual object type.
public abstract class Animal { public abstract void MakeSound(); }
public class Dog : Animal { public override void MakeSound() => Console.WriteLine("Woof!"); }
public class Cat : Animal { public override void MakeSound() => Console.WriteLine("Meow!"); }
Animal[] animals = { new Dog(), new Cat() };
foreach (var animal in animals) animal.MakeSound(); // Woof! then Meow!
Summary Matrix
| Principle | Primary Objective | Key C# Mechanism |
|---|---|---|
| Encapsulation | Protect object state | private, protected, properties |
| Abstraction | Reduce complexity | interface, abstract classes |
| Inheritance | Eliminate code duplication | : class inheritance |
| Polymorphism | Uniform interface for different types | override, virtual, interfaces |
12. SOLID Principles with C# Code Examples
Question: Explain the SOLID principles with quick C# code examples.
Answer: The SOLID principles are five design guidelines for writing maintainable, testable, and scalable object-oriented code.
1. Single Responsibility Principle (SRP)
A class should have one, and only one, reason to change.
public class Invoice
{
public decimal CalculateTotal() => 100.00m;
}
public class InvoiceRepository
{
public void Save(Invoice invoice) => Console.WriteLine("Saving to DB...");
}
2. Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal amount);
}
public class RegularDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal amount) => amount * 0.9m;
}
public class VIPDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal amount) => amount * 0.8m;
}
3. Liskov Substitution Principle (LSP)
Derived classes must be substitutable for their base classes without breaking behavior.
// Violation: Square inheriting from Rectangle where setting width alters height.
// Solution: Use a shared abstraction that guarantees consistent behavior.
public abstract class Shape { public abstract double CalculateArea(); }
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea() => Width * Height;
}
public class Square : Shape
{
public double Side { get; set; }
public override double CalculateArea() => Side * Side;
}
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend upon interfaces they do not use.
public interface IPrinter { void Print(string document); }
public interface IScanner { void Scan(string document); }
public class BasicPrinter : IPrinter
{
public void Print(string document) => Console.WriteLine($"Printing {document}");
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
public interface IEngine { void Start(); }
public class V8Engine : IEngine { public void Start() => Console.WriteLine("V8 roaring..."); }
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine) { _engine = engine; }
public void StartCar() => _engine.Start();
}
13. Value Types vs Reference Types in C#
Question: Value types vs reference types.
Answer: Every type in C# is categorized as either a value type or a reference type. The key distinction lies in how memory is allocated and how data is passed.
Core Differences
| Feature | Value Types | Reference Types |
|---|---|---|
| Data Storage | Stores the actual value directly | Stores a reference (memory address) to the data |
| Memory Allocation | Allocated on the Stack (or inline in objects) | Allocated on the Managed Heap; pointer stored on Stack |
Assignment (=) | Copies the value (changes don’t affect each other) | Copies the reference (both point to same object) |
| Nullability | Cannot be null by default (requires Nullable<T>) | Can be null by default |
| Garbage Collection | Cleaned up immediately when out of scope | Managed and cleaned up by the Garbage Collector |
| Examples | int, double, bool, char, decimal, struct, enum | class, interface, delegate, string, object, arrays |
Memory & Assignment Behavior
Value Types — Independent Copies:
int x = 10;
int y = x; // Copying value: y gets 10
y = 20;
Console.WriteLine(x); // Output: 10 (x is unchanged!)
Reference Types — Shared Memory:
public class Person { public string Name { get; set; } }
Person p1 = new Person { Name = "Alex" };
Person p2 = p1; // Copying reference: p2 points to p1's object
p2.Name = "Jordan";
Console.WriteLine(p1.Name); // Output: Jordan (p1 is modified!)
Stack vs. Heap Allocation
- Stack: A fast, LIFO memory region managed directly by the CPU. When a method exits, its stack frame is reclaimed instantly.
- Heap: A larger, flexible pool for dynamic allocations. Objects here are cleaned up by the Garbage Collector.
Special Note on string: Although string is a reference type, it is immutable. Any operation that seems to modify a string actually creates a brand-new string object.
Struct vs. Class
- Use a
structfor small, immutable data structures (e.g.,Point,Vector) where GC pressure should be minimized. - Use a
classfor complex domain entities, business logic, or objects requiring inheritance and identity.
14. string vs StringBuilder in C# — When to Use Which
Question: When to use string vs StringBuilder?
Answer:
Use string when:
- You have a fixed or small number of string operations.
- Performing simple concatenations (the C# compiler optimizes
string a + string busingstring.Concat). - You need thread safety or key lookup stability (e.g., dictionary keys).
Use StringBuilder when:
- You are modifying strings an unknown or large number of times (e.g., inside
fororforeachloops). - You are building complex formatted documents, JSON/XML payloads, or file content dynamically.
// ✅ StringBuilder — efficient for repeated modifications
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
}
string result = sb.ToString();
// ❌ string — creates a new object on each concatenation (10,000 heap allocations!)
string result = "";
for (int i = 0; i < 10000; i++)
result += i.ToString();
15. How Does Garbage Collection Work in .NET?
Question: What is Garbage Collection in .NET?
Answer: Garbage Collection (GC) in .NET is an automatic memory management feature provided by the Common Language Runtime (CLR). It reclaims heap memory occupied by objects that are no longer in use.
Managed Heap and Object Generations
The GC manages the Managed Heap, divided into three generations based on the observation that newer objects have shorter lifetimes than older ones:
| Generation | Purpose | Typical Contents | Collection Frequency |
|---|---|---|---|
| Gen 0 | Short-lived objects | Temporary variables, local loop variables | Extremely frequent |
| Gen 1 | Buffer / transition zone | Objects that survived Gen 0 collection | Moderate |
| Gen 2 | Long-lived objects | Application-wide data, singletons, static variables | Infrequent |
| LOH | Large objects | Objects ≥ 85,000 bytes (large arrays, byte buffers) | Infrequent (with Gen 2) |
The Three Phases of a Collection
- Marking Phase — Pauses application threads (Stop-the-World), scans GC roots (CPU registers, local stack variables, static fields,
GCHandletables), and marks all reachable objects as live. - Relocating Phase — Updates references to surviving objects so they point to their new memory locations.
- Compacting Phase — Reclaims dead objects and compacts surviving objects towards the beginning of the heap to eliminate fragmentation. Surviving objects are promoted to the next generation (Gen 0 → Gen 1, Gen 1 → Gen 2).
Unmanaged Resources — The IDisposable Pattern
The GC only manages heap memory. It does not automatically clean up unmanaged resources (file handles, database connections, network sockets, OS handles). C# uses the IDisposable pattern and using statement for deterministic cleanup:
public class ResourceHolder : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing) { /* free managed resources */ }
// free unmanaged resources here
_disposed = true;
}
}
~ResourceHolder() { Dispose(false); } // Finalizer fallback
}
Best Practices to Keep GC Efficient
- Avoid unnecessary object allocations in hot loops.
- Use
StringBuilderandSpan<T>to reduce temporary heap objects. - Use
usingstatements to releaseIDisposableresources immediately. - Always call
GC.SuppressFinalize()inDispose()to avoid promoting objects to Gen 1/2. - Avoid calling
GC.Collect()— let the CLR manage collection timing automatically.
16. Where Is IDisposable Used in .NET Core?
Question: Where are IDisposable patterns already used in .NET Core?
Answer: IDisposable is baked into virtually every part of .NET Core where external OS handles, network streams, database connections, or memory buffers are managed.
Common Built-in Types Implementing IDisposable
| Category | Types | Why It Needs Disposal |
|---|---|---|
| Database & ORM | DbContext, DbConnection, SqlConnection, MySqlConnection | Closes underlying socket/pool connections to prevent pool exhaustion |
| File Systems | StreamReader, StreamWriter, FileStream, BinaryReader | Releases file locks held by the OS; flushes buffered bytes to disk |
| Networking & Web | HttpClient, HttpResponseMessage, SocketsHttpHandler | Releases open TCP/HTTP socket connections and stream buffers |
| Dependency Injection | ServiceProvider, IServiceScope | Disposes all Scoped and Transient services instantiated within that scope |
| Threading & Async | CancellationTokenSource, Timer, SemaphoreSlim, ReaderWriterLockSlim | Unregisters OS wait handles and timer callbacks |
| Cryptography & Security | SHA256, Aes, RSA, X509Certificate2 | Clears sensitive cryptographic key material from memory |
| Logging | ILoggerFactory, Process | Flushes log buffers; closes OS process/thread handles |
Key .NET Core Framework Mechanisms Built Around IDisposable
1. The using Statement / Declaration
// Modern C# using declaration (disposed at end of enclosing scope)
using var stream = new FileStream("data.txt", FileMode.Open);
// Traditional using block
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
} // Automatically calls connection.Dispose() here
2. Dependency Injection Container
When you register a service with a Scoped or Transient lifetime, the DI container tracks it if it implements IDisposable:
public class MyController : ControllerBase
{
private readonly MyDbContext _db;
public MyController(MyDbContext db) { _db = db; }
// _db is created per-request (Scoped)
// When the HTTP request finishes, ASP.NET Core automatically disposes the scope and calls _db.Dispose().
}
3. Asynchronous Cleanup (IAsyncDisposable)
In .NET Core 3.0+, IAsyncDisposable supports non-blocking, asynchronous cleanup:
public async Task ProcessDataAsync()
{
await using var asyncStream = new Utf8JsonWriter(outputStream);
await asyncStream.FlushAsync();
}
Note on
HttpClient: Although it implementsIDisposable, best practice in .NET Core is to reuse instances viaIHttpClientFactoryto avoid socket exhaustion.
17. How Does the Garbage Collector Work Under the Hood?
Question: How does GC work under the hood?
Answer: The .NET Garbage Collector is a generational, mark-sweep-compact collector implemented in the CLR. Here’s how it operates at the memory and execution level.
1. Memory Architecture: Managed Heap & Segments
The CLR requests virtual memory from the OS and divides the Managed Heap into two regions:
- Small Object Heap (SOH): Stores objects < 85,000 bytes. Divided into Gen 0, Gen 1, and Gen 2.
- Large Object Heap (LOH): Stores objects ≥ 85,000 bytes.
Allocations are fast using a Bump Pointer mechanism (next_free_address + object_size). When a thread exceeds its Thread Local Allocation Buffer (TLAB) or Gen 0 fills up, a collection is triggered.
2. Execution Phases of a Collection
When a GC is triggered, it runs through four phases:
- Suspension Phase — Application threads are paused at GC-safe points.
- Marking Phase — Scans GC roots (CPU registers, stack variables, statics,
GCHandletables) and recursively marks all reachable objects as live. - Plan & Relocate Phase — Decides whether to Compact (move objects together) or Sweep (free dead space). Calculates new addresses for surviving objects.
- Compact / Sweep & Resume Phase — Surviving objects are moved to their compacted locations, references are updated, and promoted to the next generation. Application threads resume.
3. Key Optimization: The Card Table (Write Barrier)
To collect Gen 0 without scanning the entire Gen 2 heap, the CLR uses a Card Table:
- The heap is divided into 512-byte “cards” (1 byte each in a bit array).
- When JIT code assigns a Gen 2 reference to a Gen 0 field, the JIT injects a Write Barrier that marks the card as “dirty.”
- During Gen 0 collection, the GC only scans roots + dirty cards in Gen 2.
| Generation | Collection Frequency | Cost / Pause | Compaction |
|---|---|---|---|
| Gen 0 | Extremely frequent (ms) | Minimal (< 1-2 ms) | Always compacts |
| Gen 1 | Moderate | Very low | Always compacts |
| Gen 2 | Infrequent | High | Compacts or sweeps |
| LOH | Infrequent | High | Sweeps (compacts on demand) |
4. GC Modes: Workstation vs. Server
- Workstation GC: Optimized for low latency and desktop responsiveness. Uses a single managed heap; GC work runs on the triggering thread or a background GC thread.
- Server GC: Optimized for throughput in server environments (ASP.NET Core). Creates one dedicated GC thread and managed heap per CPU core; collections run in parallel across all cores.
5. Background & Concurrent GC
To prevent long “Stop-the-World” pauses during heavy Gen 2 collections:
- Background GC (default in modern .NET): Gen 2 collections run concurrently on a separate GC thread while application threads continue allocating in Gen 0/1.
- Ephemeral GC Interrupts: If Gen 0 fills up during a background Gen 2 collection, the GC briefly pauses the Gen 2 collector, runs a fast foreground Gen 0/1 collection, then resumes the Gen 2 collection.
18. IEnumerable vs IQueryable — Which One to Use?
Question: What is the difference between IEnumerable and IQueryable?
Answer: Both are interfaces in .NET used to query collections, but they differ fundamentally in where execution occurs and how queries are evaluated.
Core Differences
| Feature | IEnumerable | IQueryable |
|---|---|---|
| Namespace | System.Collections / System.Collections.Generic | System.Linq |
| Execution Location | In-Memory (Client-side) | Out-of-Memory (Database/Remote Server) |
| Data Provider | LINQ to Objects, Arrays, Lists | LINQ to Entities (EF Core), SQL, Cosmos |
| Query Representation | Compiled Delegates (Func<T, bool>) | Expression Trees (Expression<Func<T, bool>>) |
| How Filtering Works | Fetches all data into memory, then filters locally | Translates LINQ to native SQL/database statements |
| Deferred Execution | Evaluates when iterated (foreach, .ToList()) | Translates and runs when iterated or materialized |
How They Execute (The Critical Difference)
IEnumerable(Client-Side Filtering)
IEnumerable<Product> products = dbContext.Products.AsEnumerable(); // Fetches ALL rows from DB!
var cheapProducts = products.Where(p => p.Price < 50).Take(10);
// SQL Executed: SELECT * FROM Products
// Filtered locally in C# RAM: Keeps top 10 cheap products
IQueryable(Server-Side Filtering)
IQueryable<Product> products = dbContext.Products; // No DB query yet
var cheapProducts = products.Where(p => p.Price < 50).Take(10).ToList();
// SQL Executed: SELECT TOP 10 * FROM Products WHERE Price < 50
// Filtered on SQL Server: Transfers ONLY 10 matching records
Expression Trees vs. Delegates
IEnumerableuses Delegates (Func<T, bool>): Compiled C# IL code that can only be executed, not parsed or translated.IQueryableuses Expression Trees (Expression<Func<T, bool>>): Data structures that represent code as a tree of nodes (AST). EF Core inspects these nodes at runtime to convert C# operators (.Where(),.Select(),==) into SQL operators (WHERE,SELECT,=).
When to Use Which
- Use
IEnumerablewhen querying in-memory collections (List<T>, arrays) or after data has been materialized. - Use
IQueryablewhen querying remote data sources (databases via EF Core) and you want to push filtering, pagination, and projections to the server.
Common Pitfall: Converting
IQueryabletoIEnumerableearly (e.g., calling.AsEnumerable()or.ToList()before applying.Where()or.Take()) causes full table scans and unnecessary data transfer.
19. Does IQueryable Inherit IEnumerable?
Question: Does IQueryable inherit IEnumerable?
Answer: Yes. IQueryable<T> inherits directly from IEnumerable<T>, which in turn inherits from the non-generic IEnumerable.
The Interface Hierarchy
IEnumerable (System.Collections)
↓
IEnumerable<T> (System.Collections.Generic)
↓
IQueryable<T> (System.Linq)
The exact C# interface definition:
namespace System.Linq
{
public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable
{
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
}
Why Does It Inherit IEnumerable?
- Polymorphism & Materialization: Any method accepting
IEnumerable<T>can also acceptIQueryable<T>. foreachSupport: C#‘sforeachloop requires a collection to implementIEnumerable. Inheriting it allows direct iteration overIQueryableobjects.- Triggering Execution: Calling
GetEnumerator()(mandated byIEnumerable) acts as the execution trigger for anIQueryable— signaling the provider (like EF Core) to translate the expression tree and execute the query.
In short: IEnumerable gives IQueryable the ability to be iterated over, while Expression and IQueryProvider give it the ability to defer execution and translate queries to remote data stores.
20. Which .NET Collection Interfaces Inherit IEnumerable?
Question: Who else inherits IEnumerable in .NET Core?
Answer: In .NET, IEnumerable<T> (and its non-generic counterpart IEnumerable) is the foundational root interface for almost every iteration and collection mechanism across the entire framework.
Core Collection Interfaces Inheriting IEnumerable<T>
IEnumerable<T>
├── ICollection<T> — Adds Count, Add, Remove, Clear, Contains
│ ├── IList<T> — Adds index-based access (list[i], Insert, RemoveAt)
│ ├── ISet<T> — Unique elements (UnionWith, IntersectWith)
│ │ └── IReadOnlySet<T>
│ ├── IReadOnlyList<T>
│ ├── IReadOnlyDictionary<TKey, TValue>
│ ├── IDictionary<TKey, TValue>
│ └── IReadOnlyCollection<T>
└── IObservable<T> — Push-based notifications (Rx.NET)
Why Everything Inherits IEnumerable
foreachLoop Compatibility: Any class implementingIEnumerablecan be used in aforeachstatement.- LINQ Engine Extension: LINQ standard query operators (
.Where(),.Select(),.GroupBy()) are extension methods written onIEnumerable<T>. Because all collections implement it, LINQ works uniformly across arrays, lists, sets, and database contexts.
21. ArrayList vs Generic Collections in C#
Question: What is the difference between ArrayList and generic collections?
Answer: In .NET, Collections are specialized classes designed to group, manage, and iterate over multiple data items in memory. The evolution of collections centers around the transition from non-generic ArrayList to type-safe Generic Collections (List<T>).
Non-Generic vs. Generic Collections
| Feature | ArrayList (Non-Generic) | List<T> (Generic) |
|---|---|---|
| Namespace | System.Collections | System.Collections.Generic |
| Type Safety | ❌ No (stores everything as object) | ✅ Yes (enforced at compile time) |
| Performance | Slow (boxing/unboxing for value types) | High performance (no boxing) |
| Compile-Time Checks | ❌ No (errors surface as runtime crashes) | ✅ Yes (caught at compile time) |
| Modern Recommendation | Deprecated / Legacy | Standard / Best Practice |
Why ArrayList is Avoided
// ❌ ArrayList — mixes types dangerously
ArrayList list = new ArrayList();
list.Add(10); // int → boxed to object
list.Add("Hello"); // string — different type, but allowed!
list.Add(DateTime.Now);
int value = (int)list[1]; // Runtime InvalidCastException!
// ✅ List<T> — type-safe at compile time
List<int> numbers = new List<int>();
numbers.Add(10); // ✅ Compiles fine
// numbers.Add("Hello"); // ❌ CS2923: Compiler Error at build time!
int value = numbers[0]; // No casting, no boxing, fast
Summary of Collection Types in .NET
| Data Structure | Non-Generic (Legacy) | Generic (Modern) | Thread-Safe |
|---|---|---|---|
| Dynamic Array | ArrayList | List<T> | ConcurrentBag<T> |
| Key-Value Pair | Hashtable | Dictionary<TKey, TValue> | ConcurrentDictionary<TKey, TValue> |
| First In, First Out | Queue | Queue<T> | ConcurrentQueue<T> |
| Last In, First Out | Stack | Stack<T> | ConcurrentStack<T> |
| Unique Elements | N/A | HashSet<T> | N/A |
| Sorted Key-Value | SortedList | SortedDictionary<TKey, TValue> | N/A |
Best Practices
- Never use
ArrayListin new code. Always preferList<T>orIReadOnlyList<T>. - Use
HashSet<T>when you need unique values and O(1) fastContains()operations. - Use
ConcurrentDictionaryorConcurrentQueuewhen multiple threads read/write concurrently.
22. Hashtable vs Dictionary vs HashSet in C#
Question: Hashtable and Dictionary<TKey, TValue>. How is HashSet<T> different from them?
Answer: All three are built on the hash table data structure — providing O(1) constant-time lookup by converting keys to numeric hash codes. They differ in type safety and data models.
1. Hashtable (Legacy, Non-Generic)
Stores both keys and values as object references (from C# 1.0, System.Collections).
using System.Collections;
Hashtable table = new Hashtable();
table.Add(1, "One");
table.Add("Two", 2); // Mixed key/value types allowed — dangerous!
string val = (string)table[1]; // Requires explicit casting
Issues: No type safety, boxing/unboxing overhead for value types, keys cannot be null.
2. Dictionary<TKey, TValue> (Modern, Generic)
The strongly-typed replacement for Hashtable (from System.Collections.Generic).
using System.Collections.Generic;
Dictionary<int, string> dict = new Dictionary<int, string>();
dict.Add(1, "One");
dict.Add(2, "Two");
// dict.Add("Three", 3); // ❌ CS0012: Compiler Error at build time!
string val = dict[1]; // No casting required — clean and safe
3. HashSet<T> vs. Dictionary — The Key Difference
While Hashtable and Dictionary<TKey, TValue> store key-value pairs, HashSet<T> stores only unique single values (no values, just keys).
- Key-Value or Single Value?
HashSet<T>stores single values only (T). - Uniqueness: Guarantees every element is distinct.
Add()returnsfalsefor duplicates. - Primary Use Case: High-performance set operations (
Union,Intersect,Difference) and instant O(1) existence checks.
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(10);
uniqueNumbers.Add(20);
bool addedAgain = uniqueNumbers.Add(10); // Returns FALSE — duplicate ignored!
Console.WriteLine(uniqueNumbers.Contains(20)); // True — O(1) instant lookup
Comparison Summary
| Feature | Hashtable | Dictionary<TKey, TValue> | HashSet<T> |
|---|---|---|---|
| Namespace | System.Collections | System.Collections.Generic | System.Collections.Generic |
| Data Model | Key-Value Pairs | Key-Value Pairs | Single Values Only |
| Generics / Type Safety | Non-generic (object) | Generic (TKey, TValue) | Generic (T) |
| Boxing/Unboxing | Yes (for value types) | No | No |
| Primary Use Case | Legacy code compatibility | General key-value mapping | Deduplication & set math |
How Hashing Works Under the Hood
When you insert or look up an item in Dictionary or HashSet:
- The runtime calls
.GetHashCode()on the key to calculate a numeric hash code. - Modulus arithmetic (
hashCode % capacity) maps the hash to an internal bucket index. - If two distinct keys produce the same bucket (a hash collision), .NET handles it using chaining (linked list in the bucket) and compares equality using
.Equals().
23. ref, out, and in Parameter Modifiers in C#
Question: Explain ref, in, and out in C#.
Answer: In C#, ref, in, and out are parameter modifiers that pass arguments by reference rather than by value. By default, passing a value type copies the entire data onto the stack. Passing by reference passes the memory address instead, allowing methods to modify the original variable or avoid copying large structures.
At a Glance Summary
| Modifier | Direct Goal | Initialized Before Passing? | Must Assign Value? | Read/Write Access Inside |
|---|---|---|---|---|
ref | Two-way binding (Read & Write) | ✅ Yes | ❌ No | Read & Write |
out | Return multiple values (Write) | ❌ No | ✅ Yes (before returning) | Write before reading |
in | Performance (Read-only reference) | ✅ Yes | ❌ No | Read-Only (forbidden to write) |
ref — Pass for Modification
For modifying the caller’s original variable:
public class Program
{
public static void Main()
{
int number = 10; // Must be initialized!
MultiplyByTwo(ref number); // Pass the reference
Console.WriteLine(number); // Output: 20
}
public static void MultiplyByTwo(ref int val)
{
val = val * 2; // Modifies caller's variable directly
}
}
out — Pass for Output
For methods that need to return multiple values. The method must assign a value before returning; the caller need not initialize first.
public class Program
{
public static void Main()
{
// Out variables can be declared inline in modern C#
if (Divide(10, 3, out int quotient, out int remainder))
{
Console.WriteLine($"Quotient: {quotient}, Remainder: {remainder}");
// Output: Quotient: 3, Remainder: 1
}
}
public static bool Divide(int dividend, int divisor, out int quotient, out int remainder)
{
if (divisor == 0)
{
quotient = 0;
remainder = 0;
return false;
}
quotient = dividend / divisor; // MUST assign before returning
remainder = dividend % divisor; // MUST assign before returning
return true;
}
}
Common Use Case:
int.TryParse(string, out int)and similar factory/TryParse methods that return both success flags and values.
in — Pass for High-Performance Read-Only
Introduced in C# 7.2, in passes a value by reference but guarantees the method cannot modify it:
public struct LargeStruct
{
public double X, Y, Z; // Large memory footprint on the stack
}
public static void ProcessData(in LargeStruct data)
{
Console.WriteLine(data.X); // ✅ Read-only access allowed
// data.X = 10.0; // ❌ CS8331: Compiler Error — read-only variable!
}
- Requirement: Must be initialized before passing.
- Primary Advantage: Passing large value types (
struct) passes a 64-bit pointer instead of copying the entire struct onto the stack for every call.
Summary Rules to Remember
- Use
refwhen you want a method to update an existing variable passed in. - Use
outwhen a method initializes and returns multiple values. - Use
infor passing large, read-only struct types to reduce stack copying overhead.
24. Is in Restricted Only to Struct Types?
Question: Is in restricted only to struct types?
Answer: No. in works with any data type in C# — primitives, classes, interfaces, delegates. However, it provides the biggest performance benefit with large struct types.
How in Behaves Across Different Types
- With Value Types / Structs (Primary Use Case): Passes a reference (pointer) instead of copying all fields onto the stack:
public struct Point3D { public double X, Y, Z; }
public void PrintPoint(in Point3D point)
{
// point.X = 10; // ❌ Compiler Error — cannot modify
Console.WriteLine($"{point.X}, {point.Y}, {point.Z}");
}
- With Primitive Types (
int,bool,double): Valid syntax, but rarely beneficial — a 64-bit pointer takes the same space as passing adoubleorint.
public void DisplayNumber(in int number)
{
// number = 5; // ❌ Compiler Error — read-only
Console.WriteLine(number);
}
- With Reference Types (
class,string, arrays):inmakes the reference itself read-only — it prevents reassigning the variable to a new object, but does not make the object’s internal properties immutable:
public class Person { public string Name { get; set; } }
public void ProcessPerson(in Person person)
{
person.Name = "Alex"; // ✅ Allowed: modifies internal property
// person = new Person(); // ❌ CS8331: Cannot reassign the reference!
}
Summary
- Allowed on non-struct types? Yes,
inworks on any type in C#. - Why use it on structs? To avoid expensive memory-copying of large value types while guaranteeing immutability.
- Why avoid it on classes/primitives? Reference types are already passed by reference; primitives are small enough that passing pointers adds unnecessary indirection overhead.
25. Access Modifiers in .NET
Question: Access modifiers in .NET.
Answer: Access modifiers control the visibility and scope of classes, struct members, methods, fields, and properties in .NET. They dictate which parts of your application can access and interact with specific code.
The 6 Access Modifiers at a Glance
| Access Modifier | Access Scope |
|---|---|
private | Accessible only within the containing class or struct. |
protected | Accessible within the containing class AND derived (child) classes. |
internal | Accessible anywhere within the same project/assembly (.dll / .exe). |
protected internal | Accessible within the same assembly OR derived classes in other assemblies. |
private protected | Accessible within the containing class AND derived classes only if they are in the same assembly. |
public | Accessible everywhere without restriction. |
Code Examples & Behavior
namespace MyLibrary // Assembly A
{
public class BaseClass
{
private int _privateVar = 1;
private protected int _privateProtectedVar = 2;
protected int _protectedVar = 3;
internal int _internalVar = 4;
protected internal int _protectedInternalVar = 5;
public int PublicVar = 6;
}
public class SameAssemblyDerived : BaseClass
{
public void TestAccess()
{
// _privateVar = 1; // ❌ CS0122: Inaccessible
_privateProtectedVar = 2; // ✅ Allowed (Same Assembly + Derived)
_protectedVar = 3; // ✅ Allowed (Derived)
_internalVar = 4; // ✅ Allowed (Same Assembly)
_protectedInternalVar = 5; // ✅ Allowed (Same Assembly or Derived)
PublicVar = 6; // ✅ Allowed (Public)
}
}
}
If we reference MyLibrary in a separate project (Assembly B):
namespace ExternalApp // Assembly B
{
public class ExternalDerived : MyLibrary.BaseClass
{
public void TestAccess()
{
// _privateVar = 1; // ❌ Inaccessible
// _privateProtectedVar = 2; // ❌ Inaccessible (Different Assembly)
_protectedVar = 3; // ✅ Allowed (Derived across assemblies)
// _internalVar = 4; // ❌ Inaccessible (Different Assembly)
_protectedInternalVar = 5; // ✅ Allowed (Derived across assemblies)
PublicVar = 6; // ✅ Allowed (Public)
}
}
}
Default Access Modifiers in C#
If you omit an access modifier, .NET applies the most restrictive default for the declaration context:
| Declared Target | Default Modifier |
|---|---|
Top-Level Types (class, struct, interface, enum) | internal |
| Class/Struct Members (fields, methods, properties) | private |
| Interface Members | public |
| Enum Members | public (cannot be changed) |
File-Local Types (C# 11+)
Introduced in C# 11, the file contextual keyword restricts a top-level type so it is visible only within the specific source file it’s written in:
// LocalHelpers.cs
file class InternalParserHelper
{
public static void Parse() { /* ... */ }
}
// Accessible anywhere inside LocalHelpers.cs,
// but invisible even to other C# files in the same project/assembly.
Tip: Practice these fundamentals with small code samples for each concept, and try to explain every one of them out loud — being able to articulate the why behind each design is exactly what interviewers look for.