C 语言中转义的插值字符串#

Escaped interpolated strings in C#

提问人: 提问时间:11/20/2020 更新时间:11/11/2023 访问量:546

问:

我发现在 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()}";
C# 字符串 SQL-Injection

评论

0赞 Samuel Liew 11/21/2020
评论不用于扩展讨论;此对话已移至 Chat

答:

2赞 Guru Stron 11/20/2020 #1

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

评论

1赞 11/20/2020
感谢这个例子,非常有创意。但我认为你是对的。我想我必须研究一下 ORM 是什么。似乎他们解决了我所有的问题:)
1赞 Guru Stron 11/20/2020
@Cowboy_Patrick不是全部,但它们可能很有帮助=)
2赞 Joel Coehoorn 11/20/2020 #2

预准备语句/参数化查询不仅仅是清理或转义输入。使用参数化查询时,参数数据将作为 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 数据类型,它可能会破坏索引并强制每行类型转换,这确实会降低性能。

评论

0赞 Servy 11/20/2020
但是你可以使用一个插值字符串,但仍然会得到一个准备好的语句,所以你的前提是错误的。
0赞 Joel Coehoorn 11/20/2020
@Servy 是的,但如果你这样做,你就不能指望插值来清理数据
0赞 Servy 11/20/2020
“这样一来,参数的额外工作就减少到每个参数一行代码。”不过,这是一项不平凡的额外工作。除了为每个值添加一行代码外,还需要确保输入字符串中的每个变量都添加一个参数,不添加输入字符串中未使用的参数,每次都需要将类型映射到 SQL 类型,并且将变量的用法从其值的来源移出, 这使得代码更难阅读。总而言之,它既更容易出错,又需要做更多的工作。
0赞 Servy 11/20/2020
该问题询问了一种使用插值字符串创建查询的方法。您可以这样做,并且仍然拥有准备好的陈述的所有好处。OP 认为您需要手动转义值才能实现此目的,但您没有这样做。OP 尝试的解决方案没有奏效并不意味着他们要求解决的问题的不同方法无法奏效。