IEnumerable vs IQueryable vs IAsyncEnumerable in .NET: Which Should Your Team Use and When?
A practical comparison of three core .NET interfaces โ learn the execution model, performance trade-offs, and decision rules for EF Core, streaming APIs, and enterprise data access in 2026.

IEnumerable, IQueryable, and IAsyncEnumerable are three of the most commonly used interfaces in .NET, yet they are routinely misused in production codebases. Choosing the wrong one at a repository or service boundary can silently destroy query performance, cause unnecessary full-table scans, or block threads under load. In this comparison, you will learn exactly how each interface executes queries, where it belongs in your architecture, and how to make the right call for your team in 2026 โ whether you are using EF Core 10, streaming large datasets, or working with in-memory collections.
๐ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐ https://www.patreon.com/CodingDroplets
Overview of the Three Interfaces
Before comparing them side by side, it helps to understand what each interface actually represents in the .NET type system.
IEnumerable is defined in System.Collections.Generic and is the base interface for all in-memory sequences in .NET. It exposes a single method: GetEnumerator(). When you write a LINQ query against an IEnumerable<T>, the query runs entirely in your application's memory โ all filtering, ordering, and projection happens in the CLR, not at the data source.
IQueryable extends IEnumerable<T> and lives in System.Linq. The critical difference is that it carries an Expression tree and a query Provider. When you build a LINQ query against an IQueryable<T>, the expression is translated by the provider โ typically by EF Core into SQL โ and executed at the database. Only the results are materialised into memory.
IAsyncEnumerable was introduced in C# 8 and .NET Core 3.0. It enables asynchronous iteration using await foreach. Instead of buffering an entire result set before returning it to the caller, the producer can yield items one at a time as they become available. This makes it the right choice for streaming large result sets, processing event feeds, or reading from sources that produce data over time.
How Is the SERP for This Topic?
When developers search for "IEnumerable vs IQueryable", they typically find old C# Corner articles and Stack Overflow threads that only compare the first two interfaces. The three-way comparison โ especially with IAsyncEnumerable and .NET 10 context โ is genuinely underserved. That is the angle this article addresses.
Side-By-Side Comparison
The following table summarises the key differences that determine which interface is correct for a given scenario.
| Dimension | IEnumerable | IQueryable | IAsyncEnumerable |
|---|---|---|---|
| Namespace | System.Collections.Generic | System.Linq | System.Collections.Generic |
| Execution location | In-memory (client) | Data source (server) | In-memory or streamed |
| Query translation | None โ LINQ runs in CLR | Translated by provider (e.g., SQL) | None โ iteration is async |
| Thread blocking | Synchronous | Synchronous until materialised | Non-blocking async |
| Best for | In-memory collections, post-fetch filtering | Database queries, deferred SQL | Large result sets, streaming, event feeds |
| Lazy evaluation | Yes | Yes | Yes (async yield) |
| Supported in EF Core | After materialisation | Native โ core query type | Via ToAsyncEnumerable() or async streams |
| When to avoid | Database queries, large sets | Already-materialised lists | Simple single-item lookups |
When Should You Use IEnumerable?
IEnumerable<T> is the right choice when the data is already in memory and you need to filter or transform it further. Common scenarios include:
Post-database filtering. Once you have called .ToList() or .ToArrayAsync() to materialise a query, the result is an in-memory collection. Any LINQ operations you apply after that point work against IEnumerable<T>. This is correct and expected.
Domain object manipulation. When you are computing derived values, applying business rules, or mapping DTOs from an already-loaded entity, IEnumerable<T> is the appropriate abstraction at the domain service layer.
Repository return types for small, bounded lists. If your repository always fetches a bounded set of records (for example, a user's last ten notifications), returning IEnumerable<T> after materialisation signals that the data is already loaded.
What to avoid: Never return IEnumerable<T> directly from a repository method that wraps an EF Core DbSet. Callers who add further LINQ operators will enumerate the entire table in memory rather than generating a narrower SQL query. This is one of the most common performance anti-patterns in .NET data access code.
When Should You Use IQueryable?
IQueryable<T> is the correct interface when you want the query provider โ EF Core in most enterprise .NET applications โ to translate LINQ expressions into optimised SQL executed at the database.
Repository and data access layer boundaries. When your repository returns an IQueryable<T>, calling layers can add Where, OrderBy, Select, and Skip/Take clauses that are folded into a single SQL statement. This is particularly valuable for search and list APIs where callers dynamically compose filters.
Composable query pipelines. IQueryable<T> enables query composition across layers. A base repository can return a filtered IQueryable<T> and an application service can add pagination or sorting before the query is ever executed.
EF Core 10 and named query filters. EF Core 10 introduced named query filters, which let you define multiple filters per entity type and selectively disable them. These filters are applied at the IQueryable<T> level before SQL generation, making composability more powerful than ever.
What to avoid: Avoid leaking IQueryable<T> across layer boundaries beyond your data access layer. If a controller action or domain service can call .Where() directly on an IQueryable<T> from a DbContext, you lose control over query shape and risk N+1 patterns or inadvertent full-table scans.
When Should You Use IAsyncEnumerable?
IAsyncEnumerable<T> solves a different class of problem. It is not about where the query executes โ it is about how results are consumed over time without blocking a thread.
Large result set streaming. When a database query returns thousands or tens of thousands of rows, buffering all of them into a List<T> before processing causes a memory spike. IAsyncEnumerable<T> allows you to process rows as they arrive from the database, keeping memory usage flat.
Server-Sent Events and streaming HTTP APIs. ASP.NET Core supports returning IAsyncEnumerable<T> directly from controller actions and minimal API endpoints. The framework serialises items as they are produced, enabling low-latency streaming responses without holding the entire payload in memory.
Event feed consumption. If you are consuming Kafka topics, Azure Service Bus messages, or any other event source, IAsyncEnumerable<T> models the unbounded, async-by-nature character of those feeds cleanly.
What to avoid: IAsyncEnumerable<T> is not a drop-in replacement for Task<IEnumerable<T>>. If you need to count, sort, or randomly access the full result set before processing, you should still materialise to a List<T> first. IAsyncEnumerable<T> shines specifically when forward-only, one-at-a-time processing is sufficient.
Real-World Trade-Offs for Enterprise Teams
N+1 Queries and Accidental Client Evaluation
Misusing IEnumerable<T> at the wrong layer is one of the leading causes of accidental client evaluation โ where EF Core silently pulls the full table into memory and applies filters locally. EF Core 6+ throws an explicit exception for unmappable client-side predicates, but the risk remains when developers store an IQueryable<T> in a variable typed as IEnumerable<T>, losing the ability to generate server-side SQL.
Memory and GC Pressure at Scale
Materialising a 200,000-row dataset into a List<T> with ToListAsync() before processing is a common bottleneck in reporting and export endpoints. Switching to IAsyncEnumerable<T> and streaming the output reduces peak heap allocation significantly, which matters in high-throughput services running under memory pressure.
How Do I Choose Between IQueryable and IAsyncEnumerable?
This is a common source of confusion. The short answer: they solve different problems and are often used together. Use IQueryable<T> to define what data you want from the database. Once EF Core begins streaming the results asynchronously via ToAsyncEnumerableAsync() or await foreach on the query, you are working with IAsyncEnumerable<T>. The two are complementary, not competing.
Performance Considerations in .NET 10
.NET 10's JIT improvements โ better inlining and loop optimisation โ benefit tightly iterated IEnumerable<T> and IAsyncEnumerable<T> workloads. NativeAOT gains in .NET 10 also improve startup performance for services that stream data via IAsyncEnumerable<T>, since trimming and precompilation eliminate interpreter overhead in hot paths.
Decision Matrix: Which Interface for Which Scenario?
| Scenario | Recommended Interface |
|---|---|
| EF Core database query with dynamic filters | IQueryable<T> |
| In-memory list transformation (post-fetch) | IEnumerable<T> |
| Streaming 10,000+ rows from database | IAsyncEnumerable<T> |
| ASP.NET Core streaming HTTP response | IAsyncEnumerable<T> |
| Returning a bounded, already-loaded list | IEnumerable<T> |
| Repository composable query builder | IQueryable<T> |
| Processing Kafka or Service Bus message feed | IAsyncEnumerable<T> |
| Sorting/counting full result before use | Materialise to List<T> first |
Anti-Patterns to Avoid
Returning IQueryable<T> from a public API or controller. This leaks data access concerns into the presentation layer and makes the query boundary impossible to test or control.
Wrapping IQueryable<T> in IEnumerable<T> at a repository interface. This loses the expression tree. Any LINQ added by the caller will run in memory after a full-table scan.
Calling .Result or .GetAwaiter().GetResult() on IAsyncEnumerable<T> iterations. Async streams must be consumed with await foreach. Blocking on async enumeration in a synchronous context will deadlock ASP.NET Core request threads.
Using IAsyncEnumerable<T> for single-item lookups. The overhead of the async state machine is not worth it when a simple FirstOrDefaultAsync() is the right tool.
Internal Resource Links
For related reading on Coding Droplets, see our guide on ASP.NET Core API Pagination: Offset vs Cursor vs Keyset and our in-depth EF Core Interview Questions for Senior .NET Developers (2026) to deepen your understanding of EF Core query behaviour.
Recommendation: What Should Your .NET Team Default To?
For most enterprise .NET teams in 2026, the default recommendation is:
Use
IQueryable<T>in your data access layer when querying a relational database via EF Core. Let the query provider do the heavy lifting.Materialise to
IEnumerable<T>(orList<T>) at the boundary where data moves from the data layer to your domain or application layer.Switch to
IAsyncEnumerable<T>when result sets are large, when building streaming HTTP responses, or when consuming event-based data sources.
This three-layer mental model aligns with the Microsoft docs guidance on EF Core querying and reflects how high-throughput .NET services are architected in practice.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
FAQ
What is the main difference between IEnumerable and IQueryable in C#?IEnumerable<T> executes LINQ queries in the application's memory. IQueryable<T> carries an expression tree that a query provider (such as EF Core) translates into a server-side query โ typically SQL โ so filtering happens at the database before any data is loaded into memory.
When should I use IAsyncEnumerable instead of Task? Use IAsyncEnumerable<T> when you want to process results as they arrive rather than buffering the entire set before starting. It is the right choice for large result sets, streaming HTTP responses, and event-driven data sources. Use Task<IEnumerable<T>> when you need the full set available before processing.
Can IQueryable be used with IAsyncEnumerable in EF Core? Yes. EF Core allows you to compose a query as IQueryable<T> and then consume it as IAsyncEnumerable<T> using await foreach directly on the query or via .ToAsyncEnumerable(). The LINQ expression is translated to SQL first; then the results are streamed asynchronously.
Does returning IQueryable from a repository violate clean architecture principles? Exposing raw IQueryable<T> from a repository to layers above the data access layer is generally discouraged in strict clean architecture because it couples those layers to EF Core's abstraction. A common compromise is to keep IQueryable<T> internal to the repository and expose only materialised collections or well-typed query specifications at the boundary.
Which interface is better for performance: IEnumerable or IQueryable? For database access, IQueryable<T> is almost always more performant because it executes the filter at the database and returns only the rows you need. IEnumerable<T> applied to a database-backed query will load all rows first. For already-materialised in-memory data, IEnumerable<T> is fine and IQueryable<T> offers no advantage.
Is IAsyncEnumerable supported in ASP.NET Core Minimal APIs? Yes. ASP.NET Core supports returning IAsyncEnumerable<T> directly from minimal API endpoint handlers. The framework streams the serialised items to the client as they are produced, which is ideal for large collection responses without buffering the full payload in memory.
Does IAsyncEnumerable work with EF Core 10? Yes. EF Core 10 supports async streaming through await foreach on any IQueryable<T> against a relational database. Combined with EF Core 10's new named query filters and improved LINQ translation, IAsyncEnumerable<T> consumption of large queries is well-supported and production-ready.






