In .NET development, selecting the right data access strategy is crucial for balancing performance, maintainability, and developer productivity. Entity Framework Core (EF Core) with LINQ provides a powerful full ORM experience, Dapper offers lightweight micro-ORM speed with raw SQL control, and Stored Procedures deliver database-level optimization and security.
This post explores a practical benchmark comparing these approaches on a real-world query: calculating team performance summaries (total tasks, completed tasks, and completion rate) with optional date range and team filters. The test uses a database with over 50,000 tasks, executed via BenchmarkDotNet in .NET (likely .NET 8+).
Understanding BenchmarkDotNet Results: A Quick Guide
BenchmarkDotNet produces detailed statistical reports to ensure reliable comparisons. Here's how to interpret the key columns in the results table:
- Mean: The average (arithmetic mean) execution time across all iterations. Lower is better.
- Error: Half of the 99.9% confidence interval—indicates the uncertainty in the Mean value.
- StdDev (Standard Deviation): Measures variability in measurements. Low StdDev means consistent results.
-
Ratio: The performance ratio relative to the baseline (here, EF Core LINQ marked as baseline).
- 1.00 = same as baseline
- <1.00 = faster (e.g., 0.50 = 2x faster)
- 1.00 = slower (e.g., 1.34 = 34% slower)
- RatioSD: Standard deviation of the ratio—shows how stable the relative performance is.
- Allocated: Managed memory allocated per operation (in KB or bytes). Lower means less GC pressure.
- Alloc Ratio: Memory allocation ratio relative to the baseline.
These metrics come from multiple warm-up and actual iterations, eliminating noise and providing statistically sound data.
The Technologies
EF Core with LINQ
EF Core translates LINQ queries to SQL, handling change tracking, migrations, and complex mappings.
Example from the benchmark:
var result = await (from task in tasksQuery
join team in _context.Teams on task.TeamId equals team.Id
group task by new { team.Id, team.Name } into g
select new TeamPerformanceSummaryResponseDto
{
TeamId = g.Key.Id,
TeamName = g.Key.Name,
TotalTasks = g.Count(),
CompletedTasks = g.Count(t => t.Status == "Completed" || t.CompletedAt != null),
CompletionRate = g.Count() > 0 ? (decimal)g.Count(t => t.Status == "Completed" || t.CompletedAt != null) * 100 / g.Count() : 0
}).ToListAsync();
Dapper with Raw SQL
Dapper maps raw SQL results to objects with minimal overhead and full SQL control.
Example:
var sql = @"
SELECT t.Id AS TeamId, t.Name AS TeamName,
COUNT(task.Id) AS TotalTasks,
SUM(CASE WHEN task.Status = 'Completed' OR task.CompletedAt IS NOT NULL THEN 1 ELSE 0 END) AS CompletedTasks,
CASE WHEN COUNT(task.Id) > 0
THEN CAST(SUM(CASE WHEN task.Status = 'Completed' OR task.CompletedAt IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(task.Id) AS DECIMAL(18,2))
ELSE 0 END AS CompletionRate
FROM Teams t
INNER JOIN Tasks task ON t.Id = task.TeamId
WHERE 1 = 1 [optional filters]
GROUP BY t.Id, t.Name
ORDER BY t.Id";
var result = await connection.QueryAsync<TeamPerformanceSummaryResponseDto>(sql, parameters);
Stored Procedures
Pre-compiled SQL in the database, called via Dapper here for fairness.
Example Procedure:
CREATE PROCEDURE sp_GetTeamPerformanceSummary
@StartDate DATETIME2 = NULL,
@EndDate DATETIME2 = NULL,
@TeamId INT = NULL
AS
BEGIN
SELECT t.Id AS TeamId, t.Name AS TeamName, ... [same aggregation logic]
FROM Teams t INNER JOIN Tasks task ON t.Id = task.TeamId
WHERE (@StartDate IS NULL OR task.CreatedAt >= @StartDate) ...
GROUP BY t.Id, t.Name
ORDER BY t.Id;
END
Calling with Dapper:
var result = await connection.QueryAsync<TeamPerformanceSummaryResponseDto>(
"sp_GetTeamPerformanceSummary", parameters, commandType: CommandType.StoredProcedure);
Real Benchmark Results (50,000+ Tasks)
The benchmark ran on modern hardware (Intel Core i7-7500U equivalent) with warm-up and multiple iterations.
Results Table:
Key Insights:
- EF Core LINQ is the fastest here (baseline), likely due to optimized query translation and connection management in this aggregation scenario.
- Dapper Raw SQL is ~34% slower but allocates less than half the memory.
- Stored Procedure is the slowest (~62% over EF) but uses the least memory (~22% of EF).
- High error/stddev in Dapper/SP suggests variability—possibly from connection handling or plan caching.
- Recent 2024-2025 benchmarks show EF Core closing the gap significantly, often outperforming or matching Dapper in complex queries when tuned (e.g., no tracking).
These results contrast with many older benchmarks where Dapper dominated simple reads, but for aggregations/joins with large data, EF Core's optimizations shine.
Overall Insights:
EF Core often leads or ties in speed for complex aggregations, thanks to modern optimizations in .NET 8+.
Dapper and Stored Procedures consistently use less memory.
Variability (higher Error/StdDev in some runs) points to factors like connection pooling and query plan caching.
Pros and Cons
EF Core LINQ
Pros:
- High productivity with type-safe LINQ.
- Excellent for complex domains and rapid development.
- Strong migrations and change tracking.
Cons:
- Higher memory usage.
- Potential for suboptimal SQL if not careful.
Dapper Raw SQL
Pros:
- Low memory footprint.
- Full SQL control for tuning.
- Great for read-heavy or high-throughput apps.
Cons:
- More boilerplate (manual SQL, parameters).
- No built-in migrations or tracking.
Stored Procedures
Pros:
- Minimal client-side allocation.
- Pre-compiled plans and security benefits.
- Ideal for regulated or legacy environments.
Cons:
- Logic in database → harder versioning/maintenance.
Less flexible for dynamic queries.
When to Choose Each (2026 Perspective)EF Core LINQ → Default for most apps. Productivity wins, and performance is now excellent (especially .NET 8+). Use for CRUD, complex models.
Dapper → Performance hotspots, APIs with massive reads, or when you need precise SQL control. Hybrid approach common.
Stored Procedures → Security-critical ops, heavy DB logic, or when DBAs own optimizations. Call via Dapper for best results.
Recent discussions (2025) confirm: EF Core is the go-to unless extreme performance demands raw SQL.
Conclusion
No universal winner—EF Core offers the best balance for most modern .NET apps in 2026, with surprising speed in aggregations. Dapper excels in memory efficiency, and Stored Procedures in allocation/security.
Always benchmark your specific workload!
The full code (including interface, DTOs, and benchmark setup) is shared above for easy replication.
The complete benchmark project is available on GitHub:
https://github.com/Productive-Build-Cycle/Reporting/tree/perf/benchmark-query
I invite you to star the repo, fork it, run the benchmarks on your setup, add more scenarios (e.g., bulk inserts, different data sizes), or share your results! Open issues for discussions or pull requests for improvements contributions are very welcome!
_
What’s your experience? Share in the comments!
What do your benchmarks show? Comment here or on GitHub!_


Top comments (1)
What surprises you most about these results?