IEnumerable
3 min readIEnumerable
TL;DR
IEnumerable<T> is the minimal "forward-only, read-only, lazy" sequence contract that every LINQ pipeline and foreach loop is built on. Returning it from APIs hides the concrete collection, enables deferred execution so filters compose without intermediate allocations, and lets you stream large or even infinite sequences via yield return — the right default unless callers actually need Count, indexed access, or mutation.
How it works
IEnumerable — Read-Only Forward Iteration
"Use IEnumerable
when you only need to iterate once, forward-only, and want lazy evaluation."
❌ Bad example:
public List<Order> GetActiveOrders()
{
var orders = _repository.GetAll(); // loads everything
return orders.Where(o => o.IsActive).ToList(); // filters in memory
}
// Caller
public void ProcessOrders()
{
List<Order> orders = GetActiveOrders(); // forces concrete type
foreach (var order in orders) { /* ... */ }
}
Returning List<T> exposes mutability, couples caller to implementation, and forces immediate materialization.
✅ Good example:
public IEnumerable<Order> GetActiveOrders()
{
var orders = _repository.GetAll();
return orders.Where(o => o.IsActive); // deferred execution
}
// Caller
public void ProcessOrders()
{
IEnumerable<Order> orders = GetActiveOrders(); // flexible contract
foreach (var order in orders) { /* ... */ }
}
👉 Returns interface, enables lazy evaluation, and hides implementation.
🔥 Using yield return for streaming:
public IEnumerable<Trade> GetTradesForDate(DateTime date)
{
using var reader = _database.ExecuteReader($"SELECT * FROM Trades WHERE Date = '{date}'");
while (reader.Read())
{
yield return new Trade
{
Id = reader.GetInt32(0),
Symbol = reader.GetString(1),
Price = reader.GetDecimal(2)
};
}
}
👉 Streams results one at a time, reducing memory pressure for large datasets.
💡 In trading systems:
- Use IEnumerable
for large result sets from databases (avoid loading millions of trades). - Enable deferred execution so filters/transformations compose efficiently.
- Prevent callers from modifying source collections accidentally.
Quick recall Q&A
IEnumerable
Depends on implementation. Arrays/lists can be re-enumerated; LINQ queries and yield-based iterators re-execute. Avoid enumerating twice without .ToList() if side effects exist.
LINQ operations build a query pipeline without executing. Enumeration (foreach, .ToList(), .Count()) triggers execution. This enables composing filters cheaply before materializing.
When you need to enumerate multiple times, require random access, or want to snapshot state. Otherwise, keep it lazy to save memory and enable streaming.
Minimal until enumeration. Each Where() wraps the previous iterator. During enumeration, predicates chain efficiently without intermediate collections.
Yes. Yield-based iterators can generate endless items (e.g., Fibonacci). Callers use .Take(n) to limit consumption. This is impossible with List
Compiler calls GetEnumerator(), then repeatedly calls MoveNext() and accesses Current. Finally calls Dispose(). You rarely implement IEnumerable
Most collections throw InvalidOperationException. Use .ToList() first if modification during iteration is required, or redesign to collect changes separately.
Iterator blocks allocate a state machine (heap object), but items are yielded on demand. This trades small upfront allocation for avoiding large arrays.
No, use IAsyncEnumerable