我发现在C#中创建准备好的语句是不必要的麻烦,通常您要做的事情是这样的:
public T GetData<T>(string userInput)
{
string selectSomething = "SELECT * FROM someTable where someCol = @userInput";
using (IDbCommand command = new SqlCommand(selectSomething))
{
IDbDataParameter parameter = new SqlParameter("@userInput", SqlDbType.NVarChar);
parameter.Value = userInput;
command.Parameters.Add(parameter);
IDataReader reader = command.ExecuteReader();
reader.Read();
}
...
}
但是现在有了插值字符串,它可以像下面这样简单:
public T GetData<T>(string userInput)
{
string selectSomething = $"SELECT * FROM someTable where someCol = {userInput}";
using (IDbCommand command = new SqlCommand(selectSomething))
{
IDataReader reader = command.ExecuteReader();
reader.Read();
}
}
仍然有一些其他的样板代码,但仍然是一个改进。有没有这样一种方法,既能获得内插字符串的舒适感,又能保持已准备语句的安全性:
string selectSomething = $"SELECT * FROM someTable where someCol = {userInput.PreventSQLInjections()}";
如果您不想使用具有FromSqlInterpotalated
方法的EF或任何其他将帮助您处理数据访问的ORM,那么您可以利用编译器使用FormattableString
类型来处理字符串插值的事实,编写类似于以下内容的帮助器方法(不完全工作,但您应该了解这个想法)来删除样板代码:
public static class SqlCommandEx
{
// maps CLR type to SqlDbType
private static Dictionary<Type, SqlDbType> typeMap;
static SqlCommandEx()
{
typeMap = new Dictionary<Type, SqlDbType>();
typeMap[typeof(string)] = SqlDbType.NVarChar;
//... all other type maps
}
public static SqlCommand FromInterpolatedString(FormattableString sql)
{
var cmdText = sql.Format;
int count = 0;
var @params = new IDbDataParameter[sql.ArgumentCount];
foreach (var argument in sql.GetArguments())
{
var paramName = $"@param_{count}";
cmdText = cmdText.Replace($"{{{count}}}", paramName);
IDbDataParameter parameter = new SqlParameter(paramName, typeMap[argument.GetType()]);
parameter.Value = argument;
@params[count] = parameter;
count++;
}
var sqlCommand = new SqlCommand(cmdText);
sqlCommand.Parameters.AddRange(@params);
return sqlCommand;
}
}
和用法:
using (IDbCommand command = SqlCommandEx.FromInterpolatedString($"Select * from table where id = {val}"))
{
...
}
但这接近于编写您自己的ORM,而您通常不应该这样做。
准备好的语句/参数化查询不仅仅是清除或转义输入。当您使用参数化查询时,参数数据将作为与SQL语句分开的值发送。参数数据永远不会直接替换到SQL中,因此注入得到了完美的保护,而转义/消毒输入永远不会得到保护。
换句话说,不要指望字符串插值来“修复”SQL参数!
而且,真的没有那么多额外的工作。这个问题显示的是添加参数的困难方法。您可以这样简化代码:
public T GetData<T>(string userInput)
{
string selectSomething = "SELECT * FROM someTable where someCol = @userInput";
using (IDbCommand command = new SqlCommand(selectSomething))
{
command.Parameters.Add("@userInput", SqlDbType.NVarChar).Value = userInput;
IDataReader reader = command.ExecuteReader();
reader.Read();
}
...
}
这使得参数的额外工作减少到每个参数一行代码。
如果您对C#类型和SQL类型之间的映射有很高的信心,您还可以进一步简化,如下所示:
command.Parameters.AddWithValue("@userInput", userInput);
只要注意这个快捷方式:如果ADO.NET猜错了SQL数据类型,它就会破坏索引并强制进行每行类型转换,这实际上会扼杀性能。