Linq ExecuteCommand doesn't understand nulls - c#

I'm having a problem when passing nulls to a ExecuteCommand() method using linq. My code is similar to the one that follows:
public void InsertCostumer(string name, int age, string address)
{
List<object> myList = new List<object>();
myList.Add(name);
myList.Add(age);
myList.Add(address);
StringBuilder queryInsert = new StringBuilder();
queryInsert.Append("insert into Customers(name, address) values ({0}, {1}, {2})");
this.myDataContext.ExecuteCommand(queryInsert.ToString(), myList.ToArray());
}
But, when a parameter is null (address, for instance), I get the following error: "A query parameter cannot be of type 'System.Object'."
The error doesn't occur if no parameters are null. I know the design in my example is a little poor, I just created a simplified example to focus on the problem. Any suggestions?

This is a known bug and Microsoft does not intend to fix it...
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=305114&wa=wsignin1.0
The work around is to either:
Drop into ADO.NET and execute the SQL Command directly
Format the string you're executing yourself and call ExecuteCommand with an empty object array (new object[0])
The second isn't a good idea as it opens you up to SQL inject attacks, but its a quick hack.

Kevin is right.
an example of his work around #1 in LinqPad. Need this (Object)s??DBNull.Value
string s = null;
//ExecuteCommand("insert into T(C1) values({0})", s); //Exception
SqlCommand cmd= new SqlCommand(){
CommandText = "insert into T(C1) values(#P0)",
Connection = new SqlConnection(this.Connection.ConnectionString),
};
//cmd.Parameters.AddWithValue("#P0", s); //SqlException
cmd.Parameters.AddWithValue("#P0", (Object)s??DBNull.Value);
cmd.Connection.Open();
cmd.ExecuteNonQuery();
cmd.Connection.Close();
Ts.OrderByDescending(t=>t.ID).Take(1).Dump();

have you tried assigning a value to those that are null? Meaning (pseudo):
If address is null then address = ""
or
If age is < 0 then age = 0
then add it to myList
or you could always use a Ternary operator:
name = name.Length < 1 ? "" : name;
age = age < 1 ? Int32.MinValue : age;
then add it to myList

Same issue for me. So stupid of MS not to fix that.
Here's my solution although I did not support all parameter types but ya get the idea. I stuck this in the DataContext class so it looks like it's built in to Linq :) .
public int ExecuteCommandEx(string sCommand, params object[] parameters)
{
object[] newParams = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
if (parameters[i] == null)
newParams[i] = "NULL";
else if (parameters[i] is System.Guid || parameters[i] is System.String || parameters[i] is System.DateTime)
newParams[i] = string.Format("'{0}'", parameters[i]);
else if (parameters[i] is System.Int32 || parameters[i] is System.Int16)
newParams[i] = string.Format("{0}", parameters[i]);
else
{
string sNotSupportedMsg = string.Format("Type of param {0} not currently supported.", parameters[i].GetType());
System.Diagnostics.Debug.Assert(false, sNotSupportedMsg);
}
}
return ExecuteCommand(string.Format(sCommand, newParams));
}

I use something like this (note I'm using the SO "IDE" so I can't, guarantee this will compile or work correctly but you'll get the idea)
public void InsertCostumer(string name, int age, string address)
{
List<object> myList = new List<object>();
myList.Add(name);
myList.Add(age);
myList.Add(address);
StringBuilder queryInsert = new StringBuilder();
queryInsert.Append("insert into Customers(name, age, address) values (");
int i = 0;
foreach (var param in myList.ToArray())
{
if (param == null)
{
queryInsert.Append("null, ");
myList.RemoveAt(i);
}
else
{
queryInsert.Append("{" + i + "}, ");
i++;
}
}
queryInsert.Remove(queryInsert.Length - 2, 2);
queryInsert.Append(")");
this.myDataContext.ExecuteCommand(queryInsert.ToString(), myList.ToArray());
}

I made a generic ParamArray Function to pass in the parms I normally would pass into the ExecuteCommand. Then have it pass back the uninterpretted SQL parms and a list of objects actually passed in.
Public Sub CommitRecords(ByVal InItems As List(Of Item)
Dim db As New DataContext(ConnectionString)
Try
For Each oItem In InItems
With oItem
Dim strParms As String = ""
Dim collParms = BuildExecuteCommandParms(strParms, .MapValue1, .MapValue2, .MapValue3, .MapValue4, .MapValue5, .MapValue6)
db.ExecuteCommand("Insert Into ItemTable (Value1, Value2, Value3, Value4, Value5, Value6)" & vbCrLf & _
"Values (" & strParms & ")", _
collParms.ToArray)
End With
Next
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub
Public Function BuildExecuteCommandParms(ByRef strParms As String, ByVal ParamArray InParms As Object()) As List(Of Object)
Dim i As Integer = 0
Dim collOutParms As New List(Of Object)
For Each oParm In InParms
If i <> 0 Then strParms &= ", "
If oParm Is Nothing Then
strParms &= "NULL"
Else
strParms &= "{" & i & "}"
collOutParms.Add(oParm)
End If
i += 1
Next
Return collOutParms
End Function

I usually use this sort of thing, not ideal but it's gets it done if you're stuck
if (myObject != null)
{
foreach (var p in ct.GetType().GetProperties())
{
if (p.GetValue(myObject , null) == null)
{
if (p.PropertyType == typeof(string))
{
p.SetValue(myObject , "Empty", null);
}
if (p.PropertyType == typeof(int))
{
p.SetValue(myObject , 0, null);
}
if (p.PropertyType == typeof(int?))
{
p.SetValue(myObject , 0, null);
}
}
}
}
This makes sure each value in the object has a value before you use the parameters in ExecuteCommand. Again, not ideal, but it works.

I didn't like using string.format since (as the current selected answer to this question says) you're opening yourself to SQL injection.
So I solved the problem by iterating through the parameters and if the parameter is null, I add NULL as a string to the command text, if it it not null, I add a placeholder that will be replaced (similar to string.format) with values by ExecuteQuery (which does the SQL injection checks).
private static T ExecuteSingle<T>(string connectionString, string sprocName, params object[] sprocParameters)
where T : class
{
var commandText = sprocName;
if (sprocParameters.Length > 0)
{
// http://stackoverflow.com/questions/859985/linq-executecommand-doesnt-understand-nulls
int counter = 0;
var nulledPlaceholders = sprocParameters
.Select(x => x == null ? "NULL" : "{" + counter ++ + "}");
commandText += " " + string.Join(",", nulledPlaceholders);
sprocParameters = sprocParameters.Where(x => x != null).ToArray();
}
var connection = new SqlConnection(connectionString);
var dc = new DataContext(connection);
return dc.ExecuteQuery<T>(commandText, sprocParameters).SingleOrDefault();
}

internal static class DataContextExtensions
{
public static int ExecuteCommandEx(this DataContext context, string command, params object[] parameters)
{
if (context == null)
throw new ArgumentNullException("context");
if (parameters != null && parameters.Length > 0)
parameters = parameters.Select(p => p ?? "NULL").ToArray();
return context.ExecuteCommand(command, parameters);
}
}

why not use nullable values?
public void InsertCostumer(string? name, int? age, string? address)
{
List<object> myList = new List<object>();
myList.Add(name.GetValueOrDefault());
myList.Add(age.GetValueOrDefault());
myList.Add(address.GetValueOrDefault());
StringBuilder queryInsert = new StringBuilder();
queryInsert.Append("insert into Customers(name, address) values ({0}, {1}, {2})");
this.myDataContext.ExecuteCommand(queryInsert.ToString(), myList.ToArray());
}

In .NET, a null/nothing string does not evaluate to an empty string, i.e. "". If you want "", then that has to be the value of the string, or if you want to represent null/nothing in SQL, you have to manually write out "NULL" if your .NET string is in fact null/nothing.
All the execute command does, is execute a SQL query, provide by you, as a String. it doesn't do anything else special in terms of that SQL string.
So, for the Execute Command to work, you have to pass in a valid SQL string, you have to manually construct the string correctly.

Related

Trying to ignore null textboxes in Winforms to filter sql search query

Currently I'm trying keeping a persistent where clause and then appending additional filter by appending if each textbox is present or not. The problem is this is giving me a good command to work with but I still receive an error when trying to use that command. If there is any simplification or guidance it would be much appreciated!
if (string.IsNullOrEmpty(make) && (string.IsNullOrEmpty(model)) && (string.IsNullOrEmpty(color)) && (string.IsNullOrEmpty(min)) && (string.IsNullOrEmpty(max)) && (string.IsNullOrEmpty(miles)))
{
SqlCommand updateDataGridViewCmd = new SqlCommand("select m.make, m.model, car.price, color.color, car.mileage, carlot.lotid, car.pic from car join makemodel as m ON m.mmid = car.mmid join color ON car.colorid = color.colorid join carlot ON carlot.carid = car.carid; ", sqlCon);
dt.Load(updateDataGridViewCmd.ExecuteReader());
dataGridView1.DataSource = dt;
}
else
{
StringBuilder sqlCommandText = new StringBuilder();
sqlCommandText.Append("select m.make, m.model, car.price, color.color, car.mileage, carlot.lotid, car.pic from car join makemodel as m ON m.mmid = car.mmid join color ON car.colorid = color.colorid join carlot ON carlot.carid = car.carid where");
string CommandText = sqlCommandText.ToString();
SqlCommand updateDataGridViewCmd = new SqlCommand(CommandText, sqlCon);
updateDataGridViewCmd.Parameters.AddWithValue("#make", make);
updateDataGridViewCmd.Parameters.AddWithValue("#model", model);
updateDataGridViewCmd.Parameters.AddWithValue("#min", min);
updateDataGridViewCmd.Parameters.AddWithValue("#max", max);
updateDataGridViewCmd.Parameters.AddWithValue("#mileage", miles);
updateDataGridViewCmd.Parameters.AddWithValue("#color", color);
if (!string.IsNullOrEmpty(make))
{
sqlCommandText.Append(" m.make = #make");
CommandText = sqlCommandText.ToString();
}
if (!string.IsNullOrEmpty(model))
{
sqlCommandText.Append(" OR m.model = #model");
CommandText = sqlCommandText.ToString();
}
if (!string.IsNullOrEmpty(min))
{
sqlCommandText.Append(" car.price between #min");
CommandText = sqlCommandText.ToString();
if (!string.IsNullOrEmpty(max))
{
sqlCommandText.Append(" AND #max");
CommandText = sqlCommandText.ToString();
}
else
{
sqlCommandText.Append(",");
CommandText = sqlCommandText.ToString();
}
}
if (!string.IsNullOrEmpty(color))
{
sqlCommandText.Append(" color.color = #color,");
CommandText = sqlCommandText.ToString();
}
if (!string.IsNullOrEmpty(miles))
{
sqlCommandText.Append(" car.price <= #mileage");
CommandText = sqlCommandText.ToString();
}
sqlCommandText.Append(";");
CommandText = sqlCommandText.ToString();
dt.Load(updateDataGridViewCmd.ExecuteReader());
dataGridView1.DataSource = dt;
}
}
}
ERROR:
You may see an error because you are not joining your condition strings either with and or or.
If ORM is not an option here (which may be practical when composiong queries) you can end the base query with where 1=1 and then chain the other conditions with and x=... or or x=...
Also you can set CommandText = sqlCommandText.ToString(); only once, after all filters have been applied
Consider this being a pseudo-code. I wrote it from my head in this editor. Basically, instead of dealing with endless conditions, create an object that will group your conditions and then group the output of instances of these objects. It should build a perfect filter for you.
public class WhereToken
{
private List<SqlParameter> _localColl;
// privates declared here
privat bool _between;
public WhereToken (string col, object[] values, SqlDbType t, SqlParameterCollection paramColl)
{
// assign privates here
}
public WhereToken (string col, object value1, object value2, SqlDbType t, SqlParameterCollection paramColl)
{
// assign privates here
_between = true;
_values = new object[]{value1, value2};
}
public string Write()
{
if (values.Length = 0)
return null;
_localColl = new List<SqlParameter>();
var b = new StringBuilder();
b.Append("(");
for (int i = 0; l < _values.Length; i++)
{
var pName = string.Concat("#", col, i);
var p = new SqlParameter(pName, values[i], _sqlType);
b.Append(_col);
if (_between)
b.Append(" BETWEEN ");
else
b.Append("=");
b.Append(pName);
if (i < values.Length - 1)
{
if (_between)
b.Append(" AND ");
else
b.Append(" OR ");
}
}
b.Append(")");
foreach(var pp in _localColl)
_paramColl.Parameters.Add(pp);
return b.ToString();
}
}
// In code
var tokens = new List<WhereToken>();
var wt1 = new WhereToken("make", new string[]{txt.Make.Text}, SqlDbType.NVarchar, sqlCommand.ParameterCollection);
tokens.Add(w1);
// in real life, you need to check if both values present, etc
var wt2 = new WhereToken("year", Convert.ToInt32(txt.YearMin.Text), Convert.ToInt32(txt.YearMax.Text), SqlDbType.Int, sqlCommand.ParameterCollection);
tokens.Add(w2);
// more items here . . . .
var builder = new StringBuilder(yourMainSQLSelect);
builder.Append(" WHERE 1=1"); // 1=1 what if the tokens don't generate
for (int i = 0; i < tokens.Length; i++)
{
string token = tokens[i].Write();
if (token == null) continue;
builder.Append(" AND ");
builder.Append(token);
}
sqlCommand.CommandText = builder.ToString();
This code ^^ should build you something like
... where 1=1 and make=#make0 and (year BETWEEN #year0 AND #year1)
Of course, you can improve built-in logic on how to parse and output from WhereToken. But this is the concept for you - parse small tokens then join them. Don't try build logic for all input fields
It seems to me, that you want to expand your WHERE with predicates if some input string are null or empty.
Let's ignore empty string, so I won't have to write: is null or empty string
SELECT ... FROM TABLECUSTOMERS WHERE <predicate1> OR <predicate2> OR ...
The predicates that you add depend on which input strings are null. It seems to me that most of the times, if the input string is null, you want to omit the predicate. Something like this:
if (inputStringName != null)
{
strBuilder.Append("OR CustomerName = #Name")
}
This is the same as:
OR (#Name != null AND #Name == CustomerName)
And if you want to use AND instead of OR:
AND (#Name == null OR #Name == CustomerName)
So my advice would be: make one SQL command, that contains all possible variables, and change the command such that the predicate also testing whether the parameter equals null
SELECT ... FROM car
JOIN ...
WHERE ( NOT #Make = NULL AND #Make = m.Make)
OR ( NOT #Model = NULL AND #Model = m.Model)
OR ( NOT #Mileage = NULL AND #Mileage > car.Mileage)
So just before you use the parameter you check whether the parameter is null or not. Depending on whether you want to append with AND or OR use something like:
OR (NOT #Value = NULL AND #Value = car.MyValue)
AND (#Value = NULL OR #Value = car.MyValue)
My SQL is a bit rusty, I use entity framework all the time, so I am a bit uncertain about the NOT =, maybe this should be !=, and maybe there should be more parentheses, but I guess you get the gist.
To prevent to have to check for Empty strings: make empty strings null, before you AddWithValue

Regex to remove escape characters (specific ones) in C#

The regex below is not what I exactly need:
Regex.Replace(value.ToString(), "[^0-9a-zA-Z]+", "")
I need to remove escape characters from my string because I am creating one SQL with string and when I have this character ' or this \r\n etc. my Sql generates an error, I cannot use : SqlParameter in this case as I just have a list of SQLs in string, but I can remove the characters that I don't want.
So, I only need to remove these characters:
\r \n ' /\
Added my codes as requested:
private static string ConvertWhetherUsesComas(object value)
{
// formats with comas or not
if (value is String)
{
// fix problem with break characters such as \/`'
value = String.Format("'{0}'", Regex.Replace(value.ToString(), "[^0-9a-zA-Z]+", ""));
}
else if (value is DateTime)
{
value = String.Format("'{0}'", value.SafeToDateTime(null).Value.ToString("yyyy-MM-dd hh:mm:ss tt"));
}
else if (value == null)
{
value = "NULL";
}
else if (value is Boolean)
{
value = value.SafeToBool(false) == false ? 0 : 1;
}
return value.ToString();
}
private static List<String> ConvertDiferencesToSql<T>(Differences<T> differences, string tableName, string primaryKey) where T : IHasId<int>
{
var result = new List<String>();
differences.New.ToList().ForEach(newItem =>
{
var fieldNames = new StringBuilder();
var fieldValues = new StringBuilder();
var properties = newItem.GetType().GetProperties().ToList();
properties.ForEach(f =>
{
var propertyName = f.Name.ToUpper() == "ID" ? primaryKey : f.Name;
var propertyValue = ConvertWhetherUsesComas(f.GetValue(newItem));
if (propertyValue == "NULL") return; // ignores null values
fieldNames.AppendFormat("{0},", propertyName);
fieldValues.AppendFormat("{0},", propertyValue);
});
var sqlFields = fieldNames.ToString(0, fieldNames.Length - 1);
var sqlValues = fieldValues.ToString(0, fieldValues.Length - 1);
result.Add(String.Format("INSERT INTO {0} ({1}) VALUES ({2});", tableName, sqlFields, sqlValues));
});
differences.Changed.ForEach(changedRecord =>
{
var fields = new StringBuilder();
changedRecord.ChangedFields.ForEach(changedField =>
{
var propertyName = changedField.Property == "ID" ? primaryKey : changedField.Property;
var propertyValue = ConvertWhetherUsesComas(changedField.NewValue);
fields.AppendFormat("{0}={1},", propertyName, propertyValue);
});
var sqlFields = fields.ToString(0, fields.Length - 1);
result.Add(String.Format("UPDATE {0} SET {1} WHERE {2}={3};", tableName, sqlFields, primaryKey, changedRecord.Id));
});
differences.Deleted.ForEach(deletedItem => result.Add(String.Format("DELETE FROM {0} WHERE {1}={2};", tableName, primaryKey, deletedItem.GetId())));
return result;
}
You can place these characters into a character class, and replace with string.Empty:
var rgx4 = new Regex(#"[\r\n'/\\]");
var tst = "\r \n ' /\\";
tst = rgx4.Replace(tst, string.Empty);
Result:
A character class usually executes faster, as with alternative list, there is a lot of back-tracking impeding performance.
If I understood correctly, you want something like this :
Regex.Replace(value.ToString(), "(\\\\n|\\\\r|'|\\/\\\\)+", "")
See here.

generic helper to get ObjectResult<T> that will match any given EntityObject

trying to make a generic SQL Query via a method that will fetch data i was making this code
public ObjectResult<theSelectedTableNameModel> getUsersRes(string sqlStr, string ColNameAsfilter, string FiltersValue,string OrderByFilter="")
{
string SqlCmd = "";
string By = "";
if (OrderByFilter.isNotEmptyOrNull())
By = string.Concat(" ORDER BY ", OrderByFilter);
SqlCmd = string.Format("{0} WHERE {1}={2}{3}", SqlStr, ColNameAsfilter, FiltersValue, By);
return anEntityName.ExecuteStoreQuery<theSelectedTableNameModel>(SqlCmd);
}
i have copied my code and edited real names and other variables /parameters so i might have made a mistake, but the question is , how could i make it more generic than this ?
this is a working approach that lets me specify the query of the sqlCommand
i wanted it to fit any entity and any model/object/table
how could it be done ?
i guess there's a ready solution for this or the engeniring of EF not ment to be generic...
i'm using asp.net 4.0 , and latest EF..
I think you need something like this
public List<T> Get(System.Linq.Expressions.Expression<Func<T, bool>> filter = null)
{
IQueryable<T> query = dbSet.AsQueryable();
if (filter != null)
{
query = query.Where(filter);
}
return query.ToList();
}
Follow this link
why not consider passing in a collection of sql paramters in to the function call as a parameter. Once you do this, you can use any number of filters of multiple types
public ObjectResult<theSelectedTableNameModel> GetEntityBySQLCommand(string sqlStr, SqlParameter[] filterParams, string OrderByFilter="")
{
var commandTextToExecute = OrderByFilter.isNotEmptyOrNull() ? sqlStr : string.Format(“{0} Order By {1}”, sqlStr, OrderByFilter);
return yourObjectContext.ExecuteStoreQuery<theSelectedTableNameModel>(commandTextToExecute, filterParams);
}
Your sqlStr should look something like the following “SELECT * FROM Customer WHERE CustId = #custID and LastActiveOn = #lastActiveDate”;
custID and lastActiveDate should be passed in via the SqlParameter collection (filterParams)
after some time of testing every possible option, while i did not find any similar code on the net..
this is my solution , as an extension method, did i conduct a bad search/research ? or just no one use it like this , although the extesion is for the type ObjectContext it does return an ObjectResult
list of optional parameters for where clause + another for order by and it does the work
string firstPartSql="SELECT * FROM YourTblName";
list of string for WHERE filter will contain elements like
"columnName=value", "columnName Like '%val%'"
list of string for Order by will contain elements like
"ColumnName" ,"ColumnName DESC"
public static ObjectResult<T> getUsersResStatic<T>(this ObjectContext Entt, string sqlBeginhing, List<string> LstSelectWhersFilter = null, List<string> LstOby = null)
{
string SqlCmd = "";
string StrWhereFilterPrefix = "";
string StrFinalWHEREFiter ="";
string StrKeyOb1 = "";
string StrKeyOb2 = "";
string StrFinalOrderBy = "";
if (LstSelectWhersFilter != null)
{
StrWhereFilterPrefix = "WHERE";
for (int CountWhers = 0; CountWhers < LstSelectWhersFilter.Count; CountWhers++)
{
if (CountWhers == 0) StrFinalWHEREFiter = string.Format(" {0} {1} ", StrWhereFilterPrefix, LstSelectWhersFilter.ElementAt(CountWhers));
else
{
StrWhereFilterPrefix = "AND";
StrFinalWHEREFiter += string.Format(" {0} {1} ", StrWhereFilterPrefix, LstSelectWhersFilter.ElementAt(CountWhers));
}
}
}
if (LstOby != null)
{
StrKeyOb1 = "ORDER BY";
if (LstOby.Count > 1)
{
StrKeyOb2 = ",";
for (int i = 0; i < LstOby.Count; i++)
{
if (i == 0) StrFinalOrderBy = string.Format("{0} {1}", StrKeyOb1, LstOby.ElementAt(i));
else
StrFinalOrderBy += string.Format("{0} {1}", StrKeyOb2, LstOby.ElementAt(i));
}
}
else StrFinalOrderBy = string.Format("{0} {1}", StrKeyOb1, LstOby.ElementAt(0));
SqlCmd = string.Format("{0} {1} {2}", sqlBeginhing, StrFinalWHEREFiter, StrFinalOrderBy);//StrKeyOb2, ob2);
}
if (LstSelectWhersFilter == null && LstOby == null) SqlCmd = sqlBeginhing;
return Entt.ExecuteStoreQuery<T>(SqlCmd);
}
any kind of comment will be helpfull (well...almost any)

Wrong number of parameters (parametrized query)

Q:
When i try to execute the following parametrized query:
INSERT INTO days (day,short,name,depcode,studycode,batchnum) values (?,?,?,?,?,?);SELECT SCOPE_IDENTITY();
through command.ExecuteScalar();
throws the following exception:
ERROR [07001] [Informix .NET provider]Wrong number of parameters.
Where is the problem?
EDIT:
public static int InsertDays(List<Day> days)
{
int affectedRow = -1;
Dictionary<string, string> daysParameter = new Dictionary<string, string>();
try
{
foreach (Day a in days)
{
daysParameter.Add("day", a.DayId.ToString());
daysParameter.Add("short", a.ShortName);
daysParameter.Add("name", a.Name);
daysParameter.Add("depcode", a.DepCode.ToString());
daysParameter.Add("studycode", a.StudyCode.ToString());
daysParameter.Add("batchnum", a.BatchNum.ToString());
affectedRow = DBUtilities.InsertEntity_Return_ID("days", daysParameter);
daysParameter.Clear();
if (affectedRow < 0)
{
break;
}
}
}
catch (Exception ee)
{
string message = ee.Message;
}
return affectedRow;
}
public static int InsertEntity_Return_ID(string tblName, Dictionary<string, string> dtParams)
{
int Result = -1;
DBConnectionForInformix DAL_Helper = new DBConnectionForInformix("");
string[] field_names = new string[dtParams.Count];
dtParams.Keys.CopyTo(field_names, 0);
string[] field_values = new string[dtParams.Count];
string[] field_valuesParam = new string[dtParams.Count];
dtParams.Values.CopyTo(field_values, 0);
for (int i = 0; i < field_names.Length; i++)
{
field_valuesParam[i] = "?";
}
string insertCmd = #"INSERT INTO " + tblName + " (" + string.Join(",", field_names) + ") values (" + string.Join(",", field_valuesParam) + ");SELECT SCOPE_IDENTITY();";
Result = int.Parse(DAL_Helper.Return_Scalar(insertCmd));
return Result;
}
You haven't shown where you're actually populating the parameter values. Given that you've got the right number of question marks, I suspect that's where the problem lies.
EDIT: Okay, now you've posted more code, it's obvious what's going wrong: your Return_Scalar method isn't accepting any actual values! You're not using field_values anywhere after populating it. You need to set the parameters in the command.
(You should also look at .NET naming conventions, by the way...)
Ensure that where you are providing the parameters that one of the values is not null. That may cause the provider to ignore the parameter. If this is your issue pass DBNull.
EDIT
As Jon stated you need to use command.Parameters to give the command the parameters to use in the query.

Can ODBC parameter place holders be named?

I did some searching and haven't found a definitive answer to my questions.
Is there a way to define which ? in a SQL query belongs to which parameter?
For example, I need to perform something like this:
SELECT * FROM myTable WHERE myField = #Param1 OR myField2 = #Param1
OR myField1 = #Param2 OR myField2 = #Param2
The same query in ODBC is:
SELECT * FROM myTable WHERE myField = ? or myField2 = ? or myField1 = ?
or myField2 = ?
Is there a way to tell the ODBC command which parameter is which besides loading parameters in twice for each value?
I suspect there isn't but could use perspective from more experienced ODBC programmers.
EDIT : The ODBC driver I'm using is a BBj ODBC Driver.
In MSDN it is explicitly stated that you cannot name the parameters which is the only way to "tell the ODBC command which parameter is which".
Although the documentation can generate a bit of confusion:
From MSDN, OdbcParameter Class:
When CommandType is set to Text, the .NET Framework Data Provider for ODBC does not support passing named parameters to an SQL statement or to a stored procedure called by an OdbcCommand. In either of these cases, use the question mark (?) placeholder.
The order in which OdbcParameter objects are added to the OdbcParameterCollection must directly correspond to the position of the question mark placeholder for the parameter in the command text.
From the above it seems to suggest that when CommandType is not set to Text maybe you can use named parameters, but unfortunately you can't:
From MSDN, OdbcCommand.CommandType Property:
When the CommandType property is set to StoredProcedure, you should set the CommandText property to the full ODBC call syntax. The command then executes this stored procedure when you call one of the Execute methods (for example, ExecuteReader or ExecuteNonQuery).
The .NET Framework Data Provider for ODBC does not support passing named parameters to an SQL statement or to a stored procedure called by an OdbcCommand. In either of these cases, use the question mark (?) placeholder...
I couldn't get it to use the named parameters - only positional parameters.
You can add all the parameters you want like below, but you have to add the values in order.
SELECT * FROM myTable WHERE myField = ? or myField1 = ? or myField2 = ?
or myField2 = ?
myOdbcCommand.Parameters.AddWithValue("DoesNotMatter", val1); //myField
myOdbcCommand.Parameters.AddWithValue("WhatYouPutHere", val2); //myField1
myOdbcCommand.Parameters.AddWithValue("DoesNotMatter", val3); //myField2
myOdbcCommand.Parameters.AddWithValue("WhatYouPutHere", val4); //myField2
As you can see from the above, the parameter names don't matter and aren't used. You can even name them all the same if you want or better yet, leave the param names empty "".
Thank you Tom for your Idea and your code.
However the code was not working correctly in my test.
So I have written a simpler (and at least in my tests working) solution to replace named parameters with positional parameters (where ? is used instead of the name):
public static class OdbcCommandExtensions
{
public static void ConvertNamedParametersToPositionalParameters(this OdbcCommand command)
{
//1. Find all occurrences of parameters references in the SQL statement (such as #MyParameter).
//2. For each occurrence find the corresponding parameter in the command's parameters list.
//3. Add the parameter to the newParameters list and replace the parameter reference in the SQL with a question mark (?).
//4. Replace the command's parameters list with the newParameters list.
var newParameters = new List<OdbcParameter>();
command.CommandText = Regex.Replace(command.CommandText, "(#\\w*)", match =>
{
var parameter = command.Parameters.OfType<OdbcParameter>().FirstOrDefault(a => a.ParameterName == match.Groups[1].Value);
if (parameter != null)
{
var parameterIndex = newParameters.Count;
var newParameter = command.CreateParameter();
newParameter.OdbcType = parameter.OdbcType;
newParameter.ParameterName = "#parameter" + parameterIndex.ToString();
newParameter.Value = parameter.Value;
newParameters.Add(newParameter);
}
return "?";
});
command.Parameters.Clear();
command.Parameters.AddRange(newParameters.ToArray());
}
}
I know that when using Oracle Rdb ODBC, I cannot use place holders name and have to use '?'; which I find extremely annoying.
I’ve had a need to write code that handles converting named parameters to ordinal parameters with the question mark. My need was with OleDb instead of Odbc… but I’m sure this would work for you if you change the types.
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Text.RegularExpressions;
namespace OleDbParameterFix {
static class Program {
[STAThread]
static void Main() {
string connectionString = #"provider=vfpoledb;data source=data\northwind.dbc";
using (var connection = new OleDbConnection(connectionString))
using (var command = connection.CreateCommand()) {
command.CommandText = "select count(*) from orders where orderdate=#date or requireddate=#date or shippeddate=#date";
command.Parameters.Add("date", new DateTime(1996, 7, 11));
connection.Open();
OleDbParameterRewritter.Rewrite(command);
var count = command.ExecuteScalar();
connection.Close();
}
}
}
public class OleDbParameterRewritter {
public static void Rewrite(OleDbCommand command) {
HandleMultipleParameterReferences(command);
ReplaceParameterNamesWithQuestionMark(command);
}
private static void HandleMultipleParameterReferences(OleDbCommand command) {
var parameterMatches = command.Parameters
.Cast<OleDbParameter>()
.Select(x => Regex.Matches(command.CommandText, "#" + x.ParameterName))
.ToList();
// Check to see if any of the parameters are listed multiple times in the command text.
if (parameterMatches.Any(x => x.Count > 1)) {
var newParameters = new List<OleDbParameter>();
// order by descending to make the parameter name replacing easy
var matches = parameterMatches.SelectMany(x => x.Cast<Match>())
.OrderByDescending(x => x.Index);
foreach (Match match in matches) {
// Substring removed the # prefix.
var parameterName = match.Value.Substring(1);
// Add index to the name to make the parameter name unique.
var newParameterName = parameterName + "_" + match.Index;
var newParameter = (OleDbParameter)((ICloneable)command.Parameters[parameterName]).Clone();
newParameter.ParameterName = newParameterName;
newParameters.Add(newParameter);
// Replace the old parameter name with the new parameter name.
command.CommandText = command.CommandText.Substring(0, match.Index)
+ "#" + newParameterName
+ command.CommandText.Substring(match.Index + match.Length);
}
// The parameters were added to the list in the reverse order to make parameter name replacing easy.
newParameters.Reverse();
command.Parameters.Clear();
newParameters.ForEach(x => command.Parameters.Add(x));
}
}
private static void ReplaceParameterNamesWithQuestionMark(OleDbCommand command) {
for (int index = command.Parameters.Count - 1; index >= 0; index--) {
var p = command.Parameters[index];
command.CommandText = command.CommandText.Replace("#" + p.ParameterName, "?");
}
}
}
}
Here is a short solution to the post: https://stackoverflow.com/a/21925683/2935383
I've wrote this code for an OpenEdge (Progress) ODBC wrapper. The DatabaseAdapter-class is this wrapper and will not shown here.
string _convertSql( string queryString, List<DatabaseAdapter.Parameter> parameters,
ref List<System.Data.Odbc.OdbcParameter> odbcParameters ) {
List<ParamSorter> sorter = new List<ParamSorter>();
foreach (DatabaseAdapter.Parameters item in parameters) {
string parameterName = item.ParameterName;
int indexSpace = queryString.IndexOf(paramName + " "); // 0
int indexComma = queryString.IndexOf(paramName + ","); // 1
if (indexSpace > -1){
sorter.Add(new ParamSorter() { p = item, index = indexSpace, type = 0 });
}
else {
sorter.Add(new ParamSorter() { p = item, index = indexComma, type = 1 });
}
}
odbcParameters = new List<System.Data.Odbc.OdbcParameter>();
foreach (ParamSorter item in sorter.OrderBy(x => x.index)) {
if (item.type == 0) { //SPACE
queryString = queryString.Replace(item.p.ParameterName + " ", "? ");
}
else { //COMMA
queryString = queryString.Replace(item.p.ParameterName + ",", "?,");
}
odbcParameters.Add(
new System.Data.Odbc.OdbcParameter(item.p.ParameterName, item.p.Value));
}
}
Utility class for sorting
class ParamSorter{
public DatabaseAdapter.Parameters p;
public int index;
public int type;
}
If the named parameter the last in string - you have to add a whitespace.
e.g. "SELECT * FROM tab WHERE col = #mycol" must "SELECT * FROM tab WHERE col = #mycol "
I altered answer provided by David Liebeherr to come up code below.
It allows for Select ##Identity as mentioned by mfeineis.
public static IDbCommand ReplaceCommndTextAndParameters(this IDbCommand command, string commandText, List<IDbDataParameter> parameters) {
command.CommandText = commandText;
command.Parameters.Clear();
foreach (var p in parameters) {
command.Parameters.Add(p);
}
return command;
}
public static IDbCommand ConvertNamedParametersToPositionalParameters(this IDbCommand command) {
var newCommand = command.GetConvertNamedParametersToPositionalParameters();
return command.ReplaceCommndTextAndParameters(newCommand.CommandText, newCommand.Parameters);
}
public static (string CommandText, List<IDbDataParameter> Parameters) GetConvertNamedParametersToPositionalParameters(this IDbCommand command) {
//1. Find all occurrences parameters references in the SQL statement (such as #MyParameter).
//2. Find the corresponding parameter in the command's 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 command's parameters list with the newParameters list.
var oldParameters = command.Parameters;
var oldCommandText = command.CommandText;
var newParameters = new List<IDbDataParameter>();
var newCommandText = oldCommandText;
var paramNames = oldCommandText.Replace("##", "??").Split('#').Select(x => x.Split(new[] { ' ', ')', ';', '\r', '\n' }).FirstOrDefault().Trim()).ToList().Skip(1);
foreach (var p in paramNames) {
newCommandText = newCommandText.Replace("#" + p, "?");
var parameter = oldParameters.OfType<IDbDataParameter>().FirstOrDefault(a => a.ParameterName == p);
if (parameter != null) {
parameter.ParameterName = $"{parameter.ParameterName}_{newParameters.Count}";
newParameters.Add(parameter);
}
}
return (newCommandText, newParameters);
}

Categories

Resources