Collection interfaces define how collections can be accessed, enumerated, modified, and queried.
The most important interfaces are:
-
IEnumerable<T>– Supports iteration. -
ICollection<T>– Supports iteration, counting, adding, and removing. -
IList<T>– Supports indexed access and modification. -
IQueryable<T>– Supports query expression construction, commonly for database queries. -
IEnumerator<T>– Controls iteration through a collection. -
IReadOnlyList<T>– Supports reading elements by index without exposing modification methods.
1. What is the difference between IEnumerable<T>, ICollection<T>, and IList<T>?
What are they?
These three interfaces provide different levels of functionality for working with collections.
1. IEnumerable<T>
-
Provides sequential access to elements.
-
Supports
foreach. -
Does not provide
Add(),Remove(), or indexed access. -
Useful when we only need to enumerate data.
2. ICollection<T>
-
Inherits from
IEnumerable<T>. -
Supports
Add(),Remove(),Clear(), andCount. -
Does not provide indexed access.
-
Useful when we need to manage a collection.
3. IList<T>
-
Inherits from
ICollection<T>. -
Supports indexed access using
[index]. -
Supports
Insert()andRemoveAt(). -
Useful when we need to access or modify elements by position.
Interface Hierarchy
IEnumerable<T>
|
v
ICollection<T>
|
v
IList<T>
|
v
List<T>
List<T> implements all three interfaces.
Example Program
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// IEnumerable - Read/Iterate
IEnumerable<int> numbers1 = new List<int> { 10, 20, 30 };
foreach (int number in numbers1)
{
Console.WriteLine(number);
}
// ICollection - Add/Remove/Count
ICollection<int> numbers2 = new List<int> { 10, 20 };
numbers2.Add(30);
numbers2.Remove(10);
Console.WriteLine(numbers2.Count);
// IList - Index Access
IList<int> numbers3 = new List<int> { 10, 20, 30 };
numbers3[0] = 100;
numbers3.Insert(1, 50);
Console.WriteLine(numbers3[0]);
}
}
Output:
10
20
30
2
100
Comparison
|
Feature |
|
|
|
|---|---|---|---|
|
|
Yes |
Yes |
Yes |
|
|
No |
Yes |
Yes |
|
|
No |
Yes |
Yes |
|
|
No |
Yes |
Yes |
|
Index |
No |
No |
Yes |
|
|
No |
No |
Yes |
|
|
No |
No |
Yes |
Key Points
-
IEnumerable<T>is suitable for iteration. -
ICollection<T>is suitable for managing collections. -
IList<T>supports indexed access. -
These interfaces define available operations, not necessarily the underlying collection's mutability.
-
For example, some
IList<T>implementations are read-only or fixed-size and may throw exceptions for unsupported modifications.
Interview Point: Choose the least powerful interface that satisfies your requirement. This reduces coupling and makes code more reusable.
2. What is the difference between IEnumerable<T> and IQueryable<T>?
What are they?
Both interfaces are used to query collections, but their execution models are different.
IEnumerable<T>
-
Commonly used with in-memory collections.
-
LINQ operators work with .NET delegates.
-
Filtering occurs in application code when using LINQ to Objects.
IQueryable<T>
-
Commonly used with Entity Framework Core.
-
Represents queries as expression trees.
-
A query provider can translate expressions into SQL or another query language.
-
Allows filtering to be performed by the database.
Diagram
IEnumerable<T>
|
v
In-Memory Data
|
v
LINQ to Objects
|
v
Filter in Application
|
v
Result
IQueryable<T>
|
v
LINQ Expression
|
v
EF Core Provider
|
v
SQL Query
|
v
Database Filtering
|
v
Result
Example Program
Assume we have an EF Core AppDbContext containing an Employees table.
Using IEnumerable<T>:
IEnumerable<Employee> employees = await context.Employees
.ToListAsync();
var result = employees
.Where(e => e.Salary > 50000);
foreach (var employee in result)
{
Console.WriteLine(employee.Name);
}
Explanation:
-
ToListAsync()executes the database query. -
All employees are loaded into application memory.
-
Where()filters the employees in memory. -
This can be inefficient for large tables.
Using IQueryable<T>:
IQueryable<Employee> employees = context.Employees;
var result = employees
.Where(e => e.Salary > 50000);
foreach (var employee in result)
{
Console.WriteLine(employee.Name);
}
Explanation:
-
context.Employeesprovides anIQueryable<Employee>. -
Where()adds a filter to the query expression. -
The query executes when the
foreachenumerates it. -
EF Core translates the supported expression into SQL.
Conceptually, the generated SQL is:
SELECT *
FROM Employees
WHERE Salary > 50000;
Only matching employees are retrieved.
Comparison
|
Feature |
|
|
|---|---|---|
|
Namespace |
|
|
|
Typical source |
In-memory collection |
Database query |
|
LINQ representation |
Delegates |
Expression trees |
|
Filtering |
Application memory |
Provider-dependent |
|
SQL translation |
No |
Supported by EF Core |
|
Deferred execution |
Supported |
Supported |
|
Common use |
|
EF Core |
Important Clarification
IEnumerable<T> does not automatically mean all records have already been loaded. An EF Core query can also be enumerated through IEnumerable<T>.
The critical difference is whether additional LINQ operators are composed into the database query or executed locally.
Key Points
-
IEnumerable<T>uses LINQ to Objects for subsequent LINQ operations. -
IQueryable<T>allows query providers to translate expressions. -
Both can support deferred execution.
-
ToList()andToListAsync()materialize the results. -
For EF Core, apply filters before materialization whenever possible.
-
IQueryable<T>is useful in the data-access layer, but exposing it across architectural boundaries can create unwanted coupling.
Interview Point: For database filtering, use IQueryable<T> to allow EF Core to execute supported filters in SQL rather than retrieving unnecessary records.
3. What is the difference between IEnumerable<T> and List<T>?
What are they?
IEnumerable<T> is an interface, whereas List<T> is a concrete collection class.
IEnumerable<T>
-
Defines a contract for iteration.
-
Does not expose modification methods.
-
Can represent arrays, lists, and other enumerable sequences.
List<T>
-
Is a concrete, resizable collection.
-
Stores elements in an internal array.
-
Supports adding, removing, sorting, and indexed access.
-
Implements several collection interfaces, including
IEnumerable<T>.
Example Program
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 10, 20, 30 };
numbers.Add(40);
numbers.Remove(10);
IEnumerable<int> result = numbers;
foreach (int number in result)
{
Console.WriteLine(number);
}
}
}
Output:
20
30
40
Explanation
-
numbersis aList<int>. -
We can add or remove elements.
-
The list is assigned to an
IEnumerable<int>variable. -
Through
result, we can enumerate elements. -
We cannot directly call
result.Add()becauseIEnumerable<T>does not expose that method.
Diagram
List<int>
|
+------+------+
| |
Add() Remove()
| |
+------+------+
|
v
IEnumerable<int>
|
v
foreach
|
v
20 -> 30 -> 40
Comparison
|
Feature |
|
|
|---|---|---|
|
Type |
Interface |
Class |
|
Create with |
Not directly |
Yes |
|
|
Yes |
Yes |
|
|
No |
Yes |
|
|
No |
Yes |
|
Index access |
No |
Yes |
|
|
No |
Yes |
|
Data storage |
Not specified |
Internal array |
Key Points
-
IEnumerable<T>defines iteration behavior. -
List<T>is an actual collection implementation. -
List<T>implementsIEnumerable<T>. -
An
IEnumerable<T>may be lazy or backed by an existing collection. -
Assigning a
List<T>toIEnumerable<T>does not copy the data or make the list immutable.
Interview Point: Prefer IEnumerable<T> for methods that only need to enumerate elements. Use List<T> when list-specific functionality is required.
4. What is the difference between IEnumerable<T> and IEnumerator<T>?
What are they?
These interfaces work together to support collection iteration.
IEnumerable<T>
Represents a sequence that can provide an enumerator.
Its important method is:
IEnumerator<T> GetEnumerator();
IEnumerator<T>
Represents the object that moves through the sequence.
Important members:
MoveNext()
Current
Reset() // From the non-generic IEnumerator interface
Dispose()
IEnumerator<T> inherits from IEnumerator and IDisposable.
Diagram
IEnumerable<int>
|
| GetEnumerator()
v
IEnumerator<int>
|
v
MoveNext()
|
v
Current
|
v
+--------+--------+
| | |
10 20 30
Example Program
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
IEnumerable<int> numbers = new List<int>
{
10, 20, 30
};
using IEnumerator<int> enumerator =
numbers.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
Output:
10
20
30
Explanation
-
numbersrepresents an enumerable sequence. -
GetEnumerator()creates or obtains an enumerator. -
MoveNext()moves to the next element. -
Currentreturns the element at the current position. -
When
MoveNext()returnsfalse, iteration ends. -
usingensures the enumerator is disposed.
Comparison
|
Feature |
|
|
|---|---|---|
|
Purpose |
Represents enumerable sequence |
Iterates through sequence |
|
|
Yes |
No |
|
|
No |
Yes |
|
|
No |
Yes |
|
Used by |
|
|
|
Responsibility |
Supplies enumerator |
Maintains iteration position |
Key Points
-
IEnumerable<T>provides the enumerator. -
IEnumerator<T>performs iteration. -
MoveNext()returnstruewhen another element is available. -
Currentreturns the current element. -
An enumerator initially sits before the first element.
-
foreachhandles these operations automatically.
Interview Point: IEnumerable<T> represents the sequence, while IEnumerator<T> is the cursor used to traverse it.
5. How does foreach work internally?
What is foreach?
foreach is a C# looping statement used to iterate over arrays, lists, and other enumerable collections.
Internally, for a typical IEnumerable<T> collection, it uses an enumerator to access elements sequentially.
Execution Flow
foreach
|
v
GetEnumerator()
|
v
MoveNext()
|
+------+------+
| |
true false
| |
v v
Current End Loop
|
v
Execute Body
|
+------------+
|
v
MoveNext()
Example Program Using foreach
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new() { 10, 20, 30 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
Output:
10
20
30
Equivalent Enumerator-Based Code
Conceptually, the above foreach is similar to:
List<int> numbers = new() { 10, 20, 30 };
using (IEnumerator<int> enumerator =
numbers.GetEnumerator())
{
while (enumerator.MoveNext())
{
int number = enumerator.Current;
Console.WriteLine(number);
}
}
Step-by-Step Explanation
Step 1: Obtain enumerator
numbers.GetEnumerator();
The collection provides an enumerator.
Step 2: Move to the next element
enumerator.MoveNext();
This returns true when an element is available.
Step 3: Read current element
enumerator.Current;
This retrieves the current element.
Step 4: Execute loop body
Console.WriteLine(number);
The current element is processed.
Step 5: Repeat
MoveNext() is called again until it returns false.
Step 6: Dispose enumerator
The enumerator is disposed if applicable, including when the loop exits through an exception.
Can we modify a collection during foreach?
Normally, we should not structurally modify a List<T> while iterating over it.
List<int> numbers = new() { 10, 20, 30 };
foreach (int number in numbers)
{
numbers.Remove(number); // Runtime exception
}
Typical exception:
InvalidOperationException:
Collection was modified; enumeration operation
may not execute.
A safer approach is:
List<int> numbers = new() { 10, 20, 30 };
numbers.RemoveAll(x => x > 10);
Key Points
-
foreachobtains an enumerator. -
MoveNext()advances the iteration. -
Currentretrieves the current element. -
The compiler handles enumerator cleanup.
-
Modifying a
List<T>during enumeration generally invalidates its enumerator. -
Arrays and some other collection types can use optimized iteration mechanisms.
-
foreachcan also work with types following the enumerator pattern without implementingIEnumerable<T>.
Interview Point: For standard enumerable collections, foreach is compiled into enumerator-based iteration, with cleanup handled automatically.
6. What is the difference between IList<T> and IReadOnlyList<T>?
What are they?
Both interfaces support indexed access, but they expose different operations.
IList<T>
-
Supports reading elements.
-
Supports updating elements through an indexer.
-
Defines
Add(),Remove(),Insert(), andRemoveAt(). -
Suitable for collections that need modification.
IReadOnlyList<T>
-
Supports reading elements using an index.
-
Exposes
Count. -
Does not expose modification methods.
-
Suitable when consumers should only read collection elements.
Example Program
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
IList<int> numbers1 = new List<int>
{
10, 20, 30
};
numbers1[0] = 100;
numbers1.Add(40);
Console.WriteLine(numbers1[0]);
IReadOnlyList<int> numbers2 = new List<int>
{
10, 20, 30
};
Console.WriteLine(numbers2[0]);
Console.WriteLine(numbers2.Count);
// Compile-time error
// numbers2.Add(40);
// numbers2[0] = 100;
}
}
Output:
100
10
3
Explanation
Using IList<T>:
numbers1[0] = 100;
numbers1.Add(40);
The interface exposes methods for modifying the collection.
Using IReadOnlyList<T>:
Console.WriteLine(numbers2[0]);
Console.WriteLine(numbers2.Count);
The interface exposes only reading operations.
The following are not allowed through IReadOnlyList<T>:
numbers2.Add(40);
numbers2.Remove(10);
numbers2[0] = 100;
Diagram
List<int>
|
+--------+--------+
| |
v v
IList<T> IReadOnlyList<T>
| |
Read/Write Read Only
| |
Add() Count
Remove() Index [0]
Insert() foreach
Index [0]
Comparison
|
Feature |
|
|
|---|---|---|
|
|
Yes |
Yes |
|
|
Yes |
Yes |
|
Index access |
Yes |
Yes |
|
Index assignment |
Defined |
No |
|
|
Defined |
No |
|
|
Defined |
No |
|
|
Defined |
No |
|
Guarantees immutability |
No |
No |
Important: Read-Only Does Not Mean Immutable
Consider:
List<int> numbers = new() { 10, 20, 30 };
IReadOnlyList<int> readOnlyNumbers = numbers;
numbers.Add(40);
Console.WriteLine(readOnlyNumbers.Count);
Output:
4
Although readOnlyNumbers is an IReadOnlyList<int>, it still references the same underlying List<int>.
Changes made through numbers are visible through readOnlyNumbers.
For stronger immutability guarantees, consider immutable collections such as ImmutableArray<T>.
Key Points
-
IList<T>exposes collection modification operations. -
IReadOnlyList<T>exposes only read operations. -
Both support indexed access.
-
IReadOnlyList<T>does not guarantee that the underlying data cannot change. -
IList<T>methods may throwNotSupportedExceptionfor implementations that do not permit modification. -
Use
IReadOnlyList<T>when a method needs indexed reading without modification capabilities.
Quick Revision – Collection Interfaces
|
Interface |
Main Purpose |
Important Members |
|---|---|---|
|
|
Iterate elements |
|
|
|
Control iteration |
|
|
|
Manage collection |
|
|
|
Indexed access and modification |
|
|
|
Indexed read access |
|
|
|
Provider-based query composition |
|
|
|
Concrete resizable collection |
|
Real-Time Scenario: ASP.NET Core Web API with EF Core
Consider an Employee API that retrieves employees from SQL Server.
public class EmployeeService
{
private readonly AppDbContext _context;
public EmployeeService(AppDbContext context)
{
_context = context;
}
public async Task<IReadOnlyList<Employee>> GetEmployeesAsync()
{
IQueryable<Employee> query = _context.Employees
.AsNoTracking()
.Where(e => e.IsActive);
List<Employee> employees = await query.ToListAsync();
return employees;
}
}
Execution Flow
EmployeeService
|
v
IQueryable<Employee>
|
v
Apply Where Filter
|
v
EF Core Provider
|
v
SQL Server
|
v
ToListAsync()
|
v
List<Employee>
|
v
IReadOnlyList<Employee>
|
v
API Layer
Why use these interfaces?
-
IQueryable<Employee>: Builds the database query and applies filtering before retrieval. -
List<Employee>: Stores the materialized results in memory. -
IReadOnlyList<Employee>: Exposes indexed, read-only access to the returned collection without exposing modification methods.
The returned IReadOnlyList<Employee> does not make the Employee objects themselves immutable.
Most Important Interview Takeaways
-
IEnumerable<T>is for iteration, whileICollection<T>adds collection management. -
IList<T>adds indexed access and positional operations. -
IQueryable<T>supports provider-based query translation, making it important in EF Core. -
IEnumerable<T>is an interface;List<T>is a concrete implementation. -
IEnumerable<T>supplies an enumerator;IEnumerator<T>controls traversal. -
foreachtypically usesGetEnumerator(),MoveNext(), andCurrent. -
IReadOnlyList<T>restricts modification through the interface but does not guarantee immutability.
For .NET Tech Lead interviews: Pay particular attention to IEnumerable<T> vs. IQueryable<T>, deferred execution, database query performance, and choosing appropriate collection interfaces for service contracts.