Executing Raw SQL and Stored Procedures
When to Bypass LINQ?
While Entity Framework Core’s LINQ provider is powerful, there are specific scenarios where dropping down to Raw SQL or Stored Procedures is the correct architectural choice:
- Complex Performance Tuning: When LINQ generates inefficient SQL (e.g., massive joins or complex subqueries) that requires a specific index hint or specialized SQL syntax.
- Existing DB Assets: When integrating with a legacy database that already contains optimized Stored Procedures.
- Bulk Operations: Performing a single SQL statement that updates thousands of rows (
UPDATE Products SET Price = Price * 1.1) is significantly faster than loading all entities into memory and saving them. - Database-Specific Features: Using syntax that EF Core doesn’t support yet (e.g., recursive Common Table Expressions or Full-Text search features).
Technical Implementation & Security
The #1 rule of Raw SQL is: NEVER use string interpolation/concatenation. This is the most common source of SQL Injection vulnerabilities.
1. Querying for Entities (FromSqlRaw / FromSqlInterpolated)
// Safe: Using Parameterized SQL
var categoryId = 5;
var products = await _context.Products
.FromSqlRaw("SELECT * FROM Products WHERE CategoryId = {0}", categoryId)
.ToListAsync();
// Even Safer: Using String Interpolation (EF Core handles the parameters)
var productsAlt = await _context.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE CategoryId = {categoryId}")
.ToListAsync();
2. Executing Commands (ExecuteSqlRaw)
Used for UPDATE, DELETE, or stored procedures that don’t return data.
var rowsAffected = await _context.Database.ExecuteSqlRawAsync(
"UPDATE Products SET Stock = 0 WHERE IsDiscontinued = {0}", true);
Practice Exercise
Implement a safe call to a stored procedure that takes multiple parameters to return a filtered list of entities.
Answer
1. The Stored Procedure (SQL Server)
CREATE PROCEDURE GetActiveProductsByPrice
@MinPrice DECIMAL(18,2),
@MaxPrice DECIMAL(18,2)
AS
BEGIN
SELECT * FROM Products
WHERE Price BETWEEN @MinPrice AND @MaxPrice
AND IsActive = 1
END
2. The Repository Implementation
using Microsoft.Data.SqlClient; // Required for SqlParameter
public async Task<List<Product>> FindActiveProducts(decimal min, decimal max)
{
// Define explicit parameters to ensure type safety and security
var minParam = new SqlParameter("@MinPrice", min);
var maxParam = new SqlParameter("@MaxPrice", max);
// EXEC [ProcedureName] [Parameters]
return await _context.Products
.FromSqlRaw("EXEC GetActiveProductsByPrice @MinPrice, @MaxPrice", minParam, maxParam)
.ToListAsync();
}
Why This Architecture Works
- Security: Using
SqlParameteror the{0}placeholder ensures that the database driver treats the input strictly as data, not as executable code. This makes SQL Injection impossible. - Performance: Stored Procedures are pre-compiled in SQL Server, which can offer a slight performance edge for extremely complex execution plans.
- Clean Code: You can still use LINQ methods after the
FromSqlRawcall (e.g.,.OrderBy(p => p.Name).Take(10)), and EF Core will attempt to compose the final SQL accordingly.
Summary
Dropping to Raw SQL is not a “failure” to use EF Core—it’s a sign of a pragmatic developer. By understanding how to use parameterized queries, you can harness the full power of your database engine without compromising the security of your application.