-1

I have a C# web application that serves as a passthrough to SQL Server; requests that detail SQL Server commands come in, we parse the request, generate the necessary .Net types and then use them to execute SqlCommands and such. The upshot of that is that the C# web application needs to be very flexible and really can't make too many assumptions about what a request "should" look like.

I recently solved a problem that was causing exceptions to be thrown when a table-valued parameter contained VARBINARY types. In the general case, we loop over the incoming request and construct a DataTable with the appropriate number of columns and rows. The data type of the columns weren't specified, and the data values were just inserted as objects. For VARBINARY types, this would result in the following error:

Implicit conversion from data type nvarchar(max) to varbinary(max) is not allowed. Use the CONVERT function to run this query.

I found this StackOverflow post, and was able to solve the problem. Please see my pseudo-code below:

for (var colNdx = 0; colNdx < requestTvp.Columns.Count; ++colNdx)
{
    myDataTable.Columns.Add();

    if (requestTvp.Rows.Empty)
    {
        continue;
    }

    if (requestTvp.Rows[0].Columns[colNdx].DataType == Bytes)
    {
        myDataTable.Columns[colNdx].DataType = typeof(SqlBinary);
    }
}

From what I can tell, this is working great. The problem occurs when the incoming TVP has no rows since I have no data to check against. In this case, we just construct a DataTable with the correct number of columns (this is specified in the request metadata about the TVP), but these columns have no explicitly set data type. The DataTable has no rows in this case. This results in the same exception:

Implicit conversion from data type nvarchar(max) to varbinary(max) is not allowed. Use the CONVERT function to run this query.

Here is the full stack trace:

System.Data.SqlClient.SqlException (0x80131904): Implicit conversion from data type nvarchar(max) to varbinary(max) is not allowed. Use the CONVERT function to run this query. The data for table-valued parameter "@tvp" doesn't conform to the table type of the parameter.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
at System.Data.SqlClient.SqlDataReader.get_MetaData()
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method)
at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)

Honestly, I don't understand why the problem is occurring at all...if there's no data in the DataTable what is even causing the invalid implicit conversion?

What makes things even more frustrating, is that this error occurs before the request even gets to my database, so I'm not able to use SQL Server Profiler to figure out exactly what is being sent to the database server.

So here's my question: how do I correctly pass an empty DataTable as table-valued parameter to a stored procedure when the corresponding User-Defined Table Type has a VARBINARY field? The only information I have about the table-valued parameter when I'm constructing the DataTable is the name of the User-Defined Table Type, the number of columns it contains, the name of the parameter in the stored procedure and the values contained in the request TVP, which in this case is essentially an empty set.

Community
  • 1
  • 1
Dan Forbes
  • 2,734
  • 3
  • 30
  • 60
  • what is the data that's in this column `Columns[colNdx]` is it a TimeStamp for example...? if so then make the typeof(DateTime) for the `myDataTable.Columns[colNdx].DataType =` assignment – MethodMan Mar 03 '16 at 20:45
  • @MethodMan - In the problem case we do not even fall into that block of code since we have no rows to consider. I realize this was not immediately apparent from the pseudo-code I originally posted, so I've amended it to make it more illustrative. – Dan Forbes Mar 03 '16 at 20:49
  • This may sound like a dumb question but, why are you making the call to the stored proc if you have no data to pass it? If possible just skip the call when you have no data. – Kevin Mar 03 '16 at 20:54
  • @Kevin - Not a dumb question, but not an acceptable solution either (-: I can't assume that just because the stored procedure is invoked with an empty TVP parameter that it will be a no-op; it's possible that it could still have side effects or even return data. – Dan Forbes Mar 03 '16 at 20:55
  • I think that you should be checking for the following `if(datatable.Rows.Count > 0)` – MethodMan Mar 03 '16 at 21:01
  • Can you add the code fragment of how you are setting up you SqlCommand object and parameters ? Something does not make sense as I recall the way a table value parameter was passed to SQL, is 1) create temp table 2) insert data into temp table, 3) pass temp table as parameter to stored proc. If there is no data in the table there is nothing to convert. – user957902 Mar 03 '16 at 21:05
  • @user957902 - Binding the parameter to the `SqlCommand` object has nothing to do with it. The `DataTable` is created and then and then bound to the command as such: `SqlCommand.Paramaters[paramNdx].Value = myDataTable`. You're absolutely correct that it is odd that I'm getting conversion errors when there is nothing to convert. That is part of why I'm asking this question. – Dan Forbes Mar 03 '16 at 21:40
  • @MethodMan - where should I be checking for that and how will it help solve this problem? – Dan Forbes Mar 03 '16 at 21:42
  • Do you have the full stack trace of when the exception is thrown ? I think this is where we are going to have to dive into the SqlCommand objects code an see where the exception is occurring. – user957902 Mar 03 '16 at 22:37
  • @user957902 - I've added the full stack trace. – Dan Forbes Mar 04 '16 at 15:29
  • I have tried to reproduce your results. Whenever I pass an empty DataTable in as a parameter, the generated Sql in the profiler looks like: `declare @p1 dbo.KrjVarBin; exec dbo.BOB @TP=@p1` No data is ever inserted into the table value parameter, so there are no data conversion issue. – user957902 Mar 04 '16 at 22:36
  • A couple of questions 1) You mention that the data comes to you as type object, however, when i try to use object as a column type in a DataTable as a tvp it throws an exception. Are you converting to a different type in the DataTable you are constructing? 2) Your comparison is to type "Bytes" which is not a defined system type. Is this your own defined type? – user957902 Mar 04 '16 at 22:45
  • I was able to reproduce the error with a source column type of String. The error is definitely server side. I can see the user error in the trace. – user957902 Mar 04 '16 at 23:03
  • @user957902 - 1) I don't believe I ever said that the column types were set as `object`; the column values were inserted as `object`s and the types were never set; 2) as I indicated in my question, the code snippet is pseudo-code; it is only meant to give people an idea of what I'm doing; it's not meant to be a reproducer. – Dan Forbes Mar 07 '16 at 17:04

1 Answers1

1

Here is the work around I have come up with. If the data table is empty, just bypass the SqlCommand objects declaration of the table type and do it yourself:

      using(SqlConnection c = new SqlConnection("<connectionString>"))
        {
            c.Open();

            DataTable emptyTable = new DataTable();

            emptyTable.Columns.Add("c1", typeof(int));
            emptyTable.Columns.Add("c2", typeof(string));

            DataRow row = emptyTable.NewRow();

            row["c1"] = 99;
            row["c2"] = new String('X', Int16.MaxValue);

           // Uncomment to make table non empty
           // emptyTable.Rows.Add(row);

            SqlCommand cmd = c.CreateCommand();

            if (emptyTable.Rows.Count > 0)
            {
                cmd.CommandText = "dbo.BOB";
                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                SqlParameter p = cmd.Parameters.AddWithValue("@TP", emptyTable);

                p.SqlDbType = SqlDbType.Structured;
                p.TypeName = "dbo.KrjVarBin";
            }

            else
            {
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = "declare @empty as dbo.KrjVarBin; execute dbo.bob @empty";
            }

            cmd.ExecuteNonQuery();

        }
user957902
  • 3,010
  • 14
  • 18
  • Thank you for providing your workaround. Unfortunately, in this case I need an answer, not a workaround, so I am not accepting this. – Dan Forbes Mar 07 '16 at 17:04
  • When I traced through it, meta data validation is being done on the data table columns vs the table type. I presume that is so that it will know how to construct the insert statements to fill the table value variable. There does not appear to be any way to turn this check off. So even if the table is empty, you still have to have the data types in the DataTable columns that are compatible with the target table type columns. – user957902 Mar 07 '16 at 19:26