I am creating a small helper function to return a DataTable. I would like to work across all providers that ADO.Net supports, so I thought about making everything use IDbCommand or DbCommand where possible.
I have reached a stumbling block with the following code:
private static DataTable QueryImpl(ref IDbConnection conn, String SqlToExecute, CommandType CommandType, Array Parameters)
{
SetupConnection(ref conn);
// set the capacity to 20 so the first 20 allocations are quicker...
DataTable dt = new DataTable();
using (IDbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = SqlToExecute;
cmd.CommandType = CommandType;
if (Parameters != null && Parameters.Length > 0)
{
for (Int32 i = 0; i < Parameters.Length; i++)
{
cmd.Parameters.Add(Parameters.GetValue(i));
}
}
dt.Load(cmd.ExecuteReader(), LoadOption.OverwriteChanges);
}
return dt;
}
When this code is executed, I receive an InvalidCastException which states the following:
The SqlParameterCollection only accepts non-null SqlParameter type objects, not String objects.
The code falls over on the line:
cmd.Parameters.Add(Parameters.GetValue(i));
Any ideas?
Any improvements to the above code is appreciated.
Actual solution:
private static readonly Regex regParameters = new Regex(#"#\w+", RegexOptions.Compiled);
private static DataTable QueryImpl(ref DbConnection conn, String SqlToExecute, CommandType CommandType, Object[] Parameters)
{
SetupConnection(ref conn);
DataTable dt = new DataTable();
using (DbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = SqlToExecute;
cmd.CommandType = CommandType;
if (Parameters != null && Parameters.Length > 0)
{
MatchCollection cmdParams = regParameters.Matches(cmd.CommandText);
List<String> param = new List<String>();
foreach (var el in cmdParams)
{
if (!param.Contains(el.ToString()))
{
param.Add(el.ToString());
}
}
Int32 i = 0;
IDbDataParameter dp;
foreach (String el in param)
{
dp = cmd.CreateParameter();
dp.ParameterName = el;
dp.Value = Parameters[i++];
cmd.Parameters.Add(dp);
}
}
dt.Load(cmd.ExecuteReader(), LoadOption.OverwriteChanges);
}
return dt;
}
Thanks for ideas/links etc. :)
I believe IDbCommand has a CreateParameter() method:
var parameter = command.CreateParameter();
parameter.ParameterName = "#SomeName";
parameter.Value = 1;
command.Parameters.Add(parameter);
You could add the code of the accepted answer to an extension method:
public static class DbCommandExtensionMethods
{
public static void AddParameter (this IDbCommand command, string name, object value)
{
var parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Value = value;
command.Parameters.Add(parameter);
}
}
I know it's not what you're asking, but I have a much simpler and more robust solution to offer.
The Microsoft Patterns and Practices library includes a Data Access Application block that is incredibly powerful and easy to use. A sample for executing a stored procedure and returning a dataset is shown below from our actual code:
object[] ParameterValues = new object[] {"1",DateTime.Now, 12, "Completed", txtNotes.Text};
Database db = DatabaseFactory.CreateDatabase("ConnectionStringName");
DataSet ds = = db.ExecuteDataSet("StoredProcName", ParameterValues);
It doesn't matter if the Connection is OleDb, ODBC, etc. The ConnectionStringName in the first line of code is just the name of the Consternating as defined in the .config file. You pass in a Connection String name, stored proc name, and an array of objects, which make up the parameters.
This is just one of the many sweet functions available.
You'll get everything you're trying to build and then some.
The official site is here: http://msdn.microsoft.com/en-us/library/ff648951.aspx
To save you some searching, the Data classes documentation are found here: http://msdn.microsoft.com/en-us/library/microsoft.practices.enterpriselibrary.data(PandP.50).aspx
(and it's free from Microsoft, and updated regularly.)
This answer is intended for slightly more specific purpose than what you're doing, but building on #Dismissile's answer, I used a Dictionary to supply the parameter name and value to a foreach loop in my personal project.
using( IDbCommand dbCommand = dbConnection.CreateCommand() )
{
dbCommand.CommandText = Properties.Settings.Default.UpdateCommand;
Dictionary<string,object> values = new Dictionary<string,object>()
{
{"#param1",this.Property1},
{"#param2",this.Property2},
// ...
};
foreach( var item in values )
{
var p = dbCommand.CreateParameter();
p.ParameterName = item.Key;
p.Value = item.Value;
dbCommand.Parameters.Add(p);
}
}
Your Parameters parameter needs to be of type IDataParameter[] and, given the error text, the concrete implementation needs be a SqlParameter[] type.
If you wish to keep your signature, you'll need a factory to derive the necessary concrete implementation.
Add using System.Data.SqlClient; and
cmd.Parameters.Add(new SqlParameter("#parameterName", value));
Related
I'm developing a server/client application in C#. In the earlier phases of development I was writing SQL codes each time and it caused spaghetti code. Now I'm trying to make it clean. My question is: How can I write a general query generator function with dynamic parameters?
private void button3_Click(object sender, EventArgs e)
{
try
{
SqlCommand cmd= new SqlCommand();
cmd.CommandText = "INSERT INTO tbl(line, name, firstvalue, secondvalue)" +
"VALUES(#line, #name, #value, #secondvalue)";
cmd.Parameters.AddWithValue("#line", comboBox4.Text);
cmd.Parameters.AddWithValue("#name", textBox2.Text);
cmd.Parameters.AddWithValue("#value", comboBox5.Text);
cmd.Parameters.AddWithValue("#secondvalue", comboBox6.Text);
cmd.Connection = Db.Db;
cmd.CommandType = CommandType.Text;
SqlDataReader dr = cmd.ExecuteReader();
MessageBox.Show("Saved");
}
catch (Exception ex)
{
MessageBox.Show(ex);
}
finally
{
Db.Close();
}
}
But I want to convert it into:
public void query(string commandText, params string[] parameters)
{
SqlCommand command = new SqlCommand();
command.CommandText = commandText;
foreach (var parameter in parameters)
//In this part, there can be lots of different queries and how can assign each parameter to relevant values
//Is there any change to assign them in order.
}
Well, if you insist on implementing such a routine (usually we use ORM) you have to parse the commandText; the simplest (but not the best) implementation is regular expressions (we Match parameter name within commandText, then Zip it with its value from parameters):
using System.Linq;
using System.Text.RegularExpressions;
...
public void query(string commandText, params string[] parameters) {
using (SqlCommand command = new SqlCommand()) {
command.Connection = myConnection; //TODO: put the right connection here
command.CommandText = commandText;
var prms = Regex
.Matches(commandText, #"\b#[A-Za-z_][A-Za-z_0-9]*\b")
.Cast<Match>()
.Zip(parameters, (match, value) => new {
name = match.Value,
value
});
foreach(var prm in prms)
command.Parameters.AddWithValue(prm.name, prm.value);
// Just execute; we have nothing to read (ExecuteReader)
command.ExecuteNonQuery();
}
}
Edit: If you want / ready to specify parameters' names, not only values you can try Tuples: for c# 7.0+
public void query(string commandText, params (string, object)[] parameters) {
...
foreach (var prm in parameters)
command.Parameters.AddWithValue(prm.Item1, prm.Item2);
...
}
usage
query(sql,
("#line", comboBox4.Text),
("#name", textBox2.Text),
("#id", 123), // please, note integer value
("#money", 456.78d),
...
);
for C# 6.0-:
public void query(string commandText, params Tuple<string, object>[] parameters) {
...
foreach (var prm in parameters)
command.Parameters.AddWithValue(prm.Item1, prm.Item2);
...
}
...
query(sql,
Tuple.Create("#line", comboBox4.Text),
Tuple.Create("#name", textBox2.Text),
...
);
Create an object with database parameters and set all values in it. Use Entity framework to do the rest or read from object when assigning.
I solved it and here is the solution. We will store them in the same array. So the parameters array will be:
parameters[0] = "#line"
parameters[1] = line
parameters[2] = "#name"
parameters[3] = name
and while posting them into AddWithValue() function
for (int i = 0; i < parameters.Length;)
{
command.Parameters.AddWithValue(parameters[i++] as string,parameters[i++]);
}
and we should call the query function when needed like this,
string commandText = "INSERT INTO tbl(line, name, firstvalue, secondvalue)" +
"VALUES(#line, #name, #value, #secondvalue)";
query("#line", line, "#name", name, "#value", firstvalue, "secondvalue", secondvalue);
Function Body
public class Database
{
public string executeScaler(string commandText, bool isStoredProcedure, Dictionary<string, object> dictParams = null)
{
string result = "";
DataTable dt = new DataTable();
SqlConnection con = ConnectionStrings.GetConnection();
SqlCommand cmd = new SqlCommand(commandText, con);
if (isStoredProcedure)
cmd.CommandType = CommandType.StoredProcedure;
if (dictParams != null)
foreach (KeyValuePair<string, object> rec in dictParams)
cmd.Parameters.AddWithValue("#" + rec.Key, rec.Value);
try
{
result = Convert.ToString(cmd.ExecuteScalar());
}
catch (Exception ex)
{
throw ex;
}
finally
{
con.Close();
con.Dispose();
}
return result;
}
}
Calling Function
Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("Email", obj.Email);
dict.Add("Password", obj.Password);
Database objDB = new Database();
objDB.executeScaler("RegisterAccount", true, dict);
I'm trying to fill a DataSet object with an OracleCommand object, I just started using Oracle, I've using SQL Server all my life.
On my NET console project I added the NuGet Oracle.ManagedDataAccess package and changed Sql objects to Oracle. Like: SqlConnection to OracleConnection.
In SQL Server it works perfectly, but in Oracle it gives me this error I don't really understand what it means.
I changed SqlConnection to OracleConnection
and SqlCommand to OracleCommand
also
command.Parameters.AddWithValue("#" + parameters[i], values[i] ?? DBNull.Value);
to
command.Parameters.Add(parameters[i], values[i] ?? DBNull.Value);
command.Parameters[i].Value = values[i];
Because AddWithValue doesn't exist on OracleCommand
This is the method that gets the data from the db.
private void GetData(string storedProcedure, IReadOnlyList<string> parameters, IReadOnlyList<object> values)
{
using (var connection = new OracleConnection(_connectionString))
{
using (
var command = new OracleCommand(storedProcedure, connection)
{
CommandType = CommandType.StoredProcedure
})
{
if (parameters != null)
for (var i = 0; i < parameters.Count; i++)
{
command.Parameters.Add(parameters[i], values[i] ?? DBNull.Value);
}
var ds = new DataSet();
connection.Open();
new OracleDataAdapter(command).Fill(ds);
_data = ds.Tables;
connection.Close();
}
}
}
These are the parameters im using.
var db = new Connector.Provider("AIA.GET_DATA",
new[]{
"Test1",
"Test2",
"Test3",
"Test4"},
new object[]{
1,
2,
3,
null});
And this is the stored procedure.
PROCEDURE GET_DATA(
Test1 in NUMBER,
Test2 in NUMBER,
Test3 in NUMBER,
Test4 in NUMBER,
TestOut out SYS_REFCURSOR
);
On the constructor of Provider it gets the connection string and uses method GetData.
It fails on:
new OracleDataAdapter(command).Fill(ds);
Oracle.ManagedDataAccess.Client.OracleException: 'ORA-03115: unsupported network datatype or representation'
Again, this works perfectly on SQL Server.
Any help is appreciated, at least, what does this error message means.
EDIT:
Thank you Luke Woodward, that was very helpful. So there was a problem with the OUT parameter, but also with the type of the parameters I was sending. So that solves the problem.
Anyway, I ended up with this new method. Which is working fine, except with nulls.
private void GetData(string storedProcedure, IReadOnlyList<string> parameters, IReadOnlyList<object> values, IReadOnlyList<string> cursors)
{
using (var connection = new OracleConnection(_connectionString))
{
using (
var command = new OracleCommand(storedProcedure, connection)
{
CommandType = CommandType.StoredProcedure
})
{
if (parameters != null)
for (var i = 0; i < parameters.Count; i++)
{
var parameter = new OracleParameter();
parameter.ParameterName = parameters[i];
if (values[i] is Enum)
parameter.Value = (int)values[i];
else
parameter.Value = values[i];
if (cursors != null && cursors.Contains(parameter.ParameterName))
{
parameter.Direction = ParameterDirection.Output;
parameter.OracleDbType = OracleDbType.RefCursor;
}
else
{
parameter.OracleDbType = GetOracleType(values[i]);
}
command.Parameters.Add(parameter);
}
var ds = new DataSet();
connection.Open();
new OracleDataAdapter(command).Fill(ds);
_data = ds.Tables;
connection.Close();
}
}
}
In Sql I could use DBNull.Value, but in Oracle I need to define OracleDbType, which is anonoying since this method worked for any object, not caring about the type, now I still don't know how to fix it to make it work with any object in Oracle. But that could be considered offtopic, this question could be marked as answered.
You need to add an OracleParameter for the OUT parameter TestOut but you are not doing this.
Add these lines after the other lines that set up the parameters:
var outParam = new OracleParameter("TestOut", OracleDbType.RefCursor, ParameterDirection.Output);
command.Parameters.Add(outParam);
You will then need to execute the command separately rather than pass the command to the OracleDataAdapter. Do this by adding the line
command.ExecuteNonQuery();
immediately after the two that add outParam.
Finally, fill the dataset from the ref cursor in the OUT parameter by replacing the line
new OracleDataAdapter(command).Fill(ds);
with
new OracleDataAdapter().Fill(ds, (OracleRefCursor)outParam.Value);
Incidentally, I got a different error when I ran your code. I got the error PLS-00306: wrong number or types of arguments in call to 'GET_DATA'.
I know how to pass one parameter to an sql query but i want to create a function to pass multiple params that will have differents type and here im stuck.
public List<T> RawSql<T>(string query, params object[] parameters)
{
var command = context.Database.GetDbConnection().CreateCommand();
command.CommandText = query;
command.CommandType = CommandType.Text;
SqlParameter parameter = new SqlParameter();
parameter.ParameterName = "#bookId";
parameter.SqlDbType = SqlDbType.Int;
parameter.Value = parameters[0];
command.Parameters.Add(parameter);
var result = command.ExecuteReader())
return result;
}
Usage :
var rows = helper.RawSql("myStoreProc #bookId", x=> new Book { Id = (bool)x[0] }, bookId);
But how i can change the RawSql function to pass multiple parameters like this :
var rows = helper.RawSql("myStoreProc #bookId, #authorName", x=> new Book { Id = (bool)x[0] }, bookId, authorName);
I would also suggest using Dapper instead of reinventing the wheel - but if you can't for some reason, I would change the method signature to accept params SqlParameter[] parameters instead of params object[] parameters - and then all you need to do in the method is command.Parameters.AddRange(parameters);.
As Marc Gravel wrote in his comment - naming the parameters is going to be the biggest problem if you are simply using object[].
Here is a method I wrote to compare values from two different days:
public DataTable sqlToDTCompare(string conStr, string stpName, DateTime startDate, DateTime endDate, int percent)
{
//receives connection string and stored procedure name
//then returns populated data table
DataTable dt = new DataTable();
using (var con = new SqlConnection(conStr))
using (var cmd = new SqlCommand(stpName, con))
using (var da = new SqlDataAdapter(cmd))
{
cmd.Parameters.Add("#StartDate", SqlDbType.Date).Value = startDate;
cmd.Parameters.Add("#EndDate", SqlDbType.Date).Value = endDate;
cmd.Parameters.Add("#Percent", SqlDbType.Int).Value = percent;
cmd.CommandType = CommandType.StoredProcedure;
da.Fill(dt);
}
return dt;
}
This method then returns that data to a DataTable (was what I needed at time of writing). You would be able to use this , with modifying to be of better fit for your needs.
What you're looking to use is something along:
SqlCommand.Parameters.Add("#Param1", SqlDbType.Type).Value = param1;
SqlCommand.Parameters.Add("#Param2", SqlDbType.Type).Value = param2;
SqlCommand.Parameters.Add("#Param3", SqlDbType.Type).Value = param3;
.....
Where .Type in SqlDbType.Type can be changed to matche whatever SQL datatype you're needing (ex. SqlDbType.Date).
I have previously done implementations along these lines.
public IEnumerable<SampleModel> RetrieveSampleByFilter(string query, params SqlParameter[] parameters)
{
using(var connection = new SqlConnection(dbConnection))
using(var command = new SqlCommand(query, connection))
{
connection.Open();
if(parameters.Length > 0)
foreach(var parameter in parameters)
command.Parameters.Add(parameter);
// Could also do, instead of loop:
// command.Parameters.AddRange(parameters);
using(var reader = command.ExecuteReader())
while(reader != null)
yield return new Sample()
{
Id = reader["Id"],
...
}
}
}
I actually wrote an extension method to read the values returned back into my object, but this allows you to pass a query and a series of parameters to simply return your object.
I would look into Dapper, saves a lot of time. But I find the problem with trying to reuse with the above type of solution creates a bit of tightly coupling often.
By doing this approach you push specific information about your query elsewhere, which separates logic directly out of the repository and tightly couples to another dependency and knowledge.
Following the post
Using Dapper.TVP TableValueParameter with other parameters
I have been able to execute table valued parameter but my question is regarding part of its implementation. Procedure seems to be working but how can i make this implementation more generic? Adding parameters in DynamicParameters with 'new {}' is very specialized implementation according requirement. I want to be able to specify my parameter name as well.. for example
public IEnumerable<TCustomEntity> SqlQuery<TCustomEntity>(string query, IDictionary<string, object> parameters,
CommandType commandType) where TCustomEntity : class
{
DataTable dt;
DynamicParameters dp;
var sqlConnection = _context.Database.Connection;
try
{
if (sqlConnection.State == ConnectionState.Closed)
{
sqlConnection.Open();
}
var dynamicParameters = new DynamicParameters();
if (parameters != null)
foreach (var parameter in parameters)
{
if (parameter.Value is int)
dynamicParameters.Add(parameter.Key, parameter.Value, DbType.Int32);
else if (parameter.Value is string)
dynamicParameters.Add(parameter.Key, parameter.Value.ToString(), DbType.String);
else if (parameter.Value is DateTime)
dynamicParameters.Add(parameter.Key, parameter.Value, DbType.DateTime);
else if (parameter.Value is Decimal)
dynamicParameters.Add(parameter.Key, parameter.Value, DbType.Decimal);
else if (parameter.Value is DataTable)
{
dt = (DataTable)parameter.Value;
dt.SetTypeName(dt.TableName);
var parameterName = parameter.Key;
dp = new DynamicParameters(new { TVPName = dt });
// Here i want TVPName replaced with parameterName/parameter.Key but it doesn't seem possible
dynamicParameters.AddDynamicParams(dp);
}
else
dynamicParameters.Add(parameter.Key, parameter.Value);
}
var test = sqlConnection.Query<TCustomEntity>(query, dynamicParameters, commandType: commandType);
return test;
}
finally
{
sqlConnection.Close();
}
}
Any advice how may i proceed on the issue by making parameter name more generic? If not, it will be specialized implementation each time i use Table value parameter
It isn't obvious to me that any of that is necessary, since:
DynamicParameters is perfectly happy with TVPs as direct elements,
all of the data types shown would be handled automatically
dapper works happily with dictionaries
dapper already opens and closed the connection appropriately if it isn't open when invoked
It seems to me that the only interesting step here is the SetTypeName, which could be done:
foreach(object val in parameters.Values)
{
if(val is DataTable) {
var dt = (DataTable)val;
dt.SetTypeName(dt.TableName);
}
}
And then pass your original parameters object in:
return sqlConnection.Query<TCustomEntity>(query, parameters, commandType: commandType);
That leaves just:
public IEnumerable<TCustomEntity> SqlQuery<TCustomEntity>(string query,
IDictionary<string, object> parameters, // suggestion: default to null
CommandType commandType // suggestion: default to CommandType.Text
) where TCustomEntity : class
{
var sqlConnection = _context.Database.Connection;
if (parameters != null) {
foreach (object val in parameters.Values) {
if (val is DataTable) {
var dt = (DataTable)val;
// suggestion: might want to only do if dt.GetTypeName() is null/""
dt.SetTypeName(dt.TableName);
}
}
}
return sqlConnection.Query<TCustomEntity>(query, parameters,
commandType: commandType);
}
I have a following stored procedure:
create or replace PROCEDURE PRODUCT_DETAILS(p_code IN VARCHAR2,
cursorParam OUT SYS_REFCURSOR)
IS
BEGIN
OPEN cursorParam FOR
select str_auth_code, str_name
from strs
where str_auth_code = p_code;
END;
How can I call it with OrmLite? I've tryied:
connection.SqlList<Product>(#"EXEC PRODUCT_DETAILS #p_code", new { p_code = code });
but it throws an exception ORA-01036: illegal variable name/number
I just tried to do it with plain old ADO.NET and it worked:
using (var conn = new OracleConnection(connectionString))
{
OracleCommand cmd = new OracleCommand();
cmd.Connection = conn;
cmd.CommandText = "PRODUCT_DETAILS";
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("p_code", OracleType.NVarChar).Value = redemptionCode;
cmd.Parameters.Add("cursorParam", OracleType.Cursor);
cmd.Parameters["cursorParam"].Direction = ParameterDirection.Output;
conn.Open();
OracleDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
Console.WriteLine(dr["Name"]);
}
conn.Close();
}
But I can't figure out how to do the same task with OrmLite.
What you have looks good. If you were concerned about the verbosity of the code, and were using a number of stored procedures, then you could use this extension method to remove some of the repeated code:
Extension Method:
public static class StoredProcExtensions
{
public static List<T> ExecStoredProcedure<T>(this IDbConnection connection, string procedureName, object parameters = null, string outputCursor = "cursorParam")
{
return connection.Exec(c => {
c.CommandText = procedureName;
c.CommandType = CommandType.StoredProcedure;
// Create the parameters from the parameters object
if(parameters != null)
foreach(var property in parameters.GetType().GetPublicProperties())
c.Parameters.Add(new OracleParameter(property.Name, property.GetValue(parameters)));
// Add the output cursor
if(outputCursor != null)
c.Parameters.Add(new OracleParameter(outputCursor, OracleDbType.RefCursor) { Direction = ParameterDirection.Output });
// Return the result list
return c.ExecuteReader().ConvertToList<T>();
});
}
}
Usage:
var download = connection.ExecStoredProcedure<ProductDownloads>(
"PRODUCT_DETAILS",
new { p_code = redemptionCode }
);
foreach (var productDownload in download)
{
Console.WriteLine(productDownload.Name);
}
So the arguments are:
Stored procedure name i.e. PRODUCT_DETAILS
Optional An object of input parameters i.e new { p_code = redemptionCode, other = "value" }
Optional The name of the output cursor - defaults to cursorParam
Note: this code is untested, because I don't have Oracle setup, but it does compile, and hopefully goes some way to simplifying your stored procedures.
So far ended up with following code:
using (var connection = factory.Open())
{
var download =
connection.Exec(c =>
{
c.CommandText = "PRODUCT_DETAILS";
c.CommandType = CommandType.StoredProcedure;
c.Parameters.Add(
new Oracle.DataAccess.Client.OracleParameter("p_code", Oracle.DataAccess.Client.OracleDbType.NVarchar2) { Value = redemptionCode });
c.Parameters.Add(
new Oracle.DataAccess.Client.OracleParameter("cursorParam", Oracle.DataAccess.Client.OracleDbType.RefCursor) { Direction = ParameterDirection.Output });
return c.ExecuteReader().ConvertToList<ProductDownloads>();
});
foreach (var productDownload in download)
{
Console.WriteLine(productDownload.Name);
}
}
But I think there should be a better way for doing this.
Late to the party, but this problem can be solved by simply adding BEGIN and END to the statement... it will work.
In my case, I was trying to refresh a materialized view, only after adding BEGIN and END will it work, otherwise it will throw OracleException ORA-00900: invalid SQL statement...
This should work:
db.ExecuteSql("BEGIN DBMS_SNAPSHOT.REFRESH('" + materializedViewName + "'); END;");