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

6

195

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?

Elasmobranch answered 8/4, 2011 at 12:56 Comment(3)
Check out this solution: Stored Procedure with Table-Valued Parameter in EF. code.msdn.microsoft.com/Stored-Procedure-with-6c194514Water
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.With
@NițuAlexandru, "Sql it's very fast when processing xml.". Not even close.Precess
L
315

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;
    }
}
Latini answered 28/5, 2012 at 5:3 Comment(11)
+1 Excellent example. Takeaways are: send a DataTable as the parameter value, set SqlDbType to Structured and TypeName to the database UDT name.Onestep
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.Coverall
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.Selfcontained
Also, DataTable is a IDisposable and hence its Dispose method should be called before it goes out of scope.Anchusin
@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".Fish
@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.Anchusin
@SørenBoisen: According to the remarks section of msdn.microsoft.com/en-us/library/… , "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.Hesse
Future Users, please note this: DataTable approach consumes the hell of a memory, whereas the IEnumerable one simply does not.Cursorial
@Cursorial 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 rowsGradely
@RuiTaborda when you need to send lots of data, use IEnumerable approch. Otherwise, use whatever.Cursorial
Is there an advantage to using the DataTable approach?Parlour
K
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.

Krucik answered 26/11, 2013 at 15:45 Comment(0)
N
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;

    }
Necking answered 7/2, 2014 at 8:47 Comment(4)
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.Extravehicular
the selector.Invoke(item) selects the property on the item most cases its a int, but it also allows you to select a string propertyNecking
can you please provide an example of how do I put selector over there?? I have a List<Guid> to pass to stored proc...Extravehicular
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,Necking
R
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);
Rumph answered 24/4, 2015 at 21:45 Comment(5)
what if the paramCollection is NULL ? How to pass empty type ?Castoff
@Castoff Obscurely, extension methods actually work against null instances. So adding a simple if(paramCollection != null) check at the top of the method will be fineStaceystaci
Updated answer with initial -if- checkRumph
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 IEnumerableDiet
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.Rumph
P
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
    };
}
Pious answered 9/7, 2019 at 9:52 Comment(0)
W
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);
}
Wheedle answered 10/11, 2022 at 3:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.