Dapper vs Entity Framework for Investment Data Reads: Why We Chose Dapper
Introduction
Every .NET engineering team eventually has the same conversation: Dapper or Entity Framework?
If you’re building a greenfield CRUD app with a clean schema, Entity Framework is genuinely great. It handles migrations, tracks changes, scaffolds your models, and lets you query with LINQ that reads almost like plain English. For a lot of applications, EF is the right call.
But investment technology isn’t a lot of applications.
When I joined the investment technology team at my company, our C# data layer needed to do something specific: move financial data reliably between Bloomberg, Clearwater, and our internal systems — hitting stored procedures that had been production-hardened over years, at query volumes and performance budgets that left little room for abstraction overhead.
This is the story of why we chose Dapper, and what we learned along the way.
The context: what our data layer actually had to do
Before we get into the ORM debate, it’s worth grounding this in what “investment data reads” actually means in practice.
Our system handles several categories of data flow:
- Bloomberg Data License feeds — scheduled SFTP file ingestion, normalized and loaded into our internal data layer
- Clearwater integration — reconciliation data moving between Clearwater and our portfolio systems
- Credit scoring inputs — structured data reads feeding a credit assessment module used by analysts
- Private placement pricing data — complex, schema-heavy records with relationships that don’t map cleanly to simple entity models
None of these are simple key-value lookups. They involve joins across multiple tables, conditional aggregations, and — critically — stored procedures that encode significant business logic built up over years of production use.
If your data layer looks anything like this, the EF vs Dapper choice stops being academic.
Why Entity Framework felt like the wrong fit
Entity Framework is an ORM, which means its job is to abstract the database away from you. That abstraction is genuinely useful when you want it. It becomes a liability when you don’t.
The stored procedure problem. EF can call stored procedures — but it’s not what it’s designed to do. The workflow is awkward: you define result types, call FromSqlRaw or ExecuteSqlRaw, and work around EF’s change tracking and model-mapping machinery for results that don’t come from tracked entities. For a team with a handful of stored procs, that’s manageable. For a team where stored procedures are the data access layer — where they encode pricing logic, business rules, and audit behavior — fighting EF’s defaults on every call gets old fast.
Legacy component compatibility. Our investment systems include legacy components that were built around stored procedures long before EF was a consideration. Migrating those stored procs to EF-managed queries wasn’t on the roadmap. It would have required understanding, rewriting, and re-testing years of accumulated business logic — a significant risk for systems that analysts and portfolio managers depend on daily. We needed a data layer that could work with those stored procedures, not around them.
Query translation opacity. With EF, the SQL you get isn’t always the SQL you wrote. LINQ-to-SQL translation works well until it doesn’t — and when you’re debugging a slow query pulling pricing records for a portfolio of complex instruments, you want to know exactly what’s hitting the database. EF’s query translation is predictable for common patterns, but edge cases exist, and in performance-sensitive contexts, unexpected query plans are a serious concern.
Change tracking overhead. EF’s change tracker is powerful, but it adds memory and CPU overhead even on read-only queries. For data pipelines running scheduled ingestion jobs where we’re reading and transforming thousands of records — not updating them — that overhead is pure cost with no benefit.
Why Dapper was the right call
Dapper is a micro-ORM. It sits one thin layer above ADO.NET, giving you object mapping without the rest of the ORM machinery. That simplicity is the point.
Stored procedures are first-class citizens. With Dapper, calling a stored procedure is straightforward:
using var connection = new SqlConnection(connectionString);
var results = await connection.QueryAsync<PricingRecord>(
"usp_GetPrivatePlacementPricing",
new { AsOfDate = asOfDate, PortfolioId = portfolioId },
commandType: CommandType.StoredProcedure
);
No result type scaffolding. No fighting the change tracker. No workarounds. You pass the proc name, the parameters, and the type you want back. Dapper handles the mapping. This single characteristic made Dapper the obvious choice for our legacy component compatibility requirement — we didn’t need to change a single stored procedure to move to a modern, clean data access layer.
You write the SQL; you get the SQL. With Dapper, what you write is what executes. There is no translation layer between your intention and the query plan. For performance-sensitive reads — pulling Bloomberg-normalized data for a scheduled process, or querying credit scoring inputs for an analyst session — this predictability is valuable. When something runs slow, you profile the SQL directly, not a generated intermediate.
Minimal overhead on read-heavy workloads. Dapper maps results directly to your objects without spinning up change tracking or identity maps. For pipelines doing high-volume reads from financial data tables, the performance difference versus EF is measurable. The Dapper benchmarks are well-documented in the .NET ecosystem, but the proof is in production: our ingestion jobs run lean.
It plays well with legacy. This was the underappreciated advantage. Our legacy components already had tested, production-verified stored procedures. Dapper let us wrap them in a modern, async C# data layer without touching the underlying SQL. We got clean interfaces, proper dependency injection, and testable repository patterns — while the stored procedures that analysts trusted stayed exactly as they were.
What the code structure looks like
We organize our data access around repository interfaces, which keeps the Dapper implementation details behind an abstraction boundary. This matters for testability — you can mock the repository in unit tests without any database infrastructure.
A typical repository looks like this:
public interface IPricingRepository
{
Task<IEnumerable<PricingRecord>> GetPrivatePlacementPricingAsync(
DateTime asOfDate,
int portfolioId);
}
public class PricingRepository : IPricingRepository
{
private readonly string _connectionString;
public PricingRepository(string connectionString)
{
_connectionString = connectionString;
}
public async Task<IEnumerable<PricingRecord>> GetPrivatePlacementPricingAsync(
DateTime asOfDate,
int portfolioId)
{
using var connection = new SqlConnection(_connectionString);
return await connection.QueryAsync<PricingRecord>(
"usp_GetPrivatePlacementPricing",
new { AsOfDate = asOfDate, PortfolioId = portfolioId },
commandType: CommandType.StoredProcedure
);
}
}
The interface is registered in your DI container, injected wherever it’s needed, and mockable in tests. The stored procedure logic lives where it always has. Nothing about the legacy system changes — you’ve just given it a clean, typed, async-friendly front door.
Where Entity Framework still makes sense
This isn’t an argument that EF is bad. It isn’t.
If your team is building a new domain model from scratch, EF’s migrations and model-first tooling are genuinely productive. If your schema is clean, relational, and maps naturally to entity classes, EF’s LINQ interface reduces boilerplate. If you need robust change tracking for a write-heavy domain — an order management system, a user account model — EF earns its weight.
The question is fit. For investment data systems with complex read patterns, performance sensitivity, and stored-procedure-based legacy components, Dapper’s thin profile is an asset. For teams building new data models in greenfield systems, EF’s scaffolding tools and migration support can accelerate early development significantly.
In a large enough codebase, using both is reasonable — EF for write-side domain models where change tracking adds value, Dapper for read-side queries and stored procedure calls where raw control matters.
The bottom line
When we evaluated Dapper against Entity Framework for our investment technology data layer, the decision came down to three things:
- Legacy stored procedures are assets, not liabilities — and we needed a tool that treated them that way
- Read-heavy financial data pipelines benefit from minimal ORM overhead
- Predictable SQL is non-negotiable when you’re debugging production data issues
Dapper gave us all three. The stored procedures our team built and tested over years stayed in place. Our ingestion pipelines got clean, async, testable interfaces. And when a query runs slow, we know exactly where to look.
If you’re working on a similar stack — C# data layer, financial systems, stored procedures that have been around longer than your current team — I’d love to hear how you’ve approached it. Drop a comment below.