1. Mention 10 solid ways to improve EF Core code in .NET Core:
Use AsNoTracking() for read-only queries
This avoids change tracking overhead and makes queries faster.
var users = await _context.Users
.AsNoTracking()
.ToListAsync();
Select only the columns you need
Do not load full entities when you only need a few fields.
var users = await _context.Users
.Select(u => new { u.Id, u.Name })
.ToListAsync();
Avoid ToList() too early
Keep filtering in the database, not in memory.
// Bad
var users = _context.Users.ToList().Where(u => u.IsActive);
// Good
var users = await _context.Users
.Where(u => u.IsActive)
.ToListAsync();
Use async methods everywhere possible
Use ToListAsync(), FirstOrDefaultAsync(), SaveChangesAsync() to improve scalability in web apps.
var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == id);
await _context.SaveChangesAsync();
Prevent N+1 query problems
Load related data properly with Include() or better projections.
var orders = await _context.Orders
.Include(o => o.Customer)
.ToListAsync();
Prefer projection over heavy Include() when possible
Include() can fetch too much data. Projection is often cleaner and faster.
var orders = await _context.Orders
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer.Name,
Total = o.Total
})
.ToListAsync();
Use pagination for large datasets
Never load huge tables at once.
var page = 1;
var pageSize = 20;
var users = await _context.Users
.OrderBy(u => u.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
Keep DbContext short-lived
Register it as scoped and do not keep it alive too long. DbContext is not thread-safe and should usually live per request.
Add proper database indexes
EF Core code can be fine, but if queried columns are not indexed, performance suffers.
A few extra high-value habits:
- turn on logging for slow query analysis
- avoid lazy loading in performance-sensitive apps
- wrap multiple writes in transactions when needed
- separate entity models from API DTOs
Here is a summary:
- Use AsNoTracking() for reads
- Project only needed fields
- Filter in SQL, not memory
- Use async methods
- Avoid N+1 queries
- Prefer projection over excessive Include()
- Paginate results
- Keep DbContext scoped
- Add indexes
- Use compiled queries for repeated workloads
2. What is the difference between IQueryable and IEnumerable in EF Core?
Answer:
IQueryable builds the query and executes it in the database only when needed.
IEnumerable works on in-memory data after the query has already executed.
IQueryable<User> query = _context.Users.Where(u => u.IsActive);
IEnumerable<User> users = query.ToList();
Key point:
Use IQueryable as long as possible so filtering, sorting, and paging happen in SQL, not memory.
3. What is the N+1 query problem in EF Core?
Answer:
It happens when EF Core loads a parent list first, then loads related data one row at a time, causing many extra database calls.
Bad example:
var customers = await context.Customers.ToListAsync();
foreach (var customer in customers)
{
var orders = await context.Orders
.Where(o => o.CustomerId == customer.Id)
.ToListAsync();
}
If you have 100 Customers, that may become 101 queries.
Fix:
Use Include() or projection.
var data = await context.Customers
.Select(c => new
{
c.Id,
c.Name,
Orders = c.Orders.Select(o => new
{
o.Id,
o.Total
}).ToList()
})
.ToListAsync();
4. When should you use Include() and when should you use projection?
Answer:
Use Include() when you truly need the full related entity graph.
Use projection with Select() when you only need specific fields.
var result = await _context.Orders
.Select(o => new
{
o.Id,
CustomerName = o.Customer.Name,
o.Total
})
.ToListAsync();
Best practice: Projection is often faster and cleaner for APIs.
5. How do migrations work in EF Core?
Answer:
Migrations allow EF Core to track model changes and apply them to the database schema.
Common commands:
dotnet ef migrations add InitialCreate
dotnet ef database update
scaffolding