Is it possible to parse sql parameters from plain commandtext?
e.g.
//cmdtext = SELECT * FROM AdWorks.Countries WHERE id = #id
SqlCommand sqlc = new SqlCommand(cmdtext);
SqlParameterCollection parCol = sqlc.Parameters //should contain now 1 paramter called '#id'
If a SQL Server is available, the best option may be to simply ask the server what it thinks; the server has parsing and metadata functions built in, for example sp_describe_undeclared_parameters.
I ended up with this extention method (since I don't think there's a built in function):
public static class SqlParExtension
{
public static void ParseParameters(this SqlCommand cmd)
{
var rxPattern = #"(?<=\= |\=)#\w*";
foreach (System.Text.RegularExpressions.Match item in System.Text.RegularExpressions.Regex.Matches(cmd.CommandText, rxPattern))
{
var sqlp = new SqlParameter(item.Value, null);
cmd.Parameters.Add(sqlp);
}
}
}
usage:
//cmdtext = SELECT * FROM AdWorks.Countries WHERE id = #id
SqlCommand sqlc = new SqlCommand(cmdtext);
sqlc.ParseParameters();
sqlc.Parameters["#id"].Value = value;
I will have to make sure about this but I'm sure you must add the range of parameters to the command. Like I say I will have to come back with this but you can try doing something like:
// Create a collection of parameters with the values that the procedure is expecting in your SQL client.
SqlParameter[] parameters = { new SqlParameter("#id", qid),
new SqlParameter("#otherValue", value) };
// Add teh parameters to the command.
sqlc.Parameters.AddRange(parameters)
You would be very welcome to have a look at my VS2015 extension, QueryFirst, that generates wrapper classes from .sql files, harvesting parameter declarations directly from your sql. You need to declare your parameters in the --designTime section of your request, but then you find them again directly as inputs to the Execute(), GetOne() or ExecuteScalar() methods. These methods return POCOs with meaningul property names. There's intellisense everywhere, and you don't have to type a line of parameter code, or connection code, or command code, or reader code, among NUMEROUS OTHER ADVANTAGES :-).
Related
I have the below block of code:
private TData ExecuteReturnData<TData>(string procName, Func<IDataReader, TData> translator, SqlConnection sqlCon, params SqlParameter[] parameters)
{
using var sqlCmd = CreateCommand(procName, sqlCon, parameters);
sqlCmd.CommandTimeout=120;
using var reader = sqlCmd.ExecuteReader();
var elems = translator(reader);
return elems
}
Here I want to see the complete SQL command execution with params in SQL Server code block. How can I check that here?
What you're asking for does not exist. It never exists at any point. The entire purpose of using parameterized queries is the parameter data is NEVER substituted directly into the SQL command string, and therefore will not be available to view in that way.
Parameterized queries are more than simply sanitizing or escaping in the parameter data in the proper way; they quarantine the data from the command, so the two can never meet.
That is, if you have this query:
SELECT * FROM Users WHERE FirstName= #FirstName
and this parameter value:
Samuel
instead of something like this:
SELECT * FROM Users WHERE FirstName = 'Samuel'
The parameter data is sent to the server in a completely separate block than the SQL command. The server receives both parts and does something more like this:
DECLARE #FirstName nvarchar(40) = LoadParameterFromClient()
SELECT * FROM Users WHERE FirstName= #FirstName
(Note: the actual mechanism for this is sp_executesql)
But for what it's worth, I tend to structure similar C# code more like this:
private IEnumerable<TData> ExecuteReturnData<TData>(string SQL, Func<IDataRecord, TData> translator, Action<SqlParameterCollection> addParams)
{
using var conn = new SqlConnection(" ... "); // My data layer knows about the database I'm using, so I don't need to pass in a conneciton
using var cmd = new SqlCommand(SQL, conn);
if (addParams is object) addParams(cmd.Parameters);
conn.Open();
using var reader = sqlCmd.ExecuteReader();
while (reader.Read())
{
yield return translator(reader);
}
}
Then I'd call it using a similar example as above like this:
var results = ExecuteReturnData<string>("SELECT FirstName, LastName FROM Users WHERE FirstName=#FirstName",
p => p.Add("#FirstName", SqlDbType.NVarchar,40).Value = "Samuel",
r => r["LastName"] + ", " + r["FirstName"]);
For more complex result types I'd have a static FromSQL(IDataRecord data) method on the target type, to avoid making this function call too difficult to read:
var results = ExecuteReturnData<User>("SELECT * FROM Users WHERE FirstName=#FirstName",
p => p.Add("#FirstName", SqlDbType.NVarchar,40).Value = "Samuel",
User.FromSQL);
As a project grows I might also collect these methods into a separate static type, to avoid over-coupling between the data layer and client code.
And of course you can run stored procedures the same way:
var results = ExecuteReturnData("exec MyProcedure #Param1, #Param2" ... );
How to see SQL command execution with Stored Procedure
I want to see the complete SQL command execution with params in SQL Server code block. How can I check that here?
In Visual Studio open the SQL Server Object Explorer > New Connection to SQL Server > Expand Database > Expand Programmatibility > Stored Procedures and Right Click on a Sproc and choose Debug Procedure. You can step though the T-SQL Code. But you can't jump into the T-SQL from the .Net Code which sounds like what you want.
Debug Procedure > Press F11 to Step into the Stored Procedure:
REF: https://learn.microsoft.com/en-us/sql/ssms/scripting/transact-sql-debugger?view=sql-server-ver16
This is what I have:
select *
from AuditQuestionnaires
where Companyid = #companyid
and RiskTypeID != #RiskIdList[x];
Why can't I add a parameter like this:
command2.Parameters.Add(new SqlParameter("RiskIdList[x]", RiskIdList[x]));
I'm using SQL Server, and this is the script I'm using in .cs file
Firstly, since you are using SQL Server, you cannot have a parameter named #RiskTypeId[x]. It is simply not a valid name.
Secondly, when passsing parameters from c# the parameter name has to include the initial "#".
Thirdly, as a general rule it is much better to construct the SQLParameter including the DataType. This means that there is no guesswork required, and guesses occasionally go wrong.
Fourthly, it seems to me that you want to iterate through an array. There is nothing to stop you doing this in c#. You would simply create a SqlParameter like this
command2.Parameters.Add(new SqlParameter { ParameterName = "#RiskTypeId", DBType = DBType.Int32, Value = RiskIDList[x] });
However I suspect that what you are really trying to do is to send a list to SQL Server. To achieve this, you need to use a User-Defined Table Type. In your case the following is sufficient:
CREATE TYPE [dbo].[IdList] AS TABLE ([Id] int);
Then you can define a parameter to your query as:
#RiskTypeIdList IdList READONLY
Finally you have to put the contents of your array into a DataTable. Because this is something I do frequently, I have a Class for this job:
public class TDataTable<int> : DataTable
{
public TDataTable(IEnumerable<T> ids) : base()
{
Columns.Add("Id", typeof(T));
var added = new HashSet<T>();
foreach (T id in ids)
{
//ensure unique values
if (added.Add(id))
{
Rows.Add(id);
}
}
}
public TDataTable() : base()
{
Columns.Add("Id", typeof(T));
}
}
You can then pass the parameter like this:
command2.Parameters.Add(new SqlParameter { ParameterName = "#RiskTypeIdList", SqlDBType = SqlDBType.Structured, Value = new TDataTable<int>(RiskIDList) });
Within your SQL you can then do what Thorsten suggested:
WHERE RiskTypeId NOT IN (SELECT Id FROM #RiskTypeIdList)
#Thorsten Kettner, its a valid SQL query, we can != in sql query.
command2.Parameters.Add(new SqlParameter("RiskIdList[x]", RiskIdList[x]));
We cannot pass "RiskIdList[x]" as SQL Parameter, we need to pass parameter like this "RiskIdList", RiskIdList[x] means that you are passing argument at runtime, but query or stored procedure has not dynamic parameter in where clause.
I have a list Called ListTypes that holds 10 types of products. Below the store procedure loops and gets every record with the product that is looping and it stores it in the list ListIds. This is killing my sql box since I have over 200 users executing this constantly all day.
I know is not a good architecture to loop a sql statement, but this the only way I made it work. Any ideas how I can make this without looping? Maybe a Linq statement, I never used Linq with this magnitude. Thank you.
protected void GetIds(string Type, string Sub)
{
LinkedIds.Clear();
using (SqlConnection cs = new SqlConnection(connstr))
{
for (int x = 0; x < ListTypes.Count; x++)
{
cs.Open();
SqlCommand select = new SqlCommand("spUI_LinkedIds", cs);
select.CommandType = System.Data.CommandType.StoredProcedure;
select.Parameters.AddWithValue("#Type", Type);
select.Parameters.AddWithValue("#Sub", Sub);
select.Parameters.AddWithValue("#TransId", ListTypes[x]);
SqlDataReader dr = select.ExecuteReader();
while (dr.Read())
{
ListIds.Add(Convert.ToInt32(dr["LinkedId"]));
}
cs.Close();
}
}
}
Not a full answer, but this wouldn't fit in a comment. You can at least update your existing code to be more efficient like this:
protected List<int> GetIds(string Type, string Sub, IEnumerable<int> types)
{
var result = new List<int>();
using (SqlConnection cs = new SqlConnection(connstr))
using (SqlCommand select = new SqlCommand("spUI_LinkedIds", cs))
{
select.CommandType = System.Data.CommandType.StoredProcedure;
//Don't use AddWithValue! Be explicit about your DB types
// I had to guess here. Replace with the actual types from your database
select.Parameters.Add("#Type", SqlDBType.VarChar, 10).Value = Type;
select.Parameters.Add("#Sub", SqlDbType.VarChar, 10).Value = Sub;
var TransID = select.Parameters.Add("#TransId", SqlDbType.Int);
cs.Open();
foreach(int type in types)
{
TransID.Value = type;
SqlDataReader dr = select.ExecuteReader();
while (dr.Read())
{
result.Add((int)dr["LinkedId"]);
}
}
}
return result;
}
Note that this way you only open and close the connection once. Normally in ADO.Net it's better to use a new connection and re-open it for each query. The exception is in a tight loop like this. Also, the only thing that changes inside the loop this way is the one parameter value. Finally, it's better to design methods that don't rely on other class state. This method no longer needs to know about the ListTypes and ListIds class variables, which makes it possible to (among other things) do better unit testing on the method.
Again, this isn't a full answer; it's just an incremental improvement. What you really need to do is write another stored procedure that accepts a table valued parameter, and build on the query from your existing stored procedure to JOIN with the table valued parameter, so that all of this will fit into a single SQL statement. But until you share your stored procedure code, this is about as much help as I can give you.
Besides the improvements others wrote.
You could insert your ID's into a temp table and then make one
SELECT * from WhatEverTable WHERE transid in (select transid from #tempTable)
On a MSSQL this works really fast.
When you're not using a MSSQL it could be possible that one great SQL-Select with joins is faster than a SELECT IN. You have to test these cases by your own on your DBMS.
According to your comment:
The idea is lets say I have a table and I have to get all records from the table that has this 10 types of products. How can I get all of this products? But this number is dynamic.
So... why use a stored procedure at all? Why not query the table?
//If [Type] and [Sub] arguments are external inputs - as in, they come from a user request or something - they should be sanitized. (remove or escape '\' and apostrophe signs)
//create connection
string queryTmpl = "SELECT LinkedId FROM [yourTable] WHERE [TYPE] = '{0}' AND [SUB] = '{1}' AND [TRANSID] IN ({2})";
string query = string.Format(queryTmpl, Type, Sub, string.Join(", ", ListTypes);
SqlCommand select = new SqlCommand(query, cs);
//and so forth
To use Linq-to-SQL you would need to map the table to a class. This would make the query simpler to perform.
I have an SQL statement that I'm executing through OleDb, the statement is something like this:
INSERT INTO mytable (name, dept) VALUES (#name, #dept);
I'm adding parameters to the OleDbCommand like this:
OleDbCommand Command = new OleDbCommand();
Command.Connection = Connection;
OleDbParameter Parameter1 = new OleDbParameter();
Parameter1.OleDbType = OleDbType.VarChar;
Parameter1.ParamterName = "#name";
Parameter1.Value = "Bob";
OleDbParameter Parameter2 = new OleDbParameter();
Parameter2.OleDbType = OleDbType.VarChar;
Parameter2.ParamterName = "#dept";
Parameter2.Value = "ADept";
Command.Parameters.Add(Parameter1);
Command.Parameters.Add(Parameter2);
The problem I've got is, if I add the parameters to command the other way round, then the columns are populated with the wrong values (i.e. name is in the dept column and vice versa)
Command.Parameters.Add(Parameter2);
Command.Parameters.Add(Parameter1);
My question is, what is the point of the parameter names if parameters values are just inserted into the table in the order they are added command? The parameter names seems redundant?
The Problem is that OleDb (and Odbc too) does not support named parameters.
It only supports what's called positional parameters.
In other words: The name you give a parameter when adding it to the commands parameters list does not matter. It's only used internally by the OleDbCommand class so it can distinguish and reference the parameters.
What matters is the order in which you add the parameters to the list. It must be the same order as the parameters are referenced in the SQL statement via the question mark character (?).
But here is a solution that allows you to use named parameters in the SQL statement. It basically replaces all parameter references in the SQL statement with question marks and reorders the parameters list accordingly.
It works the same way for the OdbcCommand class, you just need to replace "OleDb" with "Odbc" in the code.
Use the code like this:
command.CommandText = "SELECT * FROM Contact WHERE FirstName = #FirstName";
command.Parameters.AddWithValue("#FirstName", "Mike");
command.ConvertNamedParametersToPositionalParameters();
And here is the code
public static class OleDbCommandExtensions
{
public static void ConvertNamedParametersToPositionalParameters(this OleDbCommand command)
{
//1. Find all occurrences of parameter references in the SQL statement (such as #MyParameter).
//2. Find the corresponding parameter in the commands parameters list.
//3. Add the found parameter to the newParameters list and replace the parameter reference in the SQL with a question mark (?).
//4. Replace the commands parameters list with the newParameters list.
var newParameters = new List<OleDbParameter>();
command.CommandText = Regex.Replace(command.CommandText, "(#\\w*)", match =>
{
var parameter = command.Parameters.OfType<OleDbParameter>().FirstOrDefault(a => a.ParameterName == match.Groups[1].Value);
if (parameter != null)
{
var parameterIndex = newParameters.Count;
var newParameter = command.CreateParameter();
newParameter.OleDbType = parameter.OleDbType;
newParameter.ParameterName = "#parameter" + parameterIndex.ToString();
newParameter.Value = parameter.Value;
newParameters.Add(newParameter);
}
return "?";
});
command.Parameters.Clear();
command.Parameters.AddRange(newParameters.ToArray());
}
}
Parameter NAMES are generic in the SQL support system (i.e. not OleDb specific). Pretty much ONLY OleDb / Odbc do NOT use them. They are there because OleDb is a specific implementation of the generic base classes.
I have a sql query for my SelectCommand on my SqlDataSource. It looks like the following:
SELECT * FROM Books WHERE BookID = #BookID
A TextBox feeds the #BookID parameter using an Asp:ControlParameter.
When I view the SelectCommand when stepping through the code, I see this:
SELECT * FROM Books WHERE BookID = #BookID
What I want to actually see is that if the person types in 3 in the TextBox, I want to see
SELECT * FROM Books WHERE BookID = 3
I can't figure out how to access the above though?
One way to view the actual query is by using SQL Profiler.
The query is never executed as
SELECT * FROM Books WHERE BookID = 3
It's actually the parameterised query with the parameter passed.
You can do a "Find/Replace" on the query with the related parameters to see what it would look like.
(This answer presumes with the SqlClient implementation.)
No, you cannot see the executed sql code. The SqlCommand class calls sp_execute (see both SqlCommand.BuildExecute methods for the exact implementation) which separates the query from the parameters. You'll need to use Sql Profiler to see the exact query executed.
You could use the provided DbCommand (from the Selecting event) to parse your CommandText and replace the parameters with their actual values. This would need some logic for escaping, and it will not be the exact query that Sql Server executes.
Public Function GenSQLCmd(ByVal InSqlCmd As String, ByVal p As Data.Common.DbParameterCollection) As String
For Each x As Data.Common.DbParameter In p
InSqlCmd = Replace(InSqlCmd, x.ParameterName, x.Value.ToString)
Next
Return InSqlCmd
End Function
I guess you won't be able to see the select statement like you wish, since the parameter is not replaced in the statement with the value 3, but sent just like you wrote it to sql server (with the parameter).
That's actually good since it will prevent one to inject some malicious sql code in your textbox, for example.
Anyway, can't you retrieve the value passed to the parameter using this:
cmd.Parameters(0).Value
where cmd is your SqlCommand?
This is the C# version of Adam's answer
public string GenSQLCmd(string InSqlCmd, System.Data.Common.DbParameterCollection p) {
foreach (System.Data.Common.DbParameter x in p) {
InSqlCmd = InSqlCmd.Replace(x.ParameterName, "'" + x.Value.ToString() + "'");
}
return InSqlCmd;
}
Usage:
string DebugQuery = GenSQLCmd(cmd.CommandText, cmd.Parameters); //cmd is a SqlCommand instance
Yes, you can view that information but you need to do a bit coding for that.
Create an extension method called ToSqlStatement
public static class SqlExtensions
{
public static string ToSqlStatement(this IDbCommand cmd)
{
var keyValue = new List<string>();
foreach (SqlParameter param in cmd.Parameters)
{
var value = param.Value == null ? "NULL" : "'" + param.Value + "'";
keyValue.Add($"{param.ParameterName}={value}");
}
return $"{(cmd.CommandType == CommandType.StoredProcedure ? "exec " : string.Empty)}{cmd.CommandText} {string.Join(", ", keyValue)}";
}
}
Add OnSelecting event handler to SqlDataSource control on your page
In you code behind
protected void sqlDataSource_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
MyLogger.WriteLine(e.Command.ToSqlStatement());
}