"EF Core is always right" and "raw SQL is always faster" are both wrong for the exact same reason — they're answers to a question that only ever has an honest answer once you know the specific query.
Somewhere on the internet right now, two developers are arguing past each other. One says "just use raw SQL, ORMs are slow." The other says "just use EF Core, hand-writing SQL is a maintenance nightmare." Both of them are describing something real — and both of them are wrong to state it as a universal rule, because the honest answer was never "always X." It's "it depends on which specific query, and here's exactly what it depends on."
You've now spent this entire Part learning EF Core deeply — query optimization, compiled queries, transactions, concurrency, connection management, stored procedures, bulk operations, caching. You've also learned, back in 138, that raw SQL done carelessly is a genuine security liability, and, in 233, that "this feels slow" is never a substitute for actually measuring where time goes. This lesson pulls all of that together into one honest, judgment-based answer to the question its title asks.
In this lesson, you'll learn the specific, legitimate cases where raw SQL or ADO.NET genuinely outperforms or better fits a given problem than EF Core, the — more common — cases where EF Core is simply the better tool, and how to make the actual decision without falling into either camp's absolutism.
This isn't a lesson about a new tool — you already have both tools. EF Core translates your C# LINQ into SQL, tracks entities, and manages the plumbing of connections, transactions, and change detection for you. Raw SQL — through ADO.NET directly, or through EF Core's own FromSqlRaw/FromSqlInterpolated from 278 — hands you full, direct control over exactly what SQL text runs, with none of EF Core's translation or tracking layered on top. The question this lesson answers is: given a specific piece of data access, which one is actually the right choice?
EF Core is an object-relational mapper: it translates LINQ expression trees into parameterized SQL at query time, manages an in-memory identity map and change-tracking graph (280), and generates INSERT/UPDATE/DELETE statements from tracked entity state on SaveChangesAsync(). Raw SQL/ADO.NET is the direct database-driver layer underneath all of that — no translation step, no tracking, no automatic statement generation; you write the exact SQL text (safely parameterized, per 138 and 278) and read results back yourself. EF Core is built on top of ADO.NET, not a replacement for it — which is exactly why dropping down to raw SQL for one specific query, inside an otherwise EF Core-driven application, is a supported, first-class capability rather than an escape from the framework.
"ORMs are slow" is describing something real: EF Core's translation layer, change tracking, and general-purpose SQL generation carry genuine overhead compared to a driver sending exactly the SQL text you wrote, and for a handful of genuinely exotic, hand-tuned queries, EF Core's LINQ translation may not produce the exact execution plan a DBA could write by hand. "Hand-written SQL is a maintenance nightmare" is also describing something real: a codebase full of string-built SQL scattered across methods loses compile-time checking on every query shape, becomes fragile the moment a column is renamed, and reintroduces the injection risk from 138/278 anywhere a developer gets careless. Both statements are true — for a specific slice of situations each one is talking about, and false as a blanket rule for everything else.
EF Core doesn't force an all-or-nothing choice. Because it's built on ADO.NET and exposes FromSqlRaw/FromSqlInterpolated/ExecuteSqlRaw/ExecuteSqlInterpolated as first-class, fully supported escape hatches, you get to default to LINQ for the vast majority of your application — where its productivity, safety, and maintainability wins are largest — and drop to raw SQL selectively, for the specific query that genuinely needs it, without rewriting or abandoning the rest of your data access layer.
Multi-table pivots, window functions, recursive CTEs, and database-specific aggregation features often express far more clearly, and sometimes execute more efficiently, as hand-written SQL than as a deeply nested LINQ expression straining to represent the same logic. Forcing a genuinely SQL-shaped report through LINQ can produce both a harder-to-read C# expression and a less efficient generated query than a human would write directly.
279's ExecuteUpdate/ExecuteDelete already close most of this gap directly in LINQ — but for the genuine remaining edge, like a large one-time bulk insert, a dedicated bulk-insert library or hand-written batch SQL still commonly outperforms row-by-row tracked entity operations, exactly as 279 covered.
Occasionally a query needs a specific index hint, a particular join strategy, or a query shape EF Core's translator simply doesn't produce from the equivalent LINQ. When the exact SQL text matters — not just the result — raw SQL is the only way to guarantee it.
This is the case most often invoked and least often earned honestly. "I think this query would be faster in raw SQL" is a guess. 233 already taught you why guesses about performance are, in practice, wrong more often than they're right. The legitimate version of this case looks like: dotnet-counters or a profiler flagged this specific endpoint as slow → .ToQueryString() or query logging showed EF Core's generated SQL for the suspect query → comparing that generated SQL against a hand-tuned equivalent showed a real, measured difference → then raw SQL for that one query is justified. Skipping straight to "raw SQL will be faster" without that chain is exactly the premature-optimization trap 233 warned about, wearing a data-access hat.
This section gets more space deliberately — for the overwhelming majority of real application code, this is the honest, common answer, not the exception.
And beyond any single technical point: EF Core is simply faster to write correct code in, for the large majority of an application's data access. That developer-productivity win compounds — fewer places for bugs to hide, faster onboarding for new team members, less code to review per feature — in a way that's easy to undervalue against a narrow, query-by-query performance comparison.
A retail application's order-management module, built almost entirely on EF Core:
// Ordinary CRUD — LINQ, no question about it
public async Task<Order?> GetOrderAsync(int orderId) =>
await context.Orders
.Include(o => o.LineItems)
.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == orderId);
public async Task UpdateShippingStatusAsync(int orderId, ShippingStatus status)
{
Order order = await context.Orders.FindAsync(orderId)
?? throw new InvalidOperationException("Order not found");
order.ShippingStatus = status;
await context.SaveChangesAsync();
}And, sitting right alongside it, one quarterly finance report — genuinely reporting-shaped, genuinely needing database-specific windowing functions no reasonable LINQ expression would produce cleanly — dropped deliberately into raw SQL:
public async Task<List<MonthlyRevenueRow>> GetMonthlyRevenueTrendAsync(int year)
{
return await context.Database
.SqlQuery<MonthlyRevenueRow>($"""
SELECT
Month,
Revenue,
Revenue - LAG(Revenue) OVER (ORDER BY Month) AS MonthOverMonthChange
FROM MonthlyRevenueView
WHERE Year = {year}
""")
.ToListAsync();
}Nothing about writing this one report in raw SQL required abandoning EF Core anywhere else in the application. That's the whole point this lesson is making: the two tools coexist, deliberately, in the same codebase, chosen query by query rather than framework by framework.
EF Core is the cordless drill you reach for by default — fast, safe, right for the overwhelming majority of jobs, and the one you'd hand a new team member without a second thought. Raw SQL is the precision hand tool in the same toolbox — a specific screwdriver bit for a specific stripped screw, a chisel for a corner the drill genuinely can't reach cleanly. A good carpenter doesn't swear allegiance to the drill and refuse the chisel, and doesn't reach for the chisel on every screw just because it's more "precise." They pick per task, based on what the task actually needs.
No — as the real-world example above shows, a single method can use raw SQL (via FromSqlInterpolated/SqlQuery) for one specific report while every other method in the same class, and the same DbContext, stays on ordinary LINQ. The decision is per-query, not per-feature or per-application.
Generated SQL that looks unfamiliar or more verbose than what you'd write by hand is not, by itself, evidence of a real performance problem — database query planners frequently produce identical or near-identical execution plans for superficially different-looking but logically equivalent SQL. Judge by measured execution time and query plan (282), not by how the SQL text reads to a human.
Writing new, ordinary CRUD queries in raw SQL from the start of a project, out of a general belief that it'll be faster, without any profiling evidence that EF Core's generated SQL for that shape of query would have been a problem. Default to LINQ; earn the switch to raw SQL with genuine complexity or measured evidence, per query.
Assuming any layer of abstraction necessarily costs meaningful, user-visible performance, without checking whether that cost is actually large enough to matter for the specific workload. Abstraction has a real, non-zero cost — but "non-zero" and "the actual bottleneck in this application" are very different claims; 233's profiling discipline is what tells them apart.
Treating a raw-SQL escape hatch as a context switch where the injection-prevention habits from 138 and 278 somehow don't apply anymore. Every raw SQL call, no matter how deliberately or rarely used, follows the exact same rule: values are parameters, never concatenated or interpolated directly into the SQL text handed to a raw-string method.
| Situation | Choose |
|---|---|
| Ordinary CRUD, filtered lists, projections, the vast majority of application data access | EF Core / LINQ |
| Complex, multi-table set-based reporting with window functions or recursive logic | Raw SQL (or a stored procedure, 278) |
| A very large one-time bulk insert beyond what 279's tools cover | A dedicated bulk-insert library or hand-written batch SQL |
| A specific query needing an exact index hint or join strategy EF Core's translator won't produce | Raw SQL for that one query |
| "This feels slow," with no profiling done yet | Profile first (233) — not raw SQL, not yet |
| A profiled, measured hot path where EF Core's generated SQL is confirmed to be the actual bottleneck | Raw SQL for that one confirmed hot path, verified with a before/after benchmark |
You've seen the honest case for both tools, and how to make the actual decision. Let's confirm the judgment landed, not just the vocabulary.
1. A developer says an endpoint "feels slow" and immediately rewrites its EF Core query as raw SQL, without measuring anything first. What does this lesson say about that approach?
Correct: B
Why B is correct: The lesson is explicit that "feels slow" without profiling is exactly the guessing 233 already warned against — the real bottleneck is frequently a missing index, an N+1 pattern, or something unrelated to how EF Core generated its SQL at all.
Why A is incorrect: This is precisely the false blanket claim the lesson rejects — raw SQL is not universally faster; it depends entirely on the specific query and what's actually causing the slowness.
Why C is incorrect: Correct parameterization avoids the security problem, but it doesn't address whether the rewrite was justified or effective in the first place — those are separate questions.
Why D is incorrect: EF Core queries absolutely can be measured and observed — that's exactly what the next, capstone lesson covers in depth.
Reinforcement: "Feels slow" is never sufficient justification on its own — profiling first is what turns a guess into a legitimate decision.
2. Which of these is the strongest, most honestly-earned case for raw SQL over LINQ, according to this lesson?
Correct: B
Why B is correct: This is exactly the reporting/complexity case the lesson names directly — genuinely SQL-shaped logic like window functions is where raw SQL's clarity and control legitimately outweigh LINQ's convenience.
Why A is incorrect: A simple single-table filter is exactly the shape of query LINQ already expresses clearly — raw SQL adds no real benefit here.
Why C is incorrect: The lesson explicitly rejects applying either tool as a blanket, application-wide policy — the decision is made per query.
Why D is incorrect: Ordinary single-row inserts are exactly the everyday CRUD case where EF Core's productivity and safety wins are largest.
Reinforcement: Genuine complexity — not a general preference — is what earns raw SQL its place for a specific query.
3. Why does EF Core's automatic SQL-injection protection matter as a real argument in EF Core's favor, given that 278 showed raw SQL can also be made safe with proper parameterization?
Correct: B
Why B is correct: The real advantage isn't that raw SQL can't be made safe — 278 showed it can. It's that LINQ-generated SQL is safe by construction, with no discipline required from any individual developer, while raw SQL's safety is only as reliable as everyone consistently following 138/278's parameterization rules every single time.
Why A is incorrect: There is a real difference in how the safety is achieved — automatic-by-construction versus dependent-on-developer-discipline — even though both can end up equally safe when done correctly.
Why C is incorrect: EF Core's own FromSqlRaw, used incorrectly, can still be unsafe, as 278 demonstrated directly — EF Core doesn't make injection impossible in every case, only in its own LINQ-generated SQL.
Why D is incorrect: This directly contradicts 278, which showed multiple genuinely safe ways to use raw SQL correctly.
Reinforcement: "Automatic by construction" versus "correct only with discipline" is the real distinction — not "safe" versus "inherently unsafe."
4. A team decides to write one specific quarterly finance report using raw SQL with window functions, while every other data-access method in the same application continues using EF Core LINQ. Is this a reasonable approach according to this lesson?
Correct: B
Why B is correct: This is exactly the real-world example the lesson walks through — a genuinely reporting-shaped query in raw SQL, sitting alongside ordinary LINQ CRUD in the same application, is the intended, supported way to use both tools together.
Why A is incorrect: EF Core is explicitly built on ADO.NET and provides FromSqlRaw/FromSqlInterpolated specifically to support this mixed approach as a first-class capability.
Why C is incorrect: "Consistency for its own sake" is exactly the kind of blanket, non-judgment-based reasoning the lesson argues against — one query's needs don't dictate the whole application's approach.
Why D is incorrect: Nothing requires removing EF Core to use raw SQL for one query — the two coexist naturally in the same DbContext-driven codebase.
Reinforcement: The decision genuinely is per query, not per application — this is the core, practical takeaway of the whole lesson.
You now have real judgment, not a slogan, for the SQL-vs-EF Core question. Next up — the capstone that ties this entire Part together: actually seeing what EF Core generates, and diagnosing a genuinely slow application step by step.
dotnetmadeeasy.com — Learn C# and .NET, the right way.