Question
In C#, I find the yield keyword confusing and I am not fully confident about when it should be used.
Given the following two implementations, which approach is preferred, and why?
Version 1: Using yield return
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
foreach (Product product in products)
{
yield return product;
}
}
}
Version 2: Returning a materialized list
public static IEnumerable<Product> GetAllProducts()
{
using (AdventureWorksEntities db = new AdventureWorksEntities())
{
var products = from product in db.Product
select product;
return products.ToList();
}
}
Short Answer
By the end of this page, you will understand what yield return does in C#, how it creates lazy sequences, how it differs from ToList(), and why this matters when working with IEnumerable<T> and database contexts such as Entity Framework. You will also learn when yield return is useful, when it is risky, and which approach is safer in database-backed methods.
Concept
yield return is a C# feature for creating an iterator method. Instead of building a full collection in memory and returning it all at once, the method returns items one at a time as the caller asks for them.
This is called deferred execution or lazy evaluation.
When a method uses yield return:
- C# transforms it into a state machine behind the scenes.
- The method does not run all at once.
- It pauses each time it reaches
yield return. - It resumes when the caller asks for the next item.
That behavior can be very useful when:
- the result set is large,
- values are expensive to compute,
- you may not need all values,
- you want to stream items instead of buffering everything first.
However, with databases there is an important detail: the data source often depends on an open connection or active context. In your example, the AdventureWorksEntities object is wrapped in a using block. If enumeration happens after the context is disposed, the sequence may fail.
That is why this topic matters in real programming: lazy sequences are powerful, but they must not outlive the resources they depend on.
In this case:
yield returnstreams the products one by one.ToList()executes the query immediately and stores the results in memory before the context is disposed.
So the main tradeoff is:
yield return= lower memory use, deferred work, but resource lifetime must stay validToList()= eager execution, higher memory use, but safer when returning data from a method that closes the database context
Mental Model
Think of yield return like serving plates of food one at a time from a kitchen.
- The caller says, "Give me the next plate."
- The method goes back into the kitchen, prepares the next plate, and hands it out.
- It does not prepare the whole banquet in advance.
By contrast, ToList() is like preparing every plate, putting them all on a large table, and then handing over the full set at once.
Now add one more detail: the kitchen closes when the using block ends.
- If you use
yield return, the kitchen must still be open while people keep asking for plates. - If the kitchen closes too soon, you cannot serve the remaining plates.
- If you use
ToList(), all plates are prepared before the kitchen closes, so callers can safely use them later.
That is the core idea behind your example.
Syntax and Examples
The basic syntax for yield return looks like this:
public static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
When used:
foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
Output:
1
2
3
The numbers are produced one at a time.
Example with computed values
public static IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i;
}
}
}
Usage:
foreach (var n in GetEvenNumbers(10))
{
Console.WriteLine(n);
}
This avoids creating a separate list manually.
Equivalent eager version using List<T>
public static IEnumerable<int> GetEvenNumbersList(int max)
{
var result = new List<int>();
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
{
result.Add(i);
}
}
return result;
}
Both return IEnumerable<int>, but they behave differently:
yield returngenerates values lazilyList<T>creates the full collection first
Database-related example
Safer pattern when returning data from a short-lived Entity Framework context:
public static IEnumerable<Product> GetAllProducts()
{
using (var db = new AdventureWorksEntities())
{
return db.Product.ToList();
}
}
This executes the query before the context is disposed.
Important note
A yield return version over a database query is only safe if enumeration happens while the context is still alive. That is often not true when you return the sequence from a method.
Step by Step Execution
Consider this iterator:
public static IEnumerable<string> GetNames()
{
Console.WriteLine("Start");
yield return "Alice";
Console.WriteLine("Middle");
yield return "Bob";
Console.WriteLine("End");
}
And this code:
var names = GetNames();
Console.WriteLine("Iterator created");
foreach (var name in names)
{
Console.WriteLine(name);
}
What happens step by step
GetNames()is called.- The method does not run fully yet.
namesnow holds an iterator object.Console.WriteLine("Iterator created")runs first.- The
foreachloop asks for the first item. - The iterator starts executing.
- It prints
Start. - It reaches
yield return "Alice";and gives backAlice. - The loop prints
Alice. - The loop asks for the next item.
- Execution resumes after the first
yield return. - It prints
Middle. - It reaches
yield return "Bob";and gives backBob. - The loop prints
Bob. - The loop asks for another item.
- Execution resumes again.
- It prints
End. - The method finishes.
Expected output:
Iterator created
Start
Alice
Middle
Bob
End
Why this matters for your database example
With yield return, the database query may not actually be consumed until the caller starts iterating.
If the context has already been disposed by then, the iterator may try to read from a dead resource. That is the danger.
Real World Use Cases
yield return is useful when you want to stream data or compute items on demand.
Common practical uses
- Reading large files line by line
- Avoid loading the whole file into memory.
- Generating sequences
- Dates, IDs, test data, number ranges.
- Filtering pipelines
- Return only matching items as they are found.
- Tree or graph traversal
- Yield each node during recursive traversal.
- Custom parsers
- Produce tokens one at a time.
Example: file streaming
public static IEnumerable<string> ReadImportantLines(string path)
{
foreach (var line in File.ReadLines(path))
{
if (line.Contains("ERROR"))
{
yield return line;
}
}
}
Where ToList() is better
- Returning query results from a method that disposes its data source
- When you need to iterate multiple times without re-running work
- When you need indexing, count, or list-specific operations
- When consistency matters and you want a snapshot of the data now
Real Codebase Usage
In real projects, developers choose between lazy and eager execution based on resource lifetime, performance, and API expectations.
Common patterns
1. Materialize before leaving a repository or service method
public List<Product> GetProducts()
{
using (var db = new AdventureWorksEntities())
{
return db.Product.Where(p => p.IsActive).ToList();
}
}
Why:
- avoids returning a query tied to a disposed context
- makes the method behavior predictable
2. Use yield return for in-memory transformations
public static IEnumerable<Product> FilterExpensiveProducts(IEnumerable<Product> products)
{
foreach (var product in products)
{
if (product.ListPrice > 1000)
{
yield return product;
}
}
}
Why:
- no database context involved
- clean and memory-efficient
3. Guard clauses before yielding
public static IEnumerable<int> GetPositiveNumbers(IEnumerable<int> numbers)
{
if (numbers == null)
{
throw new ArgumentNullException(nameof(numbers));
}
foreach (var n in numbers)
{
if (n > 0)
{
yield return n;
}
}
}
4. Avoid exposing delayed execution accidentally
Sometimes a method returns IEnumerable<T> but callers assume the data is already loaded. In codebases, teams often prefer returning List<T> or IReadOnlyList<T> when the result is meant to be fully materialized.
Practical guideline
- If your method depends on an external resource that will soon be closed, materialize with
ToList(). - If your method computes or filters in-memory data and does not rely on a short-lived resource,
yield returnis often a great fit.
Common Mistakes
1. Using yield return with a disposed database context
Broken pattern:
public static IEnumerable<Product> GetAllProducts()
{
using (var db = new AdventureWorksEntities())
{
foreach (var product in db.Product)
{
yield return product;
}
}
}
Why it is risky:
- enumeration may happen after the method returns
- the context may already be disposed
- this can cause runtime errors
How to avoid it:
public static List<Product> GetAllProducts()
{
using (var db = new AdventureWorksEntities())
{
return db.Product.ToList();
}
}
2. Assuming yield return runs immediately
Broken assumption:
var data = GetNumbers();
// Expecting the iterator code to have already executed here
Reality:
- the code usually runs only when you iterate over
data
3. Enumerating a lazy sequence multiple times unintentionally
var products = GetExpensiveProducts();
var count = products.Count();
var first = products.First();
Problem:
- the sequence may run twice
- if it hits a database or expensive logic, this wastes work
Fix:
var products = GetExpensiveProducts().ToList();
var count = products.Count;
var first = products.First();
4. Returning IEnumerable<T> when callers really need a concrete collection
If callers need:
- indexing
- count without recomputation
- repeated iteration
then returning a list may be clearer.
5. Overusing yield return
Sometimes a LINQ query is simpler than writing a manual iterator.
Instead of:
public static IEnumerable<Product> GetActiveProducts(IEnumerable<Product> products)
{
foreach (var product in products)
{
if (product.IsActive)
{
yield return product;
}
}
}
You could write:
public static IEnumerable<Product> GetActiveProducts(IEnumerable<Product> products)
{
return products.Where(product => product.IsActive);
}
Choose the clearer option.
Comparisons
| Approach | Execution style | Memory usage | Safe after DB context is disposed? | Best for |
|---|---|---|---|---|
yield return | Deferred/lazy | Usually lower | Usually no, if it depends on the context | Streaming, in-memory iterators |
ToList() | Immediate/eager | Higher | Yes, because data is materialized first | Repository results, snapshots |
Return raw LINQ query (IQueryable<T> or deferred IEnumerable<T>) | Deferred | Lower at first | Risky if context is disposed | Query composition inside same context scope |
yield return vs ToList()
- Use
yield returnwhen you want to produce items one by one. - Use
ToList()when you need all results loaded now.
IEnumerable<T> vs List<T>
| Type | What it means |
|---|---|
IEnumerable<T> | A sequence you can iterate over |
List<T> | A concrete collection stored in memory |
A method can return IEnumerable<T> even if it internally uses ToList(), but the behavior is still eager if the list was already created.
Preferred choice for the original example
For the exact database example shown, Version 2 is generally preferred because it executes the query while the context is still alive and returns a safe in-memory collection.
The yield return version is not better just because it is more lazy. In this case, laziness introduces a lifetime problem.
Cheat Sheet
yield return quick reference
public static IEnumerable<int> GetValues()
{
yield return 1;
yield return 2;
}
Key rules
yield returnreturns one item at a time.- Execution is deferred until enumeration begins.
- The method pauses at each
yield return. - The method resumes on the next iteration request.
- The return type is usually
IEnumerable<T>orIEnumerator<T>.
Use yield return when
- you want lazy evaluation
- you want to stream large results
- you are iterating over in-memory data
- you are writing custom iterators
Use ToList() when
- you need a snapshot now
- the source depends on a disposable resource
- you will iterate multiple times
- you need list operations like indexing or
Count
Important edge case
Avoid returning a lazy sequence that depends on a disposed resource:
using (var db = new MyContext())
{
return db.Products; // risky if enumerated later
}
Safer:
using (var db = new MyContext())
{
return db.Products.ToList();
}
Quick decision guide
- Database context closing soon? ->
ToList() - Large in-memory stream? ->
yield return - Need repeated access? ->
ToList() - Need one-pass generation? ->
yield return
FAQ
What does yield return do in C#?
It creates a lazy iterator that returns items one at a time instead of building the full result immediately.
Is yield return faster than ToList()?
Not always. It can reduce memory use and avoid unnecessary work, but it may be slower if you enumerate multiple times or if the source depends on expensive operations.
Why can yield return be dangerous with Entity Framework?
Because the query may execute while iterating, and if the DbContext has already been disposed, enumeration can fail.
Should I always use ToList() in repository methods?
Not always, but it is often safer when the method creates and disposes the database context internally.
Can I return IEnumerable<T> even if I use ToList()?
Yes. The method signature can still be IEnumerable<T>, even though the data has already been materialized into a list.
When is yield return a good choice?
It is a good choice for custom iterators, file processing, sequence generation, and in-memory filtering where resource lifetime is not a problem.
Does yield return load everything into memory?
No. That is one of its main benefits. It usually generates values as needed.
Which version is preferred in the original question?
For that specific database example, the ToList() version is generally preferred because it safely materializes the results before the context is disposed.
Mini Project
Description
Build a small product filtering utility that demonstrates the difference between lazy iteration with yield return and eager materialization with ToList(). The project uses an in-memory list of products so you can safely focus on iterator behavior without database lifetime issues.
Goal
Create methods that filter products lazily and eagerly, then compare how and when each approach executes.
Requirements
- Create a
Productclass withName,Price, andIsActiveproperties. - Create a sample list with at least five products.
- Write one method that uses
yield returnto return active products. - Write one method that uses
ToList()to return expensive products. - Print the results of both methods and observe the execution order.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.