EF Core 3.1 中 FromSqlRaw 极不安全,因无参数化校验且不支持 FromSqlInterpolated;应改用 DbCommand + SqlParameter 手动执行并显式事务控制,严格匹配类型与列序,禁用字符串拼接和 AddWithValue。

别直接拼接字符串,也别盲目信“只是查个数据”,原生SQL在.NET Core 3.1里只要没参数化,就等于把数据库钥匙挂在URL上。
为什么 EF Core 的 FromSqlRaw 在 3.1 里特别危险
.NET Core 3.1 的 EF Core 3.1.x 版本对 FromSqlRaw 的参数校验极弱——它不会拦截传入的非参数化字符串,也不会警告你 string.Format 或 $"" 拼出来的 SQL。一旦你写成 context.Users.FromSqlRaw("SELECT * FROM Users WHERE Name = '" + name + "'"),攻击者输个 O'Reilly 就能闭合单引号,后面跟 ; DROP TABLE Users-- 就真执行了。
- EF Core 3.1 不支持
FromSqlInterpolated(那是 5.0+ 才加的) -
FromSqlRaw只认SqlParameter对象,不认@param占位符字符串本身 - 即使你用了
SqlParameter,如果类型不匹配(比如把DateTime当string传),SQL Server 可能隐式转换失败,导致空结果或异常,但不会报注入风险
用 DbCommand + SqlParameter 手动执行才可控
绕过 EF Core 的查询管道,直连 Database.GetDbConnection(),用 ADO.NET 原生方式执行,是 3.1 里最稳妥的路径。它强制你显式构造参数,且类型、方向、大小都可控制。
- 必须调用
connection.Open(),EF Core 的上下文不保证连接已打开 -
SqlParameter的DbType要和列类型一致(如DbType.Int32对应INT,DbType.AnsiString对应VARCHAR) - 避免用
AddWithValue:它会根据 .NET 类型推断 SQL 类型,int? null可能被当成INT,但实际列是SMALLINT,导致计划缓存污染 - 示例:
using (var connection = context.Database.GetDbConnection())
{
await connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM Orders WHERE Status = @status AND CreatedAt > @since";
command.Parameters.Add(new SqlParameter("@status", SqlDbType.VarChar) { Value = "Shipped" });
command.Parameters.Add(new SqlParameter("@since", SqlDbType.DateTime2) { Value = DateTime.UtcNow.AddDays(-7) });
<pre class="brush:php;toolbar:false;"> using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
// 手动映射
var orderId = reader.GetInt32(0);
var total = reader.GetDecimal(3);
}
}
}}
执行 DML(INSERT/UPDATE/DELETE)时必须显式开启事务
.NET Core 3.1 的 EF Core 默认不为原生 SQL 开启事务,哪怕你在 SaveChanges 后立刻执行 FromSqlRaw,也不在同一个事务里。如果你要删一批记录再记日志,没事务包裹,删成功了但日志写失败,数据就不可逆地丢了。
- 必须用
context.Database.BeginTransaction()获取DbContextTransaction - 把
DbCommand的Connection和Transaction都显式赋值 - 不能依赖
using自动回滚:3.1 的事务对象不实现IDisposable的完整语义,需手动Rollback()或Commit() - 示例关键行:
using var transaction = await context.Database.BeginTransactionAsync();
try
{
command.Connection = context.Database.GetDbConnection();
command.Transaction = transaction;
await command.ExecuteNonQueryAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
查询结果映射别偷懒用 DataTable 或反射自动绑定
3.1 的 SqlDataAdapter.Fill(DataTable) 看似方便,但它把所有字段当 object 存,后续取值要反复 Convert.ToInt32(row["Id"]),性能差还容易抛 InvalidCastException;而用 SqlDataReader 手动读,虽然多几行代码,但零装箱、类型明确、内存友好。
- 禁止用
reader["ColumnName"]:它走哈希查找,比reader.GetInt32(0)慢 3–5 倍 - 列序必须和 SQL 中
SELECT顺序严格一致,否则GetInt32(1)可能读到DateTime字段,直接崩 - 对可能为 NULL 的列,先用
IsDBNull(2)判断,再调对应GetXXX方法 - 如果返回结构固定,建议封装为
record(C# 9+ 可用,3.1 支持)或轻量class,避免后期魔改 SQL 导致映射错位
最易被忽略的一点:所有原生 SQL 的连接字符串、命令文本、参数名,必须和数据库实际 schema 完全一致——EF Core 3.1 不做任何列名别名重写或大小写归一化,SELECT user_id 和实体里 UserId 属性之间,没有自动映射这回事。手写 SQL 就得手写到底,半途交给 ORM 处理,只会埋下静默失败的坑。










