C# 中的转义内插字符串

Escaped interpolated strings in C#

我发现在 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()}";

如果您不想使用具有 FromSqlInterpolated 方法的 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,而您通常不应该这样做。

准备好的 statements/parameterized 查询比仅仅清理或转义输入更进一步。当您使用参数化查询时,参数数据作为 独立值 从 SQL 语句发送。参数数据 永远不会 直接代入 SQL,因此注入得到了完美的保护,escaping/sanitizing 输入永远不会。

换句话说,不要指望“固定”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 数据类型错误,它可能会破坏索引并强制执行每行类型转换,这确实会降低性能。