How to get newly inserted Ids from identity primary key column when using SqlBulkCopy - c#

I'm using C# and .NET Core 6. I want to bulk insert about 100 rows at once into database and get back their Ids from BigInt identity column.
I have tried lot of different variants, but still do not have the working solution. When I preview table variable, the Id column has DbNull value and not the newly inserted Id.
How to get Ids of newly inserted rows?
What I have:
SQL Server table:
CREATE TABLE dbo.ExamResult
(
Id bigint IDENTITY(1, 1) NOT NULL,
Caption nvarchar(1024) NULL,
SortOrder int NOT NULL,
CONSTRAINT PK_ExmRslt PRIMARY KEY CLUSTERED (Id)
) ON [PRIMARY]
GO
C# code:
private static void BulkCopy(IEnumerable<ExamResultDb> examResults, SqlConnection connection)
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, null))
{
var table = CreateDataTable(examResults);
bulkCopy.DestinationTableName = "ExamResult";
bulkCopy.EnableStreaming = true;
foreach (var item in table.Columns.Cast<DataColumn>())
{
if (item.ColumnName != "Id")
{
bulkCopy.ColumnMappings.Add(item.ColumnName, item.ColumnName);
}
}
using (var reader = new DataTableReader(table))
{
bulkCopy.WriteToServer(reader);
}
}
}
private static DataTable CreateDataTable(IEnumerable<ExamResultDb> examResults)
{
var table = new DataTable("ExamResult");
table.Columns.AddRange(new[]
{
new DataColumn("Id", typeof(long)),
new DataColumn("Caption", typeof(string)),
new DataColumn("SortOrder", typeof(int))
});
////table.Columns[0].AutoIncrement = true;
////table.PrimaryKey = new[] { table.Columns[0] };
foreach (var examResult in examResults)
{
var row = table.NewRow();
row["Caption"] = examResult.Caption;
row["SortOrder"] = examResult.SortOrder;
table.Rows.Add(row);
}
return table;
}

You need an OUTPUT clause on your insert, but Bulk Copy does not allow this kind of customization.
While nowhere near as neat, you could do this by using a Table Valued Parameter and a custom INSERT statement. TVPs use the bulk copy mechanism, so this should still be pretty fast, although there will be two inserts: one to the TVP and one to the real table.
First create a table type
CREATE TYPE dbo.Type_ExamResult AS TABLE
(
Caption nvarchar(1024) NULL,
SortOrder int NOT NULL
);
This function will iterate the rows as SqlDatRecord
private static IEnumerable<SqlDataRecord> AsExamResultTVP(this IEnumerable<ExamResultDb> examResults)
{
// fine to reuse object, see https://stackoverflow.com/a/47640131/14868997
var record = new SqlDataRecord(
new SqlMetaData("Caption", SqlDbType.NVarChar, 1024),
new SqlMetaData("SortOrder", SqlDbType.Int)
);
foreach (var examResult in examResults)
{
record.SetString(0, examResult.Caption);
record.SetInt32(0, examResult.SortOrder);
yield return record; // looks weird, see above link
}
}
Finally insert using OUTPUT
private static void BulkCopy(IEnumerable<ExamResultDb> examResults, SqlConnection connection)
{
const string query = #"
INSERT dbo.ExamResult (Caption, SortOrder)
OUTPUT Id, SortOrder
SELECT t.Caption, t.SortOrder
FROM #tvp t;
";
var dict = examResults.ToDictionary(er => er.SortOrder);
using (var comm = new SqlCommand(query, connection))
{
comm.Parameters.Add(new SqlParameter("#tmp", SqlDbType.Structured)
{
TypeName = "dbo.Type_ExamResult",
Value = examResults.AsExamResultTVP(),
});
using (var reader = comm.ExecuteReader())
{
while(reader.Read())
dict[(int)reader["SortOrder"]].Id = (int)reader["Id"];
}
}
}
Note that the above code assumes that SortOrder is a natural key within the dataset to be inserted. If it is not then you will need to add one, and if you are not inserting that column then you need a rather more complex MERGE statement to be able to access that column in OUTPUT, something like this:
MERGE dbo.ExamResult er
USING #tvp t
ON 1 = 0 -- never match
WHEN NOT MATCHED THEN
INSERT (Caption, SortOrder)
VALUES (t.Caption, t.SortOrder)
OUTPUT er.Id, t.NewIdColumn;

Related

Pass table as input param to stored procedure using odbc

I have something like that on DB side:
CREATE TYPE dbo.idstable AS TABLE
(
Idx bigint NOT NULL PRIMARY KEY
);
GO
CREATE PROCEDURE [usp_GetIds]
#input idstable READONLY
AS
SELECT * from #input
go
Id like to call usp_GetIds from C# code using odbc.
using (var command = new OdbcCommand("usp_GetIds", conn))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddRange(parameters);
using (var reader = command.ExecuteReader())
{
....
}
}
Right now I stuck on something like that:
var parameters = new[] { new OdbcParameter("#input", CreateDataTable(ids)) };
private static DataTable CreateDataTable(IEnumerable<int> Ids)
{
DataTable table = new DataTable();
table.Columns.Add("Idx", typeof(Int23));
foreach (int id in Ids)
{
table.Rows.Add(id);
}
return table;
}
and message:
"No mapping exists from object type System.Data.DataTable to a known managed provider native type."
Is there any way to pass table as input param to stored procedure using odbc?
or I have to use SqlConnection/SqlCommand/SqlParameter?

Optimizing my insertion to SQL table while parsing through a long list of string

I am not sure if the way I'm inserting to values while I parse is the most efficient way of doing this. I am splitting the logic where each info will be stored based upon whether it's a string or int value. The each info's ID will be stored in Info table where ID is the ID and InfoDescription is the name.
ID Name DataType
1 weight int
2 price int
3 avilability string
CREATE TABLE [dbo].[Info](
[InfoID] [smallint] NOT NULL,
[InfoDescription] [varchar](100) NOT NULL,
[CreateDate] [datetime] NOT NULL
)
so weight will have INfoID of 1 and InfoDescription of "weight"
I am going to insert values into this table while I parse through a string.
CREATE TABLE [dbo].[fruitLog]
(
[LogID] [bigint] NOT NULL,//auto increment maybe
[InfoID] [smallint] NOT NULL,
[InfoInt] [bigint] NOT NULL,
[InfoString] [varchar](100) NOT NULL,
)
For example something like "weight:50, price:10, avilability: yes"
-weight will have InfoID = 1, InfoInt=50(0 is default), InfoString=""(since weight is a type of int and not a string) for fruitLog
-avilability will have InfoID = 3, InfoInt=0 (0 is default), InfoString="yes" for fruitLog
This is the logic of my code:
//making a key value pair for the Info table so if I have a key of "Weight", I have it's value as 1(ID).
Dictionary<string,int> MsgList = new Dictionary<string,int>();
var list = result from "SELECT * FROM Info"//didn't want to write the whole code out
foreach(var item in list)
{
MsgList.Add(item.InfoDescription, item.InfoID);
}
foreach(var fruits in listOfFruits)
{
get.InfoCOMMAND();// will return something like {weight:50, price:10, avilability: yes}
//loop through the Info text and parse
{
if (parsedVariable.Equals("weight"))
{
InertIntoSQL( MsgList["weight"] , Convert.ToInt32(parsedVariable.value),"");
}
if (parsedVariable.Equals("price"))
{
InertIntoSQL( MsgList["price"] , Convert.ToInt32(parsedVariable.value, "");
}
if (parsedVariable.Equals("avilability"))
{
InertIntoSQL( MsgList["avilability"] , 0 , parsedVariable.value.toString());
}
}
}
InertIntoSQL will be a method that will insert those variables to the table.\
I'm going to have over 30 columns for the diagnostic log so I was wondering if there was an more efficient way of doing this. Also if you know the best way to parse through the string let me know. Thank you.
I think what you're looking for is the SqlBulkCopy. I had to use it a little bit ago to populate a bunch of data into SQL for analysis purposes - here's what my function looked like:
private void PopulateTable(DataTable data, string tableName)
{
string connectionString = #"Server=PC40808\SQLEXPRESS;Database=Scratchpad;Trusted_Connection=True;";
SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
SqlTransaction transaction = conn.BeginTransaction();
try
{
SqlBulkCopy copy = new SqlBulkCopy(conn, SqlBulkCopyOptions.KeepIdentity, transaction);
copy.DestinationTableName = tableName;
copy.WriteToServer(data);
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
MessageBox.Show(ex.ToString());
}
finally
{
conn.Close();
}
}
... basically, you create a DataTable that matches your SQL table. Populate the rows of your DataTable - and then, finally, you submit the DataTable to SQL via SqlBulkCopy (all part of the System.Data.SqlClient library.)
Anyway, if it helps, here's my function that creates a blank DataTable for my particular case:
private DataTable GetBlankDTForRetrievals()
{
DataTable retVal = new DataTable("retrievals");
retVal.Columns.Add("ObjectID", typeof(string));
retVal.Columns.Add("DateTimeStamp", typeof(string));
retVal.Columns.Add("Username", typeof(string));
retVal.Columns.Add("DocClass", typeof(string));
retVal.Columns.Add("Func", typeof(string));
return retVal;
}
... as well as a function that populated my DataTable:
private DataTable GetDataTableForSingleRetrievalFile(string fileLoc)
{
DataTable retVal = GetBlankDTForRetrievals();
string[] lines = System.IO.File.ReadAllLines(fileLoc);
foreach(string line in lines.Where(l => l.Length > 94).Where((l) => l.StartsWith(" ")))
{
DataRow rowToAdd = retVal.NewRow();
rowToAdd["ObjectID"] = line.Substring(48, 15);
rowToAdd["DateTimeStamp"] = line.Substring(1, 17);
rowToAdd["Username"] = line.Substring(32, 15);
rowToAdd["DocClass"] = line.Substring(69, 20);
rowToAdd["Func"] = line.Substring(90, 4);
retVal.Rows.Add(rowToAdd);
}
return retVal;
}

Insert multiple rows with parameters Sql Server

I am trying to figure out if there is a way to perform a multiple values insert in Sql Server while using parameters, to be precise, having a command like this:
com = new SqlCommand("insert into myTable values (#recID,#tagID)", con);
com.Parameters.Add("#recID", SqlDbType.Int).Value = recID;
com.Parameters.Add("#tagID", SqlDbType.Int).Value = tagID;
com.ExecuteNonQuery();
Is there a way to perform a multiple values single insert with parameters taking into account that parameters may be different for each value? (Example: tagID may be always different)
I have been searching in Internet but no luck so far, thanks in advance, greetings.
You can use a table valued parameters : How to pass table value parameters to stored procedure from .net code
First, create the type, in SQL Server :
CREATE TYPE [dbo].[myTvpType] AS TABLE
(
[RecordID] int,
[TagID] int
)
And the C# code to insert your data :
internal void InsertData(SqlConnection connection, Dictionary<int, int> valuesToInsert)
{
using (DataTable myTvpTable = CreateDataTable(valuesToInsert))
using (SqlCommand cmd = connection.CreateCommand())
{
cmd.CommandText = "INSERT INTO myTable SELECT RecordID, TagID FROM #myValues";
cmd.CommandType = CommandType.Text;
SqlParameter parameter = cmd.Parameters.AddWithValue("#myValues", myTvpTable);
parameter.SqlDbType = SqlDbType.Structured;
cmd.ExecuteNonQuery();
}
}
private DataTable CreateDataTable(Dictionary<int, int> valuesToInsert)
{
// Initialize the DataTable
DataTable myTvpTable = new DataTable();
myTvpTable.Columns.Add("RecordID", typeof(int));
myTvpTable.Columns.Add("TagID", typeof(int));
// Populate DataTable with data
foreach(key in valuesToInsert.Key)
{
DataRow row = myTvpTable.NewRow();
row["RecordID"] = valuesToInsert[key];
row["TagID"] = key;
}
}
You can do this by sending your data as an xml string and convert in into table in a stored procedure in sql. For example:
suppose I am sending multiple rows to add/update in an sql table then here are the steps:
Convert your class or list of class into an xml string using following method:
public static string SerializeObjectToXmlString(object value)
{
var emptyNamepsaces = new XmlSerializerNamespaces(new[] {
XmlQualifiedName.Empty });
var serializer = new XmlSerializer(value.GetType());
var settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = true;
using (var stream = new StringWriter())
using (var writer = XmlWriter.Create(stream, settings))
{
serializer.Serialize(writer, value, emptyNamepsaces);
return stream.ToString();
}
}
Now while sending data to the database convert your class object into xml
string (Here I am using entity framework in my code, you can do this without using it as well):
bool AddUpdateData(List<MyClass> data)
{
bool returnResult = false;
string datatXml = Helper.SerializeObjectToXmlString(data);
var sqlparam = new List<SqlParameter>()
{
new SqlParameter() { ParameterName = "dataXml", Value = datatXml}
};
var result = this.myEntity.Repository<SQL_StoredProc_ComplexType>().ExecuteStoredProc("SQL_StoredProc", sqlparam);
if (result != null && result.Count() > 0)
{
returnResult = result[0].Status == 1 ? true : false;
}
return returnResult;
}
Now your SQL Code:
3.1 Declare a table variable:
DECLARE #tableVariableName TABLE
(
ID INT, Name VARCHAR(20)
)
3.2 Insert Your xml string into Table variable
INSERT INTO #tableVariableName
SELECT
Finaldata.R.value ('(ID/text())[1]', 'INT') AS ID,
Finaldata.R.value ('(Name/text())[1]', 'VARCHAR(20)') AS Name
FROM #MyInputXmlString.nodes ('//ArrayMyClass/MyClass') AS Finaldata (R)
3.3 Finally insert this table value into your sql table
INSERT INTO MyTable (ID, Name)
SELECT ID, Name
FROM #tableVariableName
This will save your effort of hitting database again and again using a for loop.
Hope it will help you

How to bulk-update a table using Sql [duplicate]

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);
}

How to pass table value parameters to stored procedure from .net code

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);
}

Categories