SQL Select rows with multiple integer values - c#

My table contains an integer value. I want to present multiple values sort of like:
select
MeetingId, StartDate, EndDate, RoomId, MeetingStatusId, Subject
from
Meeting
where
RoomId in (#roomids )
and StartDate >= #start and EndDate <= #end
and CreatedById = #user
But how do I construct the #roomids parameter in C# to be integers? I tried casting RoomId to a varchar, but that didn't work.

You can use SQL Server STRING_SPLIT function and use the parameter as varchar:
select
MeetingId, StartDate, EndDate, RoomId, MeetingStatusId, Subject
from
Meeting
where
RoomId in (SELECT cast(VALUE as int) FROM dbo.string_split(#roomids) )
and StartDate >= #start and EndDate <= #end
and CreatedById = #user
Reference: https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-2017
If you don't have the build in split string function due to your SQL Server version, here's an article on how to create one:
https://sqlperformance.com/2012/07/t-sql-queries/split-strings

A table-valued parameter is a good choice here. For starters you'd create a custom table type in SQL Server, like so:
create type dbo.IdentifierList as table (Identifier int not null);
Here's a simple C# function to create an instance of a query parameter having this type:
SqlParameter CreateIdentifierTableParameter(string name, IEnumerable<int> identifiers)
{
// Build a DataTable whose schema matches that of our custom table type.
var identifierTable = new DataTable(name);
identifierTable.Columns.Add("Identifier", typeof(long));
foreach (var identifier in identifiers)
identifierTable.Rows.Add(identifier);
return new SqlParameter
{
ParameterName = name, // The name of the parameter in the query to be run.
TypeName = "dbo.IdentifierList", // The name of our table type.
SqlDbType = SqlDbType.Structured, // Indicates a table-valued parameter.
Value = identifierTable, // The table created above.
};
}
Then you write your query with the #RoomIds parameter treated as if it were any other table in your database, call the function created above to build the table-valued parameter, and then add it to your SQL command just like you would any other SqlParameter. For instance:
void GetMeetings(IEnumerable<int> roomIdentifiers)
{
// A simplified version of your query to show just the relevant part:
const string sqlText = #"
select
M.*
from
Meeting M
where
exists (select 1 from #RoomIds R where M.RoomId = R.Identifier);";
using (var sqlCon = new SqlConnection("<your connection string here>"))
{
sqlCon.Open();
using (var sqlCmd = new SqlCommand(sqlText, sqlCon))
{
sqlCmd.Parameters.Add(CreateIdentifierTableParameter("RoomIds", roomIdentifiers));
// Execute sqlCmd here in whatever way is appropriate.
}
}
}
This seems like a lot of work at first, but once you've defined the SQL type and written some code to create instances of it, it's really easy to re-use wherever you need it.

Try This
string RooomList = "5,3,7";
string DateS = "01-01-2017";
string DateE = "12-31-2017";
string userT = "Rami";
string sqlText = string.Format(#"
select MeetingId, StartDate, EndDate, RoomId, MeetingStatusId, Subject
from Meeting
where
RoomId in ({0} )
and StartDate >= {1} and EndDate <= {2}
and CreatedById = {3} ", RooomList, DateS , DateE , userT);

Related

How to convert a datetime string to datetime in SQL Server

From within my C# app I'm calling a stored procedure with a TVP. A couple of columns are datetime. A call to the SP might look like:
declare #p1 dbo.MyTvp
insert into #p1 values('2020-03-19 00:00:01','2020-03-30 23:59:59')
exec MySp #criteria=#p1
The above code is automatically generated in C#. In the SP, the part handling the dates is:
declare #datefrom datetime;
---
SET #sql = CONCAT(#sql, ' AND date_from >= ''', #datefrom, '''');
SQL Server locale is German.
The above throws an error due to conversion from varchar to datetime. However, if the datetime values that I pass are formatted as follows:
declare #p1 dbo.MyTvp
insert into #p1 values('19.03.2020 00:00:01','30.03.2020 23:59:59')
exec MySp #criteria=#p1
The SP works fine.
The class used as a source is:
public class MyCriteria
{
public DateTime DateFrom { get; set; }
}
And the table type is:
CREATE TYPE [dbo].[MyTvp] AS TABLE(
[DateFrom] [datetime] NULL
)
I convert an instance of MyCriteria into a DataTable using an extension method, and then use Dapper to execute the SP:
var criteria = new List<MyCriteria>() { myCriteria }.ToDataTable();
return await conn.QueryAsync<SomeResult>(new CommandDefinition("MySp", new { criteria }, commandType: CommandType.StoredProcedure, cancellationToken: ct));
What I don't understand is at what stage does the conversion from datetime to varchar or DateTime to string occurs.
So how exactly do I need to convert the dates to get the SP to work? Should I do the conversion at the DB level or in my C# app?
EDIT
This is the extension method used to convert a class to a datatable so that it can be passed on as a TVP to the SP:
public static DataTable ToDataTable<T>(this IEnumerable<T> items)
{
var dataTable = new DataTable(typeof(T).Name);
//Get all the properties not marked with Ignore attribute
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(x => x.GetCustomAttributes(typeof(XmlIgnoreAttribute), false).Length == 0).ToList();
//Set column names as property names
foreach (var property in properties)
{
if (!property.PropertyType.IsEnum && !property.PropertyType.IsNullableEnum())
{
var type = property.PropertyType;
//Check if type is Nullable like int?
if (Nullable.GetUnderlyingType(type) != null)
type = Nullable.GetUnderlyingType(type);
dataTable.Columns.Add(property.Name, type);
}
else dataTable.Columns.Add(property.Name, typeof(int));
}
//Insert property values to datatable rows
foreach (T item in items)
{
var values = new object[properties.Count];
for (int i = 0; i < properties.Count; i++)
{
values[i] = properties[i].GetValue(item, null);
}
dataTable.Rows.Add(values);
}
return dataTable;
}
EDIT 2
The problem is the SQL that is being generated by C#/Dapper which is used to populate the TVP passed to the SP. A simple test can be seen by doing the following:
DECLARE #test TABLE (
[DateCol] datetime NOT NULL
);
INSERT INTO #test VALUES ('2020-02-19 00:00:01'); --doesnt work
INSERT INTO #test VALUES (CONVERT(datetime, '2020-02-19 00:00:01', 120)); --works
The CONVERT function returns the date in the same format as the first INSERT statement. However the first statement doesn't work.
From discussion in the comments, it sounds like a: the data in the TVP is typed (datetime), and b: there is only one row in this case; that's great - it means we can simplify hugely; what we'd want to do here is pull the values from the TVP into locals, and just work with those. Now, based on #datefrom in the example code, it sounds like you've already done the first step, so all we need to do is fix how the dynamic SQL is composed and executed. In the question we have:
SET #sql = CONCAT(#sql, ' AND date_from >= ''', #datefrom, '''');
which is presumably followed later by:
EXEC (#sql);
Instead, we can parameterize the dynamic SQL:
SET #sql = #sql + ' AND date_from >= #datefrom ';
and pass the parameters into our dynamic SQL:
exec sp_executesql #sql, N'#datefrom datetime', #datefrom
The second parameter to sp_executesql gives the definition string for all the actual parameters, which come after it sequentially.
Now the code is entirely safe from SQL injection, and you don't have any string/date conversions to worry about.
Note that the parameter names don't need to be the same in the "inside" and "outside" parts, but they often are (for convenience and maintainability).

Stored Procedure, Passing Array of Dates, comma delimited. Conversion Error

I am trying to send a comma delimited string of dates to my stored procedure, to be used in an IN clause.
However, I am getting the error "Conversion failed when converting date and/or time from character string."
I am trying to use these dates to find prices that match.
C#:
StringBuilder LastDaysOfEveryMonth = new StringBuilder();
DataAccess da = new DataAccess();
SqlCommand cm = new SqlCommand();
cm.CommandType = CommandType.StoredProcedure;
var pList = new SqlParameter("#DateList", DbType.String);
pList.Value = LastDaysOfEveryMonth.ToString();
cm.Parameters.Add(pList);
...
cm.CommandText = "spCalculateRollingAverage";
DataSet ds = da.ExecuteDataSet(ref cm);
When debugging it, the value of the passed string is :
'2013-07-31','2013-08-30','2013-09-30','2013-10-31','2013-11-29','2013-12-31',
'2014-01-31','2014-02-28','2014-03-31','2014-04-03',
with the DbType String and SQLDbType NvarChar.
Any advice would be much appreciated!
SQL:
CREATE PROCEDURE [dbo].[spCalculateRollingAverage]
#StartDate DateTime,
#EndDate DateTime,
#Commodity nvarchar(10),
#PeakType nvarchar (10),
#BaseID int,
#NumberOfMonths int,
#DateList nvarchar(MAX)
AS
BEGIN
select TermDescription,ContractDate,Price,SortOrder into #tbtp from BaseTermPrice
inner hash join Term
on
Term.TermID = BaseTermPrice.TermID
where
BaseID = #BaseID and ((#PeakType IS NULL and PeakType is null) or
(#PeakType IS NOT NULL and PeakType=#PeakType))
and ((#DateList IS NULL and ContractDate between #StartDate and #EndDate)
or (#StartDate IS NULL and ContractDate in (#DateList)))
order by
ContractDate,SortOrder
You cannot use a varchar variable in an IN clause like this. You have to either add it to a dynamic SQL to execute, or - split it into temp table/temp variable.
For example using this SplitString function you can do something like this:
or (#StartDate IS NULL and ContractDate in
(SELECT Name from dbo.SplitString(#DateList))))

How do I convert DateTime into string?

I have a method that requires 3 string so:
public <List> MethodName(string date1, string date2, string number)
{
//
}
in my database date1 and date2 are stored as DateTime and number is stored as Int so how would I write a SQL query that will search based on these three parameters?
I have converted my date1 and date2 like this:
DateTime firstdate = Convert.ToDateTime(date1);
string fdate = firstdate.ToLongDateString();
DateTime seconddate = Convert.ToDateTime(date1);
string sdate = seconddate .ToLongDateString();
My SQL query is:
SELECT * From TableName
WHERE [Time] > #date1 AND
[Time] < #date2 AND [StaffCode] =#StaffCode;
command.Parameters.AddWithValue("#date1", fdate);
command.Parameters.AddWithValue("#date2", sdate );
command.Parameters.AddWithValue("#StaffCode", number);
conn.Open();
SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{ get the data ...}
I am sure that my SQL query is wrong.
Since your database columns are already strongly typed, just ensure that your C# parameters are typed as System.DateTime and int (System.Int32) respectively before binding them to your query. Your sql is then simply:
SELECT col1, col2, col3
FROM TableName
WHERE [Time] > #date1 AND
[Time] < #date2 AND [StaffCode] =#StaffCode;
If you allow for inclusive dates, you can use BETWEEN, i.e.
WHERE [Time] BETWEEN #date1 AND #date2
AND [StaffCode] = #StaffCode
i.e. avoid the need to convert a Date to a string altogether. Wherever possible, try and keep a strong type system all the way through your code, both C# and SQL - this will save a lot of pain during conversion. e.g. if possible, see if you can change the signature to:
public List<MethodName>(DateTime date1, DateTime date2, int number)
{
// Bind the DateTimes, not a string!
command.Parameters.AddWithValue("#date1", date1);
command.Parameters.AddWithValue("#date2", date2);
command.Parameters.AddWithValue("#StaffCode", number);
Edit Don't use SELECT * - use SELECT Col1, Col2, ...
Conversion:
DateTime firstdate = Convert.ToDateTime(date1);
string fdate = firstdate.ToString("yyyy-MM-dd");
DateTime firstdate2 = Convert.ToDateTime(date2);
string fdate2 = firstdate2.ToString("yyyy-MM-dd");
SQL Query:
#"SELECT * From TestTable WHERE Time BETWEEN CONVERT(Date,#date1) AND CONVERT(Date,#date2) AND StaffCode=CONVERT(int,#StaffCode)"
command.Parameters.AddWithValue("#date1", fdate);
command.Parameters.AddWithValue("#date2", fdate2);
command.Parameters.AddWithValue("#StaffCode", number);

Execute stored procedure with array-like string separated by comma [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Help with a sql search query using a comma delimitted parameter
I want to write a stored procedure that performs a select on a table and need one input variable of type varchar(max).
I'd like to send a bunch of values separated by , as the input parameter, e.g.
'Jack','Jane','Joe'
and then get the rows that contain one of these names.
In SQL the code would be
Select * from Personnel where Name in ('Jack','Joe','Jane');
Now I want to have a variable in my C# app, say strNames and fill it like
string strNames = "'Jack','Joe','Jane'";
and send this variable to the SP and execute it. Something like
Select * from Personnel where Name in (''Jack','Joe','Jane'') -- this is wrong
But how can I tell SQL Server to run such command?
I need to make this happen and I know it's possible, please give me the clue.
First of all, the single names don't need to be quoted when you pass them to the stored procedure.
using (SqlCommand cmd = new SqlCommand("MyStoredProc", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#longFilter", "Jack,Jill,Joe");
using (SqlDataReader reader = cmd.ExecuteReader())
{
...
}
}
Then, in the stored procedure, you can use simple text functions and a temporary table as follows to split up the string at the commas and an an entry to the temporary table for each part of the string:
DECLARE #temp AS TABLE (Name NVARCHAR(255))
IF ISNULL(#longFilter, '') <> ''
BEGIN
DECLARE #s NVARCHAR(max)
WHILE LEN(#longFilter) > 0
BEGIN
IF CHARINDEX(',', #longFilter) > 0
BEGIN
SET #s = LTRIM(RTRIM(SUBSTRING(#longFilter, 1, CHARINDEX(',', #longFilter) - 1)))
SET #longFilter = SUBSTRING(#longFilter, CHARINDEX(',', #longFilter) + 1, LEN(#longFilter))
END ELSE
BEGIN
SET #s = LTRIM(RTRIM(#longFilter))
SET #longFilter= ''
END
-- This was missing until 20140522
INSERT INTO #temp (Name) VALUES (#s)
END
END
Later use the following SELECT to get a list of all people the name of which is in #temp or all of them if #temp doesn't contain any rows (unfiltered result):
SELECT * FROM Personnel WHERE Name IN (SELECT Name FROM #temp) OR (SELECT COUNT(*) FROM #temp) = 0
You could use Table Valued Parameters.
Basically, you could insert a list of values as a parameter in the procedure, and use them as a table, something along the lines of
Select * from Personnel
where Name in (select name from #NamesTable).
Now, the specifics
To use table valued parameters, the type of the parameter must be predefined in sql server, using
create type NamesTable as table (Name varchar(50))
You can then use the defined type as a parameter in the procedure
create procedure getPersonnelList
#NamesTable NamesTable readonly
as
begin
select * from personnel
where Name in (select Name from #NamesTable)
end
You can see that in action, in this SQL Fiddle
On the C# side of things you need to create the parameter. If you have the names in a collection, and build the string, you can just use that to generate the parameter, and if they are a comma-separated string, a quick string.Split could take care of that. Since I do not know your specifics, I'll assume you have a List<string> called names. You'll need to convert that to a table valued parameter to be sent to the procedure, using something like:
DataTable tvparameter = new DataTable();
tvparameter.Columns.Add("Name", typeof(string));
foreach (string name in names)
{
tvparameter.Rows.Add(name);
}
You can find more info on how to generate a TVP in C# code in the SO Question..
Now you just need to send that parameter to the procedure, and that's that. Here is a complete console program that executes the procedure and outputs the results.
List<string> names = new List<string> { "Joe", "Jane", "Jack" };
using (SqlConnection cnn = new SqlConnection("..."))
{
cnn.Open();
using (SqlCommand cmd = new SqlCommand("getPersonnelList", cnn))
{
cmd.CommandType = CommandType.StoredProcedure;
DataTable tvparameter = new DataTable();
tvparameter.Columns.Add("Name", typeof(string));
foreach (string name in names)
{
tvparameter.Rows.Add(name);
}
cmd.Parameters.AddWithValue("#NamesTable", tvparameter);
using (SqlDataReader dr = cmd.ExecuteReader())
{
while (dr.Read())
{
Console.WriteLine("{0} - {1}", dr["ID"], dr["Name"]);
}
}
}
}
I guess you need Split Function in Sql Server to break Comma-Separated Strings into Table. Please refer these links.
Split Function in Sql Server to break Comma-Separated Strings into Table
SQL User Defined Function to Parse a Delimited String
You can select the data from table using
Select * from
Personnel where
Name in (select items from dbo.Split ('Jack,Joe,Jane',','))
You could simply check if Name is contained in the string. Note the commas at the start of the end to ensure you match the full name
string strNames = ",Jack,Joe,Jane,";
The the SQL becomes
select * from Personnel where PATINDEX('%,' + Name + ',%', #strNames) > 0
See http://www.sqlfiddle.com/#!3/8ee5a/1

How to pass sqlparameter to IN()? [duplicate]

This question already has answers here:
Pass Array Parameter in SqlCommand
(11 answers)
Closed 6 years ago.
For some reason the Sqlparameter for my IN() clause is not working. The code compiles fine, and the query works if I substitute the parameter with the actual values
StringBuilder sb = new StringBuilder();
foreach (User user in UserList)
{
sb.Append(user.UserId + ",");
}
string userIds = sb.ToString();
userIds = userIds.TrimEnd(new char[] { ',' });
SELECT userId, username
FROM Users
WHERE userId IN (#UserIds)
You have to create one parameter for each value that you want in the IN clause.
The SQL needs to look like this:
SELECT userId, username
FROM Users
WHERE userId IN (#UserId1, #UserId2, #UserId3, ...)
So you need to create the parameters and the IN clause in the foreach loop.
Something like this (out of my head, untested):
StringBuilder sb = new StringBuilder();
int i = 1;
foreach (User user in UserList)
{
// IN clause
sb.Append("#UserId" + i.ToString() + ",");
// parameter
YourCommand.Parameters.AddWithValue("#UserId" + i.ToString(), user.UserId);
i++;
}
Possible "cleaner" version:
StringBuilder B = new StringBuilder();
for (int i = 0; i < UserList.Count; i++)
YourCommand.Parameters.AddWithValue($"#UserId{i}", UserList[i].UserId);
B.Append(String.Join(",", YourCommand.Parameters.Select(x => x.Name)));
If you are using SQL 2008, you can create a stored procedure which accepts a Table Valued Parameter (TVP) and use ADO.net to execute the stored procedure and pass a datatable to it:
First, you need to create the Type in SQL server:
CREATE TYPE [dbo].[udt_UserId] AS TABLE(
[UserId] [int] NULL
)
Then, you need to write a stored procedure which accepts this type as a parameter:
CREATE PROCEDURE [dbo].[usp_DoSomethingWithTableTypedParameter]
(
#UserIdList udt_UserId READONLY
)
AS
BEGIN
SELECT userId, username
FROM Users
WHERE userId IN (SELECT UserId FROM #UserIDList)
END
Now from .net, you cannot use LINQ since it does not support Table Valued Parameters yet; so you have to write a function which does plain old ADO.net, takes a DataTable, and passes it to the stored procedure: I've written a generic function I use which can do this for any stored procedure as long as it takes just the one table-typed parameter, regardless of what it is;
public static int ExecStoredProcWithTVP(DbConnection connection, string storedProcedureName, string tableName, string tableTypeName, DataTable dt)
{
using (SqlConnection conn = new SqlConnection(connection.ConnectionString))
{
SqlCommand cmd = new SqlCommand(storedProcedureName, conn);
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter p = cmd.Parameters.AddWithValue(tableName, dt);
p.SqlDbType = SqlDbType.Structured;
p.TypeName = tableTypeName;
conn.Open();
int rowsAffected = cmd.ExecuteNonQuery(); // or could execute reader and pass a Func<T> to perform action on the datareader;
conn.Close();
return rowsAffected;
}
}
Then you can write DAL functions which use this utility function with actual names of stored procedures; to build on the example in your question, here is what the code would look like:
public int usp_DoSomethingWithTableTypedParameter(List<UserID> userIdList)
{
DataTable dt = new DataTable();
dt.Columns.Add("UserId", typeof(int));
foreach (var userId in updateList)
{
dt.Rows.Add(new object[] { userId });
}
int rowsAffected = ExecStoredProcWithTVP(Connection, "usp_DoSomethingWithTableTypedParameter", "#UserIdList", "udt_UserId", dt);
return rowsAffected;
}
Note the "connection" parameter above - I actually use this type of function in a partial DataContext class to extend LINQ DataContext with my TVP functionality, and still use the (using var context = new MyDataContext()) syntax with these methods.
This will only work if you are using SQL Server 2008 - hopefully you are and if not, this could be a great reason to upgrade! Of course in most cases and large production environments this is not that easy, but FWIW I think this is the best way of doing this if you have the technology available.
SQL Server sees your IN clause as:
IN ('a,b,c')
What it needs to look like is:
IN ('a','b','c')
There is a better way to do what you're trying to do.
If the user id's are in the DB, then the IN clause should be changed to a subquery, like so:
IN (SELECT UserID FROM someTable WHERE someConditions)
This is a hack -- it doesn't work well with indexes, and you have to be careful it works right with your data, but I've used it successfully in the past:
#UserIDs LIKE '%,' + UserID + ',%' -- also requires #UserID to begin and end with a comma

Categories