Dapper vs EF Core — My Take After Using Both
My actual opinion after using both — not a neutral comparison, this is what I'd pick and why.

I've used both Dapper and EF Core for quite a while now, and honestly, I don't think one of them is simply better than the other.
Actually, my own usage is not even something like "EF for writes and Dapper for reads."
For me, it is more about how complicated the data I need is and how I want to think about the query.
I have been working with SQL and relational databases since college, so SQL is already pretty natural for me. Because of that, when I have to deal with several related tables and need a very specific result, I often find Dapper more comfortable.
But when I just need to quickly get something from the database, especially for validation or a simple application workflow, EF Core is usually my first choice.
That's where my current opinion comes from.
The way I actually use them
A very common situation for me is something like this:
I need to validate something, and as part of that validation I need to check whether a user exists.
With EF Core, that's pretty straightforward.
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == request.UserId); Or maybe I don't even need the entire user.
var exists = await dbContext.Users.AnyAsync(x => x.Id == request.UserId);For something like this, I don't really see a reason to write SQL manually.
EF makes it quick.
I can express what I need directly, and move on.
This is probably where I use EF the most in my current work.
Where I start preferring Dapper
The situation changes when the data requirement becomes more complicated.
Imagine I need to get a user, their orders, and payment information together.
Maybe I need some specific fields from the user, some order information, payment status, totals, dates, and a few other related pieces of data.
And I need all of that as one particular result.
This is where I personally start preferring Dapper.
I can write the SQL myself:
SELECT
u.id,
u.name,
o.id AS order_id,
o.order_date,
p.id AS payment_id,
p.status AS payment_status
FROM users u
LEFT JOIN orders o
ON o.user_id = u.id
LEFT JOIN payments p
ON p.order_id = o.id
WHERE u.id = @UserId;I know exactly what I'm asking the database for.
I can look at the joins.
I can change the projection.
I can add a condition.
I can inspect the query directly in PostgreSQL.
And then Dapper maps the result to whatever structure I need.
That feels very natural to me.
Why EF Core feels different to me here
This is probably where my SQL background influences my opinion quite a lot.
I don't necessarily think EF Core joins are objectively bad or even difficult.
I just personally find more complicated relational queries easier to reason about when I'm looking directly at SQL.
With EF, I might end up writing something like:
var result = await dbContext.Users
.Where(u => u.Id == userId)
.Include(u => u.Orders)
.ThenInclude(o => o.Payment)
.FirstOrDefaultAsync();And this is perfectly valid.
But once the requirement becomes more specific, I personally start thinking:
"Okay, what SQL is this actually going to generate?"
Then I might need to think about tracking, navigation properties, projections, includes, query splitting, the shape of the generated SQL, and whether I'm actually retrieving more data than I need.
At that point, I often find it easier to just write the SQL.
Again, that's probably more about how I think about relational data than EF Core itself being bad.
EF Core is still my first choice for simple things
This is something I probably wouldn't have appreciated when I first started using these tools.
If I need to do something simple, I don't want to write SQL just for the sake of writing SQL.
For example:
var user = await dbContext.Users
.FirstOrDefaultAsync(x => x.Id == userId);or:
var exists = await dbContext.Users
.AnyAsync(x => x.Email == email);or:
var user = await dbContext.Users
.Where(x => x.Id == userId)
.Select(x => new
{
x.Id,
x.Name,
x.Email
})
.FirstOrDefaultAsync();These are simple enough that EF Core makes my life easier.
I don't need to create a SQL string.
I don't need to manually map the result.
I don't need to think about basic connection handling.
I just express what I need.
And for these cases, I like EF.
Dapper gives me a different kind of control
The thing I really enjoy about Dapper is that it doesn't try to hide the database from me.
If I want SQL, I write SQL.
If I want a particular projection, I write that projection.
If I need several joins, I write those joins.
If I need a PostgreSQL-specific feature, I can use it directly.
There is very little distance between what I write and what the database receives.
That's especially useful for me because I already think in terms of tables, relationships, joins and SQL.
So Dapper feels less like learning another abstraction and more like putting a small mapping layer around SQL.
But Dapper isn't automatically better
This is where I think a lot of comparisons get too simplistic.
"Dapper is faster than EF Core" might be true in certain benchmarks.
But that doesn't automatically mean I should use Dapper for everything.
If I have a simple operation that takes five lines of EF Core and I replace it with SQL, now I've introduced more code and another thing to maintain without necessarily solving a real problem.
For example, I don't think this:
var user = await dbContext.Users
.FirstOrDefaultAsync(x => x.Id == userId);needs to become:
const string sql = """
SELECT id, name, email
FROM users
WHERE id = @Id
""";
var user = await connection
.QuerySingleOrDefaultAsync<User>(
sql,
new { Id = userId });just because Dapper might have lower overhead.
The second version isn't automatically better.
For that particular operation, I personally prefer the first one.
Where I think the real trade-off is
For me, the decision looks more like this:
Simple database operation
↓
EF Core
↓
"Just get me this data"
Whereas:
Complex relational query
↓
Multiple joins
Specific projection
SQL-heavy logic
↓
Dapper
↓
"I know exactly what data I want"
That's a much better description of how I actually use them.
It's not:
EF = writes
Dapper = reads
And it's not:
EF = slow
Dapper = fast
It's more about abstraction vs control.
What about performance?
I don't want to make a strong claim here without measuring it properly.
Dapper generally has less overhead because it is doing less. EF Core has more functionality and therefore more machinery around the query.
But the database itself is still going to be a huge part of the equation.
A bad query with no appropriate index isn't going to become good just because I used Dapper.
A well-designed EF Core query isn't automatically going to be a performance problem either.
The actual things I'd want to measure are query execution time, generated SQL, database execution plans, amount of data returned, tracking overhead, allocations, and behavior under realistic concurrency.
So if I ever write a second part of this note, I'd rather benchmark an actual workload than copy numbers from some random Dapper vs EF Core benchmark.
My current preference
If someone asked me today which one they should use, my answer would probably be:
Use EF Core when it makes the operation simpler. Use Dapper when having direct control over the SQL makes the operation simpler.
That's basically how I use them myself.
If I need to quickly fetch a user for validation, check whether something exists, update a straightforward entity, or perform a simple query, EF Core is usually enough for me.
If I need to pull together data from several related tables and produce a specific result shape, I often prefer Dapper because writing the SQL directly is easier for me to understand and control.
And because of my background with SQL and relational databases, I might naturally lean toward Dapper more than someone who is very comfortable thinking in terms of ORM entities and navigation properties.
That's probably the most important part of my opinion.
I'm not choosing Dapper because "Dapper good, EF bad."
I'm choosing it in situations where SQL is the easier abstraction for me.
And I'm choosing EF Core when the abstraction saves me work without getting in my way.
That's my current view.
It might change as I get more experienced with EF Core's advanced querying, compiled queries, projections, split queries, and its generated SQL.
And that's actually something I want to test rather than assume.