I use ADO.NET to delete some data from DB like this:
using (SqlConnection conn = new SqlConnection(_connectionString))
{
try
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("Delete from Table where ID in (#idList);", conn))
{
cmd.Parameters.Add("#idList", System.Data.SqlDbType.VarChar, 100);
cmd.Parameters["#idList"].Value = stratIds;
cmd.CommandTimeout = 0;
cmd.ExecuteNonQuery();
}
}
catch (Exception e)
{
//_logger.LogMessage(eLogLevel.ERROR, DateTime.Now, e.ToString());
}
finally
{
conn.Close();
}
}
That code executes without Exception but data wasn't deleted from DB.
When I use the same algorithm to insert or update DB everything is OK.
Does anybody know what is the problem?
You can't do that in regular TSQL, as the server treats #idList as a single value that happens to contain commas. However, if you use a List<int>, you can use dapper-dot-net, with
connection.Execute("delete from Table where ID in #ids", new { ids=listOfIds });
dapper figures out what you mean, and generates an appropriate parameterisation.
Another option is to send in a string and write a UDF to perform a "split" operation, then use that UDF in your query:
delete from Table where ID in (select Item from dbo.Split(#ids))
According to Marc's Split-UDF, this is one working implementation:
CREATE FUNCTION [dbo].[Split]
(
#ItemList NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #IDTable TABLE (Item VARCHAR(50))
AS
BEGIN
DECLARE #tempItemList NVARCHAR(MAX)
SET #tempItemList = #ItemList
DECLARE #i INT
DECLARE #Item NVARCHAR(4000)
SET #tempItemList = REPLACE (#tempItemList, ' ', '')
SET #i = CHARINDEX(#delimiter, #tempItemList)
WHILE (LEN(#tempItemList) > 0)
BEGIN
IF #i = 0
SET #Item = #tempItemList
ELSE
SET #Item = LEFT(#tempItemList, #i - 1)
INSERT INTO #IDTable(Item) VALUES(#Item)
IF #i = 0
SET #tempItemList = ''
ELSE
SET #tempItemList = RIGHT(#tempItemList, LEN(#tempItemList) - #i)
SET #i = CHARINDEX(#delimiter, #tempItemList)
END
RETURN
END
And this is how you could call it:
DELETE FROM Table WHERE (ID IN (SELECT Item FROM dbo.Split(#idList, ',')));
I want to give this discussion a little more context. This seems to fall under the topic of "how do I get multiple rows of data to sql". In #Kate's case she is trying to DELETE-WHERE-IN, but useful strategies for this user case are very similar to strategies for UPDATE-FROM-WHERE-IN or INSERT INTO-SELECT FROM. The way I see it there are a few basic strategies.
String Concatenation
This is the oldest and most basic way. You do a simple "SELECT * FROM MyTable WHERE ID IN (" + someCSVString + ");"
Super simple
Easiest way to open yourself to a SQL Injection attack.
Effort you put into cleansing the string would be better spent on one of the other solutions
Object Mapper
As #MarcGravell suggested you can use something like dapper-dot-net, just as Linq-to-sql or Entity Framework would work. Dapper lets you do connection.Execute("delete from MyTable where ID in #ids", new { ids=listOfIds }); Similarly Linq would let you do something like from t in MyTable where myIntArray.Contains( t.ID )
Object mappers are great.
However, if your project is straight ADO this is a pretty serious change to accomplish a simple task.
CSV Split
In this strategy you pass a CSV string to SQL, whether ad-hoc or as a stored procedure parameter. The string is processed by a table valued UDF that returns the values as a single column table.
This has been a winning strategy since SQL-2000
#TimSchmelter gave a great example of a csv split function.
If you google this there are hundreds of articles examining every aspect from the basics to performance analysis across various string lengths.
Table Valued Parameters
In SQL 2008 custom "table types" can be defined. Once the table type is defined it can be constructed in ADO and passed down as a parameter.
The benefit here is it works for more scenarios than just an integer list -- it can support multiple columns
strongly typed
pull string processing back up to a layer/language that is quite good at it.
This is a fairly large topic, but Table-Valued Parameters in SQL Server 2008 (ADO.NET) is a good starting point.
Related
I'm writing a C# class library in which one of the features is the ability to create an empty data table that matches the schema of any existing table.
For example, this:
private DataTable RetrieveEmptyDataTable(string tableName)
{
var table = new DataTable() { TableName = tableName };
using var command = new SqlCommand($"SELECT TOP 0 * FROM {tableName}", _connection);
using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
dataAdapter.Fill(table);
return table;
}
The above code works, but it has a glaring security vulnerability: SQL injection.
My first instinct is to parameterize the query like so:
using var command = new SqlCommand("SELECT TOP 0 * FROM #tableName", _connection);
command.Parameters.AddWithValue("#tableName", tableName);
But this leads to the following exception:
Must declare the table variable "#tableName"
After a quick search on Stack Overflow I found this question, which recommends using my first approach (the one with sqli vulnerability). That doesn't help at all, so I kept searching and found this question, which says that the only secure solution would be to hard-code the possible tables. Again, this doesn't work for my class library which needs to work for arbitrary table names.
My question is this: how can I parameterize the table name without vulnerability to SQL injection?
An arbitrary table name still has to exist, so you can check first that it does:
IF EXISTS (SELECT 1 FROM sys.objects WHERE name = #TableName)
BEGIN
... do your thing ...
END
And further, if the list of tables you want to allow the user to select from is known and finite, or matches a specific naming convention (like dbo.Sales%), or belongs to a specific schema (like Reporting), you could add additional predicates to check for those.
This requires you to pass the table name in as a proper parameter, not concatenate or token-replace. (And please don't use AddWithValue() for anything, ever.)
Once your check that the object is real and valid has passed, then you will still have to build your SQL query dynamically, because you still won't be able to parameterize the table name. You still should apply QUOTENAME(), though, as I explain in these posts:
Protecting Yourself from SQL Injection in SQL Server - Part 1
Protecting Yourself from SQL Injection in SQL Server - Part 2
So the final code would be something like:
CREATE PROCEDURE dbo.SelectFromAnywhere
#TableName sysname
AS
BEGIN
IF EXISTS (SELECT 1 FROM sys.objects
WHERE name = #TableName)
BEGIN
DECLARE #sql nvarchar(max) = N'SELECT *
FROM ' + QUOTENAME(#TableName) + N';';
EXEC sys.sp_executesql #sql;
END
ELSE
BEGIN
PRINT 'Nice try, robot.';
END
END
GO
If you also want it to be in some defined list you can add
AND #TableName IN (N't1', N't2', …)
Or LIKE <some pattern> or join to sys.schemas or what have you.
Provided nobody has the rights to then modify the procedure to change the checks, there is no value you can pass to #TableName that will allow you to do anything malicious, other than maybe selecting from another table you didn’t expect because someone with too much access was able to create before calling the code. Replacing characters like -- or ; does not make this any safer.
You could pass the table name to the SQL Server to apply quotename() on it to properly quote it and subsequently only use the quoted name.
Something along the lines of:
...
string quotedTableName = null;
using (SqlCommand command = new SqlCommand("SELECT quotename(#tablename);", connection))
{
SqlParameter parameter = command.Parameters.Add("#tablename", System.Data.SqlDbType.NVarChar, 128 /* nvarchar(128) is (currently) equivalent to sysname which doesn't seem to exist in SqlDbType */);
parameter.Value = tableName;
object buff = command.ExecuteScalar();
if (buff != DBNull.Value
&& buff != null /* theoretically not possible since a FROM-less SELECT always returns a row */)
{
quotedTableName = buff.ToString();
}
}
if (quotedTableName != null)
{
using (SqlCommand command = new SqlCommand($"SELECT TOP 0 FROM { quotedTableName };", connection))
{
...
}
}
...
(Or do the dynamic part on SQL Server directly, also using quotename(). But that seems overly and unnecessary tedious, especially if you will do more than one operation on the table in different places.)
Aaron Bertrand's answer solved the problem, but a stored procedure is not useful for a class library that might interact with any database. Here is the way to write RetrieveEmptyDataTable (the method from my question) using his
answer:
private DataTable RetrieveEmptyDataTable(string tableName)
{
const string tableNameParameter = "#TableName";
var query =
" IF EXISTS (SELECT 1 FROM sys.objects\n" +
$" WHERE name = {tableNameParameter})\n" +
" BEGIN\n" +
" DECLARE #sql nvarchar(max) = N'SELECT TOP 0 * \n" +
$" FROM ' + QUOTENAME({tableNameParameter}) + N';';\n" +
" EXEC sys.sp_executesql #sql;\n" +
"END";
using var command = new SqlCommand(query, _connection);
command.Parameters.Add(tableNameParameter, SqlDbType.NVarChar).Value = tableName;
using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
var table = new DataTable() { TableName = tableName };
Connect();
dataAdapter.Fill(table);
Disconnect();
return table;
}
I need help, I have a problem while inserting a statement in SQL.
I call a SQL statement from my ASP.NET program, some variables contain quotes so when the insert is fired I have an exception like:
Exception Details: System.Data.SqlClient.SqlException: Incorrect syntax near 'xxxxx'.
Unclosed quotation mark after the character string ''.
I don't want the content of my variable to be changed...
Any idea how to handle this?
The C# part :
SqlCommand cmdInsertAssessment = new SqlCommand("xxxxxxx", sqlCnx);
cmdInsertAssessment.CommandType = CommandType.StoredProcedure;
cmdInsertAssessment.Parameters.AddWithValue("#templateID", templateID);
cmdInsertAssessment.Parameters.AddWithValue("#companyID", companyID);
cmdInsertAssessment.Parameters.AddWithValue("#userID",userID);
cmdInsertAssessment.Parameters.AddWithValue("#opn",opn);
cmdInsertAssessment.Parameters.AddWithValue("#mn",Mm);
cmdInsertAssessment.Parameters.AddWithValue("#max",max);
cmdInsertAssessment.Parameters.AddWithValue("#remarque",remarque);
cmdInsertAssessment.Parameters.AddWithValue("#templateTheme",templateTheme);
cmdInsertAssessment.Parameters.AddWithValue("#name", sName);
cmdInsertAssessment.Parameters.AddWithValue("#finished", iFinished);
cmdInsertAssessment.Parameters.AddWithValue("#datenow", dtNow);
try
{
cmdInsertAssessment.ExecuteNonQuery();
}
catch (Exception e)
{
}
SQL part :
CREATE PROCEDURE ["xxxxxxx"] #templateID int,
#companyID int,
#userID int,
#opn nvarchar(255),
#mn nvarchar(255),
#max int,
#remarque nvarchar(255),
#templateTheme nvarchar(255),
#name nvarchar(255),
#finished int,
#datenow datetime
AS
BEGIN
DECLARE
#points AS FLOAT
SET #points=0
IF(#mn='M')
BEGIN
IF(#opn='O')
BEGIN
SET #points=10
END
IF(#opn='P')
BEGIN
SET #points=2
END
END
IF(#mn!='M')
BEGIN
IF(#opn='O')
BEGIN
SET #points=2
END
if(#opn='P')
BEGIN
SET #points=1
END
END
IF(#remarque=NULL)
BEGIN
SET #remarque='nothing'
END
MERGE INTO [dbo].[Assessment] as target
USING (SELECT #templateID,#companyID,#userID,#opn,#points,#max,#remarque,#templateTheme,#datenow,#name,#finished)
As source (_templateID,_companyID,_userID,_opn,_points,_max,_remarque,_templateTheme,_datenow,_name,_finished)
ON target.TemplateID=source._templateID
AND target.TemplateTheme=source._templateTheme
AND target.NameAssessment=source._name
WHEN MATCHED THEN
UPDATE SET Points = source._points, Remarque = source._remarque, FillDate= source._datenow, Finished = source._finished, OPN = source._opn
WHEN NOT MATCHED THEN
INSERT (TemplateID, CompanyID, UserID, OPN, Points, Max, Remarque, TemplateTheme, FillDate, NameAssessment,Finished)
VALUES (source._templateID,source._companyID,source._userID,source._opn,source._points,source._max,source._remarque,source._templateTheme,source._datenow,source._name,source._finished);
END
GO
Thanks :)
Taking things from the begining ! Your procedure calculates a number of points, based on parameters you supply (#mn, #opn), then inserts or updates table Assessment. The first thing to say is that this is not a job for Merge. Merge is intended to operate on two tables, and using it for a row and a table is using a sledgehammer to crack a nut. You should really use
IF EXISTS( SELECT ID FROM ASSESSMENT WHERE... )
then write a classic insert and a classic update. Your procedure will be easier to understand, easier to maintain, and likely run much faster.
If you're still reading, I'll keep going. The calculation of points, business logic that uses nothing from the DB, will be much happier in the C#. Wherever you put it, you can use ternary operators to shorten those either-or choices. The following replaces 20 lines in your procedure.
var points = (mn == 'm')?(opn == 'O'?10:2):(opn == 'O'?2:1);
The assignment starting IF( #remarque = null ) can be done with a null coalescing operator ISNULL() in sql, ?? in C#.
And if you're still still reading, grab QueryFirst. You'll get a v.clean separation between SQL and C# and all your parameter creation will be done for you.
Because you said you wanted to use stored procedures
using (SqlConnection cnn = new SqlConnection(/*Connection String*/))
{
using (SqlCommand cmd = new SqlCommand("MyStoredProcedure", cnn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#param1", "Value 1");
cmd.Parameters.AddWithValue("#param2", "xxxxxx");
cnn.Open();
}
}
Hey I was using parametrized queries for my application which worked just fine but now (I don't know why) they aren't replaced anymore with the values...
So instead of running something like "SELECT [TABLE_NAME] FROM [MyDefinetelyExistingDatabase]"; it tries to execute "SELECT [TABLE_NAME] FROM [#targetDatabase]"; which, of course, will fail.
var dataBaseToGetTablesFrom = "MyDefinetelyExistingDatabase";
var results = new List<string>();
const string query = #"SELECT
[TABLE_NAME] AS tableName
FROM
[#targetDatabase].[INFORMATION_SCHEMA].[TABLES] ;";
using (var context = new ConnectionHandler(true))
{
if (context.Connection.State != ConnectionState.Open)
throw new ConnectionFailedException(context.Connection.State);
using (var command = new SqlCommand(query, context.Connection))
{
command.Parameters.AddWithValue("#targetDatabase", dataBaseToGetTablesFrom);
using (var reader = command.ExecuteReader())
{
if (!reader.HasRows)
return results.ToArray();
while (reader.Read())
results.Add(reader.GetString(0));
}
}
}
return results.ToArray();
I now tried different formats and things to add the parameters but it results in the same...
I don't want to do this by inserting the values into the query directly via string.Format eg but I want to have those parameters (which work properly at different places in the code (???) but not where I want.
In fact, I need to use parameters in every statement and must be able to address different databases by calling them like [DB].[Table-Schema].[Table]
[EDIT]
Hey guys, figured the problem some days ago and thought I share it with you.
As far as I have noticed, my problem at the whole was to try to replace the databasename and / or in some other examples, the table name as well.
So this won't work which makes clearly sense to me as the server can't prepare to execute a statement if it doesn't even know on which table it should work and therefore doesn't know anything about the structure etc.
So I changed my statements to fit my new knowledge and it worked as expected like a charm.
I don't know what ConnectionHandler is, but if that is your own code you can implement it with SqlConnectionStringBuilder which will allow you to use a variable to assign the InitialCatalog instead of putting the database name in the query. This would be preferable to dynamic sql which requires careful sanitization.
You would need dynamic sql for this something like.....
DECLARE #Sql NVARCHAR(MAX);
SET #Sql = N' SELECT [TABLE_NAME] AS tableName '
+ N' FROM ' + QUOTENAME(#targetDatabase) + N'.[INFORMATION_SCHEMA].[TABLES]'
Exec sp_executesql #Sql
I'm able to insert the the items in a single statement but what I want to do is to have another version using a Stored Procedures. How do I do that. Here's my code:
private void button1_Click(object sender, EventArgs e)
{
#region Get Values
string[] array = {textBox1.Text+":"+textBox5.Text,textBox2.Text+":"+textBox6.Text,textBox3.Text+":"+textBox7.Text,textBox4.Text+":"+textBox8.Text};
string query = "";
string product = "";
int qty = 0;
for (int i = 0; i < array.Length; i++ )
{
product = array[i].ToString().Substring(0,array[i].ToString().IndexOf(':'));
qty = int.Parse(array[i].ToString().Substring(array[i].ToString().IndexOf(':')+1));
if (string.IsNullOrEmpty(query))
{
query = "Insert Into MySampleTable Values ('"+product+"','"+qty+"')";
}
else
{
query += ",('" + product + "','" + qty + "')";
}
}
#endregion
string connect = "Data Source=RANDEL-PC;Initial Catalog=Randel;Integrated Security=True";
SqlConnection connection = new SqlConnection(connect);
connection.Open();
string insert = query;
SqlCommand command = new SqlCommand(query,connection);
command.ExecuteNonQuery();
command.Dispose();
connection.Close();
connection.Dispose();
label5.Visible = true;
label5.Text = insert;
}
}
Sir/Ma'am, Your answers would be of great help and be very much appreciated. Thank you++
In SQL Server 2008+ there are easier ways to insert multiple rows in a single statement. For example this syntax is valid:
INSERT dbo.table(col1, col2) VALUES
(1, 2),
(2, 3),
(3, 4);
The above will insert three rows. On older versions you can do slightly more verbose things such as:
INSERT dbo.table(col1, col2)
SELECT 1, 2
UNION ALL SELECT 2, 3
UNION ALL SELECT 3, 4;
Of course your ExecuteNonQuery does not have to be a single command, you can pass this as a single string and it will still work:
INSERT dbo.table(col1, col2) VALUES(1, 2);
INSERT dbo.table(col1, col2) VALUES(2, 3);
INSERT dbo.table(col1, col2) VALUES(3, 4);
If you want to do this in a stored procedure, you can easily perform a split on multi-valued parameters, for example if you pass in the following string:
1,2;2,3;3,4
You could process those values using a function like the one I posted here:
Split value pairs and a create table using UDF
So your procedure might look like this:
CREATE PROCEDURE dbo.AddOrderLineItems
#LineItems VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.OrderItems(Product, Quantity)
SELECT Product, Quantity FROM dbo.MultiSplit(#LineItems);
END
GO
And you would call it using the C# equivalent of:
EXEC dbo.AddOrderLineItems #LineItems = '1,2;2,3;3,4';
Or you could use table-valued parameters as suggested by Alexey. A quick example:
CREATE TYPE OrderLineItem AS TABLE
(
Product INT,
Quantity INT
);
Then you can create a procedure:
CREATE PROCEDURE dbo.AddOrderLineItems
#LineItems OrderLineItem READONLY
-- other parameters
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.OrderItems(Product, Quantity)
SELECT Product, Quantity FROM #LineItems;
END
GO
Then create the equivalent TVP in your C# code (I'm not the guy you want doing that; you can see an example here).
However there are some caveats, please look at this question:
Creating a generalized type for use as a table value parameter
If you want to pass multiple values into a stored procedure you have two ways:
And ugly one: pass your values as a separate string, split it in your store procedure, do bulk insert. You will find tonnes of examples of it in Google.
A clever one: use table-value parameters, the feature supported by both ADO.NET and SQL Server. Then you will be able to pass a parameter value and have it as a normal table variable in your stored procedure.
I have a c# application (2008) that gets data from sql server (2005).
I have a view in sql server that prepares data for display, something like this (simplified):
select Places.Name as [Location], Parts.Name as [Part Name]
from Places inner join Parts
on Places.Id=Parts.Location
I have to filter this with "where" statement that is built in code and is like:
where (Places.Id=1 or Places.Id=15) and
(Parts.Id=56 or Parts.Id=8 or Parts.Id=32)
I can of course keep the basic select statement in my code, but i likw to have things defined only in one place :) and the question is if there is any way to get the select statement behind the view in sql server? Or to get the contents of stored procedure?
Thanks a lot!
Take a look at Information Schema View, you may find your solution.
Using the information schema views as jani suggested is one option.
Another is using the sp_helptext system stored procedure. sp_helptext YourView or sp_helptext YourStoredProcedure gets you the entire object definition.
You can find more information about the at sp_helptext system stored procedure here.
If you want a stored procedure to execute your query (and combining your basic query string, with your where clause), you can accomplish this by using the following code:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string selectCommand = "EXEC sp_YourStoredProcedure #whereClause";
SqlCommand command = new SqlCommand(selectCommand, connection);
command.Parameters.Add("#whereClause", System.Data.SqlDbType.NVarChar);
command.Parameters["#whereClause"] = whereClause;
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.NextResult())
{
string location = reader.GetString(0);
string partName = reader.GetString(1);
// do something
}
}
connection.Close();
}
Edit: Example of dynamic stored procedure:
CREATE PROCEDURE sp_YourStoredProcedure
(
#whereClause NVARCHAR(MAX)
)
AS
BEGIN
DECLARE #sql AS NVARCHAR(MAX)
SET #sql = N'
select Places.Name as [Location], Parts.Name as [Part Name]
from Places inner join Parts
on Places.Id=Parts.Location '
+ #whereClause
EXEC sp_executesql #sql
END