EF Core Raw SQL Queries: FromSql, SqlQuery, and ExecuteSql Explained
A friendly, beginner guide to raw SQL in EF Core: FromSql for entities, SqlQuery for scalars, ExecuteSql for writes, and how to stay safe from SQL injection.
A note passed to the kitchen
Imagine you run a small restaurant. Most of the time, your waiter takes the order using a neat printed menu. The waiter ticks boxes, and the kitchen knows exactly what to cook. This is clean, safe, and rarely goes wrong. That printed menu is like LINQ in EF Core.
But sometimes a guest wants something special that is not on the menu. Maybe extra spicy, no onions, half rice. So the waiter writes a handwritten note and passes it to the kitchen. The kitchen reads the exact words and cooks it. That handwritten note is raw SQL.
Raw SQL is powerful. You can ask for almost anything. But there is a catch. If you let a stranger write whatever they want on that note, they might write something harmful, like "give me the keys to the safe." A careful waiter never copies a guest's words straight onto the kitchen note. Instead, the waiter puts the request into safe, fixed boxes. In EF Core, those safe boxes are called parameters, and they protect you from a serious problem called SQL injection.
This article teaches you how to write those handwritten notes safely.
What "raw SQL" means in EF Core
EF Core usually writes SQL for you. You write C# LINQ, and EF Core translates it into SQL behind the scenes. Raw SQL means you write the SQL text yourself and hand it to EF Core. EF Core sends it to the database and brings back the result.
There are three main jobs, and EF Core gives you a method family for each one.
| Method family | What it returns | Best use |
|---|---|---|
FromSql / FromSqlRaw | Full entity types from your model | Reading rows that map to a class like Product |
SqlQuery / SqlQueryRaw | Scalars or simple non-entity types | Reading a count, a name, or a small custom shape |
ExecuteSql / ExecuteSqlRaw | Number of affected rows (an int) | Writing data: UPDATE, DELETE, INSERT |
Notice the pattern. Each family has a safe interpolated version (FromSql, SqlQuery, ExecuteSql) and a manual version (FromSqlRaw, SqlQueryRaw, ExecuteSqlRaw). The interpolated versions are easier and safer for beginners. The "Raw" versions give you more control but ask you to handle parameters yourself.
Why use raw SQL at all?
LINQ is wonderful, and it should be your first choice. It is type-safe, the compiler checks it, and it reads almost like English. So why touch raw SQL?
Here are the honest reasons:
- LINQ cannot express it. Some database features, like window functions, full-text search, or special hints, do not have a clean LINQ shape.
- You need a stored procedure. Your team may already have tuned procedures in the database.
- Performance. For one heavy reporting query, a hand-written SQL statement can be faster than what LINQ generates.
- A quick one-off. Sometimes a small admin task is simpler as one line of SQL.
A good rule: default to LINQ, escape to raw SQL only when you must. Do not reach for raw SQL just because you know SQL well. The cost is that you lose the compiler's help and you must think about safety yourself.
Choosing between LINQ and raw SQL
Steps
Start
You have a query to write
Can LINQ do it?
Try the safe, typed path first
Use LINQ
Best default for most work
Need raw SQL
LINQ falls short here
Returns entities?
Decide on the method
Use FromSql
Maps rows to your model
Use SqlQuery or ExecuteSql
Scalars or writes
Reading entities with FromSql
FromSql runs your SQL and maps each row to an entity in your model. The columns coming back must match the properties of the entity. Let us read products in a category.
// Safe by default: the value goes in through string interpolation.
var category = "Beverages";
List<Product> products = await context.Products
.FromSql($"SELECT * FROM Products WHERE Category = {category}")
.ToListAsync();That {category} looks like plain string interpolation, but EF Core is smart here. It does not glue the word "Beverages" into the SQL text. Instead, it sends the SQL with a placeholder, like WHERE Category = @p0, and ships the value separately. The database treats the value as data, never as a command. That is what keeps you safe.
Because FromSql returns a real IQueryable, you can add more LINQ on top of it. This is called composition. EF Core wraps your SQL as a subquery and applies the rest.
var category = "Beverages";
var cheapDrinks = await context.Products
.FromSql($"SELECT * FROM Products WHERE Category = {category}")
.Where(p => p.Price < 100) // added by EF Core as extra SQL
.OrderBy(p => p.Name)
.ToListAsync();There are two rules for composition to work. Your SQL must be composable (a plain SELECT works; a stored procedure call usually does not), and it must return all the columns the entity needs.
FromSqlRaw when you build the string yourself
Sometimes the SQL text is dynamic, for example a table name decided at runtime. Then you use FromSqlRaw and pass values as explicit parameters. Never glue user input straight into the string.
using Microsoft.Data.SqlClient;
var category = userInput; // could be anything a user typed
var products = await context.Products
.FromSqlRaw(
"SELECT * FROM Products WHERE Category = {0}",
category) // passed as a parameter, not concatenated
.ToListAsync();The {0} here is a parameter placeholder, not C# interpolation. EF Core turns it into a real SQL parameter. This is safe. What would be dangerous is writing "... WHERE Category = '" + category + "'". We will look at why in the safety section.
Reading scalars and DTOs with SqlQuery
FromSql only works for entity types in your model. But often you just want a number, a name, or a small custom shape that is not an entity. For that, EF Core gives you SqlQuery.
// Read a single scalar value: how many active users?
var minAge = 18;
List<int> counts = await context.Database
.SqlQuery<int>($"SELECT COUNT(*) FROM Users WHERE Age >= {minAge}")
.ToListAsync();
int activeUsers = counts.Single();SqlQuery<T> lives on context.Database, not on a DbSet. You tell it the type T you expect back. It can be a simple type like int or string, or a small class (a DTO) whose property names match the returned columns.
public class SalesSummary
{
public string Region { get; set; } = "";
public decimal Total { get; set; }
}
var summary = await context.Database
.SqlQuery<SalesSummary>(
$"SELECT Region, SUM(Amount) AS Total FROM Orders GROUP BY Region")
.ToListAsync();Here the column alias AS Total matters. The column name in the result must match the property name Total, or EF Core cannot fill it in. Like FromSql, the interpolated SqlQuery keeps your values safe as parameters, and you can compose more LINQ on top.
| You want | Use this | Lives on |
|---|---|---|
A list of Product entities | FromSql | DbSet<Product> |
A single int or string | SqlQuery<T> | context.Database |
| A small report DTO | SqlQuery<T> | context.Database |
| To change data, no result rows | ExecuteSql | context.Database |
Changing data with ExecuteSql
When your SQL changes data and returns no rows, use ExecuteSql. It runs the command and gives back the count of affected rows. This is great for bulk updates without loading entities into memory first.
var oldPrice = 100m;
var newPrice = 90m;
int rowsAffected = await context.Database
.ExecuteSqlAsync(
$"UPDATE Products SET Price = {newPrice} WHERE Price = {oldPrice}");
Console.WriteLine($"Updated {rowsAffected} products.");One important thing to remember: ExecuteSql does not go through change tracking. It talks straight to the database. So if you already loaded some of those products into memory, the copies in memory still hold the old price. They are now stale. Reload them if you need fresh values.
ExecuteSql skips change tracking
Steps
Load entities
You query some products
Entities tracked
EF Core watches them
Run ExecuteSql
Direct UPDATE to DB
DB rows change
Database is now newer
Memory unchanged
Tracked copies are stale
Reload to refresh
Re-query if you need them
The big danger: SQL injection
This is the most important part of the whole article. Please read it slowly.
SQL injection is when an attacker writes special text into an input box so that their text becomes part of your SQL command. If you build SQL by gluing strings together, a clever user can break out of the value and run their own commands.
Picture a login form. Imagine you wrote this bad code:
// NEVER do this. This is vulnerable to SQL injection.
var name = userInput;
var bad = await context.Users
.FromSqlRaw("SELECT * FROM Users WHERE Name = '" + name + "'")
.ToListAsync();If a normal user types Asha, the SQL becomes ... WHERE Name = 'Asha'. Fine. But if an attacker types ' OR '1'='1, the SQL becomes ... WHERE Name = '' OR '1'='1', which is always true, and now they see every user. Worse inputs can delete tables. The problem is that the user's text became part of the command, not just data.
The fix is parameters. With the safe interpolated methods, EF Core turns each value into a parameter automatically. The value is sent to the database in a separate box, clearly labelled "this is data, not a command." No matter what the user types, it can never change the shape of your SQL.
So the rule is short and strong:
- Do use
FromSql,SqlQuery, andExecuteSqlwith{value}interpolation. - Do pass explicit parameters when using the
Rawversions. - Never build SQL with
+and user input.
The interpolated methods (FromSql, SqlQueryInterpolated, ExecuteSql) are safe because EF Core intercepts the C# interpolation and converts every {value} into a @pN database parameter before the SQL ever leaves your application. That single fact is your shield.
A clear comparison of the method families
It helps to see all six methods side by side, grouped by job, with how they handle values.
| Method | Job | Value handling | Safety |
|---|---|---|---|
FromSql | Read entities | Interpolation to parameters | Safe |
FromSqlRaw | Read entities | You pass DbParameter or {0} args | Safe if you parameterize |
SqlQuery<T> | Read scalars / DTOs | Interpolation to parameters | Safe |
SqlQueryRaw<T> | Read scalars / DTOs | You pass parameters | Safe if you parameterize |
ExecuteSql | Write data | Interpolation to parameters | Safe |
ExecuteSqlRaw | Write data | You pass parameters | Safe if you parameterize |
A few practical tips
A short list of habits that will save you trouble:
- Match your columns. For
FromSql, theSELECTmust return every column the entity needs, with matching names. ForSqlQuery, useASaliases so column names match DTO properties. - Async by default. Prefer
ToListAsync,ExecuteSqlAsync, and so on, so your app stays responsive. - Keep transactions in mind. A raw write and a tracked
SaveChangesare separate operations. Wrap them in one transaction if they must succeed or fail together. - Watch out for stored procedures. They usually cannot be composed with extra LINQ, so call
ToListfirst, then filter in memory if needed. - Validate input too. Parameters stop SQL injection, but you should still check that values make sense for your business rules.
A small end-to-end example
Let us tie it together. Suppose an admin page lets a user pick a category and a maximum price, then shows matching products and a total count.
public async Task<(List<Product> items, int total)> SearchAsync(
string category, decimal maxPrice)
{
// Read entities, safely parameterized, then compose with LINQ.
var items = await _context.Products
.FromSql($"SELECT * FROM Products WHERE Category = {category}")
.Where(p => p.Price <= maxPrice)
.OrderBy(p => p.Name)
.ToListAsync();
// Read a scalar count with the same safe pattern.
var counts = await _context.Database
.SqlQuery<int>(
$"SELECT COUNT(*) FROM Products WHERE Category = {category} AND Price <= {maxPrice}")
.ToListAsync();
return (items, counts.Single());
}Every user value flows in through {...}, so EF Core makes parameters for you. The code reads almost like LINQ, yet you keep the power of raw SQL where you need it. This is the sweet spot: raw when you must, safe by habit.
Quick recap
- Raw SQL lets you write your own SQL and hand it to EF Core, like passing a handwritten note to the kitchen.
FromSqlreads full entities and can be composed with more LINQ.SqlQuery<T>reads scalars or small DTOs that are not in your model, and lives oncontext.Database.ExecuteSqlchanges data and returns the number of affected rows; it skips change tracking, so reload stale entities.- The interpolated methods (
FromSql,SqlQuery,ExecuteSql) turn each{value}into a safe SQL parameter automatically. - Never build SQL by joining strings with user input; that is how SQL injection happens.
- Default to LINQ; reach for raw SQL only when LINQ cannot do the job or when you need extra performance.
References and further reading
- SQL Queries — EF Core (Microsoft Learn)
- RelationalQueryableExtensions.FromSql Method (Microsoft Learn)
- Preventing SQL injection in C# with Entity Framework (Snyk)
- EF Core Raw SQL Queries (Milan Jovanovic)
- Executing Raw SQL Queries using Entity Framework Core (Learn EF Core)
Related Posts
EF Core Query Splitting: Fix Slow Queries and Cartesian Explosion
Learn how EF Core query splitting (AsSplitQuery) fixes the cartesian explosion problem with simple examples, diagrams, and real performance numbers. Know when to split and when not to.
Calling Views, Stored Procedures and Functions in EF Core
A friendly, beginner guide to calling database views, stored procedures, and functions in EF Core with FromSql, SqlQuery, ExecuteSql, and ToView.
The Correct Way to Use Batch Update and Batch Delete in EF Core
Learn the correct, safe way to use ExecuteUpdate and ExecuteDelete batch methods in EF Core, with transactions, change tracker tips, and EF Core 10 features.
Soft Delete with EF Core: Delete Data Without Losing It
Learn soft delete in EF Core the right way. Use an interceptor and global query filters to hide deleted rows automatically, with simple examples, diagrams, code, and best practices for .NET 10.
Multi-Tenant Applications With EF Core: A Beginner's Guide
Learn multi-tenancy in EF Core the simple way. Isolate tenant data with global query filters, ITenantService, and named filters in .NET 10, with diagrams and code.
Eager Loading of Child Entities in EF Core: A Beginner's Guide
Learn eager loading in EF Core with Include and ThenInclude. Load child entities in one query, avoid the N+1 problem, and use filtered Include with simple examples.