DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

.NET EF Core Should Be Your Default in 2026 — Not Dapper

.NET EF Core Should Be Your Default in 2026 — Not Dapper

.NET EF Core Should Be Your Default in 2026 — Not Dapper

TL;DR — The performance gap that once justified defaulting to Dapper has narrowed dramatically. In modern production systems, network latency, I/O, and architectural clarity dominate request cost. EF Core should now be your default — and Dapper your precision tool.

This is not ideology.

This is 2026 reality.


The Outdated Default

For years, the advice was simple:

“Default to Dapper. Use EF Core only when you need ORM features.”

The reasoning was benchmark-driven.

// Dapper
var users = await connection.QueryAsync<User>(
    "SELECT * FROM Users WHERE IsActive = 1");
Enter fullscreen mode Exit fullscreen mode

versus

// EF Core
var users = await db.Users
    .Where(u => u.IsActive)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Microbenchmarks showed Dapper outperforming EF Core by multiples.

The problem?

Those benchmarks were:

  • Single-table queries
  • CPU-bound
  • Run in isolation
  • Detached from real application architecture

Production systems are not microbenchmarks.


What Changed in EF Core

Recent EF Core releases (7, 8, and into 2026 previews) delivered:

  • Faster query pipeline
  • Improved materialization
  • Better expression caching
  • Reduced tracking overhead
  • Compiled query optimizations
  • Split query improvements

The result: real-world parity for many workloads.

The old headline — “Dapper is 3x faster” — no longer holds universally.

And performance parity changes the default decision.


Production Reality: Where Time Is Actually Spent

Consider a typical API request:

  1. Middleware pipeline
  2. Authentication
  3. Logging
  4. Network round-trip to database
  5. ORM materialization
  6. Serialization
  7. Network response

In most services:

  • Database round-trip: milliseconds
  • ORM materialization: microseconds
  • Serialization: often more expensive than ORM
  • Logging & filters: measurable cost

If the database call consumes less than ~40% of total request time, switching ORMs will not materially change your latency.

This is the architectural lens most teams miss.


Developer Velocity Is the Real Multiplier

Let’s compare failure modes.

EF Core — Compile-Time Safety

var orders = await db.Orders
    .Where(o => o.CreatedAtDate > DateTime.UtcNow)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

If CreatedAtDate does not exist, this fails at compile time.

Dapper — Runtime Risk

await connection.QueryAsync<Order>(
    "SELECT * FROM Orders WHERE CreatedAtDate > @date",
    new { date = DateTime.UtcNow });
Enter fullscreen mode Exit fullscreen mode

A typo compiles.

It fails in production.

The difference is not philosophical.

It is operational.

Over years, compile-time safety compounds into fewer incidents.


EF Core and Complex Object Graphs

Modern applications are not flat tables.

They include:

  • Aggregates
  • Navigation properties
  • Owned types
  • Relationships
  • Value objects

EF Core handles this natively:

public async Task<OrderSummaryDto?> GetOrderSummaryAsync(int orderId)
{
    return await _db.Orders
        .AsNoTracking()
        .Where(o => o.Id == orderId)
        .Select(o => new OrderSummaryDto
        {
            OrderId = o.Id,
            OrderDate = o.CreatedAt,
            CustomerName = o.Customer.FirstName + " " + o.Customer.LastName,
            Items = o.Items.Select(i => new OrderItemDto
            {
                ProductName = i.Product.Name,
                Quantity = i.Quantity,
                LineTotal = i.Quantity * i.UnitPrice
            }).ToList(),
            Total = o.Items.Sum(i => i.Quantity * i.UnitPrice) + o.TaxAmount
        })
        .FirstOrDefaultAsync();
}
Enter fullscreen mode Exit fullscreen mode

Single projection.

Single LINQ pipeline.

Strong typing across joins.

The Dapper equivalent requires:

  • Manual SQL
  • Multi-mapping
  • splitOn configuration
  • Manual aggregation logic

That complexity is not free.


Performance Patterns That Actually Matter

EF Core performance is not about ORM choice.

It is about usage patterns.

1. Disable Tracking for Reads

var products = await db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Tracking removed → overhead removed.


2. Project Only What You Need

var list = await db.Products
    .AsNoTracking()
    .Select(p => new ProductListItem
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price
    })
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Reduced payload.

Reduced memory pressure.

Reduced serialization cost.


3. Avoid N+1

var orders = await db.Orders
    .Include(o => o.Items)
    .ThenInclude(i => i.Product)
    .AsSplitQuery()
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

AsSplitQuery() prevents join explosion.

This matters more than ORM selection.


4. Drop to SQL When Necessary

var metrics = await db.Database
    .SqlQueryRaw<DashboardMetric>(sql, parameters)
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

EF Core does not prevent optimization.

It allows escape hatches.


Where Dapper Still Wins

Let’s be precise.

Dapper is superior when:

  • Bulk inserts exceed 10,000 rows
  • You have known, measured hotspots
  • You need extremely narrow reporting queries
  • You want full SQL control and accept manual mapping

Example hybrid pattern:

public async Task BulkInsertProductsAsync(List<Product> products)
{
    if (products.Count > 10000)
    {
        await _connection.ExecuteAsync(sql, products);
    }
    else
    {
        await _db.Products.AddRangeAsync(products);
        await _db.SaveChangesAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

This is not ideology.

It is selective optimization.


Production-Grade EF Configuration

Your configuration matters more than your ORM.

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(30), null);
            sqlOptions.CommandTimeout(30);
        });

    if (builder.Environment.IsDevelopment())
    {
        options.EnableDetailedErrors();
        options.EnableSensitiveDataLogging();
    }
});
Enter fullscreen mode Exit fullscreen mode

And model tuning:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .HasIndex(o => new { o.CustomerId, o.CreatedAt });

    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => !o.IsDeleted);

    modelBuilder.Entity<Order>()
        .HasOne(o => o.Customer)
        .WithMany(c => c.Orders)
        .OnDelete(DeleteBehavior.Restrict);
}
Enter fullscreen mode Exit fullscreen mode

Indexes and query filters will outperform ORM swapping.

Every time.


Decision Framework for 2026

START

  • Are you handling proven hotspots with high throughput?

    • Profile first. Then consider Dapper.
  • Do you operate with complex relationships?

    • EF Core.
  • Does your team value refactor safety?

    • EF Core.
  • Are you bulk-processing massive datasets?

    • Dapper (for that path only).

DEFAULT → EF Core.


The Honest Recommendation

Default to EF Core.

Then:

  1. Measure your system.
  2. Identify bottlenecks.
  3. Optimize targeted paths.
  4. Introduce Dapper surgically where justified.

Prematurely defaulting to Dapper introduces:

  • Manual SQL debt
  • Runtime errors
  • Reduced refactor safety
  • Increased cognitive load

Developer velocity compounds.

Compile-time guarantees compound.

Architectural clarity compounds.


Final Thought

In 2026, performance parity is real for most workloads.

The debate is no longer about speed.

It is about maintainability, safety, and engineering leverage.

EF Core should be your default.

Dapper should be your scalpel.

— Written by Cristian Sifuentes

Full‑stack engineer · .NET architect · Systems thinker

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.