199

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?

3
  • Check out this solution: Stored Procedure with Table-Valued Parameter in EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514 Commented Sep 22, 2016 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 <Items><Item value="sdadas"/><Item value="sadsad"/>...</Items>. The process on Sql Server is also simple. Using this method, you can always add a new attribute to <item> if you need more information. Commented Dec 22, 2016 at 7:51
  • 4
    @NițuAlexandru, "Sql it's very fast when processing xml.". Not even close.
    – nothrow
    Commented Dec 12, 2018 at 12:25

6 Answers 6

320

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;
    }
}
11
  • 29
    +1 Excellent example. Takeaways are: send a DataTable as the parameter value, set SqlDbType to Structured and TypeName to the database UDT name.
    – lc.
    Commented Jan 25, 2013 at 9: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. Commented Jan 21, 2016 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. Commented Mar 13, 2016 at 23:09
  • 5
    @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". Commented Jul 19, 2016 at 8:56
  • 4
    Future Users, please note this: DataTable approach consumes the hell of a memory, whereas the IEnumerable one simply does not.
    – AgentFire
    Commented Mar 3, 2018 at 9:58
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.

16

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;

    }
4
  • 1
    Would you please let me know that what do I pass as parameter? Func<T, TProperty> selector? Can't it be simply tbl.Rows.Add(item) and no need of that parameter.
    – GDroid
    Commented Mar 18, 2015 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
    Commented Mar 20, 2015 at 16:44
  • can you please provide an example of how do I put selector over there?? I have a List<Guid> to pass to stored proc...
    – GDroid
    Commented Mar 23, 2015 at 3: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
    Commented Apr 24, 2015 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);
5
  • 1
    what if the paramCollection is NULL ? How to pass empty type ?
    – Muflix
    Commented Aug 31, 2016 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
    Commented Mar 16, 2017 at 21:41
  • 1
    Updated answer with initial -if- check Commented Mar 25, 2017 at 2: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 Commented May 25, 2017 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. Commented May 30, 2017 at 6: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
    };
}
-1

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.