原始 SQL、存储过程与插值
使用 FromSqlRaw、FromSqlInterpolated 和存储过程安全地执行原始 SQL,同时避免注入。
原始 SQL、存储过程与插值 是 CoddyKit 上的免费 C# Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
何时在 EF Core 中使用原始 SQL
大多数查询使用 LINQ 都能正常工作,但某些场景需要原始 SQL:复杂分析、数据库供应商特定的函数、存储过程,或 LINQ 翻译效果不佳且对性能要求高的查询。
EF Core 提供了安全使用原始 SQL 的方式,同时不会牺牲安全性。
FromSqlRaw:安全的参数化查询
FromSqlRaw 执行原始 SQL,并将结果映射回实体。请始终使用 {0} 占位符,绝不要进行字符串拼接,以防止 SQL 注入。
// SAFE: parameterized
var products = await _db.Products
.FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 100m)
.ToListAsync();
// DANGEROUS: never do this
// FromSqlRaw("SELECT * FROM Products WHERE Price > " + price)FromSqlInterpolated:插值字符串的安全性
FromSqlInterpolated 接受 C# 插值字符串。EF Core 会自动将插入的值转换为 SQL 参数,既安全又易读。
decimal minPrice = 100m;
string category = "Electronics";
var products = await _db.Products
.FromSqlInterpolated(
$"SELECT * FROM Products WHERE Price > {minPrice} AND Category = {category}")
.OrderBy(p => p.Name)
.ToListAsync();
// Values become @p0, @p1 parameters automatically在 FromSql 后组合 LINQ
您可以在 FromSql 后链接 LINQ 运算符。EF Core 会将原始 SQL 包装为子查询,然后在其上应用 LINQ,这对于添加筛选或分页非常有用。
decimal min = 50m;
var page = await _db.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {min}")
.Where(p => p.IsActive) // additional LINQ filter
.OrderBy(p => p.Price)
.Skip(0).Take(20) // pagination
.ToListAsync();
// Generates: SELECT ... FROM (raw SQL) AS p WHERE ...执行存储过程
请通过 FromSqlRaw 调用返回实体行的存储过程。对于不返回行的过程(例如 INSERT/UPDATE),请使用 ExecuteSqlRawAsync。
// Stored proc returning Products
var products = await _db.Products
.FromSqlRaw("EXEC GetProductsByCategory {0}", "Books")
.ToListAsync();
// Stored proc with no return rows
await _db.Database.ExecuteSqlRawAsync(
"EXEC ArchiveOldOrders {0}",
DateTime.UtcNow.AddYears(-1));使用 ExecuteSqlInterpolated 执行 DML
ExecuteSqlInterpolated 以插值字符串安全方式运行 DML 语句(INSERT、UPDATE、DELETE)。它会返回受影响的行数。
int days = 30;
int affected = await _db.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE Orders SET Status = 'Expired' WHERE PlacedAt < {DateTime.UtcNow.AddDays(-days)}");
Console.WriteLine($"{affected} orders expired");查询非实体类型
使用 SqlQuery<T>(.NET 7+)查询不属于 EF Core 实体的任意标量类型或 DTO。无需 DbSet。
// Query a DTO directly — no entity needed
var stats = await _db.Database
.SqlQuery<ProductStats>(
$"SELECT Category, COUNT(*) AS Count, AVG(Price) AS AvgPrice FROM Products GROUP BY Category")
.ToListAsync();
public record ProductStats(string Category, int Count, decimal AvgPrice);用于视图的无键实体类型
使用 HasNoKey() 和 ToView() 将数据库视图或原始查询结果映射到无键实体类型。您可以像查询 DbSet 一样查询它,而无需主键。
public class ProductSalesSummary
{
public string Category { get; set; } = "";
public int TotalSold { get; set; }
public decimal Revenue { get; set; }
}
// In OnModelCreating:
modelBuilder.Entity<ProductSalesSummary>()
.HasNoKey()
.ToView("vw_ProductSalesSummary");
// Query:
var summary = await _db.Set<ProductSalesSummary>().ToListAsync();避免 SQL 注入陷阱
黄金法则:绝不要将用户输入拼接到 SQL 字符串中。始终通过 FromSqlRaw 占位符、FromSqlInterpolated 或 EF Core 的 LINQ 使用参数化查询。
// SAFE
var name = userInput;
var result = await _db.Products
.FromSqlInterpolated($"SELECT * FROM Products WHERE Name = {name}")
.ToListAsync();
// INJECTION RISK (never do this)
// FromSqlRaw("SELECT * FROM Products WHERE Name = '" + name + "'");使用 ADO.NET 回退处理输出参数
EF Core 原生不支持存储过程输出参数。遇到这种情况时,请使用 _db.Database.GetDbConnection() 回退到 ADO.NET。
await _db.Database.OpenConnectionAsync();
var conn = _db.Database.GetDbConnection();
using var cmd = conn.CreateCommand();
cmd.CommandText = "EXEC GetOrderCount @CustomerId, @Count OUTPUT";
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@CustomerId", 5));
var outParam = new SqlParameter("@Count", SqlDbType.Int) { Direction = ParameterDirection.Output };
cmd.Parameters.Add(outParam);
await cmd.ExecuteNonQueryAsync();
int count = (int)outParam.Value;实战:全文搜索回退
LINQ 无法表达数据库特定的全文搜索谓词。请使用 FromSqlInterpolated 来利用原生 FREETEXT 或文本搜索向量功能。
string query = "keyboard mechanical";
var results = await _db.Products
.FromSqlInterpolated(
$"SELECT * FROM Products WHERE FREETEXT(Name, {query})")
.AsNoTracking()
.Take(20)
.ToListAsync();快速检查
对于参数化查询,为什么 FromSqlInterpolated 比字符串拼接更安全?
回顾:EF Core 中的原始 SQL
要点:
FromSqlRaw:使用 {0} 占位符;绝不要拼接用户输入FromSqlInterpolated:安全的插值字符串 → ADO.NET 参数- 在 FromSql 后链接 LINQ 运算符,以添加筛选、排序和分页
ExecuteSqlInterpolatedAsync:安全执行 DMLSqlQuery<T>:查询非实体 DTO(.NET 7+)- 对于输出参数或复杂存储过程,请直接使用 ADO.NET
常见问题解答
「原始 SQL、存储过程与插值」课时是免费的吗?
是的 — 「原始 SQL、存储过程与插值」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「原始 SQL、存储过程与插值」这节课中我会学到什么?
使用 FromSqlRaw、FromSqlInterpolated 和存储过程安全地执行原始 SQL,同时避免注入。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「原始 SQL、存储过程与插值」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 预先加载、延迟加载与显式加载
- 原始 SQL、存储过程与插值
- 编译查询与性能
- 全局查询筛选器与拥有实体