TL;DR: Use Entity Framework Core for 90% of your application (CRUD, complex relational graphs, developer velocity). Drop down to Dapper for the 10% of queries that are hyper-performance sensitive or require massive bulk data processing. You don't have to choose just one—they work perfectly together.
The debate has raged in the .NET community for over a decade: Should you use the heavy-duty, full-featured Entity Framework (EF) Core, or the blazing-fast, lightweight Dapper?
With the massive performance improvements in EF Core 9, the lines are blurrier than ever. Let's break down the realities of both tools in modern architecture.
Entity Framework Core 9: The Productivity King
EF Core is a full Object-Relational Mapper (ORM). It abstracts away SQL entirely, allowing you to interact with your database using C# LINQ queries.
Why You Should Use EF Core
- Developer Velocity: You can build out complex data models and queries in minutes.
- Change Tracking: When you update an entity, EF Core automatically knows exactly which fields changed and generates the optimal
UPDATEstatement. - Migrations: EF Core handles database schema evolutions seamlessly.
- Strong Typing: Your queries are checked at compile-time. If a column name changes, your code won't compile, saving you from production runtime crashes.
❌ The Dark Side of EF Core
// This innocent looking code can generate a massive N+1 query problem,
// fetching thousands of rows unnecessarily and locking the database.
var orders = await _context.Orders.Include(o => o.OrderLines).ToListAsync();
Dapper: The Performance King
Created by the team at StackOverflow, Dapper is a "Micro-ORM". It doesn't write SQL for you, nor does it track changes. It does exactly one thing: maps the result of a SQL query directly to a C# object, and it does it faster than anything else.
Why You Should Use Dapper
- Raw Speed: Dapper adds virtually zero overhead on top of the native
SqlDataReader. - Complex SQL Control: When you have a massive, 15-join analytical query with CTEs (Common Table Expressions) and window functions, writing raw SQL is much easier than wrestling with LINQ.
- Memory Efficiency: Without a Change Tracker eating up memory, Dapper is perfect for reading massive datasets.
❌ The Dark Side of Dapper
// You have to write raw SQL strings.
// If you rename 'CreatedAt' to 'CreatedDate' in the database,
// this code compiles fine but crashes at runtime.
var sql = "SELECT Id, Name, CreatedAt FROM Users WHERE Status = @Status";
var users = await connection.QueryAsync<User>(sql, new { Status = 1 });
The Ultimate Solution: Hybrid Architecture
The biggest mistake developers make is treating EF Core and Dapper as mutually exclusive. The best architecture uses both.
They both utilize the underlying DbConnection, meaning they can easily share the same transaction pipeline!
graph LR
A[Client Request] --> B{Action Type}
B -- Write / Complex Graph --> C[EF Core]
B -- Heavy Read / Analytics --> D[Dapper]
C --> E[(SQL Server)]
D --> E
The Hybrid Implementation
Use EF Core for all your INSERT, UPDATE, and DELETE operations where Change Tracking is a massive time-saver. Use Dapper for your heavy SELECT operations on your reporting dashboards.
public class UserService
{
private readonly ApplicationDbContext _context;
// Use EF Core for standard business logic
public async Task UpdateUserStatusAsync(int id, string status)
{
var user = await _context.Users.FindAsync(id);
user.Status = status;
await _context.SaveChangesAsync(); // Easy, safe, change-tracked.
}
// Use Dapper for a heavy analytical read operation
public async Task<IEnumerable<UserReportDto>> GetHeavyUserReportAsync()
{
// Extract the raw connection from EF Core!
var connection = _context.Database.GetDbConnection();
var sql = @"
SELECT u.Id, u.Name, COUNT(o.Id) as OrderCount
FROM Users u
JOIN Orders o ON u.Id = o.UserId
GROUP BY u.Id, u.Name";
return await connection.QueryAsync<UserReportDto>(sql);
}
}
Final Verdict
Stop fighting. Start your project with EF Core. When (and only when) a specific query becomes a bottleneck, extract the DbConnection from EF Core and rewrite that single query in Dapper. You get 100% of the productivity with 100% of the performance.