I am new to coding and looking for some help on how to pass multiple values to a single parameter in an inline SQL query. I have framed the below query, but I heard this could result in SQL-injection issue. Kindly help on how can I frame the below by using parameter based in the SQL query.
string query = "Select ID, email FROM DBTABLE WHERE email in (";
var stringBuiler = new StringBuilder();
using (StringReader stringReader = new StringReader(DownloadIDtextBox.Text))
{
string line;
string prefix = "";
while ((line = stringReader.ReadLine()) != null)
{
stringBuiler.Append(prefix);
prefix = ",";
stringBuiler.Append("'" + line + "'");
}
}
query += stringBuiler.ToString() + ")";
SqlDataAdapter da = new SqlDataAdapter(query, Connection);
DataTable dt = new DataTable();
da.Fill(dt);
Just want to mention that ID is GUID format.
If you are doing it manually, the process would be (basically):
var stringBuiler = new StringBuilder("Select ID, email FROM DBTABLE WHERE email in (");
// create "cmd" as a DB-provider-specific DbCommand instance, with "using"
using (...your reader...)
{
int idx = 0;
...
while ((line = stringReader.ReadLine()) != null)
{
// ...
Guid val = Guid.Parse(line);
// ...
var p = cmd.CreateParameter();
p.Name = "#p" + idx;
p.Value = val;
if (idx != 0) stringBuiler.Append(",");
stringBuiler.Append(p.Name);
cmd.Parameters.Add(cmd);
idx++;
}
}
cmd.CommandText = stringBuiler.Append(")").ToString();
and use that... meaning: you don't use inline SQL - you use fully parameterized SQL. There are tools in the ORM/micro-ORM families that will help immensely here, though - making it a one-liner.
Related
I am trying to implement an ADO.NET code which executes the SQL query with multiple parameters. Looks like SQL parameter limit is 2100 and does not accept more than this limit. How do I achieve with my below code to have this accept more than the limitation.
I am finding it difficult to understand the implementations when validating online articles related how to send the queries in subsets or chunks to fulfill my request.
This is my code:
using (Connection = new SqlConnection(CS))
{
Connection.Open();
string query = "SELECT FamilyID, FullName, Alias FROM TABLE (nolock) WHERE FamilyID IN ({0})";
var stringBuiler = new StringBuilder();
var familyIds = new List<string>();
string line;
while ((line = TextFileReader.ReadLine()) != null)
{
line = line.Trim();
if (!familyIds.Contains(line) & !string.IsNullOrEmpty(line))
{
familyIds.Add(line);
}
}
var sqlCommand = new SqlCommand
{
Connection = Connection,
CommandType = CommandType.Text
};
var index = 0; // Reset the index
var idParameterList = new List<string>();
foreach (var familyId in familyIds)
{
var paramName = "#familyId" + index;
sqlCommand.Parameters.AddWithValue(paramName, familyId);
idParameterList.Add(paramName);
index++;
}
sqlCommand.CommandText = String.Format(query, string.Join(",", idParameterList));
var dt = new DataTable();
using (SqlDataReader sqlReader = sqlCommand.ExecuteReader())
{
dt.Load(sqlReader);
}
try
{
if (dt.Rows.Count > 0)
{
OutputdataGridView.DataSource = lstDownloadOwnerOutput;
OutputdataGridView.ColumnHeadersDefaultCellStyle.Font = new Font(DataGridView.DefaultFont, FontStyle.Bold);
OutputdataGridView.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
Gridviewdisplaylabel.Text = "Total no of rows: " + this.OutputdataGridView.Rows.Count.ToString();
}
else if (dt.Rows.Count == 0)
{
MessageBox.Show("Data returned blank!!!");
}
}
catch (Exception Ex)
{
if (Connection != null)
{
Connection.Close();
}
MessageBox.Show(Ex.Message);
}
}
Having a WHERE IN clause with 2100, or even 100, parameters is generally not good coding practice. You might want to consider putting those values into a separate bona fide table, e.g.
families (ID int PK, ...)
Then, you may rewrite your query as:
SELECT FamilyID, FullName, Alias
FROM TABLE (nolock)
WHERE FamilyID IN (SELECT ID FROM families);
You could also express the above using an EXISTS clause or a join, but all three approaches might just optimize to a very similar query plan anyway.
You can just add a table load call every 2000 parameters in your code:
var index = 0; // Reset the index
var idParameterList = new List<string>();
var dt = new DataTable();
foreach (var familyId in familyIds) {
var paramName = "#familyId" + index;
sqlCommand.Parameters.AddWithValue(paramName, familyId);
idParameterList.Add(paramName);
index++;
if (index > 2000) {
sqlCommand.CommandText = String.Format(query, string.Join(",", idParameterList));
using (SqlDataReader sqlReader = sqlCommand.ExecuteReader())
dt.Load(sqlReader);
sqlCommand.Parameters.Clear();
idParameterList.Clear();
index = 0;
}
}
if (index > 0) {
sqlCommand.CommandText = String.Format(query, string.Join(",", idParameterList));
using (SqlDataReader sqlReader = sqlCommand.ExecuteReader())
dt.Load(sqlReader);
}
For dynamic sql like this, I generally recommend using a Table-Valued Parameter.
It does require a bit of setup: you have to create a user-defined Type in the DB to hold the values, but that is a fairly trivial operation:
CREATE TYPE PrimaryKeyType AS TABLE ( VALUE INT NOT NULL );
We generally use these in conjunction with stored procedures:
CREATE PROCEDURE dbo.getFamily(#PrimaryKeys PrimaryKeyType READONLY)
AS
SELECT FamilyID, FullName, Alias
FROM TABLE (nolock) INNER JOIN #PrimaryKeys ON TABLE.FamilyID = #PrimaryKeys.Value
GO
However, you can also use inline SQL if you prefer.
Assigning the values to the stored proc or inline parameter is fairly straightforward, but there is one gotcha (more later):
public static void AssignValuesToPKTableTypeParameter(DbParameter parameter, ICollection<int> primaryKeys)
{
// Exceptions are handled by the caller
var sqlParameter = parameter as SqlParameter;
if (sqlParameter != null && sqlParameter.SqlDbType == SqlDbType.Structured)
{
// The type name may look like DatabaseName.dbo.PrimaryKeyType,
// so remove the database name if it is present
var parts = sqlParameter.TypeName.Split('.');
if (parts.Length == 3)
{
sqlParameter.TypeName = parts[1] + "." + parts[2];
}
}
if (primaryKeys == null)
{
primaryKeys = new List<int>();
}
var table = new DataTable();
table.Columns.Add("Value", typeof(int));
foreach (var wPrimaryKey in primaryKeys)
{
table.Rows.Add(wPrimaryKey);
}
parameter.Value = table;
}
The thing to watch out for here is the naming of the parameter. See the code in the method above that removes the database name to resolve this issue.
If you have dynamic SQL, you can generate a correct parameter using the following method:
public static SqlParameter CreateTableValuedParameter(string typeName, string parameterName)
{
// Exceptions are handled by the caller
var oParameter = new SqlParameter();
oParameter.ParameterName = parameterName;
oParameter.SqlDbType = SqlDbType.Structured;
oParameter.TypeName = typeName;
return oParameter;
}
Where typeName is the name of your type in the DB.
This question already has answers here:
Passing a varchar full of comma delimited values to a SQL Server IN function
(27 answers)
Closed 5 years ago.
I have a stored procedure that uses the IN statement in the select condition.
SELECT *
FROM vwCashTransactions
WHERE TransactionTimeStamp BETWEEN '2017-01-30 ' AND '2017-12-01'
AND Country IN ('MY', 'BD')
ORDER BY TransactionTimeStamp DESC
I need to pass the country string from backend code.
This is the code I have written
if (manageCountries != null && manageCountries.Trim().Length > 0)
{
string[] words = manageCountries.Split(',');
string queryManageString = "";
int i = 0;
foreach (string word in words)
{
if (i != 0)
{
queryManageString += "','";
}
i++;
queryManageString += "'" + word + "'";
}
_DataTable = Global.DatabaseServices.GetTransactionReport("", startPeriod, endPeriod, queryManageString);
Somehow I am not getting the values. I am sure the issue is with the querymanageString. The way it is built is missing something. Can someone give an idea how I can achieve it?
Here's the code for calling the database:
public DataTable GetTransactionReport(string AccountCode, DateTime FromDate, DateTime ToDate, string ManagedCountry)
{
DataTable dataTable = new DataTable();
SqlCommand sqlCommand = new SqlCommand();
sqlCommand.CommandText = "[GetTransactionReport]";
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.Parameters.AddWithValue("#AccountCode", AccountCode);
sqlCommand.Parameters.AddWithValue("#FromDate", FromDate);
sqlCommand.Parameters.AddWithValue("#ToDate", ToDate);
sqlCommand.Parameters.AddWithValue("#ManagedCountry", ManagedCountry);
sqlCommand.CommandTimeout = 300;
ExecuteQuery(dataTable, sqlCommand);
sqlCommand.Dispose();
return dataTable;
}
public int ExecuteQuery(DataTable dt, SqlCommand cmd)
{
int rowCount = 0;
SqlDataAdapter da = null;
try
{
if (cmd.Connection == null)
cmd.Connection = GetSqlConnection();
da = new SqlDataAdapter();
da.SelectCommand = cmd;
rowCount = da.Fill(dt);
}
catch (Exception ex)
{
throw new DatabaseException(ex);
}
finally
{
cmd.Connection.Close();
cmd.Connection.Dispose();
cmd.Connection = null;
da.Dispose();
}
return rowCount;
}
It's not very clear how you pass the parameters, but it seems that you pass a delimited string. This will not work. Your procedure needs a list of country ids, not a string with a delimiter.
You can either do some magic in the stored procedure, splitting string and stuff like that, or create your own type.
Try something like this:
CREATE TYPE [dbo].[StringList] AS TABLE
([StringValue] [varchar](200) NULL)
Then your stored procedure has a parameter of type StringList, which can be used just like a normal table:
ALTER PROCEDURE [dbo].[MySproc]
#ids AS dbo.StringList READONLY
AS
BEGIN
SET NOCOUNT ON;
...etc..
And, finally, in your code use a DataTable for the values:
DataTable idsDT = new DataTable();
idsDT.Columns.Add("StringValue", typeof(string));
// fill datatable here
And the command parameter should be SqlDbType.Structured
var cmd = new SqlCommand(....)
SqlParameter countryParam = cmd.Parameter.AddWithValue("ids", idsDT);
countryParam.SqlDbType = SqlDbType.Structured;
It seems, there is something wrong in your for loop where you create comma seperated single quote string. Update for loop with below:
string[] words = manageCountries.Split(',');
string queryManageString = "";
int i = 0;
foreach (string word in words)
{
if (i != 0)
{
queryManageString += ",'" + word + "'";
}
else
{
queryManageString += "'" + word + "'";
}
i++;
}
OR if you don't want to go with for loop, here is one line solution
queryManageString = string.Join(",", words.Select(x => string.Format("'{0}'", x)));
Update: as the original question was essentially answered, I've marked this as complete.
I have a C# project in which I'd like to query a database. The SQL query will SELECT from a table, and in the WHERE clause I want to filter the results from a pre-defined list of values specified in C#.
List<string> productNames = new List<string >() { "A", "B", "C" };
foreach (name in productNames)
{
string query = #"
SELECT *
FROM products
WHERE name IN (#name);";
// Execute query
MySqlCommand cmd = new MySqlCommand(query, dbConn);
cmd.Parameters.AddWithValue("name", name);
MySqlDataReader row = cmd.ExecuteReader();
while (row.Read())
{
// Process result
// ...
}
}
However I'm getting an error:
There is already an open DataReader associated with this Connection
which must be closed first
Is it not possible then to use a for loop to add parameters this way to a SELECT statement?
You need to dispose your object to not get the exception. However you don't need to iterate over values and run a query for every value in the list. Try the following code. It makes a parameter for every value and adds it to command to use in "IN (...)" clause.
Also "using" keywords handles disposing objects.
List<string> productsIds = new List<string>() { "23", "46", "76", "88" };
string query = #"
SELECT *
FROM products
WHERE id IN ({0});";
// Execute query
using (MySqlCommand cmd = new MySqlCommand(query, dbConn))
{
int index = 0;
string sqlWhere = string.Empty;
foreach (string id in productsIds)
{
string parameterName = "#productId" + index++;
sqlWhere += string.IsNullOrWhiteSpace(sqlWhere) ? parameterName : ", " + parameterName;
cmd.Parameters.AddWithValue(parameterName, id);
}
query = string.Format(query, sqlWhere);
cmd.CommandText = query;
using (MySqlDataReader row = cmd.ExecuteReader())
{
while (row.Read())
{
// Process result
// ...
}
}
}
You are doing few things wrong. Let me point out them:
Everything is fine in the first iteration of the loop, when you are in second iteration the First Reader associated with the command remains opened and that result in current error.
You are using Where IN(..) you you can specify the values within IN as comma separated so there is no need to iterate through parameters.
You can use String.Join method to construct this values with a comma separator.
Now take a look into the following code:
List<int> productsIds = new List<int>() { 23, 46, 76, 88 };
string idInParameter = String.Join(",", productsIds);
// Create id as comma separated string
string query = "SELECT * FROM products WHERE id IN (#productId);";
MySqlCommand cmd = new MySqlCommand(query, dbConn);
cmd.Parameters.AddWithValue("#productId", idInParameter);
MySqlDataReader row = cmd.ExecuteReader();
while (row.Read())
{
// Process result
// ...
}
Please note if the id field in the table is not integers then you have to modify the construction of idInParameter as like the following:
idInParameter = String.Join(",", productsIds.Select(x => "'" + x.ToString() + "'").ToArray());
Pass the comma separated productIds string instead of looping. Read more about IN here.
string productIdsCommaSeparated = string.Join(",", productsIds.Select(n => n.ToString()).ToArray())
string query = #"
SELECT *
FROM products
WHERE id IN (#productId);";
// Execute query
MySqlCommand cmd = new MySqlCommand(query, dbConn);
cmd.Parameters.AddWithValue("productId", productIdsCommaSeparated );
MySqlDataReader row = cmd.ExecuteReader();
while (row.Read())
{
// Process result
// ...
}
The error you are getting is because you do not close the MySqlDataReader. You must close it to get rid of error before you call ExecuteReader in next iteration but this is not proper way in this case.
From what I tried and tested seems best solution (especially for text column types) is to create a list of individual parameters and add it as a range the to the query and parameters.
e.g:
List<MySqlParameter> listParams = new List<MySqlParameter>();
for (int i = 0; i < listOfValues.Length; i++)
{
listParams.Add(new MySqlParameter(string.Format("#values{0}", i),
MySqlDbType.VarChar) { Value = listOfValues[i] });
}
string sqlCommand = string.Format("SELECT data FROM table WHERE column IN ({0})",
string.Join(",", listParams.Select(x => x.ParameterName)));
......
using (MySqlCommand command = new MySqlCommand(sqlCommand, connection)
{
............
command.Parameters.AddRange(listParams.ToArray());
............
}
I can't seem to get this working:
My table column headers are 'genre' 'artist' 'album'
and the params I'm passing in are (type, filter, value) ("artist", "genre", "Rock") where there are two rows in the db with "Rock" for the genre.
When I follow the debugger, the 'while (reader.Read())' must return false because the loop is never entered and thus nothing written to the List.
public static List<String> getWithFilter(String type, String filter, String value)
{
List<String> columnData = new List<String>();
string query = "SELECT #type FROM Music WHERE" +
" #filter = '#value'";
SqlConnection connection = Database.GetConnection();
SqlCommand getData = new SqlCommand(query, connection);
getData.Parameters.AddWithValue("#type", type);
getData.Parameters.AddWithValue("#filter", filter);
getData.Parameters.AddWithValue("#value", value);
connection.Open();
using (connection)
{
using (getData)
{
using (SqlDataReader reader = getData.ExecuteReader())
{
while (reader.Read())
{
columnData.Add(reader.GetString(0));
}
}
}
}
return columnData;
}
You cannot use parameters for the names of columns and you don't put quotes around them when using them. Right now your query is the equivalent of
SELECT 'artist' FROM Music WHERE 'genre' = '#value'
You can do the following instead.
string query = "SELECT " + type + " FROM Music WHERE " + filter + " = #value";
And just remove the lines that create the #type and #fitler parameters.
You're looking either for formatting or string interpolation (requires C# 6.0):
string query =
$#"SELECT {type}
FROM Music
WHERE {filter} = #value";
...
getData.Parameters.AddWithValue("#value", value);
Formatting is a bit more wordy:
string query = String.Format(
#"SELECT {0}
FROM Music
WHERE {1} = #value", type, filter);
I assuming that you're using .net 2
DateTime current = DateTime.Now;
Console.WriteLine(current);
SqlConnection conn = new SqlConnection();
string q = "SELECT #field FROM student";
SqlDataAdapter da = new SqlDataAdapter(q, conn);
da.SelectCommand.Parameters.AddWithValue("#field", "snName");
DataTable dt = new System.Data.DataTable();
conn.Open();
da.Fill(dt);
conn.Close();
List<string> names = new List<string>();
foreach (DataRow dr in dt.Rows)
{
names.Add(dr[0].ToString());
}
Console.WriteLine("Fetching {0} data for {1}", names.Count, DateTime.Now - current);
Console.ReadKey();
You can use lambda expression to mapping the datatable in .net >4
I am trying to filter my gridview on the basis of checkboxlist selected.
So here is what I tried
string constr = ConfigurationManager.ConnectionStrings["OracleConn"].ConnectionString;
string strQuery = "select sr_no, type, stage, ref_no, ref_date, party_name, amount, remarks, exp_type, " +
"voucher_no, cheque_no,cheque_dt, chq_favr_name from XXCUS.XXACL_PN_EXPENSE_INFO";
string condition = string.Empty;
foreach (ListItem li in ddlStatus.Items)
{
condition += li.Selected ? string.Format("'{0}',", li.Value) : string.Empty;
if (!string.IsNullOrEmpty(condition))
{
condition += string.Format(" Where type IN ({0})", condition.Substring(0, condition.Length - 1));
}
using (OracleConnection conn = new OracleConnection(constr))
{
using (OracleCommand cmd = new OracleCommand(strQuery + condition))
{
using (OracleDataAdapter sda = new OracleDataAdapter(cmd))
{
cmd.Connection = conn;
using (DataTable dtcheck = new DataTable())
{
sda.Fill(dtcheck);
GridExpInfo.DataSource = dtcheck;
GridExpInfo.DataBind();
}
}
}
}
but I am getting error as
ORA-00933: SQL command not properly ended
My debugged query is
select sr_no, type, stage, ref_no, ref_date, party_name, amount, remarks, exp_type, voucher_no, cheque_no,cheque_dt, chq_favr_name
from XXCUS.XXACL_PN_EXPENSE_INFO'10',
Where type IN ('10')
I took reference from here
You should prepare the where condition using a List of strings and run the query after you have completed the query builder phase.
// Create a list of your conditions to put inside the IN statement
List<string>conditions = new List<string>();
foreach (ListItem li in ddlStatus.Items)
{
if(li.Selected)
conditions.Add($"'{li.Value}'");
}
// Now build the where condition used
string whereCondition = string.Empty;
if (condition.Count > 0)
{
// This produces something like "WHERE type in ('10', '11', '12')"
// The values for the IN are directly concatenated together
whereCondition = " Where type IN (" + string.Join(",", conditions) + ")";
}
using (OracleConnection conn = new OracleConnection(constr))
{
using (OracleCommand cmd = new OracleCommand(strQuery + whereCondition))
{
....
Consider that this approach is very exposed to Sql Injection attacks and to a parsing problems if your selected values are strings containing quotes
Instead if you start using parameters then your code should be changed to
int count = 1;
List<OracleParameter> parameters = new List<OracleParameter>();
List<string>conditions = new List<string>();
foreach (ListItem li in ddlStatus.Items)
{
if(li.Selected)
{
// parameters are named :p1, :p2, :p3 etc...
conditions.Add($":p{count}");
// Prepare a parameter with the same name of its placeholder and
// with the exact datatype expected by the column Type, assign to
// it the value and add the parameter to the list
OracleParameter p = new OracleParameter($":p{count}",OracleType.NVarChar);
p.Value = li.Value;
parameters.Add(p);
count ++;
}
}
if (condition.Count > 0)
{
// This produces something like "WHERE type in (:p1, :p2, :p3)"
// The values are stored before in the parameter list
whereCondition = " Where type IN (" + string.Join(",", conditions) + ")";
}
using (OracleConnection conn = new OracleConnection(constr))
{
using (OracleCommand cmd = new OracleCommand(strQuery + whereCondition))
{
// Add the parameters with the expected names, type and value.
cmd.Parameters.AddRange(parameters.ToArray());
....
It is a little more lines but this prevents Sql Injection and there is no more problem in parsing strings