I have spent nearly thirty years building software on the Microsoft stack and have worked with .NET since its early days. Whenever I start a new application, I eventually face the same question: how should it talk to the database?
For a long time, my answer has been Dapper and parameterized T-SQL rather than Entity Framework. That choice is not based on a belief that Entity Framework is slow.
Dapper is fast and stays close to raw ADO.NET, but EF Core has improved substantially over the years. Microsoft also provides extensive guidance for finding and fixing performance problems. For most applications, the difference between the two is unlikely to be the deciding factor.
I choose Dapper because I want control over the SQL.
I have spent a large part of my career working directly in SQL Server, so writing a query does not feel like plumbing that ought to be hidden from me. I want to see the statement the database receives. I want to examine its execution plan, understand how it uses the available indexes, and tune it when necessary. With Dapper, the query in my code is the query I investigate when something runs slowly.
That is a better fit for the way I work than starting with LINQ and then examining the SQL generated from it. EF Core provides tools for doing that, of course, but it adds a translation step that I usually do not need.
Dapper originated at Stack Overflow and was built to solve the sort of data-access problems Stack Overflow encountered in production. Its role is deliberately limited: it executes SQL and maps the returned rows to objects. It does not try to manage the database for me.
I also like that Dapper is small enough to understand. When a library sits between an application and its data, being able to follow what it does is valuable. That has become more important to me, not less, as my projects have grown older.
None of this makes Entity Framework a bad choice. A team that prefers LINQ, designs its schema alongside its object model, and relies on code-first migrations may be more productive with EF Core. It provides useful features that Dapper intentionally does not.
Most of the systems I work on have a different history. Their databases tend to outlive the applications that use them. This year, for example, I rewrote a web application originally built in 2013 and moved it to .NET 10. The production database did not need to change. A new application was simply taking its place in front of an existing schema, and Dapper handled that arrangement without trying to redesign it.
That is why I continue to use it. Dapper is not the most ambitious part of the architecture, and I do not want it to be. I know what SQL it will execute, I know where to look when a query misbehaves, and I can replace the application without asking the database to follow it.
For a data-access layer, boring and predictable are useful qualities. After this many years, they are the qualities I value most.