DEV Community

Cover image for Dapper Micro-ORM Explained
Manik
Manik

Posted on

Dapper Micro-ORM Explained

What Is Dapper?

Dapper is classed as a micro ORM. That means it is still an Object–Relational Mapper, but it is intentionally lightweight, opinionated, and minimal.

Unlike full-featured ORMs such as Entity Framework or NHibernate, Dapper focuses on performance and control, providing just enough abstraction to map query results to objects—without hiding SQL from the developer.

Put simply:

Dapper executes SQL you write, maps the results to POCOs, and gets out of the way.

It does not:

  • Generate SQL
  • Track entities
  • Manage change detection
  • Enforce a domain model

And that is precisely why it is fast and predictable.

Why Dapper?

The primary reason teams choose Dapper over Entity Framework is control.

With EF, it’s easy to end up with:

  1. Complex LINQ queries
  2. Unpredictable SQL generation
  3. Poor execution plans
  4. Surprises that only show up in production

With Dapper:

  1. You write the SQL
  2. You know exactly what runs
  3. DBAs can review and optimise queries
  4. Performance issues are visible and diagnosable
--Example: fetching a single column by ID
SELECT EventName
FROM Events
WHERE Id = @Id

Enter fullscreen mode Exit fullscreen mode

There is no translation layer, no expression tree, and no guesswork.

When You Should Not Use Dapper

There are legitimate cases where Dapper is the wrong tool.

1. Heavy reliance on Entity Framework features

If your existing application depends on:

  1. Change tracking
  2. Lazy loading
  3. Cascade updates
  4. EF-specific behaviours

then switching to Dapper will be painful.
Dapper does no entity tracking. It works with plain POCOs. If you expect “EF magic”, you will be disappointed.

2. Teams that cannot or will not write SQL

  • This is uncomfortable to say, but it’s real.
  • Junior developers often resist Dapper because:
  • They must write SQL
  • They can’t hide behind LINQ
  • Mistakes are more visible

If your team is still learning ASP.NET and C#, adding SQL into the mix may slow them down initially.

That said: if you work with databases professionally, you should know SQL. Avoiding it only delays the problem.

Who came up with Dapper?

Dapper was created by the team at Stack Overflow.

They built it to solve a real production problem:

  • LINQ-to-SQL and early ORMs were consuming excessive CPU
  • SQL generation was opaque and inefficient
  • Scaling web servers became difficult

Dapper was introduced to:

  • Reduce overhead
  • Eliminate SQL generation costs
  • Improve predictability under load

It has since become one of the most widely used data access libraries in .NET.

What Dapper Actually Is

Dapper is:

  • A simple object mapper for .NET
  • Often called the “King of Micro ORMs” for speed
  • An extension over ADO.NET
  • At a high level, working with Dapper involves three steps:
  • Create an IDbConnection
  • Write SQL for CRUD operations
  • Execute the SQL using Dapper extension methods

Dapper supports:

  • SQL Server
  • PostgreSQL
  • MySQL
  • Oracle
  • SQLite

Core Dapper Extension Methods

Dapper extends IDbConnection with the following commonly used methods:

  1. - Execute
  2. - Executes a command and returns affected row count
  3. - Query
  4. - Executes a query and maps results to objects
  5. - QueryFirst
  6. - Returns the first row (throws if none)
  7. - QueryFirstOrDefault
  8. - Returns first row or default
  9. - QuerySingle
  10. - Expects exactly one row
  11. - QuerySingleOrDefault
  12. - One or zero rows only
  13. - QueryMultiple
  14. - Reads multiple result sets in a single round trip

Dapper supports both synchronous and asynchronous APIs. In modern .NET (6+), you should default to async.

Using Dapper in ASP.NET Web API (.NET 6+)

//Packages Needed
dotnet add package Dapper
dotnet add package Microsoft.Data.SqlClient

Enter fullscreen mode Exit fullscreen mode

DTO

public sealed class EventDto
{
    public int Id { get; init; }
    public string EventName { get; init; } = string.Empty;
}

Enter fullscreen mode Exit fullscreen mode

Interface

public interface IEventRepository
{
    Task<EventDto?> GetByIdAsync(int id);
}


Enter fullscreen mode Exit fullscreen mode

Interface*

public interface IEventRepository
{
    Task<EventDto?> GetByIdAsync(int id);
}

//Repository Using Dapper
public sealed class EventRepository : IEventRepository
{
    private readonly IDbConnection _connection;

    public EventRepository(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<EventDto?> GetByIdAsync(int id)
    {
        const string sql = """
            SELECT Id, EventName
            FROM Events
            WHERE Id = @Id
        """;

        return await _connection.QuerySingleOrDefaultAsync<EventDto>(
            sql,
            new { Id = id }
        );
    }
}



Enter fullscreen mode Exit fullscreen mode

Dependency Injection (Program.cs)

builder.Services.AddScoped<IDbConnection>(_ =>
    new SqlConnection(builder.Configuration.GetConnectionString("Default"))
);

builder.Services.AddScoped<IEventRepository, EventRepository>();

Enter fullscreen mode Exit fullscreen mode

This keeps:

  • Connection lifetime scoped per request
  • SQL explicit
  • Dapper isolated behind repositories

Web API Endpoint

[ApiController]
[Route("api/events")]
public sealed class EventsController : ControllerBase
{
    private readonly IEventRepository _repository;

    public EventsController(IEventRepository repository)
    {
        _repository = repository;
    }

    [HttpGet("{id:int}")]
    public async Task<IActionResult> Get(int id)
    {
        var result = await _repository.GetByIdAsync(id);
        return result is null ? NotFound() : Ok(result);
    }
}


Enter fullscreen mode Exit fullscreen mode

Repository Pattern: Reality Check

Let’s be blunt.

  • Generic repositories + Unit of Work are an anti-pattern in most real systems
  • They add abstraction without adding clarity
  • They actively hide performance problems
    A repository should:

  • Work with one aggregate

  • Express business intent

  • Use Dapper internally

  • Own its transaction boundary (per database)

If multiple aggregates must change together:

  • Your business process is the unit of work
  • Use domain events
  • Make handlers idempotent
  • Avoid distributed transactions

Dapper does not dictate architecture. Bad architecture with Dapper is still bad architecture.

Where Dapper Shines (Real Scenarios)

This is where Dapper earns its keep—especially in legacy systems.

  1. LINQ-to-SQL or EF choking on large datasets
  • Reporting queries
  • Historical data
  • Analytics
  • Batch processing
  1. Legacy WebForms / ASP.NET MVC apps
  • EF 4.x / LINQ-to-SQL performance issues
  • Gradual migration strategy
  • Co-existence with existing ORM
  1. Stored-procedure-heavy systems
  • Dapper maps SP results cleanly
  • No fighting EF abstractions
  1. High-throughput APIs
  • Reduced allocations
  • No tracking overhead
  • Predictable SQL execution
  1. Read-heavy CQRS read models
  • Dapper for reads
  • EF or other tools for writes (hybrid approach)

Final Take

Dapper is not a silver bullet.
It will not save bad SQL, bad schema design, or bad architecture.

But if:

  • You care about performance
  • You want control
  • You work with legacy systems
  • You need predictability at scale

Then Dapper is one of the most reliable tools in the .NET ecosystem.

Learn Dapper
Dapper, Entity Framework and Hybrid Apps

Top comments (0)