Pass Array Parameter in SqlCommand

2018-12-31 17:00发布

问题:

I am trying to pass array parameter to SQL commnd in C# like below, but it does not work. Does anyone meet it before?

string sqlCommand = \"SELECT * from TableA WHERE Age IN (@Age)\";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;
sqlComm.Parameters.Add(\"@Age\", SqlDbType.NVarChar);
StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + \",\");
     }
}

sqlComm.Parameters[\"@Age\"].Value = sb.ToString().TrimEnd(\',\');

回答1:

You will need to add the values in the array one at a time.

var parameters = new string[items.Length];
var cmd = new SqlCommand();
for (int i = 0; i < items.Length; i++)
{
    parameters[i] = string.Format(\"@Age{0}\", i);
    cmd.Parameters.AddWithValue(parameters[i], items[i]);
}

cmd.CommandText = string.Format(\"SELECT * from TableA WHERE Age IN ({0})\", string.Join(\", \", parameters));
cmd.Connection = new SqlConnection(connStr);

UPDATE: Here is an extended and reusable solution that uses Adam\'s answer along with his suggested edit. I improved it a bit and made it an extension method to make it even easier to call.

public static class SqlCommandExt
{

    /// <summary>
    /// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
    /// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN ({paramNameRoot}))
    /// </summary>
    /// <param name=\"cmd\">The SqlCommand object to add parameters to.</param>
    /// <param name=\"paramNameRoot\">What the parameter should be named followed by a unique value for each value. This value surrounded by {} in the CommandText will be replaced.</param>
    /// <param name=\"values\">The array of strings that need to be added as parameters.</param>
    /// <param name=\"dbType\">One of the System.Data.SqlDbType values. If null, determines type based on T.</param>
    /// <param name=\"size\">The maximum size, in bytes, of the data within the column. The default value is inferred from the parameter value.</param>
    public static SqlParameter[] AddArrayParameters<T>(this SqlCommand cmd, string paramNameRoot, IEnumerable<T> values, SqlDbType? dbType = null, int? size = null)
    {
        /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
         * Each item in the array will end up being it\'s own SqlParameter so the return value for this must be used as part of the
         * IN statement in the CommandText.
         */
        var parameters = new List<SqlParameter>();
        var parameterNames = new List<string>();
        var paramNbr = 1;
        foreach (var value in values)
        {
            var paramName = string.Format(\"@{0}{1}\", paramNameRoot, paramNbr++);
            parameterNames.Add(paramName);
            SqlParameter p = new SqlParameter(paramName, value);
            if (dbType.HasValue)
                p.SqlDbType = dbType.Value;
            if (size.HasValue)
                p.Size = size.Value;
            cmd.Parameters.Add(p);
            parameters.Add(p);
        }

        cmd.CommandText = cmd.CommandText.Replace(\"{\" + paramNameRoot + \"}\", string.Join(\",\", parameterNames));

        return parameters.ToArray();
    }

}

It is called like this...

var cmd = new SqlCommand(\"SELECT * FROM TableA WHERE Age IN ({Age})\");
cmd.AddArrayParameters(\"Age\", new int[] { 1, 2, 3 });

Notice the \"{Age}\" in the sql statement is the same as the parameter name we are sending to AddArrayParameters. AddArrayParameters will replace the value with the correct parameters.



回答2:

I wanted to expand on the answer that Brian contributed to make this easily usable in other places.

/// <summary>
/// This will add an array of parameters to a SqlCommand. This is used for an IN statement.
/// Use the returned value for the IN part of your SQL call. (i.e. SELECT * FROM table WHERE field IN (returnValue))
/// </summary>
/// <param name=\"sqlCommand\">The SqlCommand object to add parameters to.</param>
/// <param name=\"array\">The array of strings that need to be added as parameters.</param>
/// <param name=\"paramName\">What the parameter should be named.</param>
protected string AddArrayParameters(SqlCommand sqlCommand, string[] array, string paramName)
{
    /* An array cannot be simply added as a parameter to a SqlCommand so we need to loop through things and add it manually. 
     * Each item in the array will end up being it\'s own SqlParameter so the return value for this must be used as part of the
     * IN statement in the CommandText.
     */
    var parameters = new string[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        parameters[i] = string.Format(\"@{0}{1}\", paramName, i);
        sqlCommand.Parameters.AddWithValue(parameters[i], array[i]);
    }

    return string.Join(\", \", parameters);
}

You can use this new function as follows:

SqlCommand cmd = new SqlCommand();

string ageParameters = AddArrayParameters(cmd, agesArray, \"Age\");
sql = string.Format(\"SELECT * FROM TableA WHERE Age IN ({0})\", ageParameters);

cmd.CommandText = sql;


Edit: Here is a generic variation that works with an array of values of any type and is usable as an extension method:

public static class Extensions
{
    public static void AddArrayParameters<T>(this SqlCommand cmd, string name, IEnumerable<T> values) 
    { 
        name = name.StartsWith(\"@\") ? name : \"@\" + name;
        var names = string.Join(\", \", values.Select((value, i) => { 
            var paramName = name + i; 
            cmd.Parameters.AddWithValue(paramName, value); 
            return paramName; 
        })); 
        cmd.CommandText = cmd.CommandText.Replace(name, names); 
    }
}

You can then use this extension method as follows:

var ageList = new List<int> { 1, 3, 5, 7, 9, 11 };
var cmd = new SqlCommand();
cmd.CommandText = \"SELECT * FROM MyTable WHERE Age IN (@Age)\";    
cmd.AddArrayParameters(\"Age\", ageList);

Make sure you set the CommandText before calling AddArrayParameters.

Also make sure your parameter name won\'t partially match anything else in your statement (i.e. @AgeOfChild)



回答3:

If you can use a tool like \"dapper\", this can be simply:

int[] ages = { 20, 21, 22 }; // could be any common list-like type
var rows = connection.Query<YourType>(\"SELECT * from TableA WHERE Age IN @ages\",
          new { ages }).ToList();

Dapper will handle unwrapping this to individual parameters for you.



回答4:

Since there is a method on

SqlCommand.Parameters.AddWithValue(parameterName, value)

it might be more convenient to create a method accepting a parameter (name) to replace and a list of values. It is not on the Parameters level (like AddWithValue) but on command itself so it\'s better to call it AddParametersWithValues and not just AddWithValues:

query:

SELECT * from TableA WHERE Age IN (@age)

usage:

sqlCommand.AddParametersWithValues(\"@age\", 1, 2, 3);

the extension method:

public static class SqlCommandExtensions
{
    public static void AddParametersWithValues<T>(this SqlCommand cmd,  string parameterName, params T[] values)
    {
        var parameterNames = new List<string>();
        for(int i = 0; i < values.Count(); i++)
        {
            var paramName = @\"@param\" + i;
            cmd.Parameters.AddWithValue(paramName, values.ElementAt(i));
            parameterNames.Add(paramName);
        }

        cmd.CommandText = cmd.CommandText.Replace(parameterName, string.Join(\",\", parameterNames));
    }
}


回答5:

If you are using MS SQL Server 2008 and above you can use table-valued parameters like described here http://www.sommarskog.se/arrays-in-sql-2008.html.

1. Create a table type for each parameter type you will be using

The following command creates a table type for integers:

create type int32_id_list as table (id int not null primary key)

2. Implement helper methods

public static SqlCommand AddParameter<T>(this SqlCommand command, string name, IEnumerable<T> ids)
{
  var parameter = command.CreateParameter();      

  parameter.ParameterName = name;
  parameter.TypeName = typeof(T).Name.ToLowerInvariant() + \"_id_list\";
  parameter.SqlDbType = SqlDbType.Structured;
  parameter.Direction = ParameterDirection.Input;

  parameter.Value = CreateIdList(ids);

  command.Parameters.Add(parameter);
  return command;
}

private static DataTable CreateIdList<T>(IEnumerable<T> ids)
{
  var table = new DataTable();
  table.Columns.Add(\"id\", typeof (T));

  foreach (var id in ids)
  {
    table.Rows.Add(id);
  }

  return table;
}

3. Use it like this

cmd.CommandText = \"select * from TableA where Age in (select id from @age)\"; 
cmd.AddParameter(\"@age\", new [] {1,2,3,4,5});


回答6:

Passing an array of items as a collapsed parameter to the WHERE..IN clause will fail since query will take form of WHERE Age IN (\"11, 13, 14, 16\").

But you can pass your parameter as an array serialized to XML or JSON:

Using nodes() method:

StringBuilder sb = new StringBuilder();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    sb.Append(\"<age>\" + item.Text + \"</age>\"); // actually it\'s xml-ish

sqlComm.CommandText = @\"SELECT * from TableA WHERE Age IN (
    SELECT Tab.col.value(\'.\', \'int\') as Age from @Ages.nodes(\'/age\') as Tab(col))\";
sqlComm.Parameters.Add(\"@Ages\", SqlDbType.NVarChar);
sqlComm.Parameters[\"@Ages\"].Value = sb.ToString();

Using OPENXML method:

using System.Xml.Linq;
...
XElement xml = new XElement(\"Ages\");

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    xml.Add(new XElement(\"age\", item.Text);

sqlComm.CommandText = @\"DECLARE @idoc int;
    EXEC sp_xml_preparedocument @idoc OUTPUT, @Ages;
    SELECT * from TableA WHERE Age IN (
    SELECT Age from OPENXML(@idoc, \'/Ages/age\') with (Age int \'text()\')
    EXEC sp_xml_removedocument @idoc\";
sqlComm.Parameters.Add(\"@Ages\", SqlDbType.Xml);
sqlComm.Parameters[\"@Ages\"].Value = xml.ToString();

That\'s a bit more on the SQL side and you need a proper XML (with root).

Using OPENJSON method (SQL Server 2016+):

using Newtonsoft.Json;
...
List<string> ages = new List<string>();

foreach (ListItem item in ddlAge.Items)
  if (item.Selected)
    ages.Add(item.Text);

sqlComm.CommandText = @\"SELECT * from TableA WHERE Age IN (
    select value from OPENJSON(@Ages))\";
sqlComm.Parameters.Add(\"@Ages\", SqlDbType.NVarChar);
sqlComm.Parameters[\"@Ages\"].Value = JsonConvert.SerializeObject(ages);

Note that for the last method you also need to have Compatibility Level at 130+.



回答7:

I want to propose another way, how to solve limitation with IN operator.

For example we have following query

select *
from Users U
WHERE U.ID in (@ids)

We want to pass several IDs to filter users. Unfortunately it is not possible to do with C# in easy way. But I have fount workaround for this by using \"string_split\" function. We need to rewrite a bit our query to following.

declare @ids nvarchar(max) = \'1,2,3\'

SELECT *
FROM Users as U
CROSS APPLY string_split(@ids, \',\') as UIDS
WHERE U.ID = UIDS.value

Now we can easily pass one parameter enumeration of values separated by comma.



回答8:

Use .AddWithValue(), So:

sqlComm.Parameters.AddWithValue(\"@Age\", sb.ToString().TrimEnd(\',\'));

Alternatively, you could use this:

sqlComm.Parameters.Add(
    new SqlParameter(\"@Age\", sb.ToString().TrimEnd(\',\')) { SqlDbType = SqlDbType. NVarChar }
    );

Your total code sample will look at follows then:

string sqlCommand = \"SELECT * from TableA WHERE Age IN (@Age)\";
SqlConnection sqlCon = new SqlConnection(connectString);
SqlCommand sqlComm = new SqlCommand();
sqlComm.Connection = sqlCon;
sqlComm.CommandType = System.Data.CommandType.Text;
sqlComm.CommandText = sqlCommand;
sqlComm.CommandTimeout = 300;

StringBuilder sb = new StringBuilder();
foreach (ListItem item in ddlAge.Items)
{
     if (item.Selected)
     {
         sb.Append(item.Text + \",\");
     }
}

sqlComm.Parameters.AddWithValue(\"@Age\", sb.ToString().TrimEnd(\',\'));

// OR

// sqlComm.Parameters.Add(new SqlParameter(\"@Age\", sb.ToString().TrimEnd(\',\')) { SqlDbType = SqlDbType. NVarChar });


回答9:

try

sqlComm.Parameters[\"@Age\"].Value = sb.ToString().Replace(\",\",\" \");


回答10:

try it like this

StringBuilder sb = new StringBuilder(); 
foreach (ListItem item in ddlAge.Items) 
{ 
     if (item.Selected) 
     { 
          string sqlCommand = \"SELECT * from TableA WHERE Age IN (@Age)\"; 
          SqlConnection sqlCon = new SqlConnection(connectString); 
          SqlCommand sqlComm = new SqlCommand(); 
          sqlComm.Connection = sqlCon; 
          sqlComm.CommandType = System.Data.CommandType.Text; 
          sqlComm.CommandText = sqlCommand; 
          sqlComm.CommandTimeout = 300; 
          sqlComm.Parameters.Add(\"@Age\", SqlDbType.NVarChar);
          sb.Append(item.Text + \",\"); 
          sqlComm.Parameters[\"@Age\"].Value = sb.ToString().TrimEnd(\',\');
     } 
} 


标签: c# tsql