Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Passing Table Valued Parameters with Dapper

0.00/5 (No votes)
31 Oct 2014 1  
Up until version 1.26 of Dapper, it was not possible to utilize TVP.

Introduction

Dapper is a high performing micro-ORM, supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird, etc., making interactions with any database quite easy to implement in a .NET solution. The introduction of version 1.26, with support for table-valued parameters, further simplifies parameterizing SQL queries.

Background

There have been many attempts at passing sets of values as a single parameter to stored procedure. Many pages of the World-Wide-Web have been filled with creative approaches to improve usability and performance, based on exchanging data as comma-delimited strings. In the end, they all fall short of passing the set as set, and letting the database use it as a Table Valued Parameter. The creators of Dapper have recognized it and allowed to pass such parameter as a DataTable.

Database Setup

Let's first create a custom type on the database itself.

CREATE TYPE [dbo].[AUDITRECORD] AS TABLE(
    [AUDIT_PK] [bigint] NULL,
    [MESSAGE] [nvarchar](500) NULL,
    [SUCCESS] [bit] NULL
)
GO

This code creates a user-defined-type in SQL server, allowing a single parameter or variable to contain multiple values in rows and columns just like a table does, hence the name Table-Valued-Parameter. A stored procedure using such parameter would be created like this:

CREATE PROCEDURE [dbo].[TestOne] @TVP dbo.AUDITRECORD READONLY
AS

DataTable Approach

First, we must define a new DataTable, followed by definition of columns, and finally the action to add rows to the newly created table. Important thing to note is that the order of the columns added must be identical to the columns defined in the database TVP.

Out of the box, Dapper provides an extension, AsTableValuedParameter, which internally converts a DataTable to a TVP of desired database side type.

var dt = new DataTable();
dt.Columns.Add("AUDIT_PK", typeof (long));
dt.Columns.Add("MESSAGE", typeof (string));
dt.Columns.Add("SUCCESS", typeof (bool));
dt.Rows.Add(1, "EHLO", null);

using (var conn = new SqlConnection("Data Source=.;Initial Catalog=Scratch;Integrated Security=true"))
{
    conn.Open();
    conn.Execute("TestOne", new {TVP = dt.AsTableValuedParameter("dbo.AUDITRECORD")},
        commandType: CommandType.StoredProcedure);
}

There is actually no conversion per se performed. Here is what is occurring during the stored procedure execution:

declare @p1 dbo.AUDITRECORD
insert into @p1 values(1,N'EHLO',NULL)

exec TestOne @TVP=@p1

Quite clever. Because Dapper can be used with many database clients, instead of having to figure out how to exchange a TVP with every database, Dapper injects extra SQL code to allow for the exchange to occur, without having to define custom types on the client.

Extension

Since lazyness is a mother of invention, let's try to simplify the process. Here is what I'd actually like to pass to Dapper as TVP:

var l = new List<AUDITRECORD>
{
    new AUDITRECORD {AUDIT_PK = 2, MESSAGE = "HELO", SUCCESS = true},
    new AUDITRECORD {AUDIT_PK = 3, MESSAGE = "EHLO", SUCCESS = false}
};
using (var conn = new SqlConnection
("Data Source=.;Initial Catalog=Scratch;Integrated Security=true"))
{
    conn.Open();
    conn.Execute("TestOne",
        new {TVP = l.AsTableValuedParameter("dbo.AUDITRECORD", 
        new[] {"AUDIT_PK", "MESSAGE", "SUCCESS"})},
        commandType: CommandType.StoredProcedure);
}

That is much simpler, but unfortunately not supported. Dapper only accepts a DataTable and not an IEnumerable<T>. To be able to pass a list of values, we must create an extension first:

public static class Extensions
{
    /// <summary>
    /// This extension converts an enumerable set to a Dapper TVP
    /// </summary>
    /// <typeparam name="T">type of enumerbale</typeparam>
    /// <param name="enumerable">list of values</param>
    /// <param name="typeName">database type name</param>
    /// <param name="orderedColumnNames">if more than one column in a TVP, 
    /// columns order must mtach order of columns in TVP</param>
    /// <returns>a custom query parameter</returns>
    public static SqlMapper.ICustomQueryParameter AsTableValuedParameter<T>
    	(this IEnumerable<T> enumerable,
        string typeName, IEnumerable<string> orderedColumnNames = null)
    {
        var dataTable = new DataTable();
        if (typeof(T).IsValueType || typeof(T).FullName.Equals("System.String"))
        {
            dataTable.Columns.Add(orderedColumnNames==null? 
            	"NONAME":orderedColumnNames.First(), typeof(T));
            foreach (T obj in enumerable)
            {
                dataTable.Rows.Add(obj);
            }                
        }
        else
        {
            PropertyInfo[] properties = typeof(T).GetProperties
            	(BindingFlags.Public | BindingFlags.Instance);
            PropertyInfo[] readableProperties = properties.Where
            	(w => w.CanRead).ToArray();
            if (readableProperties.Length > 1 && orderedColumnNames == null)
                throw new ArgumentException("Ordered list of column names 
                must be provided when TVP contains more than one column");
            var columnNames = (orderedColumnNames ?? 
            	readableProperties.Select(s => s.Name)).ToArray();
            foreach (string name in columnNames)
            {
                dataTable.Columns.Add(name, readableProperties.Single
                	(s => s.Name.Equals(name)).PropertyType);
            }

            foreach (T obj in enumerable)
            {
                dataTable.Rows.Add(
                    columnNames.Select(s => readableProperties.Single
                    	(s2 => s2.Name.Equals(s)).GetValue(obj))
                        .ToArray());
            }
        }
        return dataTable.AsTableValuedParameter(typeName);
    }
}

Per C# rules, all extension methods must be public and static, and placed inside a public and static class.

If the type of T is ValueType, we can simply skip Reflection and add a NONAME column. This will come in handy later and is also a reason for the orderedColumnNames parameter to be optional.

Otherwise, using Reflection, we obtain a list of public getters available for a type. Then we check if we have more than one, because if we do, we also need to have an ordered, not sorted, list of column names, in the exact order as the TVP definition in the database.

Then we proceed with the boiler-plate code, creating a data table, adding column definitions with the property type, and finally adding all entities as rows to the newly created data table. When executed, we will get this:

declare @p1 dbo.AUDITRECORD
insert into @p1 values(2,N'HELO',1)
insert into @p1 values(3,N'EHLO',0)

exec TestOne @TVP=@p1

If your set-based-parameters are always a list of long numbers, you can pass them to the database as list as long as you have a common TVP defined.

CREATE TYPE TVPBIGINT AS TABLE(
  [KEY] BIGINT NULL
  )

Then in your stored procedures, you can use it like this:

CREATE PROCEDURE [dbo].[TestTwo] 
  @customerKeys dbo.TVPBIGINT READONLY,
  @productKeys dbo.TVPBIGINT READONLY
AS
SET NOCOUNT ON
SELECT * FROM SomeTable WHERE 
  CUST_KEY IN (SELECT [KEY] FROM @customerKeys) 
  OR PROD_KEY IN (SELECT [KEY] FROM @productKeys) 
  -- do something with the data 
SET NOCOUNT OFF
GO

Your C# code will pass the parameters like this:

using (var conn = new SqlConnection
	("Data Source=.;Initial Catalog=Scratch;Integrated Security=true"))
{
    conn.Open();
    conn.Execute("TestTwo",
        new
        {
            customerKeys = custKeyList.AsTableValuedParameter("dbo.TVPBIGINT"),
            productKeys = prodKeyList.AsTableValuedParameter("dbo.TVPBIGINT")
        },
        commandType: CommandType.StoredProcedure);
}

As long as both, custKeyList and prodKeyList are enumerables of long, the stored procedures will be executed quickly and efficiently:

declare @p1 dbo.TVPBIGINT
insert into @p1 values(N'1')
insert into @p1 values(N'3')
insert into @p1 values(N'5')

declare @p2 dbo.TVPBIGINT
insert into @p2 values(N'31')
insert into @p2 values(N'75')
insert into @p2 values(N'712')

exec TestTwo @customerKeys=@p1,@productKeys=@p2

Summary

Dapper is a great development time saver when it comes to interactions with a database, with miniscule impact on performance. When coupled with simple extensions, it provides for time and performance savings in our day-to-day data processing.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here