提问者:小点点

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()}";

共2个答案

匿名用户

如果您不想使用具有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数据类型,它就会破坏索引并强制进行每行类型转换,这实际上会扼杀性能。