Entity Framework Core makes database operations easier by allowing developers to work with C# objects instead of writing SQL queries for every operation. When building ASP.NET Core APIs, developers frequently need to retrieve data along with its related records.
For example, an API might return a list of orders with customer information, order items, product names, and payment details.
Entity Framework Core provides different ways to retrieve this information. Two commonly used approaches are Include and Select.
Although both can retrieve related data, they serve different purposes. Include loads related entities into navigation properties, while Select allows developers to choose exactly which data should be returned.
Choosing the wrong approach can result in unnecessary database queries, large response payloads, excessive memory usage, and slower API responses.
In this article, we will explore the differences between Include and Select in EF Core, understand how they affect SQL queries, and learn when to use each approach in production applications.
Understanding Include in Entity Framework Core
The Include method is used for eager loading in Entity Framework Core.
Eager loading means retrieving related entities as part of the database query rather than loading them later when their navigation properties are accessed.
Consider an application with two entities: Customer and Order.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public ICollection<Order> Orders { get; set; } = [];
}
public class Order
{
public int Id { get; set; }
public decimal TotalAmount { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
}
A customer can have multiple orders. The Orders property represents the relationship between the two entities.
Suppose an API needs to retrieve customers along with their orders.
You can use Include:
var customers = await _context.Customers
.Include(c => c.Orders)
.ToListAsync();
The Include method tells EF Core to load the related Orders collection for each customer.
The query is executed when ToListAsync is called.
EF Core generally translates this query into a SQL statement using a JOIN to retrieve the related records.
The returned objects contain Customer entities with their Orders navigation properties populated.
Using ThenInclude for Nested Relationships
Sometimes an API needs more than one level of related information.
For example, an order may contain multiple items, and each item may reference a product.
You can use ThenInclude to load nested relationships.
var customers = await _context.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.OrderItems)
.ThenInclude(i => i.Product)
.ToListAsync();
This query retrieves customers, their orders, order items, and related products.
Include and ThenInclude are useful when the application needs complete entity relationships rather than only a few selected properties.
Understanding Select Projection in EF Core
The Select method provides another way to retrieve database information.
Instead of loading complete entities, Select allows developers to define exactly which properties should be returned.
This process is called projection.
Consider the same customer and order example.
Suppose the API only needs the customer ID, customer name, and total number of orders.
You do not need to load every order entity.
Instead, use Select:
var customers = await _context.Customers
.Select(c => new
{
c.Id,
c.Name,
OrderCount = c.Orders.Count()
})
.ToListAsync();
This query returns only the selected information.
EF Core translates the projection into SQL and retrieves the required columns and calculated value.
The application does not need to load complete Customer and Order entities to calculate the order count.
This can reduce unnecessary data transfer and object creation.
Using Select With DTOs
For production APIs, returning anonymous objects is not always convenient. A dedicated Data Transfer Object provides a clear response structure.
Create a customer response model:
public sealed class CustomerResponse
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int OrderCount { get; set; }
}
Now project the database result directly into the DTO:
var customers = await _context.Customers
.Select(c => new CustomerResponse
{
Id = c.Id,
Name = c.Name,
OrderCount = c.Orders.Count()
})
.ToListAsync();
This approach is particularly useful for APIs because the response contains only the information required by the client.
It also prevents internal entity properties from being exposed unnecessarily.
Include vs Select: Understanding the Difference
Both approaches can retrieve related information, but they produce different result structures.
Consider the following requirements:
An administrative screen needs complete customer and order entities for further processing.
A customer dashboard only needs the customer's name, order count, and total spending.
The first scenario may benefit from Include, while the second is better suited to Select.
Here is a comparison.
Feature | Include | Select |
|---|
Primary purpose | Load related entities | Project selected data |
Result | Entity objects | DTOs, anonymous objects, or scalar values |
Related data | Loads navigation properties | Selects required related values |
Column selection | Generally loads mapped entity columns | Retrieves only selected columns |
Change tracking | Entities are tracked by default | Depends on whether entities are included in the projection |
API response design | Often requires additional mapping | Can project directly into response DTOs |
Best suited for | Working with entity relationships | Read-only API responses and reporting |
The important point is that Include and Select are not direct replacements for each other in every scenario.
Include determines which related entities are loaded. Select determines the shape of the result.
Comparing Include and Select With a Real API Example
Let's consider an order management API.
The API needs to return the order ID, customer name, order date, total amount, and product names.
Approach 1. Using Include
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(i => i.Product)
.ToListAsync();
This query loads complete Order entities, related Customer entities, OrderItems, and Product entities.
The API can then map these objects into response models.
However, it retrieves more information than the response actually requires.
For example, the Product entity might contain internal descriptions, inventory information, or other properties that the API never uses.
Approach 2. Using Select
Create a response DTO:
public sealed class OrderResponse
{
public int OrderId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public List<string> Products { get; set; } = [];
}
Now write the projection:
var orders = await _context.Orders
.Select(o => new OrderResponse
{
OrderId = o.Id,
CustomerName = o.Customer.Name,
OrderDate = o.OrderDate,
TotalAmount = o.TotalAmount,
Products = o.OrderItems
.Select(i => i.Product.Name)
.ToList()
})
.ToListAsync();
This query retrieves the information required by the API response.
The Product navigation does not need an Include because its Name property is selected directly through the projection.
EF Core translates the projection into SQL based on the database provider and query structure.
For a read-only API endpoint, this is generally a cleaner approach.
Does Select Automatically Disable Change Tracking?
This is an important detail that developers sometimes misunderstand.
Select does not automatically disable change tracking.
When a query projects only scalar values or DTO properties without returning entity instances, there are no entity instances for EF Core to track.
For example:
var customers = await _context.Customers
.Select(c => new CustomerResponse
{
Id = c.Id,
Name = c.Name,
OrderCount = c.Orders.Count()
})
.ToListAsync();
This projection does not return Customer entities, so the result does not contain entities to track.
However, consider this example:
var customers = await _context.Customers
.Select(c => new
{
Customer = c,
c.Name
})
.ToListAsync();
The result contains Customer entity instances.
EF Core tracks those entities by default in a tracking query.
If the application only needs read-only data, you can use AsNoTracking with entity-based queries.
var customers = await _context.Customers
.AsNoTracking()
.Include(c => c.Orders)
.ToListAsync();
AsNoTracking avoids normal change-tracking overhead for the returned entities.
However, adding AsNoTracking to a projection containing only scalar values or DTO properties does not provide an additional tracking benefit because there are no entity instances to track.
Understanding Performance Problems With Include
Include is useful, but loading too many related collections can create performance problems.
One important issue is called cartesian explosion.
Consider a blog that has multiple posts and multiple contributors.
var blogs = await _context.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.ToListAsync();
Both Posts and Contributors are collection navigations at the same level.
If one blog has 10 posts and 10 contributors, a single joined query can produce 100 rows for that blog.
This happens because the relational query combines the matching rows from both collections.
As the number of related collections increases, the result can become significantly larger than expected.
Using AsSplitQuery
EF Core provides AsSplitQuery to load related collections through separate SQL queries.
var blogs = await _context.Blogs
.Include(b => b.Posts)
.Include(b => b.Contributors)
.AsSplitQuery()
.ToListAsync();
Instead of retrieving everything through one large joined result, EF Core executes multiple queries.
This can reduce duplicated rows, but it introduces additional database round trips and may require more memory for buffering intermediate results.
It is not automatically faster in every situation.
Choose between single and split queries based on the number of related records, network latency, and consistency requirements.
When Should You Use Include or Select?
The right approach depends on what the application needs to do with the retrieved data.
Scenario | Recommended approach | Reason |
|---|
Loading an entity for editing | Include | Related entities may be needed for updates |
Displaying a customer list | Select | Only required fields need to be returned |
Generating dashboard summaries | Select | Aggregated values can be calculated in the database |
Loading an order with all items | Include | The application needs the complete entity relationship |
Returning API response DTOs | Select | Avoids loading unnecessary entity properties |
Exporting selected database fields | Select | Reduces data retrieval |
Loading several related collections | Include with AsSplitQuery when appropriate | Helps avoid excessive joined result rows |
A useful rule is simple: if the application needs entity objects and their relationships, Include is appropriate. If it only needs specific values, prefer Select.
Advantages and Disadvantages
Advantages of Include
Simple relationship loading: Developers can load related entities without manually writing JOIN operations.
Entity tracking: Loaded entities can be modified and persisted through SaveChanges.
Readable query structure: Include and ThenInclude clearly describe the relationships required by the application.
Useful for business operations: Complete entity graphs can be convenient when application logic needs to work with related objects.
Disadvantages of Include
Unnecessary columns: Complete entities may contain properties that the application does not need.
Larger result sets: Multiple collection relationships can produce cartesian explosion.
Memory usage: Loading large entity graphs creates more objects in application memory.
Change-tracking overhead: Tracking large numbers of entities requires additional processing.
Advantages of Select
Reduced data retrieval: Only the required columns are selected.
Better API response design: Developers can project directly into DTOs.
Less unnecessary object creation: Complete entity graphs are not required for simple read operations.
Database-side calculations: Counts, sums, and other translatable expressions can be calculated in SQL.
Disadvantages of Select
More projection code: Developers must define the response structure explicitly.
Translation limitations: Not every C# expression can be translated into SQL.
No automatic entity graph: If complete entities are needed, a projection must explicitly include them.
More maintenance when response models change: Projection definitions may need updating when DTO properties change.
Best Practices for Better EF Core API Performance
Project Only the Required Properties
Avoid loading complete entities when an API only needs a few fields.
Use Select to retrieve the exact data required by the response model.
Avoid Unnecessary Includes
Do not add Include statements simply because navigation properties exist.
Load related entities only when the application actually needs them.
Use AsNoTracking for Read-Only Entity Queries
When returning entity instances that will not be modified, consider AsNoTracking to avoid normal change-tracking overhead.
Watch for Cartesian Explosion
When loading multiple collection navigations, inspect the generated SQL and consider AsSplitQuery when it provides a better execution strategy.
Limit the Number of Returned Records
Large API responses can create unnecessary database, memory, and network overhead.
Use pagination with appropriate ordering instead of retrieving every matching record.
Inspect Generated SQL
Use EF Core logging or database query tools to understand what SQL is being executed.
Do not assume that a shorter LINQ expression always produces a more efficient database query.
Common Mistakes and Troubleshooting
Problem | Possible reason | Recommended solution |
|---|
API retrieves too many columns | Complete entities are being loaded unnecessarily | Use Select projection |
API response contains missing related data | Required navigation properties were not loaded or projected | Use Include or select the required related properties |
Multiple collection Includes produce large results | Cartesian explosion | Evaluate AsSplitQuery or a narrower projection |
Read-only queries consume additional resources | Entities are being tracked unnecessarily | Consider AsNoTracking |
Changes are not saved | DTOs or scalar projections are being modified instead of tracked entities | Load or attach the required entity and update it correctly |
Projection fails at runtime | An expression cannot be translated by the database provider | Simplify the expression or perform appropriate client-side processing after filtering |
API becomes slow with large datasets | Too many records are being returned | Add pagination and review database indexes |
Summary
Include and Select are both important parts of writing efficient Entity Framework Core queries, but they solve different problems.
Include is useful when an application needs complete related entities and their navigation properties. Select is generally more appropriate when an API needs only specific properties, calculated values, or response DTOs.
For read-heavy ASP.NET Core APIs, projection with Select can help reduce unnecessary data retrieval, object creation, and response size.
However, Include remains valuable for entity-based operations, especially when the application needs to modify related records.
The best approach is to understand what the API actually needs before writing the query. Retrieve only the required data, avoid unnecessary relationships, inspect generated SQL, and use split queries when they provide a genuine benefit.
These practices help keep EF Core queries easier to maintain while improving the efficiency of database operations in production applications.
Join the conversation! Your thoughts help the community grow.