45

I am trying to pass a table-value parameter to a stored procedure, but I keep getting an exception (see below).

SqlCommand c = new SqlCommand("getPermittedUsers", myConn) { CommandType = CommandType.StoredProcedure };

c.Parameters.AddWithValue("@intNotifyingUserId", notifyingUserId);
c.Parameters.AddWithValue("@tSelectedPdfIds", sharedPdfs).SqlDbType = SqlDbType.Structured;

SqlDataReader dr = c.ExecuteReader();

The type is defined on the server like this:

CREATE TYPE [dbo].[IdList] AS TABLE(
    [Id] [int] NOT NULL
)

I have tried passing sharedPdfs as a List<int>, and IQueryable<int>, but keep getting the following exception:

Object must implement IConvertible.

Anyone know what I am doing wrong? The documentation implies that I should be able to pass a list as a TVP but doesn't give any examples.

Thank you.

nw.
  • 4,795
  • 8
  • 37
  • 42

3 Answers3

56

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;
    }
}
Ryan Prechel
  • 6,592
  • 5
  • 23
  • 21
  • 1
    was trying to get tvp working from what i found on http://sqlperformance.com/2012/08/t-sql-queries/splitting-strings-now-with-less-t-sql , but kept getting dbnull values. Your example ended up working for me, might be the parameter.typename. Thanks! – Phil Apr 04 '16 at 15:29
  • NB: For those who don't wish to create a custom type for this, a workaround is to pass data as an xml type, then query against that. – JohnLBevan Jun 10 '17 at 12:15
  • 2
    This is not working in .Net CORE 3.1. Always gives an error of type "Failed to convert parameter value from a to a IEnumerable`1." – Yatharth Varshney Jan 27 '20 at 13:22
  • 1
    I had some trouble with type conversion in .Net Core 3.1 as well. It worked when I used command.Parameters.Add("@Display",SqlDbType.Structured)) {TypeName="dbo.PageViewTableType", Value=CreateSqlDataRecords(ids)} – Steven McCormack Feb 04 '20 at 19:28
21

You can pass the parameter as a DataTable, IEnumerable<SqlDataRecord>, or DbDataReader.

Jared Moore
  • 3,765
  • 26
  • 31
  • 1
    Yes, those are the supported types for a Table-Value Parameter in SqlCommand. The full documentation is here: http://msdn.microsoft.com/en-us/library/bb675163.aspx . See 1/3 of the way down: "System.Data.SqlClient supports populating table-valued parameters from DataTable, DbDataReader or System.Collections.Generic.IEnumerable ([T:System.Collections.Generic.IEnumerable`1)] objects." – Jared Moore Sep 16 '11 at 17:22
  • 1
    is this also possible in Oracle? – Hector Sanchez Oct 24 '11 at 21:10
  • Table-Valued Parameters is a SQL Server feature. Oracle may or may not have its own similar feature, I do not know. – Jared Moore Oct 25 '11 at 22:36
  • @Mr. No but check out UDTs and ntypes. A UDT is analogous to a class, an ntype to a collection/list. I've passed very complex objects between .net and the database using this mechanism. You need to be using ODP.net, and you need to be aware of which version also as support for UDTs was only added relatively recently. – PeteH Nov 22 '12 at 14:26
5

Adding a new answer with updated links.

According to the documentation (learn.microsoft.com), you can use one of the following parameter types:

SqlClient supports populating table-valued parameters from DataTable, DbDataReader or IEnumerable \ SqlDataRecord objects. You must specify a type name for the table-valued parameter by using the TypeName property of a SqlParameter. The TypeName must match the name of a compatible type previously created on the server.

Not included in the docs but important for high-performance apps, a sample using IEnumerable<SqlDataRecord> (.NET Core 3.1, async):

  using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
  using SqlConnection connection = this.GetConnection();
  await connection.OpenAsync(timeout.Token);

  using SqlCommand command = connection.CreateCommand();
  command.CommandType = CommandType.StoredProcedure;
  command.CommandText = "Mycommand";

  IEnumerable<SqlDataRecord> records = // ...

  SqlParameter parameter = command.Parameters.Add("@MyObjects", SqlDbType.Structured);
  parameter.TypeName = "MyCustomTableType";
  parameter.Value = records;

  await command.ExecuteNonQueryAsync(timeout.Token);

Example using a DataTable:

  // Create a DataTable with the modified rows.  
  DataTable addedCategories = CategoriesDataTable.GetChanges(DataRowState.Added);  

  // Configure the SqlCommand and SqlParameter.  
  SqlCommand insertCommand = new SqlCommand("usp_InsertCategories", connection);  
  insertCommand.CommandType = CommandType.StoredProcedure;  
  SqlParameter tvpParam = insertCommand.Parameters.AddWithValue("@tvpNewCategories", addedCategories);  
  tvpParam.SqlDbType = SqlDbType.Structured;  

  // Execute the command.  
  insertCommand.ExecuteNonQuery(); 

Example using DbDataReader:

 // Assumes connection is an open SqlConnection.  
 // Retrieve data from Oracle.  
 OracleCommand selectCommand = new OracleCommand(  
     "Select CategoryID, CategoryName FROM Categories;",  
     oracleConnection);  
 OracleDataReader oracleReader = selectCommand.ExecuteReader(  
     CommandBehavior.CloseConnection);  

  // Configure the SqlCommand and table-valued parameter.  
  SqlCommand insertCommand = new SqlCommand(  
      "usp_InsertCategories", connection);  
  insertCommand.CommandType = CommandType.StoredProcedure;  
  SqlParameter tvpParam =  
      insertCommand.Parameters.AddWithValue(  
      "@tvpNewCategories", oracleReader);  
  tvpParam.SqlDbType = SqlDbType.Structured;  

  // Execute the command.  
  insertCommand.ExecuteNonQuery(); 
Chris Gillum
  • 14,526
  • 5
  • 48
  • 61