192

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?

Dale K
  • 25,246
  • 15
  • 42
  • 71
Marek Kwiendacz
  • 9,524
  • 14
  • 48
  • 72
  • Check out this solution: Stored Procedure with Table-Valued Parameter in EF. https://code.msdn.microsoft.com/Stored-Procedure-with-6c194514 – Carl Prothman Sep 22 '16 at 15:47
  • In a case like this one, I usually concatenate strings and split them on the server side or pass even an xml if I have multiple columns. Sql it's very fast when processing xml. You can try all the methods and check the processing time and after that choose the best method. An XML would look like .... The process on Sql Server is also simple. Using this method, you can always add a new attribute to if you need more information. – Nițu Alexandru Dec 22 '16 at 07:51
  • 4
    @NițuAlexandru, "Sql it's very fast when processing xml.". Not even close. – nothrow Dec 12 '18 at 12:25

6 Answers6

312

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;
    }
}
Kjartan
  • 18,591
  • 15
  • 71
  • 96
Ryan Prechel
  • 6,592
  • 5
  • 23
  • 21
  • 28
    +1 Excellent example. Takeaways are: send a `DataTable` as the parameter value, set `SqlDbType` to `Structured` and `TypeName` to the database UDT name. – lc. Jan 25 '13 at 09:52
  • 10
    If you are going to reuse an instance of a reference type in a loop (SqlDataRecord in your example), please please add a comment on why it is safe to do so in this particular instance. – Søren Boisen Jan 21 '16 at 11:42
  • 2
    This code is wrong: empty table valued parameters should have their value set to `null`. `CreateSqlDataRecords` will never return `null` if given an empty `ids` parameter. – ta.speot.is Mar 13 '16 at 23:09
  • Also, `DataTable` is a `IDisposable` and hence its `Dispose` method should be called before it goes out of scope. – Crono Apr 18 '16 at 19:21
  • 4
    @Crono: `DataTable`(or `DataSet`) only implement it because they have to suppiort drag&drop capabilities in Visual-Studio, so they implement `IComponent` which implements `IDisposable`. If you don't use the designer but create it manually there's no reason to dispose it (or to use the `using`-statement). So this is one of the exceptions of the golden rule "dispose everything that implements `IDisposable`". – Tim Schmelter Jul 19 '16 at 08:56
  • 2
    @TimSchmelter As a rule of thumb I always call `Dispose` methods, even if it's only so that Code Analysis won't warn me if I don't. But I agree that in this specific scenario where base `DataSet` and `DataTable` instances are used, calling `Dispose` wouldn't do anything. – Crono Aug 30 '16 at 16:58
  • 2
    @SørenBoisen: According to the remarks section of https://msdn.microsoft.com/en-us/library/microsoft.sqlserver.server.sqldatarecord.aspx , "When writing common language runtime (CLR) applications, you should re-use existing SqlDataRecord objects instead of creating new ones every time. ". This doesn't always apply, but it probably applies here. – Brian Dec 04 '17 at 18:59
  • 3
    **Future Users**, please note this: `DataTable` approach consumes the hell of a memory, whereas the `IEnumerable` one simply does not. – AgentFire Mar 03 '18 at 09:58
  • 1
    @AgentFire Is this only for situations where you need to send a lot of data in that `DataTable` or for all? In my use case I'll just need to send a table with one column and no more than 10 rows – Rui Taborda Jul 10 '18 at 09:14
  • 2
    @RuiTaborda when you need to send lots of data, use `IEnumerable` approch. Otherwise, use whatever. – AgentFire Jul 10 '18 at 15:21
  • 1
    Is there an advantage to using the `DataTable` approach? – Panzercrisis Sep 22 '20 at 21:10
34

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.

KyleMit
  • 30,350
  • 66
  • 462
  • 664
Scotty.NET
  • 12,533
  • 4
  • 42
  • 51
15

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;

    }
Martea
  • 507
  • 1
  • 5
  • 18
  • Would you please let me know that what do I pass as parameter? Func selector? Can't it be simply tbl.Rows.Add(item) and no need of that parameter. – GDroid Mar 18 '15 at 20:59
  • the selector.Invoke(item) selects the property on the item most cases its a int, but it also allows you to select a string property – Martea Mar 20 '15 at 16:44
  • can you please provide an example of how do I put selector over there?? I have a List to pass to stored proc... – GDroid Mar 23 '15 at 03:31
  • guidList.ToTabledValuedParameter(x=>x), since x is the guid in your case, the return will be a DataTable with one column(id) with a list of guids, – Martea Apr 24 '15 at 11:55
5

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);
Shahzad Qureshi
  • 1,776
  • 14
  • 15
  • 1
    what if the paramCollection is NULL ? How to pass empty type ? – Muflix Aug 31 '16 at 12:53
  • 2
    @Muflix Obscurely, extension methods actually work against null instances. So adding a simple `if(paramCollection != null)` check at the top of the method will be fine – Rhumborl Mar 16 '17 at 21:41
  • 1
    Updated answer with initial -if- check – Shahzad Qureshi Mar 25 '17 at 02:53
  • 2
    Maybe a bit pedantic, but I'd use `IEnumerable` instead of `List` in the signature, that way you can pass anything that is `IEnumerable`, not just lists, Since you're not using any function specific to `List`, I don't really see a reason not to us `IEnumerable` – Noémie Lord May 25 '17 at 14:47
  • Using List allows you to use the shorthand data.ForEach(), otherwise you'd have to actually write a foreach loop. Which could work also, but I like writing things as short as possible. – Shahzad Qureshi May 30 '17 at 06:02
1

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
    };
}
Andrii Viazovskyi
  • 777
  • 2
  • 9
  • 15
0

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);
}
Nur.B
  • 319
  • 2
  • 4