Working with a SqlCommand in C# I've created a query that contains a IN (list...) part in the where clause. Instead of looping through my string list generating the list I need for the query (dangerous if you think in sqlInjection). I thought that I could create a parameter like:
SELECT blahblahblah WHERE blahblahblah IN #LISTOFWORDS
Then in the code I try to add a parameter like this:
DataTable dt = new DataTable();
dt.Columns.Add("word", typeof(string));
foreach (String word in listOfWords)
{
dt.Rows.Add(word);
}
comm.Parameters.Add("LISTOFWORDS", System.Data.SqlDbType.Structured).Value = dt;
But this doesn't work.
Questions:
Am I trying something impossible?
Did I took the wrong approach?
Do I have mistakes in this approach?
Thanks for your time :)
What you are trying to do is possible but not using your current approach. This is a very common problem with all possible solutions prior to SQL Server 2008 having trade offs related to performance, security and memory usage.
This link shows some approaches for SQL Server 2000/2005
SQL Server 2008 supports passing a table value parameter.
I hope this helps.
You want to think about where that list comes from. Generally that information is in the database somewhere. For example, instead of this:
SELECT * FROM [Table] WHERE ID IN (1,2,3)
You could use a subquery like this:
SELECT * FROM [Table] WHERE ID IN ( SELECT TableID FROM [OtherTable] WHERE OtherTableID= #OtherTableID )
If I understand right, you're trying to pass a list as a SQL parameter.
Some folks have attempted this before with limited success:
Passing Arrays to Stored Procedures
Arrays and Lists in SQL 2005
Passing Array of Values to SQL Server without String Manipulation
Using MS SQL 2005's XML capabilities to pass a list of values to a command
Am I trying something impossible?
No, it isn't impossible.
Did I took the wrong approach?
Your approach is not working (at least in .net 2)
Do I have mistakes in this approach?
I would try "Joel Coehoorn" solution (2nd answers) if it is possible.
Otherwise, another option is to send a "string" parameter with all values delimited by an separator. Write a dynamic query (build it based on values from string) and execute it using "exec".
Another solution will be o build the query directly from code. Somthing like this:
StringBuilder sb = new StringBuilder();
for (int i=0; i< listOfWords.Count; i++)
{
sb.AppendFormat("p{0},",i);
comm.Parameters.AddWithValue("p"+i.ToString(), listOfWords[i]);
}
comm.CommandText = string.Format(""SELECT blahblahblah WHERE blahblahblah IN ({0})",
sb.ToString().TrimEnd(','));
The command should look like:
SELECT blah WHERE blah IN (p0,p1,p2,p3...)...p0='aaa',p1='bbb'
In MsSql2005, "IN" is working only with 256 values.
I would recommend setting the parameter as a comma delimited string of values and use a Split function in SQL to turn that into a single column table of values and then you can use the IN feature.
http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=50648 - Split Functions
If you want to pass the list as a string in a parameter, you could just build the query dynamically.
DECLARE #query varchar(500)
SET #query = 'SELECT blah blah WHERE blahblah in (' + #list + ')'
EXECUTE(#query)
I used to have the same problem, I think there is now way to do this directly over the ADO.NET API.
You might consider inserting the words into a temptable (plus a queryid or something) and then refering to that temptable from the query. Or dynamically creating the query string and avoid sql injection by other measures (e.g. regex checks).
This is an old question but I've come up with an elegant solution for this that I love to reuse and I think everyone else will find it useful.
First of all you need to create a FUNCTION in SqlServer that takes a delimited input and returns a table with the items split into records.
Here is the following code for this:
ALTER FUNCTION [dbo].[Split]
(
#RowData nvarchar(max),
#SplitOn nvarchar(5) = ','
)
RETURNS #RtnValue table
(
Id int identity(1,1),
Data nvarchar(100)
)
AS
BEGIN
Declare #Cnt int
Set #Cnt = 1
While (Charindex(#SplitOn,#RowData)>0)
Begin
Insert Into #RtnValue (data)
Select
Data = ltrim(rtrim(Substring(#RowData,1,Charindex(#SplitOn,#RowData)-1)))
Set #RowData = Substring(#RowData,Charindex(#SplitOn,#RowData)+1,len(#RowData))
Set #Cnt = #Cnt + 1
End
Insert Into #RtnValue (data)
Select Data = ltrim(rtrim(#RowData))
Return
END
You can now do something like this:
Select Id, Data from dbo.Split('123,234,345,456',',')
And fear not, this can't be susceptible to Sql injection attacks.
Next write a stored procedure that takes your comma delimited data and then you can write a sql statement that uses this Split function:
CREATE PROCEDURE [dbo].[findDuplicates]
#ids nvarchar(max)
as
begin
select ID
from SomeTable with (nolock)
where ID in (select Data from dbo.Split(#ids,','))
end
Now you can write a C# wrapper around it:
public void SomeFunction(List<int> ids)
{
var idsAsDelimitedString = string.Join(",", ids.Select(id => id.ToString()).ToArray());
// ... or however you make your connection
var con = GetConnection();
try
{
con.Open();
var cmd = new SqlCommand("findDuplicates", con);
cmd.Parameters.Add(new SqlParameter("#ids", idsAsDelimitedString));
var reader = cmd.ExecuteReader();
// .... do something here.
}
catch (Exception)
{
// catch an exception?
}
finally
{
con.Close();
}
}
Related
This question already has answers here:
Sanitize table/column name in Dynamic SQL in .NET? (Prevent SQL injection attacks)
(3 answers)
Closed 5 months ago.
Edit: I marked that the linked question was useful, but I didn't intend to mean that it completely answered my question here, which after reviewing others' comments and answers here, I realize I need two different escaped versions of table, one as a safe single-quoted identifier, and one as a safe single-bracketed identifier.
I need to drop a SQL server table from C# with the table name as a parameter.
This is my code:
private static void DropSqlTableIfExists(string connectionString, string table)
{
string query = "if object_id (#table, 'U') is not null begin drop table #table; end";
using SqlConnection conn = new(connectionString);
using SqlCommand command = new(query, conn);
command.Parameters.AddWithValue("#table", table);
conn.Open();
command.ExecuteNonQuery();
}
The above code gives me an error, Incorrect syntax near '#table'.
I could of course put the table name directly into the query string, but that would allow code injection, which I need to avoid.
What is the best way to go about this?
EDIT 2:
Based on feedback, I updated the code to look like this:
private static void DropSqlTableIfExists(string connectionString, string table)
{
string tableSafeQuoted = $"'{table.Replace("'", "''")}'";
string tableSafeBracketed = $"[{table.Replace("]", "]]").Replace(".", "].[")}]";
string query = $"if object_id ({tableSafeQuoted}, 'U') is not null begin drop table {tableSafeBracketed}; end";
using SqlConnection conn = new(connectionString);
using SqlCommand command = new(query, conn);
conn.Open();
command.ExecuteNonQuery();
}
tbh, I'm not sure if it's 100% safe, due to the oddity of the . replacement.. but I think unless someone gives me a good reason not to, I will leave it like this..
You'll need to escape/encode the table name as an identifier. In SQL Server, this is typically done by replacing any closing brackets (]) in the name with two consecutive brackets (]]), and then surrounding the result with brackets ([...]).
You can either write a simple method to do this yourself, or take on a dependency like ScriptDom, which allows you to do this:
var escapedTableName = Identifier.EncodeIdentifier(tableName);
If your SQL Server database is set to use double-quoted identifiers, do something similar, but with double-quotes instead of brackets. Or, with ScriptDom:
var escapedTableName = Identifier.EncodeIdentifier(tableName, QuoteType.DoubleQuote);
Note that encoding the table name makes any .s act as if they're part of the table name itself. If your table name is supposed to have qualifiers (database, schema), each of those must be escaped individually, so you'll probably want to have them passed in as a separate argument, or create a separate type to represent the combination of these names.
There is probably a more elegant way to do this, but here's one possible way:
declare #dynamicsql nvarchar(max)
set #dynamicsql = 'DROP TABLE dbo.' + QUOTENAME(#table) + '';
if object_id (#table, 'U') is not null
begin
EXEC sp_executesql #dynamicsql;
end
so in your specific case it will be:
private static void DropSqlTableIfExists(string connectionString, string table)
{
string query = #"
declare #dynamicsql nvarchar(max)
set #dynamicsql = 'DROP TABLE dbo.' + QUOTENAME(#table) + '';
if object_id (#table, 'U') is not null
begin
EXEC sp_executesql #dynamicsql;
end
";
using SqlConnection conn = new(connectionString);
using SqlCommand command = new(query, conn);
command.Parameters.AddWithValue("#table", table);
conn.Open();
command.ExecuteNonQuery();
}
I hope it helps!
I am having an issue generating a SQL query using C#. To troubleshoot, I made the button that executes the query also display the query text in a textbox on the form. What's perplexing is that I get an error saying "Incorrect syntax near 'IF'" when the program tries to execute the query, but if I copy/paste the query from the textbox to SSMS it works fine.
The variable that stores the query looks like:
string myQuery = #"
SELECT DISTINCT filter.id_column INTO #temp1
FROM MasterDB.dbo.filter filter
LEFT JOIN ClientDB.dbo.codes codetable
ON filter.id_column=codetable.id_column
WHERE codetable.name IS NULL
DECLARE #code_id1 INT;
SET #code_id1 = (SELECT MAX(code_num) FROM ClientDB.dbo.codes)+1
EXEC('ALTER TABLE #temp1 ADD tempID INT IDENTITY(' + #code_id1 + ',1)')
GO
IF (SELECT COUNT(*) FROM #temp1)>0
BEGIN
DECLARE #code_id2 INT;
SET #code_id2 = (SELECT MAX(tempID) FROM #temp1)+1
UPDATE ClientDB.dbo.track
SET next=#code_id2 WHERE [trackname]='account'
END";
The C# code to populate the textbox with the query text and then run the query looks like:
using (SqlConnection myConnection = new SqlConnection(HostConnStr))
using (SqlCommand myCommand = myConnection.CreateCommand())
{
myCommand.CommandText = myQuery;
this.textBox1.Text = myCommand.CommandText;
myConnection.Open();
try { myCommand.ExecuteNonQuery(); }
catch (SqlException s) { MessageBox.Show(s.ToString()); }
myConnection.Close();
}
Does anyone know why the query text can be copied to SSMS and run fine, but throws a SQL exception when executed from C#? And how do I make the query run?
Critique on the query design will be appreciated, but I am more concerned with simply getting the query to execute since it does what I need it to do as-is.
EDIT: This may be a duplicate (I was thrown off by the error being near 'IF' when it appears that 'GO' is the problem, so my searches were in the wrong direction. However, I am still not sure that the answers provided in similar questions will work since I am under the impression that splitting the query into multiple commands will fail due to the later part of the query referencing a temporary table in the earlier part (will the temporary table not become unavailable after the first command is finished?).
It's the GO statement. You can replace it with ; in most instances.
In TSQL it's OK to have multiple statements separated by GO. In the ADO.NET version you can't do this.
The way to do this would be spilt the string on the GO and execute each independently. Such as this example,
string scriptText = #"...."
//split the script on "GO" commands
string[] splitter = new string[] { "\r\nGO\r\n" };
string[] commandTexts = scriptText.Split(splitter, StringSplitOptions.RemoveEmptyEntries);
foreach (string commandText in commandTexts)
{
//execute commandText
}
Recent bug report states that a method being called is crashing the service causing it to restart. After troubleshooting, the cause was found to be an obnoxious Oracle SQL call with thousands of strings passed. There is a collection of strings being passed to a method from an external service which often is more than 10,000 records. The original code used a where clause on the passed collection using the LIKE keyword, which I think is really, really bad.
public IList<ContainerState> GetContainerStates(IList<string> containerNumbers)
{
string sql =
String.Format(#"Select CTNR_NO, CNTR_STATE FROM CONTAINERS WHERE CTRN_SEQ = 0 AND ({0})",
string.Join("OR", containerNumbers
.Select(item => string.Concat(" cntr_no LIKE '", item.SliceLeft(10), "%' ")))
);
return DataBase.SelectQuery(sql, MapRecordToContainerState, new { }).ToList();
}
Clarification of in house methods used which may be confusing:
DataBase.SelectQuery is an internal library method using generics which gets passed the sql string, a function to map the records to .NET objects, and the parameters being passed and returns an IEnumerable of Objects of type retuned by the Mapping function.
SliceLeft is an extension method from another internal helper library that just returns the first part of a string up to the number of characters specified by the parameter.
The reason that the LIKE statement was apparently used, is that the strings being passed and the strings in the database only are guaranteed to match the first 10 characters. Example ("XXXX000000-1" in the strings being passed should match a database record like "XXXX000000-8").
I believed that the IN clause using the SUBSTR would be more efficent than using multiple LIKE clauses and replaced the code with:
public IList<ContainerRecord> GetContainerStates(IList<string> containerNumbers)
{
string sql =
String.Format(#"Select CTNR_NO, CNTR_STATE FROM CONTAINERS WHERE CTRN_SEQ = 0 AND ({0})",
string.Format("SUBSTR(CNTR_NO, 1, 10) IN ({0}) ",
string.Join(",", containerNumbers.Select(item => string.Format("\'{0}\'", item.SliceLeft(10) ) ) )
)
);
return DataBase.SelectQuery(sql, MapRecordToContainerState, new { }).ToList();
}
This helped slightly, and there were fewer issues in my tests, but when there are huge amounts of records passed, there is still an exception thrown and core dumps occur, as the SQL is longer than the server can parse during these times. The DBA suggests saving all the strings being passed to a temporary table, and then joining against that temp table.
Given that advice, I changed the function to:
public IList<ContainerRecord> GetContainerStates(IList<string> containerNumbers)
{
string sql =
#"
CREATE TABLE T1(cntr_num VARCHAR2(10));
DECLARE GLOBAL TEMPORARY TABLE SESSION.T1 NOT LOGGED;
INSERT INTO SESSION.T1 VALUES (:containerNumbers);
SELECT
DISTINCT cntr_no,
'_IT' cntr_state
FROM
tb_master
WHERE
cntr_seq = 0
AND cntr_state IN ({0})
AND adjustment <> :adjustment
AND SUBSTR(CTNR_NO, 1, 10) IN (SELECT CNTR_NUM FROM SESSION.T1);
";
var parameters = new
{
#containerNumbers = containerNumbers.Select( item => item.SliceLeft(10)).ToList()
};
return DataBase.SelectQuery(sql, MapRecordToContainerState, parameters).ToList();
}
Now I'm getting a "ORA-00900: invalid SQL statement". This is really frustrating, how can I properly write a SQL Statement that will put this list of strings into a temporary table and then use it in a SELECT Statement to return the list I need?
There are couple possible places could cause this error, it seams that the "DECLARE GLOBAL TEMPORARY" is a JAVA API, I don't think .net has this function. Please try "Create global temporary table" instead. And, I don't know whether your internal API could handle multiple SQLs in one select sql. As far as I know, ODP.net Command class can only execute one sql per call. Moreover, "create table" is a DDL, it therefore has its own transaction. I can't see any reason we should put them in the same sql to execute. Following is a sample code for ODP.net,
using (OracleConnection conn = new OracleConnection(BD_CONN_STRING))
{
conn.Open();
using (OracleCommand cmd = new OracleCommand("create global temporary table t1(id number(9))", conn))
{
// actually this should execute once only
cmd.ExecuteNonQuery();
}
using (OracleCommand cmd = new OracleCommand("insert into t1 values (1)", conn)) {
cmd.ExecuteNonQuery();
}
// customer table is a permenant table
using (OracleCommand cmd = new OracleCommand("select c.id from customer c, t1 tmp1 where c.id=tmp1.id", conn)) {
cmd.ExecuteNonQuery();
}
}
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Help with a sql search query using a comma delimitted parameter
I want to write a stored procedure that performs a select on a table and need one input variable of type varchar(max).
I'd like to send a bunch of values separated by , as the input parameter, e.g.
'Jack','Jane','Joe'
and then get the rows that contain one of these names.
In SQL the code would be
Select * from Personnel where Name in ('Jack','Joe','Jane');
Now I want to have a variable in my C# app, say strNames and fill it like
string strNames = "'Jack','Joe','Jane'";
and send this variable to the SP and execute it. Something like
Select * from Personnel where Name in (''Jack','Joe','Jane'') -- this is wrong
But how can I tell SQL Server to run such command?
I need to make this happen and I know it's possible, please give me the clue.
First of all, the single names don't need to be quoted when you pass them to the stored procedure.
using (SqlCommand cmd = new SqlCommand("MyStoredProc", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#longFilter", "Jack,Jill,Joe");
using (SqlDataReader reader = cmd.ExecuteReader())
{
...
}
}
Then, in the stored procedure, you can use simple text functions and a temporary table as follows to split up the string at the commas and an an entry to the temporary table for each part of the string:
DECLARE #temp AS TABLE (Name NVARCHAR(255))
IF ISNULL(#longFilter, '') <> ''
BEGIN
DECLARE #s NVARCHAR(max)
WHILE LEN(#longFilter) > 0
BEGIN
IF CHARINDEX(',', #longFilter) > 0
BEGIN
SET #s = LTRIM(RTRIM(SUBSTRING(#longFilter, 1, CHARINDEX(',', #longFilter) - 1)))
SET #longFilter = SUBSTRING(#longFilter, CHARINDEX(',', #longFilter) + 1, LEN(#longFilter))
END ELSE
BEGIN
SET #s = LTRIM(RTRIM(#longFilter))
SET #longFilter= ''
END
-- This was missing until 20140522
INSERT INTO #temp (Name) VALUES (#s)
END
END
Later use the following SELECT to get a list of all people the name of which is in #temp or all of them if #temp doesn't contain any rows (unfiltered result):
SELECT * FROM Personnel WHERE Name IN (SELECT Name FROM #temp) OR (SELECT COUNT(*) FROM #temp) = 0
You could use Table Valued Parameters.
Basically, you could insert a list of values as a parameter in the procedure, and use them as a table, something along the lines of
Select * from Personnel
where Name in (select name from #NamesTable).
Now, the specifics
To use table valued parameters, the type of the parameter must be predefined in sql server, using
create type NamesTable as table (Name varchar(50))
You can then use the defined type as a parameter in the procedure
create procedure getPersonnelList
#NamesTable NamesTable readonly
as
begin
select * from personnel
where Name in (select Name from #NamesTable)
end
You can see that in action, in this SQL Fiddle
On the C# side of things you need to create the parameter. If you have the names in a collection, and build the string, you can just use that to generate the parameter, and if they are a comma-separated string, a quick string.Split could take care of that. Since I do not know your specifics, I'll assume you have a List<string> called names. You'll need to convert that to a table valued parameter to be sent to the procedure, using something like:
DataTable tvparameter = new DataTable();
tvparameter.Columns.Add("Name", typeof(string));
foreach (string name in names)
{
tvparameter.Rows.Add(name);
}
You can find more info on how to generate a TVP in C# code in the SO Question..
Now you just need to send that parameter to the procedure, and that's that. Here is a complete console program that executes the procedure and outputs the results.
List<string> names = new List<string> { "Joe", "Jane", "Jack" };
using (SqlConnection cnn = new SqlConnection("..."))
{
cnn.Open();
using (SqlCommand cmd = new SqlCommand("getPersonnelList", cnn))
{
cmd.CommandType = CommandType.StoredProcedure;
DataTable tvparameter = new DataTable();
tvparameter.Columns.Add("Name", typeof(string));
foreach (string name in names)
{
tvparameter.Rows.Add(name);
}
cmd.Parameters.AddWithValue("#NamesTable", tvparameter);
using (SqlDataReader dr = cmd.ExecuteReader())
{
while (dr.Read())
{
Console.WriteLine("{0} - {1}", dr["ID"], dr["Name"]);
}
}
}
}
I guess you need Split Function in Sql Server to break Comma-Separated Strings into Table. Please refer these links.
Split Function in Sql Server to break Comma-Separated Strings into Table
SQL User Defined Function to Parse a Delimited String
You can select the data from table using
Select * from
Personnel where
Name in (select items from dbo.Split ('Jack,Joe,Jane',','))
You could simply check if Name is contained in the string. Note the commas at the start of the end to ensure you match the full name
string strNames = ",Jack,Joe,Jane,";
The the SQL becomes
select * from Personnel where PATINDEX('%,' + Name + ',%', #strNames) > 0
See http://www.sqlfiddle.com/#!3/8ee5a/1
I have this program that is feeding me data. I take this data (string) and parse it so that the different fields can go into the respective db table column. I can parse the string but I can't find the right function or way to send them to the db. This is my second time working with sql server or database in general. I have done inserts this way
MyCommand.CommandType = System.Data.CommandType.Text;
MyCommand.CommandText = "INSERT INTO TimeStampTable(ID, TimeStamp) VALUES ('24', 'sep 13, 2009')";
From what I know, CommandType only allows either text or a stored procedure. In this case, I would want to insert the string that is being parsed.
string teststring = dtString;
string[] result = teststring.Split(',', ' ', ':', '=');
Console.WriteLine("The parsed string looks like this:");
foreach (string word in result)
{
Console.WriteLine(word);
}
This is my code that parses my incoming string. So I receive name address zip state, etc. I would like for name to go to col1, address to go to col2, etc. I think the ideal way to do this would be to convert my loop to something like this
foreach (string word in result)
{
SqlDatasource.InsertCommand=Insert into Tablename col1 col2 col3(word);
}
Does anyone have any suggestions?
I will answer your question directly, but there are many different ways that you could go about performing the same thing (I will list some at the end)
String insertQuery = "INSERT INTO TABLENAME (Col1, Col2, Col3...) VALUES (";
//This is also assuming that your data is in the same order as the columns
int isFirstLoop = true
foreach(string word in result)
{
if(!isFirstLoop)
insertQuery += ","
insertQuery += word;
isFirstLoop = false;
}
insertQuery += ")";
SqlDataSource.InsertCommand = insertQuery;
NOTE: this is very open to SQL Injection, so keep that in mind (do you trust your incoming source). There are ways to clean the data, but ultimately, I suggest some of the methods listed below
Alternatives:
Use a stored procedure over direct TSQL. Then you can map your data to SQLParameters, which (I believe) are built to scrub the data to protect against SQL Injection
Use a very basic ORM and/or LINQ so that you can work with objects directly. Then you only need to read the data into a POCO
I am sure there are other ways, however for some reason I am drawing a blank. I think that is because these are the most used alternatives :)