The C# Fundamentals I'd Not Skip Before a .NET Interview


25 Questions — Jump to a topic

On this page

  1. 1. What is Encapsulation in C#?
  2. 2. Expression-Bodied vs Traditional Property Syntax in C#
  3. 3. Why Doesn’t C# Support Multiple Inheritance?
  4. 4. Explain Inheritance with a Real-World Example
  5. 5. How Does protected set Work in C# Properties?
  6. 6. What Are Generics in C#?
  7. 7. What Are Boxing and Unboxing in C#?
  8. 8. What Are Delegates in C#?
  9. 9. Demonstrate Delegates with a Calculator Example
  10. 10. Multicast Delegates — Audit Log Pipeline
  11. 11. The Four Pillars of OOP in C#
  12. 12. SOLID Principles with C# Code Examples
  13. 1. Single Responsibility Principle (SRP)
  14. 2. Open/Closed Principle (OCP)
  15. 3. Liskov Substitution Principle (LSP)
  16. 4. Interface Segregation Principle (ISP)
  17. 5. Dependency Inversion Principle (DIP)
  18. 13. Value Types vs Reference Types in C#
  19. 14. string vs StringBuilder in C# — When to Use Which
  20. 15. How Does Garbage Collection Work in .NET?
  21. 16. Where Is IDisposable Used in .NET Core?
  22. 1. The using Statement / Declaration
  23. 2. Dependency Injection Container
  24. 3. Asynchronous Cleanup (IAsyncDisposable)
  25. 17. How Does the Garbage Collector Work Under the Hood?
  26. 18. IEnumerable vs IQueryable — Which One to Use?
  27. 19. Does IQueryable Inherit IEnumerable?
  28. 20. Which .NET Collection Interfaces Inherit IEnumerable?
  29. 21. ArrayList vs Generic Collections in C#
  30. 22. Hashtable vs Dictionary vs HashSet in C#
  31. 1. Hashtable (Legacy, Non-Generic)
  32. 2. Dictionary<TKey, TValue> (Modern, Generic)
  33. 3. HashSet<T> vs. Dictionary — The Key Difference
  34. 23. ref, out, and in Parameter Modifiers in C#
  35. ref — Pass for Modification
  36. out — Pass for Output
  37. in — Pass for High-Performance Read-Only
  38. 24. Is in Restricted Only to Struct Types?
  39. 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 private so 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 Balance field is private — you must use Deposit() or Withdraw(), 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 the return keyword — 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

  1. The Diamond Problem (Ambiguity): When two base classes inherit from a common parent, the derived class faces conflicting method implementations and state duplication.
  2. Compiler/Runtime Simplicity: Single inheritance gives every object a linear memory layout and simple vtable lookups.
  3. 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 (for get): Anyone can read Balance from anywhere.
  • protected set: Only the class itself and derived child classes can modify Balance.

Who Can Do What?

Code LocationRead (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?

  1. Enforces Encapsulation: Prevents external code from corrupting state (e.g., setting a negative balance).
  2. Allows Inheritance Flexibility: Unlike private set, protected set lets 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:

  1. Lack of Type Safety: Any object could be added, leading to runtime InvalidCastException.
  2. Performance Overhead (Boxing/Unboxing): Converting value types to object and 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

  1. 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>();
  1. 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
  1. 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

  1. Safety: Every type in C# derives from System.Object, so converting any value type to object always succeeds.
  2. Type Hierarchy: Assigning a derived type to a base type reference is always implicit in C#.

Why Unboxing is Explicit

  1. Runtime Failure Risk: The compiler can’t verify what value type is inside the object. Casting to the wrong type throws InvalidCastException.
  2. Null Reference Risk: An object can be null, but a value type can’t. Unboxing null throws NullReferenceException.

Behind the Scenes (Memory & Performance)

During Boxing:

  1. Memory is allocated on the managed heap for the value + runtime overhead.
  2. The value is copied from the stack to the heap.
  3. A reference to the heap location is returned.

During Unboxing:

  1. The CLR checks the object isn’t null and matches the requested type.
  2. 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., LogToConsole or LogToFile).
  • 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:

DelegateSignatureExample
Action<T1, T2>Takes parameters, returns voidAction<string> log = msg => Console.WriteLine(msg);
Func<T, TResult>Takes parameters, returns a valueFunc<int, int, int> add = (a, b) => a + b;
Predicate<T>Takes one parameter, returns boolPredicate<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

  1. Order of Execution: Methods run in FIFO (First-In, First-Out) order.
  2. 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).
  3. 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-else block inside Calculate() 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) → int signature. 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

PrinciplePrimary ObjectiveKey C# Mechanism
EncapsulationProtect object stateprivate, protected, properties
AbstractionReduce complexityinterface, abstract classes
InheritanceEliminate code duplication: class inheritance
PolymorphismUniform interface for different typesoverride, 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

FeatureValue TypesReference Types
Data StorageStores the actual value directlyStores a reference (memory address) to the data
Memory AllocationAllocated 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)
NullabilityCannot be null by default (requires Nullable<T>)Can be null by default
Garbage CollectionCleaned up immediately when out of scopeManaged and cleaned up by the Garbage Collector
Examplesint, double, bool, char, decimal, struct, enumclass, 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 struct for small, immutable data structures (e.g., Point, Vector) where GC pressure should be minimized.
  • Use a class for 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 b using string.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 for or foreach loops).
  • 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:

GenerationPurposeTypical ContentsCollection Frequency
Gen 0Short-lived objectsTemporary variables, local loop variablesExtremely frequent
Gen 1Buffer / transition zoneObjects that survived Gen 0 collectionModerate
Gen 2Long-lived objectsApplication-wide data, singletons, static variablesInfrequent
LOHLarge objectsObjects ≥ 85,000 bytes (large arrays, byte buffers)Infrequent (with Gen 2)

The Three Phases of a Collection

  1. Marking Phase — Pauses application threads (Stop-the-World), scans GC roots (CPU registers, local stack variables, static fields, GCHandle tables), and marks all reachable objects as live.
  2. Relocating Phase — Updates references to surviving objects so they point to their new memory locations.
  3. 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 StringBuilder and Span<T> to reduce temporary heap objects.
  • Use using statements to release IDisposable resources immediately.
  • Always call GC.SuppressFinalize() in Dispose() 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

CategoryTypesWhy It Needs Disposal
Database & ORMDbContext, DbConnection, SqlConnection, MySqlConnectionCloses underlying socket/pool connections to prevent pool exhaustion
File SystemsStreamReader, StreamWriter, FileStream, BinaryReaderReleases file locks held by the OS; flushes buffered bytes to disk
Networking & WebHttpClient, HttpResponseMessage, SocketsHttpHandlerReleases open TCP/HTTP socket connections and stream buffers
Dependency InjectionServiceProvider, IServiceScopeDisposes all Scoped and Transient services instantiated within that scope
Threading & AsyncCancellationTokenSource, Timer, SemaphoreSlim, ReaderWriterLockSlimUnregisters OS wait handles and timer callbacks
Cryptography & SecuritySHA256, Aes, RSA, X509Certificate2Clears sensitive cryptographic key material from memory
LoggingILoggerFactory, ProcessFlushes 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 implements IDisposable, best practice in .NET Core is to reuse instances via IHttpClientFactory to 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:

  1. Suspension Phase — Application threads are paused at GC-safe points.
  2. Marking Phase — Scans GC roots (CPU registers, stack variables, statics, GCHandle tables) and recursively marks all reachable objects as live.
  3. Plan & Relocate Phase — Decides whether to Compact (move objects together) or Sweep (free dead space). Calculates new addresses for surviving objects.
  4. 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.
GenerationCollection FrequencyCost / PauseCompaction
Gen 0Extremely frequent (ms)Minimal (< 1-2 ms)Always compacts
Gen 1ModerateVery lowAlways compacts
Gen 2InfrequentHighCompacts or sweeps
LOHInfrequentHighSweeps (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

FeatureIEnumerableIQueryable
NamespaceSystem.Collections / System.Collections.GenericSystem.Linq
Execution LocationIn-Memory (Client-side)Out-of-Memory (Database/Remote Server)
Data ProviderLINQ to Objects, Arrays, ListsLINQ to Entities (EF Core), SQL, Cosmos
Query RepresentationCompiled Delegates (Func<T, bool>)Expression Trees (Expression<Func<T, bool>>)
How Filtering WorksFetches all data into memory, then filters locallyTranslates LINQ to native SQL/database statements
Deferred ExecutionEvaluates when iterated (foreach, .ToList())Translates and runs when iterated or materialized

How They Execute (The Critical Difference)

  1. 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
  1. 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

  • IEnumerable uses Delegates (Func<T, bool>): Compiled C# IL code that can only be executed, not parsed or translated.
  • IQueryable uses 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 IEnumerable when querying in-memory collections (List<T>, arrays) or after data has been materialized.
  • Use IQueryable when querying remote data sources (databases via EF Core) and you want to push filtering, pagination, and projections to the server.

Common Pitfall: Converting IQueryable to IEnumerable early (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?

  1. Polymorphism & Materialization: Any method accepting IEnumerable<T> can also accept IQueryable<T>.
  2. foreach Support: C#‘s foreach loop requires a collection to implement IEnumerable. Inheriting it allows direct iteration over IQueryable objects.
  3. Triggering Execution: Calling GetEnumerator() (mandated by IEnumerable) acts as the execution trigger for an IQueryable — 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

  1. foreach Loop Compatibility: Any class implementing IEnumerable can be used in a foreach statement.
  2. LINQ Engine Extension: LINQ standard query operators (.Where(), .Select(), .GroupBy()) are extension methods written on IEnumerable<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

FeatureArrayList (Non-Generic)List<T> (Generic)
NamespaceSystem.CollectionsSystem.Collections.Generic
Type Safety❌ No (stores everything as object)✅ Yes (enforced at compile time)
PerformanceSlow (boxing/unboxing for value types)High performance (no boxing)
Compile-Time Checks❌ No (errors surface as runtime crashes)✅ Yes (caught at compile time)
Modern RecommendationDeprecated / LegacyStandard / 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 StructureNon-Generic (Legacy)Generic (Modern)Thread-Safe
Dynamic ArrayArrayListList<T>ConcurrentBag<T>
Key-Value PairHashtableDictionary<TKey, TValue>ConcurrentDictionary<TKey, TValue>
First In, First OutQueueQueue<T>ConcurrentQueue<T>
Last In, First OutStackStack<T>ConcurrentStack<T>
Unique ElementsN/AHashSet<T>N/A
Sorted Key-ValueSortedListSortedDictionary<TKey, TValue>N/A

Best Practices

  • Never use ArrayList in new code. Always prefer List<T> or IReadOnlyList<T>.
  • Use HashSet<T> when you need unique values and O(1) fast Contains() operations.
  • Use ConcurrentDictionary or ConcurrentQueue when 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() returns false for 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

FeatureHashtableDictionary<TKey, TValue>HashSet<T>
NamespaceSystem.CollectionsSystem.Collections.GenericSystem.Collections.Generic
Data ModelKey-Value PairsKey-Value PairsSingle Values Only
Generics / Type SafetyNon-generic (object)Generic (TKey, TValue)Generic (T)
Boxing/UnboxingYes (for value types)NoNo
Primary Use CaseLegacy code compatibilityGeneral key-value mappingDeduplication & set math

How Hashing Works Under the Hood

When you insert or look up an item in Dictionary or HashSet:

  1. The runtime calls .GetHashCode() on the key to calculate a numeric hash code.
  2. Modulus arithmetic (hashCode % capacity) maps the hash to an internal bucket index.
  3. 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

ModifierDirect GoalInitialized Before Passing?Must Assign Value?Read/Write Access Inside
refTwo-way binding (Read & Write)✅ Yes❌ NoRead & Write
outReturn multiple values (Write)❌ No✅ Yes (before returning)Write before reading
inPerformance (Read-only reference)✅ Yes❌ NoRead-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

  1. Use ref when you want a method to update an existing variable passed in.
  2. Use out when a method initializes and returns multiple values.
  3. Use in for 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

  1. 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}");
}
  1. With Primitive Types (int, bool, double): Valid syntax, but rarely beneficial — a 64-bit pointer takes the same space as passing a double or int.
public void DisplayNumber(in int number)
{
    // number = 5;  // ❌ Compiler Error — read-only
    Console.WriteLine(number);
}
  1. With Reference Types (class, string, arrays): in makes 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, in works 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 ModifierAccess Scope
privateAccessible only within the containing class or struct.
protectedAccessible within the containing class AND derived (child) classes.
internalAccessible anywhere within the same project/assembly (.dll / .exe).
protected internalAccessible within the same assembly OR derived classes in other assemblies.
private protectedAccessible within the containing class AND derived classes only if they are in the same assembly.
publicAccessible 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 TargetDefault Modifier
Top-Level Types (class, struct, interface, enum)internal
Class/Struct Members (fields, methods, properties)private
Interface Memberspublic
Enum Memberspublic (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.