DEV Community

Cover image for EF Core vs. Dapper: Which .NET Data Access Tool Should You Use
Vijay Amalan for BoldSign

Posted on • Originally published at boldsign.com

EF Core vs. Dapper: Which .NET Data Access Tool Should You Use

Use EF Core when you want a full ORM that boosts productivity and long-term maintainability (LINQ, change tracking, migrations, cross-database providers). Use Dapper when you want maximum SQL control and minimal overhead, especially for read-heavy or performance-critical endpoints. If you’re building a large system, a hybrid approach (using EF Core for writes and Dapper for reads) often provides the best balance.

How do you choose between EF Core and Dapper in 60 seconds

Choose EF Core if most of these are true:

  • You’re building CRUD-heavy business features.
  • You want LINQ and strongly typed query composition.
  • You benefit from change tracking and unit-of-work patterns.
  • You want migrations and schema evolution built in (Microsoft Learn).
  • You want easy provider switching across databases (Microsoft Learn).

Choose Dapper if most of these are true:

  • You need handwritten SQL for complex joins, common table expressions (CTEs), or reporting.
  • You’re optimizing a hot path where overhead matters.
  • Your queries return data transfer objects (DTOs) or projections, not domain aggregates.
  • You want SQL that’s transparent to debug and tune.

Choose hybrid (EF Core and Dapper) if:

  • Writes benefit from EF Core’s tracking and transactions.
  • Reads need fast projections and SQL control (dashboards, search, reporting).
  • You’re already doing command query responsibility segregation (CQRS)-style separation, even informally.

What is EF Core and why does it exist

Entity Framework Core (EF Core) is Microsoft’s modern, cross-platform, open-source ORM for .NET that lets you work with a database using .NET objects and reduces repetitive data-access code. (Microsoft Learn)

At its core, EF Core exists to help developers program against a conceptual model instead of writing and maintaining large amounts of database plumbing manually—a goal that historically shaped Entity Framework as a concept. (Microsoft Learn)

What is Dapper and why does it exist

Dapper is a simple object mapper for .NET that focuses on executing SQL and efficiently mapping results to objects with minimal abstraction. (GitHub)

It exists to keep data access fast, explicit, and SQL-first, especially when teams want to avoid the overhead and “magic” of a full ORM.

What does ORM vs. micro ORM mean in practice

An ORM like EF Core typically provides:

A micro ORM like Dapper typically provides:

  • SQL execution helpers.
  • Fast mapping from rows to objects.
  • Minimal overhead and minimal “model management.”

Mental model of EF Core vs. Dapper

  • EF Core is a full-service kitchen: you place an order (LINQ/model), it cooks (SQL translation), serves (materialization), and cleans up (tracking).
  • Dapper is your own kitchen: you cook the SQL yourself, and Dapper helps plate it efficiently.

How does EF Core map data and track changes

EF Core is centered around a DbContext, which represents a session with the database and enables querying and saving. (Microsoft Learn)

When queries return entity types, EF Core tracks entities by default, keeping state in a change tracker so modifications can be persisted on SaveChanges. (Microsoft Learn)

Simplified procedure (text diagram):

EF Core query execution flow
EF Core query execution flow

EF Core explicitly passes a representation of the LINQ query to the provider, and the provider translates it to the database-specific language, such as SQL. (Microsoft Learn)

How does Dapper map query results to objects

Dapper is intentionally thin and performs very little work beyond executing SQL and mapping result rows to objects. This makes its runtime behavior predictable and its performance characteristics easy to diagnose under load. It works like this:

  • You write the SQL.
  • Dapper executes it using ADO.NET.
  • Each result row is mapped directly to an object or DTO by matching column names to properties.

There is no change tracking, relationship fix-up, or schema model. As stated on its GitHub page, Dapper’s focus is being a “simple object mapper.”

What EF Core features matter most in real systems

These are the features that usually drive teams toward EF Core for core business logic.

How do DbContext and DbSet shape your architecture

A quick explanation of how EF Core “wants” you to structure data access.

EF Core uses a model made up of entity classes and a context object (DbContext) representing a database session. (Microsoft Learn)

This naturally fits:

  • Unit-of-work patterns.
  • Aggregate-centric domain modeling.
  • Transactional write workflows.

Why do migrations change how teams manage schema

Migrations are a workflow decision as much as a feature.

EF Core migrations are source-controlled changes generated by comparing the current model to a previous snapshot, and EF Core records applied migrations in a history table. (Microsoft Learn)

This makes schema evolution part of normal development, not a separate manual process.

How do lazy loading and eager loading affect performance

Loading strategy is one of the most common sources of “EF is slow” incidents.

EF Core supports lazy loading via proxies, but it can cause extra database roundtrips (the classic N+1 problem). (Microsoft Learn)

That’s why many production teams prefer explicit eager loading for predictable query shapes.

What Dapper features matter most in real systems

These are the features that usually drive teams toward Dapper for read paths and reporting.

Why does SQL-first data access improve debugging and tuning

When the query is the product, SQL-first often wins.

Because you own the SQL, you can:

  • Copy and paste queries into your DB console.
  • Tune indexes and query plans directly.
  • Use database-specific features (CTEs, window functions, and hints where appropriate).

Why does no change tracking change your write strategy

This is where micro ORM simplicity can become a cost.

Dapper doesn’t track entity state, so writes are explicit:

  • You decide when to INSERT and UPDATE.
  • You manage concurrency patterns (row version/time stamps, etc.)
  • You build your own unit-of-work behavior (if needed).

EF Core vs. Dapper: Feature comparison table

Category EF Core (Full ORM) Dapper (Micro ORM)
Query style LINQ translated to SQL (Microsoft Learn) Raw SQL you write (GitHub)
Change tracking Yes by default (Microsoft Learn) No
Migrations Built-in workflow (Microsoft Learn) No
Loading related data Supports strategies; lazy loading has N+1 risk (Microsoft Learn) Manual joins and multimapping
Cross-database support Provider model (Microsoft Learn) Depends on ADO.NET provider and your SQL dialect
Best fit CRUD and domain modeling Read-heavy and complex SQL/projections
Debugging SQL Often requires logging and inspection Direct—SQL is already explicit

Where performance differences really come from

This section explains performance in terms of framework overhead, query shape, and database behavior.

When does EF Core overhead matter

EF Core can be fast, but features have costs.

EF Core tracking keeps entity instances in the change tracker, and no-tracking queries are generally quicker in read-only scenarios because there’s no need to set up tracking info. (Microsoft Learn)

EF Core overhead tends to show up when:

  • You load lots of entities into memory.
  • You do high-frequency read paths with tracking on.
  • You rely on lazy loading and trigger many roundtrips. (Microsoft Learn)

When does Dapper win (and when is it irrelevant)

Dapper’s gains are biggest when your app is CPU- or alloc-heavy, not DB-bound.

Dapper often shines when:

  • You return lightweight DTO projections.
  • You run many short queries per second.
  • You avoid extra allocations and tracking.

But if your bottleneck is:

  • network latency
  • slow queries or missing indexes
  • lock contention

…then switching ORMs won’t save you.

Code: Simple SELECT with EF Core vs. Dapper

Side-by-side examples make the difference in abstraction level obvious.

EF Core (LINQ and optional no-tracking)

C#

    // Entities: User { Id, FirstName, LastName, IsActive }
    var users = await db.Users
        .AsNoTracking() // best default for read-only lists
        .Where(u => u.IsActive)
        .OrderBy(u => u.LastName)
        .Select(u => new { u.Id, u.FirstName, u.LastName }) //projection helps performance
        .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

EF Core’s tracking versus no-tracking behavior is an explicit lever, and no-tracking is typically preferred for read-only queries. (Microsoft Learn)

Dapper (SQL and parameters)

C#

    const string sql = """
    SELECT Id, FirstName, LastName
    FROM Users
    WHERE IsActive = @IsActive
    ORDER BY LastName;
    """;
    var users = await connection.QueryAsync(
        sql,
        new { IsActive = true }
    );
    public sealed record UserListItem(int Id, string FirstName, string LastName);
Enter fullscreen mode Exit fullscreen mode

Code: Insert/update with EF Core vs. Dapper

Writes are where EF Core’s unit-of-work model changes day-to-day productivity.

EF Core (Tracked entity + SaveChanges)

C#

    db.Users.Add(new User
    {
        FirstName = "John",
        LastName = "V",
        IsActive = true
    });
    await db.SaveChangesAsync();
Enter fullscreen mode Exit fullscreen mode

Dapper (explicit SQL)

C#

    const string insertSql = """
    INSERT INTO Users (FirstName, LastName, IsActive)
    VALUES (@FirstName, @LastName, @IsActive);
    """;
    await connection.ExecuteAsync(insertSql, new 
    { 
        FirstName = "John",
        LastName = "V",
        IsActive = true
    });
Enter fullscreen mode Exit fullscreen mode

Code: Performance-focused query patterns you can actually use

Practical techniques that help more than “switch ORM” does.

How do you speed up hot EF Core read paths with compiled queries

Compiled queries can reduce repeated translation overhead on frequently executed queries.

EF Core provides EF.CompileQuery to create a compiled query delegate. (Microsoft Learn)

C#

    private static readonly Func> ActiveUsersQuery =
        EF.CompileAsyncQuery((AppDbContext db, bool isActive) =>
            db.Users
              .AsNoTracking()
              .Where(u => u.IsActive == isActive)
              .OrderBy(u => u.LastName)
              .Select(u => new UserListItem(u.Id, u.FirstName, u.LastName)));
    public async Task> GetActiveUsersAsync(bool isActive, CancellationToken ct) 
    {
        var result = new List();
        await foreach (var item in ActiveUsersQuery(db, isActive).WithCancellation(ct))
            result.Add(item);
        return result;
    }
Enter fullscreen mode Exit fullscreen mode

How do you keep Dapper fast and safe for hot endpoints

The “easy win” is correct parameterization and tight projections.

C#

    const string sql = """
    SELECT Id, FirstName, LastName
    FROM Users
    WHERE IsActive = @IsActive AND LastName >= @Start
    ORDER BY LastName
    OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;
    """;
    var page = await connection.QueryAsync(sql, new
    {
        IsActive = true,
        Start = "M",
        Skip = 0,
        Take = 50
    });
Enter fullscreen mode Exit fullscreen mode

Which tool fits common .NET application types

The following table is a scenario-based mapping that developers and architects can use during design review.

Workload EF Core Fit Dapper Fit Why
CRUD-heavy business apps Excellent Limited EF Core reduces boilerplate and supports tracking and migrations (Microsoft Learn)
Read-heavy APIs Good Excellent Dapper excels at fast projections and SQL control
Reporting and analytics Limited Excellent Hand‑tuned SQL and CTEs often matter more than entity modeling
Enterprise apps (long-lived) Excellent Good EF Core improves consistency; hybrid is often best
Microservices Good Good Choose per service: EF for write domain, Dapper for query services

Choose per service: EF for write domain, Dapper for query services

When does a hybrid EF Core + Dapper approach make sense

A hybrid EF Core–Dapper approach is commonly used in larger systems that need strong transactional guarantees for writes and high-performance, predictable reads.

This approach is inspired by the core idea of Command Query Responsibility Segregation (CQRS), which separates data-modifying operations (commands) from data-retrieval operations (queries). However, it does not implement full CQRS. Instead, it applies the principle in a pragmatic way, without introducing additional architectural complexity, such as command/query handler pipelines, message buses, separate read/write models, or event sourcing.

A hybrid approach is usually justified when:

  • The write model benefits from EF Core, including change tracking, relationship management, and transactional workflows. (Microsoft Learn)
  • The read model benefits from explicit SQL, optimized projections, and high-throughput query execution.
  • Schema evolution is managed through EF Core migrations, keeping database changes centralized. (Microsoft Learn)

Simplified data access flow (CQRS-inspired)

Simplified CQRS-style data access flow using EF Core and Dapper
Simplified CQRS-style data access flow using EF Core and Dapper

What this diagram shows:

  • EF Core is used for database writes, where tracking and transactions are valuable.
  • Dapper is used for database reads, where explicit SQL and minimal overhead provide better performance.
  • Both reads and writes operate against the same database.
  • This separation reflects CQRS principles without the full CQRS infrastructure.

How to implement EF Core for writes and Dapper for reads in an ASP.NET Core app

Straightforward setup with safe connection/transaction handling patterns.

Step 1: Keep EF Core as your primary unit of work

Use DbContext for write workflows and schema management:

  • Keep writes in an application service or command handler.
  • Use migrations for schema changes. (Microsoft Learn)

Step 2: Add a query service that uses Dapper for projections

Make reads explicit, DTO-oriented, and easy to tune.

C#

    public sealed class UserReadService
    {
        private readonly AppDbContext _db;
        public UserReadService(AppDbContext db) => _db = db;
        public async Task> SearchUsersAsync(string term, CancellationToken ct)
        {
            var conn = _db.Database.GetDbConnection();
            const string sql = """
            SELECT TOP (50) Id, FirstName, LastName
            FROM Users
            WHERE LastName LIKE @Term OR FirstName LIKE @Term
            ORDER BY LastName;
            """;
            // Ensure connection is open (EF opens it for its own operations; be explicit here)
            if (conn.State != System.Data.ConnectionState.Open)
                await conn.OpenAsync(ct);
            var rows = await conn.QueryAsync(sql, new { Term = $"%{term}%" });
            return rows.AsList();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Step 3: Share a transaction when you truly need read-your-writes

Only do this when required; otherwise keep reads and writes separate for simplicity.

In EF Core, you can start a transaction via DbContext.Database. Then you can pass the underlying DbTransaction to Dapper so that both participate in the same transaction boundary.

Note: Exact APIs vary slightly across EF Core versions and providers; if your team has a standard pattern, align with it. If uncertain, this needs to be clarified by the engineering team.

Best practices for EF Core and Dapper in production

Operational habits that prevent 80% of incidents.

How do you keep EF Core predictable under load

Treat EF Core as a powerful tool that needs guardrails.

  • Prefer projections (SELECT) for read endpoints instead of returning full entities.
  • Use no-tracking for read-only queries. (Microsoft Learn)
  • Avoid accidental lazy loading in request paths (N+1 risk). (Microsoft Learn)
  • Log SQL, timings, and parameter values with appropriate redaction.

How do you keep Dapper safe and maintainable

Most Dapper problems are not performance—they’re governance.

  • Always parameterize (never string-concatenate user input).
  • Store SQL in named queries (constants/files) with code review.
  • Prefer explicit column lists (avoid SELECT *).
  • Write integration tests for complex queries (schema drift happens).

What should you monitor regardless of ORM choice

Observability beats ideology.

  • Query duration percentiles (P50/P95/P99)
  • DB CPU, locks, deadlocks, and slow query logs
  • Connection pool saturation
  • Error rates and timeouts at the API layer

Common mistakes developers make with EF Core and Dapper

These are predictable failure modes you can prevent with code review:

  • Using EF Core tracking for read endpoints by default: Tracking has purpose, but read-only queries are generally faster than no-tracking. (Microsoft Learn)
  • Allowing lazy loading in high-traffic code paths: Lazy loading can cause extra round trips (N+1). (Microsoft Learn)
  • Blaming the ORM for slow queries without checking the DB plan: Missing indexes and bad query shapes dominate most performance issues.
  • Overusing Dapper for complex write workflows: You’ll rebuild unit of work, concurrency handling, and relationship integrity manually.
  • Letting SQL sprawl: Without conventions, Dapper codebases can become string soup.

Summary: EF Core vs. Dapper

  • EF Core is a full ORM that improves productivity with LINQ, tracking, migrations, and providers. (Microsoft Learn)
  • Dapper is a simple object mapper that keeps SQL explicit and overhead low. (GitHub)
  • Performance differences matter most on hot read paths, large result sets, or when tracking or lazy loading is misused. (Microsoft Learn)
  • A hybrid approach is often the most practical option in real systems.

Add fast, reliable eSignatures to your .NET apps with BoldSign’s developer-friendly API—start integrating BoldSign’s API today

Try BoldSign Free

Related blogs

Note: This blog was originally published at boldsign.com

Top comments (0)