Skip to main content
SEMastery
Data Accessbeginner

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.

13 min readUpdated April 13, 2026

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 familyWhat it returnsBest use
FromSql / FromSqlRawFull entity types from your modelReading rows that map to a class like Product
SqlQuery / SqlQueryRawScalars or simple non-entity typesReading a count, a name, or a small custom shape
ExecuteSql / ExecuteSqlRawNumber 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.

The three raw SQL jobs in EF Core and what each one gives back

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

Start
Can LINQ do it?
Use LINQ
Need raw SQL
Returns entities?
Use FromSql
Use SqlQuery or ExecuteSql

Steps

1

Start

You have a query to write

2

Can LINQ do it?

Try the safe, typed path first

3

Use LINQ

Best default for most work

4

Need raw SQL

LINQ falls short here

5

Returns entities?

Decide on the method

6

Use FromSql

Maps rows to your model

7

Use SqlQuery or ExecuteSql

Scalars or writes

A simple decision path before you write a single line of SQL

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.

How FromSql composes your raw SQL with extra LINQ

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 wantUse thisLives on
A list of Product entitiesFromSqlDbSet<Product>
A single int or stringSqlQuery<T>context.Database
A small report DTOSqlQuery<T>context.Database
To change data, no result rowsExecuteSqlcontext.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

Load entities
Entities tracked
Run ExecuteSql
DB rows change
Memory unchanged
Reload to refresh

Steps

1

Load entities

You query some products

2

Entities tracked

EF Core watches them

3

Run ExecuteSql

Direct UPDATE to DB

4

DB rows change

Database is now newer

5

Memory unchanged

Tracked copies are stale

6

Reload to refresh

Re-query if you need them

Why in-memory entities can go stale after a raw write

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.

Safe parameter path versus unsafe string-building path

So the rule is short and strong:

  • Do use FromSql, SqlQuery, and ExecuteSql with {value} interpolation.
  • Do pass explicit parameters when using the Raw versions.
  • 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.

MethodJobValue handlingSafety
FromSqlRead entitiesInterpolation to parametersSafe
FromSqlRawRead entitiesYou pass DbParameter or {0} argsSafe if you parameterize
SqlQuery<T>Read scalars / DTOsInterpolation to parametersSafe
SqlQueryRaw<T>Read scalars / DTOsYou pass parametersSafe if you parameterize
ExecuteSqlWrite dataInterpolation to parametersSafe
ExecuteSqlRawWrite dataYou pass parametersSafe if you parameterize

A few practical tips

A short list of habits that will save you trouble:

  • Match your columns. For FromSql, the SELECT must return every column the entity needs, with matching names. For SqlQuery, use AS aliases 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 SaveChanges are 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 ToList first, 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 simple safety checklist before sending raw SQL

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.
  • FromSql reads full entities and can be composed with more LINQ.
  • SqlQuery<T> reads scalars or small DTOs that are not in your model, and lives on context.Database.
  • ExecuteSql changes 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

Related Posts