191

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(',');
rene
  • 41,474
  • 78
  • 114
  • 152
Yongwei Xing
  • 12,983
  • 24
  • 70
  • 90
  • 14
    Not really the topic, but it seems to me like having Age as a column in a table is a bad idea, since it will need to be updated constantly. People get older right? Maybe you should consider having a column DateOfBirth instead? – Kjetil Watnedal Mar 04 '10 at 09:52
  • question with good answer here: http://stackoverflow.com/questions/83471/string-list-in-sqlcommand-through-parameters-in-c-sharp – Adam Butler Feb 17 '12 at 05:42

11 Answers11

217

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.

Brian
  • 37,399
  • 24
  • 94
  • 109
  • 13
    Does this method have the security issue, like sql injection? – Yongwei Xing Mar 04 '10 at 08:02
  • 8
    Because you are putting the values into parameters there is no risk of sql injection. – Brian Mar 04 '10 at 17:29
  • This is what I was looking for but I had a question. If the OP had several of the same columns to add to the SQL, how would we do this? Example. SELECT * FROM TableA WHERE Age = ({0}) OR Age = ({1}). (how would we do it with the cmd.Parameters) – Cocoa Dev Jan 29 '13 at 15:11
  • My answer is for adding the values of an array as parameters to your sql statement. Based on your sql, you just need regular, named parameters. "SELECT * FROM TableA WHERE Age = @Age1 OR Age = @Age2" – Brian Jan 29 '13 at 23:04
  • Visual Studio throws a CA2100 warning on the extension method: cmd.CommandText = cmd.CommandText.Replace("{" + paramNameRoot + "}", string.Join(separator, parameterNames)); – Ranjith Venkatesh Sep 29 '14 at 13:13
  • It is safe to ignore the warning. The warning is intended to help you avoid sql injection, but there is no chance of that with this code. In fact, the purpose of this code is to help avoid sql injection. – Brian Sep 30 '14 at 01:05
  • 1
    I like this, and only made the following modification after extracting the placeholder string to a variable: `var paramPlaceholder = "{" & paramNameRoot & "}";` `Debug.Assert(cmd.CommandText.Contains(paramPlaceholder), "Parameter Name Root must exist in the Source Query");` This should help devs if they forget to match paramNameRoot with the query. – MCattle Jun 20 '16 at 18:28
  • 3
    This is wrong approach, Table-Valued parameter should be used [https://stackoverflow.com/a/10409710/1565525](https://stackoverflow.com/a/10409710/1565525) – Fabio Aug 25 '17 at 14:14
  • Minor issue with this answer is with the [`AddWithValue`](https://blogs.msmvps.com/jcoehoorn/blog/2014/05/12/can-we-stop-using-addwithvalue-already/) function, any chance you could fix that? – DavidG Jan 03 '18 at 01:37
  • @DavidG I took a quick look at the AddWithValue function and nothing jumped out at me as incorrect. Can you be more specific? As a note, this is based on .Net 4.x, so perhaps there is a difference due to .Net version? – Brian Jan 04 '18 at 18:43
  • it throws exception when no items are there. exception is : Incorrect syntax near ')'. solution is need to check if there any items or not, if not then no need to have "IN ({0})" part in the sql statement. – Shantu May 18 '18 at 08:45
  • 1
    This answer is wrong because it has poor scalability and performance and it promotes bad coding practices. – Igor Levicki Mar 22 '20 at 13:41
  • I faced a problem when using the extension. it is braking the SQL transaction. so i used the solution you've provided before the update. though good efforts – Firas Shrourou Jun 03 '20 at 14:48
  • Not being able to do `@Age` in the query is a deal-breaker – hyankov Sep 03 '20 at 15:54
43

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)

J Adam Rogers
  • 765
  • 6
  • 5
  • 1
    Here is a generic variation that works with array of values of any type and is usable as an extension method: public static void AddArrayParameters( this SqlCommand cmd, string name, IEnumerable values) { 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); } – Adam Nemitoff Jan 13 '14 at 15:44
  • Minor issue with this answer is with the [`AddWithValue`](https://blogs.msmvps.com/jcoehoorn/blog/2014/05/12/can-we-stop-using-addwithvalue-already/) function, any chance you could fix that? – DavidG Jan 03 '18 at 01:37
  • This answer is wrong because it has poor scalability and performance and it promotes bad coding practices. – Igor Levicki Mar 22 '20 at 13:42
  • @IgorLevicki could you explain why? – Borislav Ivanov Jun 22 '23 at 15:54
  • @BorislavIvanov Because you are using `String.Format` in your first example to create SQL command string among other things. Also, it's much easier to just learn to use Dapper and proper database design patterns instead of rolling your own. – Igor Levicki Jun 24 '23 at 15:07
34

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    Dapper pulls lots of dependencies :( – mlt Feb 19 '20 at 01:55
  • 2
    @mlt huh? no, it doesn't; on netfx: "no dependencies"; on ns2.0, just "System.Reflection.Emit.Lightweight" - and we could probably remove that if we added a necroreapp target – Marc Gravell Feb 19 '20 at 07:11
  • I didn't mean to hijack the discussion, but I did... Thus far I use Npgsql that handles arrays fine as in `'{1,2,3}'` style arguments to a function (not a WHERE IN clause), but I'd rather use plain ODBC if not array hassle. I presume I would need Dapper ODBC as well in this case. Here is what it wants to pull. https://snipboard.io/HU0RpJ.jpg . Perhaps I should read on more on Dapper... – mlt Feb 20 '20 at 21:01
23

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});
Gregor Slavec
  • 4,814
  • 1
  • 26
  • 24
  • 1
    The line `table.Rows.Add(id);` results in a [minor code smell](https://rules.sonarsource.com/csharp/RSPEC-3220) when using SonarQube. I used this alternative inside the foreach: `var row = table.NewRow(); row["id"] = id; table.Rows.Add(row);`. – pogosama Jul 06 '18 at 11:26
  • 1
    Can I dynamically/temporarily create the table int32_id_list (in your example) within the CommandText? E. g. when using SqlDataAdapter(command).Fill(dataTable)? – Andreas Reiff Sep 09 '20 at 20:19
13

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));
    }
}
tridy
  • 1,166
  • 1
  • 12
  • 21
10

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.

user2399170
  • 622
  • 7
  • 11
  • 'string_split' is not a recognized built-in function name. So I used this to build my own string split https://stackoverflow.com/a/697543/74585 – Matthew Lock Jun 16 '22 at 10:18
  • 'string_split' is a standard function https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver16 probably you use not compatible version of SQLSever – user2399170 Jun 17 '22 at 07:58
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+.

Lukasz Matysiak
  • 891
  • 7
  • 11
0

Just changing DbType might be enough:

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

var itens = string.Join(',', ddlAge.Items);
sqlComm.Parameters.Add(
    new SqlParameter("@Age", itens)
    {
        DbType = DbType.String
    });
Rony Mesquita
  • 541
  • 4
  • 9
-1

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 });
Kyle Rosendo
  • 25,001
  • 7
  • 80
  • 118
-2

Overview: Use the DbType to set the parameter type.

var parameter = new SqlParameter();
parameter.ParameterName = "@UserID";
parameter.DbType = DbType.Int32;
parameter.Value = userID.ToString();

var command = conn.CreateCommand()
command.Parameters.Add(parameter);
var reader = await command.ExecuteReaderAsync()

The stored procedure can then split the incoming string into a temp table using the string_split function. The incoming string can be varchar(max) size as the limiting factor.

In the initial example the data was string was built by string builder, therefore, it is not an object. If it were an object then you would pass it as a binary object byte array .

Golden Lion
  • 3,840
  • 2
  • 26
  • 35
-6

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(',');
     } 
} 
Kyle Rosendo
  • 25,001
  • 7
  • 80
  • 118
Ballin
  • 44
  • 5