Using two dimensional array as Dapper parameter list - c#

In Dapper it is possible to query a list with where in using this code:
var sql = "SELECT * FROM Invoice WHERE Kind IN #Kind;";
using (var connection = My.ConnectionFactory())
{
connection.Open();
var invoices = connection.Query<Invoice>(sql, new {Kind = new[] {InvoiceKind.StoreInvoice, InvoiceKind.WebInvoice}}).ToList();
}
But I'd like to query a list of products by multiple suppliers and their product code. So to do this, I tried creating an array of arrays. This is my method in my repository:
public Dictionary<int, Dictionary<string, Product>> GetGroupedListByRelationIdAndProductCode(Dictionary<int, List<string>> productKeysByRelationId)
{
Dictionary<int, Dictionary<string, Product>> list = new Dictionary<int, Dictionary<string, Product>>();
string sql = "SELECT * FROM Products WHERE 1=1 ";
int i = 0;
foreach (KeyValuePair<int, List<string>> productKeys in productKeysByRelationId)
{
sql = sql + " AND (ManufacturerRelationId = " + productKeys.Key + " AND ManufacturerProductCode in #ProductCodeList[" + i + "] )";
++i;
}
using (var conn = _connectionFactory.CreateConnection())
{
conn.Open();
var param = new { ProductCodeList = productKeysByRelationId.Select(x => x.Value.ToArray()).ToArray() };
var productsList = conn.Query<Product>(sql, param).ToList();
if (productsList.Count > 0)
{
foreach (var product in productsList)
{
list[product.ManufacturerRelationId][product.ManufacturerProductCode] = product;
}
}
}
return list;
}
This gives me this error though: System.ArgumentException: 'No mapping exists from object type System.String[] to a known managed provider native type.'
Any suggestions on how to do this?

You have several problems with your code.
If you have more than one item in your productKeysByRelationId you will end up getting SQL like:
WHERE 1=1 AND ManufacturerRelationId = 1 ... AND ManufacturerRelationId = 2
That's not likely to return any results, you need to sprinkle some OR's in there.
The error you are getting is because you have something like:
AND ManufacturerProductCode in #ProductCodeList[0]
Dapper cannot handle that. It mostly expects the query parameters to be an object with aptly named members of a few simple types or array.
Thankfully Dapper has a solution to that, DynamicParameters to the rescue!
You can construct your query like this:
var queryParams = new DynamicParameters();
foreach (var productKeys in productKeysByRelationId)
{
sql = sql + $" ... AND ManufacturerProductCode in #ProductCodeList{i} )";
queryParams.Add($"ProductCodeList{i}", productKeys.Value);
i++;
}
Now you have your query parameters in the right format, so you can just do this:
var productsList = conn.Query<Product>(sql, queryParams).ToList();
That ought to fix it, but you really should try to parameterize the ManufacturerRelationId also. It's not just about SQL injection, there might be some SQL cache performance related things.
You might also gain some clarity in code by using the SqlBuilder from Dapper.Contrib.

Related

How to compile complex queries with SqlKata first and execute later

I want to create and compile a SQL query using SqlKata and then in some other function execute this query. It's not obvious based on the documentation how this is done, especially for complex queries.
I have tried converting the query to a string and then running it, but this does not seem to be the right thing to do and the string does not compile well for complex queries.
public class TradeLoader : ITradeLoader
{
public SqlResult CreateTradeQuery(Target targets, Group groups, DateTime fromTime)
{
var compiler = new SqlServerCompiler();
var query = new Query().FromRaw(#"
[trades] AS t WITH (NOLOCK)
LEFT JOIN [info] AS i WITH (NOLOCK)
ON t.Id = i.Id
")
.Select("i.Name", "t.Price", "t.Volume")
.WhereTime("t.CreatedDate", ">", fromTime);
var subQuery = new Query(#"
[trades] AS t WITH (NOLOCK)
LEFT JOIN [info] AS i WITH (NOLOCK)
ON t.Id = i.Id
");
foreach (var target in targets)
{
subQuery.OrWhere(q => q.WhereIn("i.Name", groups.Keys).WhereIn("i.GroupName", target.Value));
}
query.Where(subQuery);
SqlResult result = compiler.Compile(query);
return result; // How to execute this somewhere else?
}
}
It's not clear to me how I can execute this query somewhere else, either using the SqlKata.Execution or System.Data.SqlClient. Preferably I should be able to run it in both.
I don't understand what you want to do.
You can use it this way.
public SqlResult CreateTradeQuery(Target targets, Group groups, DateTime fromTime)
{
var query = GetDefaultQuery();
query.Select("i.Name", "t.Price", "t.Volume");
query.Where("t.CreatedDate", ">", fromTime);
query.Where(GetSubQuery(targets,groups),"subQuery");
SqlResult result = compiler.Compile(query);
return result;
}
private Query GetSubQuery(Target targets, Group groups)
{
var subQuery = GetDefaultQuery();
foreach (var target in targets)
{
subQuery.OrWhere(q => q.WhereIn("i.Name", groups.Keys).WhereIn("i.GroupName", target.Value));
}
return subQuery;
}
private Query GetDefaultQuery()
{
var query = new Query("trades as t");
query.LeftJoin("info as i","i.Id","t.Id");
return query;
}
This is probably basic knowledge for those who are proficient in C#/.NET, but what I wanted to do was to inject params into the SQL query created with SqlKata and then run it without using SqlKata. In the end, I did it in the following way (and there are probably other ways to do this too):
public class TradeLoader : ITradeLoader
{
...
public async Task<SomeDataType> RunQuery()
{
...
var sqlCommand = CreateTradeQuery(someTargets, someGroups, someDateTime);
using (SqlConnection sql = new SqlConnection(_connectionString))
{
sql.Open();
var command = new SqlCommand(sqlCommand.Sql, sql) {CommandTimeout = _timeout};
foreach (var (name, value) in sqlCommand.NamedBindings)
{
var parameter = command.CreateParameter();
parameter.ParameterName = name;
parameter.Value = value;
command.Parameters.Add(parameter);
}
using (SqlDataReader dataReader = await command.ExecuteReaderAsync())
{
while (await dataReader.ReadAsync())
{
// read data here
}
}
}
return data;
}
}
This allows you to build your queries with SqlKata, but run the query using other libraries like in this example where I use System.Data.SqlClient.

How to build dynamic parameterised query using dapper?

How to build dynamic parameterised query using dapper?
I have columns and their values in KeyValuePair
e.g.
Key | Value
------| -------
FName | Mohan
LName | O'reily
Gender| Male
I want to build dynamic SQL statement using dapper and execute it,
string statement = "SELECT * FROM Employee WHERE 1 = 1 ";
List<KeyValuePair<string,string>> lst = new List<KeyValuePair<string,string>>();
lst.Add(new KeyValuePair<string,String>("FName","Kim"));
lst.Add(new KeyValuePair<string,String>("LName","O'reily"));
lst.Add(new KeyValuePair<string,String>("Gender","Male"));
foreach(var kvp in lst)
{
statement += " AND "+ kvp.Key +" = '"+ kvp.Value +"'";
}
using (var connection = _dataAccessHelper.GetOpenConnection())
{
try
{
//CommandDefinition cmd = new CommandDefinition(statement);
var searchResult = await connection.QueryAsync<dynamic>(statement);
Above query fails because there is special character in query.
I found that for parameterised statements CommandDefinition can be used,
how to use CommandDefinition to execute the above statement without any error?
or
is there any better way to build dynamic sql statements?
DapperQueryBuilder is an alternative to Dapper SqlBuilder and would work like this:
List<KeyValuePair<string,string>> lst = new List<KeyValuePair<string,string>>();
lst.Add(new KeyValuePair<string,String>("FName","Kim"));
lst.Add(new KeyValuePair<string,String>("LName","O'reily"));
lst.Add(new KeyValuePair<string,String>("Gender","Male"));
using (var connection = _dataAccessHelper.GetOpenConnection())
{
var query = connection.QueryBuilder($#"SELECT * FROM Employee /**where**/");
foreach(var kvp in lst)
query.Where($"{kvp.Key:raw} = {kvp.Value}");
var searchResult = await query.QueryAsync<dynamic>();
}
The output is fully parametrized SQL (WHERE FName = #p0 AND LName = #p1 etc).
You just have to be aware that strings that you interpolate using raw modifier are not passed as parameters (in the code above the column name kvp.Key is dynamic value but can't be parameter), and therefore you should ensure that the column names (kvp.Key) are safe. The other interpolated values (kvp.Value in the example) look like regular (unsafe) string interpolation but the library converts them to SqlParameters.
Disclaimer: I'm one of the authors of this library
Don't build the query as text. You can use the Dapper SqlBuilder, it goes something like this:
List<KeyValuePair<string,string>> lst = new List<KeyValuePair<string,string>>();
lst.Add(new KeyValuePair<string,String>("FName","Kim"));
lst.Add(new KeyValuePair<string,String>("LName","O'reily"));
lst.Add(new KeyValuePair<string,String>("Gender","Male"));
var builder = new SqlBuilder();
var select = builder.AddTemplate("select * from Employee /**where**/");
foreach (var kvPair in lst)
{
builder.Where($"{kvPair.Key} = #{kvPair.Key}", new { kvPair.Value });
}
using (var connection = _dataAccessHelper.GetOpenConnection())
{
try
{
var searchResult = await connection.QueryAsync<dynamic>(select.RawSql, select.Parameters);
}
...
You should never try to escape parameters yourself, leave it to Dapper. Then you will also be protected against SQL-injection.
There is no reason to use a list of key value pairs to build an SQL statement with dynamic parameters. You can simply put placeholders in the query, for example #FName
from the example above, and provide the values for those placeholders as the second parameter of QueryAsync method by passing in an anonymous type with its keys corresponding to the placeholders and values to the dynamic values that you want to use for the query.
string statement = "SELECT * FROM Employee WHERE FName=#FName AND LName=#LName AND Gender=#Gender";
...
var searchResult = await connection.QueryAsync<dynamic>(statement, new { FName = "Kim", LName = "O'reily", Gender="Male" });
You can use the DynamicParameters class for generic fields.
Dictionary<string, object> Filters = new Dictionary<string, object>();
Filters.Add("UserName", "admin");
Filters.Add("Email", "admin#admin.com");
var builder = new SqlBuilder();
var select = builder.AddTemplate("select * from SomeTable /**where**/");
var parameter = new DynamicParameters();
foreach (var filter in Filters)
{
parameter.Add(filter.Key, filter.Value);
builder.Where($"{filter.Key} = #{filter.Key}");
}
var searchResult = appCon.Query<ApplicationUser>(select.RawSql, parameter);

Dapper query with dynamic list of filters

I have a c# mvc app using Dapper. There is a list table page which has several optional filters (as well as paging). A user can select (or not) any of several (about 8 right now but could grow) filters, each with a drop down for a from value and to value. So, for example, a user could select category "price" and filter from value "$100" to value "$200". However, I don't know how many categories the user is filtering on before hand and not all of the filter categories are the same type (some int, some decimal/double, some DateTime, though they all come in as string on FilterRange).
I'm trying to build a (relatively) simple yet sustainable Dapper query for this. So far I have this:
public List<PropertySale> GetSales(List<FilterRange> filterRanges, int skip = 0, int take = 0)
{
var skipTake = " order by 1 ASC OFFSET #skip ROWS";
if (take > 0)
skipTake += " FETCH NEXT #take";
var ranges = " WHERE 1 = 1 ";
for(var i = 0; i < filterRanges.Count; i++)
{
ranges += " AND #filterRanges[i].columnName BETWEEN #filterRanges[i].fromValue AND #filterRanges[i].toValue ";
}
using (var conn = OpenConnection())
{
string query = #"Select * from Sales "
+ ranges
+ skipTake;
return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
}
}
I Keep getting an error saying "... filterRanges cannot be used as a parameter value"
Is it possible to even do this in Dapper? All of the IEnumerable examples I see are where in _ which doesn't fit this situation. Any help is appreciated.
You can use DynamicParameters class for generic fields.
Dictionary<string, object> Filters = new Dictionary<string, object>();
Filters.Add("UserName", "admin");
Filters.Add("Email", "admin#admin.com");
var builder = new SqlBuilder();
var select = builder.AddTemplate("select * from SomeTable /**where**/");
var parameter = new DynamicParameters();
foreach (var filter in Filters)
{
parameter.Add(filter.Key, filter.Value);
builder.Where($"{filter.Key} = #{filter.Key}");
}
var searchResult = appCon.Query<ApplicationUser>(select.RawSql, parameter);
You can use a list of dynamic column values but you cannot do this also for the column name other than using string format which can cause a SQL injection.
You have to validate the column names from the list in order to be sure that they really exist before using them in a SQL query.
This is how you can use the list of filterRanges dynamically :
const string sqlTemplate = "SELECT /**select**/ FROM Sale /**where**/ /**orderby**/";
var sqlBuilder = new SqlBuilder();
var template = sqlBuilder.AddTemplate(sqlTemplate);
sqlBuilder.Select("*");
for (var i = 0; i < filterRanges.Count; i++)
{
sqlBuilder.Where($"{filterRanges[i].ColumnName} = #columnValue", new { columnValue = filterRanges[i].FromValue });
}
using (var conn = OpenConnection())
{
return conn.Query<Sale>(template.RawSql, template.Parameters).AsList();
}
You can easily create that dynamic condition using DapperQueryBuilder:
using (var conn = OpenConnection())
{
var query = conn.QueryBuilder($#"
SELECT *
FROM Sales
/**where**/
order by 1 ASC
OFFSET {skip} ROWS FETCH NEXT {take}
");
foreach (var filter in filterRanges)
query.Where($#"{filter.ColumnName:raw} BETWEEN
{filter.FromValue.Value} AND {filter.ToValue.Value}");
return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
}
Or without the magic word /**where**/:
using (var conn = OpenConnection())
{
var query = conn.QueryBuilder($#"
SELECT *
FROM Sales
WHERE 1=1
");
foreach (var filter in filterRanges)
query.Append($#"{filter.ColumnName:raw} BETWEEN
{filter.FromValue.Value} AND {filter.ToValue.Value}");
query.Append($"order by 1 ASC OFFSET {skip} ROWS FETCH NEXT {take}");
return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
}
The output is fully parametrized SQL, even though it looks like we're doing plain string concatenation.
Disclaimer: I'm one of the authors of this library
I was able to find a solution for this. The key was to convert the List to a Dictionary. I created a private method:
private Dictionary<string, object> CreateParametersDictionary(List<FilterRange> filters, int skip = 0, int take = 0)
{
var dict = new Dictionary<string, object>()
{
{ "#skip", skip },
{ "#take", take },
};
for (var i = 0; i < filters.Count; i++)
{
dict.Add($"column_{i}", filters[i].Filter.Description);
// some logic here which determines how you parse
// I used a switch, not shown here for brevity
dict.Add($"#fromVal_{i}", int.Parse(filters[i].FromValue.Value));
dict.Add($"#toVal_{i}", int.Parse(filters[i].ToValue.Value));
}
return dict;
}
Then to build my query,
var ranges = " WHERE 1 = 1 ";
for(var i = 0; i < filterRanges.Count; i++)
ranges += $" AND {filter[$"column_{i}"]} BETWEEN #fromVal_{i} AND #toVal_{i} ";
Special note: Be very careful here as the column name is not a parameter and you could open your self up to injection attacks (as #Popa noted in his answer). In my case those values come from an enum class and not from user in put so I am safe.
The rest is pretty straight forwared:
using (var conn = OpenConnection())
{
string query = #"Select * from Sales "
+ ranges
+ skipTake;
return conn.Query<Sale>(query, filter).AsList();
}

IBM db2Command SQL SELECT * FROM IN #namedParameter

When using the C# code below to construct a DB2 SQL query the result set only has one row. If I manually construct the "IN" predicate inside the cmdTxt string using string.Join(",", ids) then all of the expected rows are returned. How can I return all of the expected rows using the db2Parameter object instead of building the query as a long string to be sent to the server?
public object[] GetResults(int[] ids)
{
var cmdTxt = "SELECT DISTINCT ID,COL2,COL3 FROM TABLE WHERE ID IN ( #ids ) ";
var db2Command = _DB2Connection.CreateCommand();
db2Command.CommandText = cmdTxt;
var db2Parameter = db2Command.CreateParameter();
db2Parameter.ArrayLength = ids.Length;
db2Parameter.DB2Type = DB2Type.DynArray;
db2Parameter.ParameterName = "#ids";
db2Parameter.Value = ids;
db2Command.Parameters.Add(db2Parameter);
var results = ExecuteQuery(db2Command);
return results.ToArray();
}
private object[] ExecuteQuery(DB2Command db2Command)
{
_DB2Connection.Open();
var resultList = new ArrayList();
var results = db2Command.ExecuteReader();
while (results.Read())
{
var values = new object[results.FieldCount];
results.GetValues(values);
resultList.Add(values);
}
results.Close();
_DB2Connection.Close();
return resultList.ToArray();
}
You cannot send in an array as a parameter. You would have to do something to build out a list of parameters, one for each of your values.
e.g.: SELECT DISTINCT ID,COL2,COL3 FROM TABLE WHERE ID IN ( #id1, #id2, ... #idN )
And then add the values to your parameter collection:
cmd.Parameters.Add("#id1", DB2Type.Integer).Value = your_val;
Additionally, there are a few things I would do to improve your code:
Use using statements around your DB2 objects. This will automatically dispose of the objects correctly when they go out of scope. If you don't do this, eventually you will run into errors. This should be done on DB2Connection, DB2Command, DB2Transaction, and DB2Reader objects especially.
I would recommend that you wrap queries in a transaction object, even for selects. With DB2 (and my experience is with z/OS mainframe, here... it might be different for AS/400), it writes one "accounting" record (basically the work that DB2 did) for each transaction. If you don't have an explicit transaction, DB2 will create one for you, and automatically commit after every statement, which adds up to a lot of backend records that could be combined.
My personal opinion would also be to create a .NET class to hold the data that you are getting back from the database. That would make it easier to work with using IntelliSense, among other things (because you would be able to auto-complete the property name, and .NET would know the type of the object). Right now, with the array of objects, if your column order or data type changes, it may be difficult to find/debug those usages throughout your code.
I've included a version of your code that I re-wrote that has some of these changes in it:
public List<ReturnClass> GetResults(int[] ids)
{
using (var conn = new DB2Connection())
{
conn.Open();
using (var trans = conn.BeginTransaction(IsolationLevel.ReadCommitted))
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = trans;
var parms = new List<string>();
var idCount = 0;
foreach (var id in ids)
{
var parm = "#id" + idCount++;
parms.Add(parm);
cmd.Parameters.Add(parm, DB2Type.Integer).Value = id;
}
cmd.CommandText = "SELECT DISTINCT ID,COL2,COL3 FROM TABLE WHERE ID IN ( " + string.Join(",", parms) + " ) ";
var resultList = new List<ReturnClass>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var values = new ReturnClass();
values.Id = (int)reader["ID"];
values.Col1 = reader["COL1"].ToString();
values.Col2 = reader["COL2"].ToString();
resultList.Add(values);
}
}
return resultList;
}
}
}
public class ReturnClass
{
public int Id;
public string Col1;
public string Col2;
}
Try changing from:
db2Parameter.DB2Type = DB2Type.DynArray;
to:
db2Parameter.DB2Type = DB2Type.Integer;
This is based on the example given here

Parameterize DocumentDB query with IN clause

I have a query somewhat like the following which I'm trying to parameterize:
List<string> poiIDs = /*List of poi ids*/;
List<string> parameterNames = /*List of parameter names*/;
string inClause = string.Join(",", parameterNames);
string query = string.Format("SELECT c.id AS poiID, c.poiName, c.latitude, c.longitude FROM c WHERE c.clusterName = #clusterName AND c.id IN ({0}) AND c.deleted = false", inClause);
IQueryable<POI> queryResult = Client.CreateDocumentQuery<POI>(Collection.SelfLink, new SqlQuerySpec
{
QueryText = query,
Parameters = new SqlParameterCollection()
{
new SqlParameter("#clusterName", "POI"),
// How do I declare the dynamically generated parameters here
// as new SqlParameter()?
}
});
How do I declare the dynamically generated parameters as new SqlParameter() for the Parameters property of SqlQuerySpec in order to create my document query?
You can create dynamic parameterized query like this:
// DocumentDB query
// POINT TO PONDER: create the formatted query, so that after creating the dynamic query we'll replace it with dynamically created "SQL Parameter/s"
var queryText = #"SELECT
us.id,
us.email,
us.status,
us.role
FROM user us
WHERE us.status = #userStatus AND us.email IN ({0})";
// contain's list of emails
IList<string> emailIds = new List<string>();
emailIds.Add("a#gmail.com");
emailIds.Add("b#gmail.com");
#region Prepare the query
// simple parameter: e.g. check the user status
var userStatus = "active";
var sqlParameterCollection = new SqlParameterCollection { new SqlParameter("#userStatus", userStatus) };
// IN clause: with list of parameters:
// first: use a list (or array) of string, to keep the names of parameter
// second: loop through the list of input parameters ()
var namedParameters = new List<string>();
var loopIndex = 0;
foreach (var email in emailIds)
{
var paramName = "#namedParam_" + loopIndex;
namedParameters.Add(paramName);
var newSqlParamter = new SqlParameter(paramName, email);
sqlParameterCollection.Add(newSqlParamter);
loopIndex++;
}
// now format the query, pass the list of parameter into that
if (namedParameters.Count > 0)
queryText = string.Format(queryText, string.Join(" , ", namedParameters));
// after this step your query is something like this
// SELECT
// us.id,
// us.email,
// us.status,
// us.role
// FROM user us
// WHERE us.status = #userStatus AND us.email IN (#namedParam_0, #namedParam_1, #namedParam_2)
#endregion //Prepare the query
// now inject the parameter collection object & query
var users = Client.CreateDocumentQuery<Users>(CollectionUri, new SqlQuerySpec
{
QueryText = queryText,
Parameters = sqlParameterCollection
}).ToList();
The following gives you a SQL query, you can then run in your DocumentDB Collection, to get the Documents by their IDs.
var query = $"SELECT * FROM p WHERE p.id IN ('{string.Join("', '", arrayOfIds)}')";
The DocumentDB SDK doesn't support parameterized IN queries.
Judging from the SO thread in the comment above, SQL does not either. As mentioned in the other thread, you can use LINQ as a workaround.
Why not use the ArrayContains method? Here is an example in node
sqlQuery = {
query: 'SELECT * FROM t WHERE ARRAY_CONTAINS(#idList, t.id)',
parameters: [
{
name: '#idList',
value: ['id1','id2','id3'],
},
],
};

Categories