# EF Core vs Dapper: When to Use Each

> EF Core and Dapper are the two most popular .NET data access libraries. EF Core provides a full ORM with change tracking, migrations, and LINQ. Dapper gives you raw SQL performance with minimal abstraction. Here is when to use each one.

Published: 2026-07-04. Last updated: 2026-07-14. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/ef-core-vs-dapper

Every .NET team eventually has this argument: **EF Core** or **Dapper**?
Half the room wants change tracking, migrations, and LINQ.
The other half wants to see exactly what SQL hits the database.

Both halves are right, just about different parts of the codebase.
Here is how I decide which tool gets which job.

## Two Philosophies

**EF Core** is a full-featured ORM. It manages your database schema, tracks entity changes, translates LINQ to SQL, and handles relationships. You work with C# objects and EF Core handles the SQL.

**Dapper** is a micro-ORM. You write SQL yourself, and Dapper maps the results to C# objects. No change tracking, no migrations, no LINQ translation. Just fast SQL execution.

Both are excellent. The question is which fits your use case.

## EF Core Strengths

### LINQ and Type Safety

```csharp
// EF Core - compile-time checked, refactoring-safe
var orders = await dbContext.Orders
    .Where(o => o.Status == OrderStatus.Active)
    .Include(o => o.Customer)
    .OrderByDescending(o => o.CreatedAt)
    .Take(10)
    .Select(o => new OrderSummary(o.Id, o.Customer.Name, o.TotalAmount))
    .ToListAsync();
```

If you rename `TotalAmount` to `Amount`, the compiler catches broken LINQ queries.
SQL strings require integration tests or execution to catch the mismatch.

### Change Tracking

```csharp
var order = await dbContext.Orders.FindAsync(orderId);
order.UpdateStatus(OrderStatus.Shipped); // Just modify the entity
await dbContext.SaveChangesAsync(); // EF Core generates the UPDATE
```

EF Core knows which properties changed and generates a targeted `UPDATE` statement - only for the modified columns.

### Migrations

```bash
dotnet ef migrations add AddShippingAddress
dotnet ef database update
```

Schema changes are version-controlled and reproducible. With Dapper, you manage **migrations** separately.

### Rich Domain Model Support

EF Core maps complex entity hierarchies, [**value objects**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals), owned types, and inheritance strategies. Mapping a **DDD aggregate** to the database is straightforward with EF Core's configuration API.

## Dapper Strengths

### Performance

Dapper has less ORM overhead for straightforward reads because it skips change tracking and LINQ translation and uses a lean materialization pipeline.
Whether that changes end-to-end latency depends on the query and database work.

Two honest caveats about that claim:

- The gap shrinks dramatically when you use EF Core well: `AsNoTracking()`, projections, and compiled queries remove most of the overhead
- The difference is measured in microseconds to low milliseconds per query - it only matters at high request rates or large row counts

You won't find benchmark numbers here, and you shouldn't take anyone else's on faith either.
Benchmark your own queries against your own schema; the results depend heavily on row size, column count, and mapping complexity.

### Full SQL Control

```csharp
var orders = await connection.QueryAsync<OrderSummary>(
    """
    SELECT o.id, c.name AS customer_name, o.total_amount
    FROM orders o
    INNER JOIN customers c ON o.customer_id = c.id
    WHERE o.status = @Status
      AND o.created_at > @CutoffDate
    ORDER BY o.created_at DESC
    LIMIT 10
    """,
    new { Status = "Active", CutoffDate = DateTime.UtcNow.AddDays(-30) });
```

You control the exact SQL. Useful for complex queries with CTEs, window functions, or database-specific features.
For mapping joined results into object graphs, see [**mastering Dapper relationship mappings**](https://milanjovanovic.tech/blog/mastering-dapper-relationship-mappings).

### Transparent Data Access

What you write is what executes. No surprises, no **N+1 queries**, no unexpected JOINs.

## When to Use EF Core

**For write operations.** Change tracking, unit of work, and transaction management make writes straightforward. You modify entities, call `SaveChanges`, and EF Core handles the rest.

**For domain-rich applications.** If you're doing **DDD** with aggregates, domain events, and complex entity hierarchies, EF Core's mapping capabilities are invaluable.

**For most of your application.** EF Core is the default choice for .NET applications. Start here unless you have a specific reason not to.

**For teams that prefer LINQ.** Type-safe, refactoring-friendly, no raw SQL to maintain.

## When to Use Dapper

**For measured read hot paths.** If profiling shows material EF Core translation or materialization overhead, Dapper gives you a lower-level path for that query.

**For complex SQL.** CTEs, recursive queries, window functions, full-text search, and database-specific features are easier to express in SQL than LINQ.

**For reporting queries.** Complex aggregations across many tables are often clearer as SQL.

**For legacy database access.** When you can't map a clean domain model to an existing schema, Dapper lets you query whatever you need without fighting the ORM.

## Using Both Together (CQRS)

The best approach: use **both**. This is natural with [**CQRS**](https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start):

- **Commands (writes)**: Use EF Core - change tracking, unit of work, domain model
- **Queries (reads)**: Use Dapper - raw performance, custom SQL, flat DTOs

![CQRS data access split: command writes flow through EF Core, query reads flow through Dapper, and both hit the same database](https://milanjovanovic.tech/blogs/articles/ef-core-vs-dapper/cqrs-ef-core-dapper.png)

```csharp
// Command handler - EF Core for writes
public class PlaceOrderCommandHandler
{
    private readonly AppDbContext _dbContext;

    public async Task<Result<Guid>> Handle(PlaceOrderCommand command, ...)
    {
        var order = Order.Create(command.CustomerId, command.Items);
        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync(cancellationToken);
        return order.Id;
    }
}

// Query handler - Dapper for reads
public class GetOrderByIdQueryHandler
{
    private readonly IDbConnection _connection;

    public async Task<Result<OrderResponse>> Handle(GetOrderByIdQuery query, ...)
    {
        var order = await _connection.QueryFirstOrDefaultAsync<OrderResponse>(
            """
            SELECT o.id, c.name AS customer_name, o.total_amount, o.status
            FROM orders o
            JOIN customers c ON o.customer_id = c.id
            WHERE o.id = @OrderId
            """,
            new { query.OrderId });

        return order is not null
            ? Result.Success(order)
            : Result.Failure<OrderResponse>(OrderErrors.NotFound);
    }
}
```

You get the best of both: EF Core's rich domain model for writes, and Dapper's raw performance for reads.

## Side-by-Side Comparison

How the two stack up, point by point:

- **Type**: EF Core is a full ORM; Dapper is a micro-ORM
- **Query language**: LINQ vs. raw SQL
- **Change tracking**: built into EF Core; Dapper has none
- **Migrations**: EF Core has them built in; with Dapper you bring your own tool
- **Read performance**: EF Core is good; Dapper is excellent
- **Write ergonomics**: EF Core handles updates for you; Dapper writes are fully manual
- **Learning curve**: EF Core is bigger to learn; Dapper is a handful of extension methods
- **Complex SQL**: limited in LINQ; unrestricted in Dapper
- **DDD support**: excellent in EF Core; limited in Dapper
- **Unit of Work**: built into EF Core; manual transaction management with Dapper

## Use Each Tool Where It Fits

EF Core is a strong default for aggregate writes, change tracking, migrations, and composable LINQ queries.
Dapper is useful when explicit SQL makes a complex read clearer or measured materialization overhead matters.
Both can share a connection and transaction, so the choice can be made per use case instead of per application.

## Frequently asked questions

### Is Dapper faster than EF Core?

For reads, yes, because Dapper skips change tracking and LINQ translation. The gap has narrowed with modern EF Core, especially when you use AsNoTracking and projections. For typical applications the difference rarely matters; for hot read paths it can.

### Can I use EF Core and Dapper in the same project?

Yes, and it is a common CQRS setup: EF Core for commands where change tracking and the domain model help, Dapper for queries where you want hand-tuned SQL. They can even share the same database connection.

### Does Dapper support migrations?

No. Dapper only executes SQL and maps results. You need a separate tool for schema management, such as EF Core migrations, FluentMigrator, or plain SQL scripts.

### When should I choose Dapper over EF Core?

When you need full control over SQL (CTEs, window functions, database-specific features), when profiling shows EF Core is the bottleneck on a read-heavy path, or when working against a legacy schema that fights ORM mapping.

### Is EF Core good enough for high-performance applications?

Usually yes. With projections, AsNoTracking, compiled queries, and pooling, EF Core performs well under heavy load. Most performance problems come from the queries developers write, not from EF Core itself.
