EF 6.x and function with table-valued parameter - c#

I have a DB function requiring a table-valued parameter as argument (#c).
CREATE TABLE Test
(
CD varchar(10) not null
)
GO
INSERT INTO Test VALUES ('TEST')
GO
CREATE TYPE [CdTable] AS TABLE (CD varchar(10));
GO
CREATE FUNCTION TestTbl ( #x varchar(10), #c CdTable READONLY )
RETURNS TABLE
AS
RETURN
SELECT t.CD
FROM test t
JOIN #c c ON t.CD = c.CD OR c.CD IS NULL
WHERE t.CD = #x
GO
DECLARE #tt AS CdTable;
INSERT INTO #tt VALUES ('TEST');
SELECT * FROM TestTbl('TEST', #tt);
DELETE FROM #tt;
INSERT INTO #tt VALUES (NULL);
SELECT * FROM TestTbl('TEST', #tt);
GO
The function is built from the EF Designer (Database First) as this in the DbContext:
[DbFunction("MyDbContext", "TestTbl")]
public virtual IQueryable<TestTbl_Result> TestTbl(Nullable<System.String> x)
{
var xParameter = user.HasValue ?
new ObjectParameter("x", x) :
new ObjectParameter("x", typeof(System.String));
return ((IObjectContextAdapter)this).ObjectContext.CreateQuery<TestTbl_Result>("[MyDbContext].[TestTbl](#x)", xParameter);
}
If I call this function passing only the available x/#x parameter I get this exception:
ex {"An error occurred while executing the command definition. See the inner exception for details."} System.Exception {System.Data.Entity.Core.EntityCommandExecutionException}
ex.InnerException {"An insufficient number of arguments were supplied for the procedure or function TestTbl."} System.Exception {System.Data.SqlClient.SqlException}
I don't know how to pass the #c parameter to the function. Anyone can help?
Thanks in advance.
p.s.: I am using MS SQL 2012 (or newer)

You should use another method ExecuteStoreQuery that allows specifying table valued parameters (SqlDbType.Structured).
using (var table = new DataTable ())
{
table.Columns.Add("cs", typeof(string));
foreach (var item in ITEMS)
table.Rows.Add(item.CD.ToString());
var param1 = new SqlParameter("#x", SqlDbType.NVarChar)
{
Value = myValue
};
var param2 = new SqlParameter("#c", SqlDbType.Structured)
{
Value = table
};
((IObjectContextAdapter)this).ObjectContext.ExecuteStoreQuery<TestTbl_Result>(
"select * from [TestTbl](#x, #c)", param1, param2);
}

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).

Error executing stored procedure with datatable parameter in c#

I'm trying to create a stored procedure to add or update a collection of object executed in c#, but not sure why it isn't executing correctly in c# while it works fine in sql.
I'm not sure how generate some useful error message to help with debugging either.
Any tip or pointer to guide me to the right direction is greatly appreciated.
Here's the sample code:
SQL script:
CREATE TYPE CodeList
AS TABLE
(
Code varchar(255) NOT NULL UNIQUE,
Name varchar(max) NOT NULL,
GeneratedDate date NULL
);
GO
PRINT 'Created CodeList Type';
GO
CREATE PROCEDURE AddOrUpdateCodes
#List AS CodeList READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE dbo.Code AS tgt
USING #List AS src
ON tgt.Code = src.Code
WHEN MATCHED THEN
UPDATE SET Name = src.Name, GeneratedDate = src.GeneratedDate
WHEN NOT MATCHED THEN
INSERT (Code, Name, GeneratedDate) VALUES (src.Code, src.Name, src.GeneratedDate);
END
GO
PRINT 'Created AddOrUpdateCodes Stored Procedure';
GO
DECLARE #List AS CodeList;
INSERT INTO #List (Code, Name, GeneratedDate) VALUES ('SQLTEST', 'SQLTEST', '2018-07-30')
EXEC AddOrUpdateCodes #List
GO
SELECT * FROM Symbol;
C# Code:
public int AddOrUpdateCodes(List<Code> codes)
{
using (var entity = new CodeEntities())
{
var dataTable = new DataTable("CodeList");
using (var reader = ObjectReader.Create(codes, "Code", "Name", "GeneratedDate"))
{
dataTable.Load(reader);
}
var sqlParameter = new SqlParameter("#List", dataTable);
sqlParameter.SqlDbType = SqlDbType.Structured;
sqlParameter.TypeName = "dbo.CodeList";
var result = entity.Database.ExecuteSqlCommand("AddOrUpdateCodes",
sqlParameter);
entity.SaveChanges();
return result;
}
}
The DbContext.Database.ExecuteSqlCommand() method helps to executes the given DDL/DML command against the database. Try using this line passing SqlParameters:
var result = entity.Database.ExecuteSqlCommand("AddOrUpdateCodes #List",
sqlParameter);
entity.SaveChanges();
return result;

Entity framework user defined table type proc

I have a solution that bulk loads data via a datatable from c#, like this :
private void LoadRemittanceLines(DataTable dt, EiseBEDbEntities context)
{
var param = new SqlParameter("Payments", SqlDbType.Structured)
{
Value = dt,
TypeName = "dbo.udtRAPayment",
Direction = ParameterDirection.Input
};
var r = context.Database.ExecuteSqlCommand("EXEC spRAPaymentInsertBulk", param);
}
The line fires, no errors, but the table yields no results. I profiler'ed my Db, and caught the code that executes. Here is a summarized, masked example:
--THIS IS ADDED BY ME TO CLEAR THE TABLE
delete from EiseBEDb..RAPayment
declare #p3 dbo.udtRAPayment
insert into #p3 values(1234879,987654,1,123.49,'2017-09-20 00:00:00','2017-09-20 00:00:00',NULL,0,'2018-07-22 00:00:00',10,0,NULL,NULL,1,N'This is a long string',NULL,NULL,NULL,N'',N'',N'1234',N'','2017-09-12 00:00:00',NULL,NULL)
--THIS CODE YIELDS RESULTS
--EXEC spRAPaymentInsertBulk #Payments=#p3
--THIS CODE YIELDS NO RESULTS
exec sp_executesql N'EXEC spRAPaymentInsertBulk',N'#Payments [dbo].[udtRAPayment] READONLY',#Payments=#p3
--THIS IS ADDED BY ME TO CHECK THE DATA
select * from EiseBEDb..RAPayment
What am I missing, and is there a way I can get passed this? The only solution I can think of is to not use EF to pass my data, but rather just making a connection to the Db manually end calling that working line of code.
Here is spRAPaymentInsertBulk, fieldnames omitted due to confidentiality.
CREATE PROCEDURE [dbo].[spRAPaymentInsertBulk]
(
#Payments udtRAPayment readonly
)
AS
begin
insert into RAPayment (Field1, field2)
select Field1, field2
from #Payments p
end
I'm using SQL Server 2017.
My problem was with the text I passed, this resolved the issue:
var r = context.Database.ExecuteSqlCommand("exec spRAPaymentInsertBulk #Payments", param);

Pass table value type to SQL Server stored procedure via Entity Framework

I created a user-defined table type in SQL Server:
CREATE TYPE dbo.TestType AS TABLE
(
ColumnA int,
ColumnB nvarchar(500)
)
And I'm using a stored procedure to insert records into the database:
create procedure [dbo].[sp_Test_CustomType]
#testing TestType READONLY
as
insert into [dbo].[myTable]
select ColumnA, ColumnB
from #testing
And I would like to use EF to execute this stored procedure, but here's the problem: how can I pass a user defined table to the stored procedure?
I tried adding the stored procedure to the model, but I'm unable to find the desired stored procedure in the updated context.
What I'm trying to do is to execute a bulk insert to a table, here's the method that I'm currently using:
List<items> itemToInsertToDB = //fetchItems;
foreach(items i in itemToInsertToDB)
{
context.sp_InsertToTable(i.ColumnA, i.ColumnB)
}
Currently, I use a foreach loop to loop through the list to insert item to DB, but if the list have a lot of items, then there will be a performance issue, so, I'm thinking of passing a list to the stored procedure and do the insert inside.
So how to solve this problem? or are there any better ways to do this?
Lets say you want to send a table with a single column of GUIDs.
First we need to create a structure using SqlMetaData which represents the schema of the table (columns).
The below code demonstrates one column named "Id" of the GUID is the SQL stored procedure parameter table type
var tableSchema = new List<SqlMetaData>(1)
{
new SqlMetaData("Id", SqlDbType.UniqueIdentifier)
}.ToArray();
Next you create a list of records that match the schema using SqlDataRecord.
The below code demonstrates how to add the items inside a list using the above created schema. Create a new SqlDataRecord for each of the items in the list. Replace SetGuid with the corresponding type and Replace Guid.NewGuid() as the corresponding value.
Repeat new SqlDataRecord for each item and add them to a List
var tableRow = new SqlDataRecord(tableSchema);
tableRow.SetGuid(0, Guid.NewGuid());
var table = new List<SqlDataRecord>(1)
{
tableRow
};
Then create the SqlParameter:
var parameter = new SqlParameter();
parameter.SqlDbType = SqlDbType.Structured;
parameter.ParameterName = "#UserIds"; //#UserIds is the stored procedure parameter name
parameter.TypeName = "{Your stored procedure type name}"
parameter.Value = table;
var parameters = new SqlParameter[1]
{
parameter
};
Then simply call the stored procedure by using the Database.SqlQuery.
IEnumerable<ReturnType> result;
using (var myContext = new DbContext())
{
result = myContext.Database.SqlQuery<User>("GetUsers #UserIds", parameters)
.ToList(); // calls the stored procedure
// ToListAsync(); // Async
{
In SQL Server, create your User-Defined Table Type (I suffix them with TTV, Table Typed Value):
CREATE TYPE [dbo].[UniqueidentifiersTTV] AS TABLE(
[Id] [uniqueidentifier] NOT NULL
)
GO
Then specify the type as a parameter (don't forget, Table Type Values have to be readonly!):
CREATE PROCEDURE [dbo].[GetUsers] (
#UserIds [UniqueidentifiersTTV] READONLY
) AS
BEGIN
SET NOCOUNT ON
SELECT u.* -- Just an example :P
FROM [dbo].[Users] u
INNER JOIN #UserIds ids On u.Id = ids.Id
END
I suggest you not using Stored Procedure to insert bulk data, but just rely to Entity Framework insert mechanism.
List<items> itemToInsertToDB = //fetchItems;
foreach(items i in itemToInsertToDB)
{
TestType t = new TestType() { ColumnA = i.ColumnA, ColumnB = i.ColumnB };
context.TestTypes.Add(t);
}
context.SaveChanges();
Entity framework will smartly perform those insertion in single transaction and (usually) in single query execution, which will almost equal to executing stored procedure. This is better rather than relying on stored procedure just to insert bulk of data.

Pass RowID from application to a SQL Server stored procedure (or alternative)

I have a method in my application which is passing list<listitem> into a stored procedure. I have created a table type data type to pass list<listitem>. Now I need to loop through in stored procedure to insert into another table. For that purpose I created row id column in the table type which is generated automatically.
Since the table type has 2 columns, it expects 2 parameters to pass from outside but I am generating it by identity column. Is there any way to avoid so that I don't pass value form outside?
public void test(List<string> listItem) {
var table = new DataTable();
table.Columns.Add("col1", typeof(string));
foreach (string col1 in listItem) { table.Rows.Add(col1); }
SqlCommand cmd1 = new SqlCommand("TableTypeUpdateAnswers", conn);
cmd1.CommandType = CommandType.StoredProcedure;
SqlParameter sqlParam = cmd1.Parameters.AddWithValue("#tvpUpdateAnswers",table);
sqlParam.SqlDbType = SqlDbType.Structured ;
sqlParam.TypeName = "dbo.AnswerTableType";
conn.Open();
try
{ }
catch (Exception e)
{ }
}
Here is the SQL to create table type :
CREATE TYPE [dbo].[AnswerTableType] AS TABLE(
RowID int not null primary key identity(1,1),
[col1] [nvarchar](50) NULL
)
And here is the stored procedure:
ALTER PROCEDURE [dbo].[TestTableType]
#TVPUPDATEANSWERS DBO.ANSWERTABLETYPE READONLY
AS
DECLARE
#CURRENTROW INT,
#VANSID int ,
#ROWSTOPROCESS INT
BEGIN
SET #CURRENTROW=0
SELECT L.col1 FROM #TVPUPDATEANSWERS L;
SET #ROWSTOPROCESS=##ROWCOUNT
WHILE #CURRENTROW<#ROWSTOPROCESS
BEGIN
SET #CURRENTROW=#CURRENTROW+1
(SELECT #VANSID = col1
FROM #TVPUPDATEANSWERS
WHERE ROWID=#CURRENTROW);
//do something with table type datatype
INSERT INTO DBO.table1(col3,col4)
VALUES(#VANSID ,#col4);
END
END
Seems like you may benefit from using this kind of approach - check it out and let me know... http://ilovedevelopment.blogspot.co.uk/2012/01/manage-multiple-updates-to-data-using-c.html

Categories