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);
}
Related
I have a SQL Server 2005 database. In a few procedures I have table parameters that I pass to a stored proc as an nvarchar (separated by commas) and internally divide into single values. I add it to the SQL command parameters list like this:
cmd.Parameters.Add("#Logins", SqlDbType.NVarchar).Value = "jim18,jenny1975,cosmo";
I have to migrate the database to SQL Server 2008. I know that there are table value parameters, and I know how to use them in stored procedures. But I don't know how to pass one to the parameters list in an SQL command.
Does anyone know correct syntax of the Parameters.Add procedure? Or is there another way to pass this parameter?
DataTable, DbDataReader, or IEnumerable<SqlDataRecord> objects can be used to populate a table-valued parameter per the MSDN article Table-Valued Parameters in SQL Server 2008 (ADO.NET).
The following example illustrates using either a DataTable or an IEnumerable<SqlDataRecord>:
SQL Code:
CREATE TABLE dbo.PageView
(
PageViewID BIGINT NOT NULL CONSTRAINT pkPageView PRIMARY KEY CLUSTERED,
PageViewCount BIGINT NOT NULL
);
CREATE TYPE dbo.PageViewTableType AS TABLE
(
PageViewID BIGINT NOT NULL
);
CREATE PROCEDURE dbo.procMergePageView
#Display dbo.PageViewTableType READONLY
AS
BEGIN
MERGE INTO dbo.PageView AS T
USING #Display AS S
ON T.PageViewID = S.PageViewID
WHEN MATCHED THEN UPDATE SET T.PageViewCount = T.PageViewCount + 1
WHEN NOT MATCHED THEN INSERT VALUES(S.PageViewID, 1);
END
C# Code:
private static void ExecuteProcedure(bool useDataTable,
string connectionString,
IEnumerable<long> ids)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
using (SqlCommand command = connection.CreateCommand())
{
command.CommandText = "dbo.procMergePageView";
command.CommandType = CommandType.StoredProcedure;
SqlParameter parameter;
if (useDataTable) {
parameter = command.Parameters
.AddWithValue("#Display", CreateDataTable(ids));
}
else
{
parameter = command.Parameters
.AddWithValue("#Display", CreateSqlDataRecords(ids));
}
parameter.SqlDbType = SqlDbType.Structured;
parameter.TypeName = "dbo.PageViewTableType";
command.ExecuteNonQuery();
}
}
}
private static DataTable CreateDataTable(IEnumerable<long> ids)
{
DataTable table = new DataTable();
table.Columns.Add("ID", typeof(long));
foreach (long id in ids)
{
table.Rows.Add(id);
}
return table;
}
private static IEnumerable<SqlDataRecord> CreateSqlDataRecords(IEnumerable<long> ids)
{
SqlMetaData[] metaData = new SqlMetaData[1];
metaData[0] = new SqlMetaData("ID", SqlDbType.BigInt);
SqlDataRecord record = new SqlDataRecord(metaData);
foreach (long id in ids)
{
record.SetInt64(0, id);
yield return record;
}
}
Further to Ryan's answer you will also need to set the DataColumn's Ordinal property if you are dealing with a table-valued parameter with multiple columns whose ordinals are not in alphabetical order.
As an example, if you have the following table value that is used as a parameter in SQL:
CREATE TYPE NodeFilter AS TABLE (
ID int not null
Code nvarchar(10) not null,
);
You would need to order your columns as such in C#:
table.Columns["ID"].SetOrdinal(0);
// this also bumps Code to ordinal of 1
// if you have more than 2 cols then you would need to set more ordinals
If you fail to do this you will get a parse error, failed to convert nvarchar to int.
Generic
public static DataTable ToTableValuedParameter<T, TProperty>(this IEnumerable<T> list, Func<T, TProperty> selector)
{
var tbl = new DataTable();
tbl.Columns.Add("Id", typeof(T));
foreach (var item in list)
{
tbl.Rows.Add(selector.Invoke(item));
}
return tbl;
}
The cleanest way to work with it. Assuming your table is a list of integers called "dbo.tvp_Int" (Customize for your own table type)
Create this extension method...
public static void AddWithValue_Tvp_Int(this SqlParameterCollection paramCollection, string parameterName, List<int> data)
{
if(paramCollection != null)
{
var p = paramCollection.Add(parameterName, SqlDbType.Structured);
p.TypeName = "dbo.tvp_Int";
DataTable _dt = new DataTable() {Columns = {"Value"}};
data.ForEach(value => _dt.Rows.Add(value));
p.Value = _dt;
}
}
Now you can add a table valued parameter in one line anywhere simply by doing this:
cmd.Parameters.AddWithValueFor_Tvp_Int("#IDValues", listOfIds);
Use this code to create suitable parameter from your type:
private SqlParameter GenerateTypedParameter(string name, object typedParameter)
{
DataTable dt = new DataTable();
var properties = typedParameter.GetType().GetProperties().ToList();
properties.ForEach(p =>
{
dt.Columns.Add(p.Name, Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType);
});
var row = dt.NewRow();
properties.ForEach(p => { row[p.Name] = (p.GetValue(typedParameter) ?? DBNull.Value); });
dt.Rows.Add(row);
return new SqlParameter
{
Direction = ParameterDirection.Input,
ParameterName = name,
Value = dt,
SqlDbType = SqlDbType.Structured
};
}
If you have a table-valued function with parameters, for example of this type:
CREATE FUNCTION [dbo].[MyFunc](#PRM1 int, #PRM2 int)
RETURNS TABLE
AS
RETURN
(
SELECT * FROM MyTable t
where t.column1 = #PRM1
and t.column2 = #PRM2
)
And you call it this way:
select * from MyFunc(1,1).
Then you can call it from C# like this:
public async Task<ActionResult> MethodAsync(string connectionString, int? prm1, int? prm2)
{
List<MyModel> lst = new List<MyModel>();
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.OpenAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = $"select * from MyFunc({prm1},{prm2})";
using (var reader = await command.ExecuteReaderAsync())
{
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
MyModel myModel = new MyModel();
myModel.Column1 = int.Parse(reader["column1"].ToString());
myModel.Column2 = int.Parse(reader["column2"].ToString());
lst.Add(myModel);
}
}
}
}
}
View(lst);
}
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.
Let me start by posting my code first:
ExecuteScalar method:
public T ExecuteScalar<T>(string sql, CommandType commandType, List<NpgsqlParameter> parameters)
{
using (NpgsqlConnection conn = Konekcija_na_server.Spajanje("spoji"))
{
return Execute<T>(sql, commandType, c =>
{
var returnValue = c.ExecuteScalar();
return (returnValue != null && returnValue != DBNull.Value && returnValue is T)
? (T)returnValue
: default(T);
}, parameters);
}
}
Execute method:
T Execute<T>(string sql, CommandType commandType, Func<NpgsqlCommand, T> function, List<NpgsqlParameter> parameters)
{
using (NpgsqlConnection conn = Konekcija_na_server.Spajanje("spoji"))
{
using (var cmd = new NpgsqlCommand(sql, conn))
{
cmd.CommandType = commandType;
if (parameters.Count > 0 )
{
foreach (var parameter in parameters)
{
cmd.Parameters.AddWithValue(parameter.ParameterName,parameter.Value);
}
}
return function(cmd);
}
}
}
Call of ExecuteScalar method:
komanda = "begin;select count(*) from radni_sati where ime=#ime and prezime=#prezime" +
" and (dolazak is not null and odlazak is not null and sati_rada is not null) and napomena='' ;commit;";
listaParametara.Add(new NpgsqlParameter { ParameterName = "#ime", Value = ime });
listaParametara.Add(new NpgsqlParameter { ParameterName = "#prezime", Value = prezime });
var nePrazni_redovi=instanca.ExecuteScalar<int>(komanda, CommandType.Text, listaParametara);
listaParametara.Clear();
Now my problem is when I call ExecuteScalar().
For some reason my ExecuteScalar always returns 0 as result and that can't be coz I tested it as a normal query in PSQL Shell and it always returns values>0 when I call legit query that has to return normal value.
First time It enters ExecuteScalar after a call, returns a returnValue from lamba operator and for example its 16, then when it goes to Execute function, somehow it returns 0 and I dont understand why, coz main thing I need ExecuteScalar for is to return count(*) value out as an int.
can you tell us how are you calling ExecuteScalar? What type is T? Also, set breakpoint to: var returnValue = c.ExecuteScalar(); and check what type is returned after you step over that line (F10). In watch window of Visual Studio you should check Type column.
I have the next code:
public static List<T> ExecuteQuery<T>(string qry, Dictionary<string, object> parameters = null)
{
using (var connection = new OdbcConnection(ConnectionString))
{
connection.Open();
var command = new OdbcCommand(qry, connection);
if (parameters != null)
{
foreach (var parameter in parameters)
{
command.Parameters.AddWithValue(parameter.Key, parameter.Value);
}
}
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
// ...
}
}
}
}
When I execute something like:
ExecuteQuery<MyClass>("SELECT * FROM MYTABLE WHERE ID = 1");
It returns all the correct data. But when I send this:
ExecuteQuery<MyClass>(
"SELECT * FROM MYTABLE WHERE ID = #Id",
new Dictionary<string, object> { { "#Id", 1 } });
doesn't return anything. I've tried with command.Parameters.Add but is the same history.
So, where is my error?
Thanks to everyone.
Try substituting the named parameter with a ?, which I believe is what OdbcCommand uses.
ExecuteQuery<MyClass>("SELECT * FROM MYTABLE WHERE ID = ?",
new Dictionary<string, object> { { "#Id", 1 } });
Also, since a Dictionary doesn't guarantee order, you may want to replace that with a List<T> in the event you have more than one parameter. A list guarantees order.
I'd also suggest specifying the OdbcType and not just adding all your parameter values as objects. It has to infer the correct data type otherwise, and it may guess wrong. You could create a more permanent class... here I decided to just use a Tuple<T,T,T>.
public static List<T> ExecuteQuery<T>(string qry, List<Tuple<string, OdbcType, object>> parameters = null)
...
...
foreach (var parameter in parameters)
{
command.Parameters.Add(parameter.Item1, parameter.Item2).Value = parameter.Item3;
}
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));