1

I have written a SQL Server CLR User-defined type (UDT) in SQL Server 2012. I have been able to access it though SQL test scripts, and have used it as a local variable, defined it in a table, and tested it through Visual Studio and SQL Server Management Studio.

We have an service that uses SQLBulkCopy in a fairly generalized fashion to pick up files that are placed in a directory, then insert their contents to the appropriate table. When I add my UDT as a column in one of those tables, I receive an error from the WriteToServer( DataTable ) invocation.

The UDT column is being passed as a System.String, in the hope that the UDT's Parse() method will be called within SQL Server to convert it to the internal type. I have also tried declaring the UDT class within this client program, and passing the data as the UDT type directly.

In either case I receive this error message (edited to take out my proprietary names)

Could not find method 'Read' for type 'MyNamespace.MyType' in assembly 'MyType'

I have reviewed as many similar questions that I can find about this error message, and they generally refer to the format of the CREATE statement. Also, they generally refer to CLR functions, not CLR types, which are slightly different. This is mine:

CREATE TYPE [dbo].[MyType]
EXTERNAL NAME [MyType].[MyNamespace.MyType]

I suspect this might not be the issue, and that, instead, it has to do with how SQLBulkCopy interacts with a SQLCLR UDT. For this particular combination it's difficult to find any in-depth explanation.

Edit #1 - It is custom serialization.

[Serializable]  
[Microsoft.SqlServer.Server.SqlUserDefinedType( Format.UserDefined, MaxByteSize = -1 )]  
public struct MyType: INullable, IBinarySerialize  

Edit #2 - Execute permission is granted

GRANT EXECUTE 
ON TYPE :: MyType
TO PUBLIC 

Edit #3 - adapted testing code

CREATE TABLE [dbo].[TestMyType]
(
    [SourceMachine]       [varchar](32)  NULL,
    [Output]              MyType NULL
)

and updated by

try
{
    DataTable dataTable = new DataTable( "[TestMyType]" );
    dataTable.Columns.Add( "SourceMachine", typeof( System.String ) );
    dataTable.Columns.Add( "Output", typeof( MyNamespace.MyType ) );

    dataTable.Rows.Add( "Ron1", MyNamespace.MyType.Parse( "This is string 1" ) );
    dataTable.Rows.Add( "Ron2", MyNamespace.MyType.Parse( "This is string 2" ) );
    dataTable.Rows.Add( "Ron3", MyNamespace.MyType.Parse( "This is string 3" ) );

    SqlBulkCopy sqlBulkCopy = new SqlBulkCopy( conn );
    sqlBulkCopy.DestinationTableName = "[TestMyType]";
    sqlBulkCopy.WriteToServer( dataTable );
}
catch ( Exception ex)
{
    System.Diagnostics.Debug.WriteLine(ex.Message);                            
    throw;
}

This gave the same error message that is shown above.

Edit #4 - Eliminate SqlBulkCopy from the issue
I have recreated the issue using a parameterized INSERT. I set it up to pass the UDT object from the client to the server as a parameter that directly uses an instance of the UDT.

string sInsert = "INSERT INTO TestMyType VALUES (?, ?)";
SqlCommand command = new SqlCommand(sInsert, conn);
SqlParameter parm1 = new SqlParameter("SourceMachine", "This is Machine 01");
SqlParameter parm2 = new SqlParameter("Output", MyNamespace.MyType.Parse( "This is INSERT 01" ) );
parm2.UdtTypeName = "MyType";
command.Parameters.Add(parm1);
command.Parameters.Add(parm2);
int nResult = command.ExecuteNonQuery();

giving

A first chance exception of type 'System.Data.SqlClient.SqlException'
    occurred in System.Data.dll
Additional information: Could not find method 'Read' for 
    type 'MyNamespace.MyType' in assembly 'MyType'
  • Is your UDT using native or custom serialization? – Solomon Rutzky Sep 07 '16 at 20:31
  • It is custom serialization. [Serializable] [Microsoft.SqlServer.Server.SqlUserDefinedType( Format.UserDefined, MaxByteSize = -1 )] public struct MyType: INullable, IBinarySerialize – RonSanderson Sep 07 '16 at 20:57
  • Sorry, I can't figure out how to make this look like a code block in a comment. I've tried everything I can find in the help pages. – RonSanderson Sep 07 '16 at 20:59
  • Is ok ;-). In comments there is only a subset of mark-down allowed. You can use back-quotes for code. Look here: [Markdown Help](http://stackoverflow.com/editing-help#comment-formatting). You can get to that help page by clicking on the **?** in the upper-right-hand corner of the editor, then click on "Advanced Help". I will take a look at this later tonight or tomorrow. Also, it helps if you use a person's `@` name so that they get notified. Notifications always go to the poster of the Question or Answer that the comment is on. – Solomon Rutzky Sep 07 '16 at 21:08
  • When you say that you tried "_declaring the UDT class within this client program, and passing the data as the UDT type directly_", do you mean that you set the column type in the `DataTable` to be your type, and then load the data? Not sure if the `Parse` method needs to be called directly. But take a look at this [answer](http://stackoverflow.com/a/5805913/577765). Also, to send the data directly via BCP / SqlBulkCopy, it would need to be in binary format, as per the [source code](http://referencesource.microsoft.com/#System.Data/System/Data/SqlClient/SqlBulkCopy.cs,649). I am testing now. – Solomon Rutzky Sep 08 '16 at 14:45
  • @srutzky - yes, I have added a reference to my client-side project for the same assembly that I have loaded in SQL Server as the user-defined type. Then on the client side, I use my string input to call the static method `MyType.Parse( myInputString )` to return an object of type `MyType`. This is of course all on the client without any invocation of SQL Server yet. Then I create a `DataTable` and declare the column to be of type `MyType`. I populate the row data of the data table with the `MyType` object I created above. Then I call `bulkCopy.WriteToServer ( datatable)`. – RonSanderson Sep 08 '16 at 15:37
  • Doing this works for me, also on SQL Server 2012. I assume your UDT is already loaded into SQL Server? The odd thing is that the error is that it can't find the `Read` method, which has to be `public` else it wouldn't compile. It might be an order of operations thing as I followed the other example that I linked to and called `Type.Parse()` within the `DataTable.Rows.Add()` call. Lemme try your way to see what happens. Well, that also worked. hmmm... – Solomon Rutzky Sep 08 '16 at 15:43
  • Can you please update the Question to include the entire and _exact_ error message, including all error/msg codes? Also, is it possible to post a snippet of the C# code that creates the UDT variable, adds it to the table, etc? Also, when exactly does the error occur? When you call `WriteToServer()` ? or at some other point? – Solomon Rutzky Sep 08 '16 at 15:49
  • Also, what Login are you using for the Destination connection? Is it a restricted user? – Solomon Rutzky Sep 08 '16 at 15:53
  • The original error message is in the original question, with only the namespace and class names replaced. This is because of corporate confidentiality requirements. I will edit the post to provide as much information as I can within these restrictions. – RonSanderson Sep 08 '16 at 15:55
  • The login id is a production-only id that allows access to all tables and views. There may be some restrictions regarding other objects. I had performed a GRANT ... TO PUBLIC on an earlier iteration of the UDT. I have since dropped everything and recreated, but I have not performed the GRANT again. Let me see... – RonSanderson Sep 08 '16 at 15:59
  • No, after the GRANT, I still have the same error. – RonSanderson Sep 08 '16 at 16:05
  • Good to know. Can you at least provide the stack trace, with proprietary names changed? – Solomon Rutzky Sep 08 '16 at 16:09
  • Also, just to rule out permissions: do you have the ability to test this operation with a sysadmin account? – Solomon Rutzky Sep 08 '16 at 16:16
  • @srutzky - I am trying to recreate what you tried, since you saw it work. I have copied code from the article you linked to, and I have at least seen a different error message. Let me work at that and get back to you. – RonSanderson Sep 08 '16 at 16:44
  • @srutzky - I have edited the original post to include my adaptation of the test process. – RonSanderson Sep 08 '16 at 16:45
  • What is the new error message? – Solomon Rutzky Sep 08 '16 at 17:04
  • The new message turned out to be a mismatch between the table I used and the `DataTable` used by `SQLBulkCopy`. (It was a transient issue only.) I resolved that and got back to the original error. I have put the updated information in the original question as Edit #3. – RonSanderson Sep 08 '16 at 17:12
  • Unfortunately, I do not get any inner exception data back from SQL Server for the System.Data.SqlClient.SqlException that is thrown on the client. – RonSanderson Sep 08 '16 at 17:17
  • Can you get the `.StackTrace` from the general `Exception` being thrown by `SqlBulkCopy` ? I posted an answer just to show what I have done so far, more completely than the bits and pieces of descriptions here. Other things to think about: again, can you try as a sysadmin? Are you using the same `AssemblyVersion` and `AssemblyFileVersion` between the Assembly loaded into SQL Server and the Assembly referenced by your client code? – Solomon Rutzky Sep 08 '16 at 17:56

2 Answers2

1

SqlBulkCopy should be able to handle SQLCLR UDT's (User-Defined Types) just fine. I have succeeded using both DbDataReader and DataTable methods.

Here is what worked for me:

C# code (I made the "client" a SQLCLR stored procedure)

using System;
using System.Data.SqlTypes;
using System.Data.SqlClient;
using Microsoft.SqlServer.Server;

public class xtra
{

    [SqlProcedure]
    public static void BcpTest(SqlInt32 TheID, SqlString TheConnectionString)
    {
        System.Data.DataTable _DataTable = new System.Data.DataTable();
        _DataTable.Columns.Add("ID", typeof(Int32));
        _DataTable.Columns.Add("SomeDate", typeof(DateTime));
        _DataTable.Columns.Add("SomeData", typeof(Type_HashTable));

        Type_HashTable _Bob = Type_HashTable.Parse(@"testKey=testVal");
        _DataTable.Rows.Add(TheID.Value, DateTime.Now, _Bob);

        _DataTable.Rows.Add(TheID.Value + 1, DateTime.Now,
           Type_HashTable.Parse(@"testKey2=testVal2"));

        SqlBulkCopy _BulkCopy = new SqlBulkCopy(TheConnectionString.Value);
        _BulkCopy.DestinationTableName = "dbo.BulkCopyUDT";

        try
        {
            _BulkCopy.WriteToServer(_DataTable);
        }
        finally
        {
            _BulkCopy.Close();
        }
    }
}

T-SQL code

-- DROP TABLE dbo.BulkCopyUDT;
CREATE TABLE dbo.BulkCopyUDT
(
  ID INT NOT NULL CONSTRAINT [PK_BulkCopyUDT] PRIMARY KEY,
  SomeDate DATETIME,
  SomeData [SQL#].[Type_HashTable]
);
GO

GRANT INSERT, SELECT ON dbo.BulkCopyUDT TO [Public];
GRANT EXECUTE ON TYPE::SQL#.Type_HashTable TO [Public];
GO

CREATE PROCEDURE dbo.SqlBulkCopy_Test
(
    @TheID INT,
    @TheConnectionString NVARCHAR(4000) = 
        N'Data Source=(local); Integrated Security=true; Initial Catalog=my_database;'
)
AS EXTERNAL NAME [my_assembly].[xtra].[BcpTest];
GO

ALTER ASSEMBLY [my_assembly] WITH PERMISSION_SET = EXTERNAL_ACCESS;
GO

The Test

EXEC dbo.SqlBulkCopy_Test 1;

SELECT *, SomeData.ToString() FROM dbo.BulkCopyUDT;

EXEC dbo.SqlBulkCopy_Test 3,
       N'Data Source=(local); User=test; Password=test; Initial Catalog=my_database;';

SELECT *, SomeData.ToString() FROM dbo.BulkCopyUDT;

I also got this working from a Console App, using both SqlBulkCopy and a parameterized ad hoc query:

using System;
using System.Data;
using System.Data.SqlClient;

namespace SqlBulkCopyUDT
{
    class Program
    {
        static void Main(string[] args)
        {
            int _TheID = Int32.Parse(args[0]);

            string _TheConnectionString = 
              @"Data Source=(local); Integrated Security=true; Initial Catalog=my_database;";
            if (args.Length > 1)
            {
                _TheConnectionString = args[1];
            }

            //DataTable _DataTable = new DataTable();
            //_DataTable.Columns.Add("ID", typeof(Int32));
            //_DataTable.Columns.Add("SomeDate", typeof(DateTime));
            //_DataTable.Columns.Add("SomeData", typeof(Type_HashTable));

            //Type_HashTable _Bob = Type_HashTable.Parse(@"testKey=testVal");
            //_DataTable.Rows.Add(_TheID, DateTime.Now, _Bob);

            //_DataTable.Rows.Add(_TheID + 1, DateTime.Now,
            //   Type_HashTable.Parse(@"testKey2=testVal2"));

            //SqlBulkCopy _BulkCopy = new SqlBulkCopy(_TheConnectionString);
            //_BulkCopy.DestinationTableName = "dbo.BulkCopyUDT";

            //try
            //{
            //    _BulkCopy.WriteToServer(_DataTable);
            //}
            //finally
            //{
            //    _BulkCopy.Close();
            //}

            using (SqlConnection _Connection = new SqlConnection(_TheConnectionString))
            {
                using (SqlCommand _Command = _Connection.CreateCommand())
                {
                    _Command.CommandType = CommandType.Text;
                    _Command.CommandText =
                        @"INSERT INTO dbo.BulkCopyUDT (ID, SomeDate, SomeData)
                         VALUES (@MyID, GETDATE(), @MyData);";

                    SqlParameter _ParamMyID = new SqlParameter("@MyID", SqlDbType.Int);
                    _ParamMyID.Value = _TheID;
                    _Command.Parameters.Add(_ParamMyID);

                    SqlParameter _ParamMyData = new SqlParameter("@MyData", SqlDbType.Udt);
                    _ParamMyData.UdtTypeName = "SQL#.Type_HashTable";
                    _ParamMyData.Value = Type_HashTable.Parse(@"testKey3=testVal3");
                    _Command.Parameters.Add(_ParamMyData);

                    _Connection.Open();
                    _Command.ExecuteNonQuery();
                }
            }
        }
    }
}

P.S. If sending the data directly to a UDT column, then it needs to be in binary form as that is the only way that SqlBulkCopy transports it, as per the source code.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171
  • There may be a difference if your stored procedure executes within the SQL Server process instead of from a separate process, as in my tests. The call from within SQL Server may not need to invoke the same processing path as when the INSERT or SqlBulkCopy deliver a binary image of the same object from a client application. – RonSanderson Sep 08 '16 at 18:38
  • @RonSanderson I wish that were a potential problem ;-). But sadly no, you cannot use the internal `Context Connection=true;` as a Destination connection string for `SqlBulkCopy`. It is a full external connection, and I tried as both a privileged user and a "test" user that has no rights outside of what I granted to `Public`. The fact that you are getting an error about it not _finding_ the `Read` method that is clearly there is quite odd, which is why I am asking to see the StackTrace, and if permissions or versioning might be an issue. – Solomon Rutzky Sep 08 '16 at 19:12
  • There is no stack trace returned from the SQL Server side where the SQLException is generated, and no InnerException or server-side stack trace is available on the client. – RonSanderson Sep 08 '16 at 20:13
  • By a different processing path, I did not mean to imply that the connection or permissions might be an issue. Instead, I was more concerned that the stored procedure code actually executes within the same CLRSQL environment as the UDT, and that may allow SQL Server to short-cut the resolution of the MyType UDT in some way, allowing the Read method to be resolved. For a call from an external process the CLR is presented with a binary image that purports to be an instance of MyType, and maybe casting it to the proper object and invoking the Read() is somehow going through a different resolution. – RonSanderson Sep 08 '16 at 20:18
  • The next step is to move my test code inside of SQL Server, as a stored procedure or function, and see if it can be invoked with a string, then transformed a MyType object and INSERTed successfully. – RonSanderson Sep 08 '16 at 20:21
  • @RonSanderson I got this working from a Console App. I updated my answer with the code. It works from both `SqlBulkCopy` and a regular ad hoc, parameterized query. For the "type", are you referencing the exact same DLL / Assembly in your app code that you loaded into SQL Server as `MyType`? – Solomon Rutzky Sep 09 '16 at 05:32
  • I have copied your syntax nearly verbatim. I still get the error. This is a good thing, since it proves the problem is not with the client side code. I have used queries where I INSERT a string value, forcing the conversion to happen in the server, and those work fine. I have one more hunch that I will try and then comment on that later. Thank you so much for your efforts on this issue. – RonSanderson Sep 09 '16 at 14:12
  • @RonSanderson One difference might be that I am connecting to SQL Server that is running on the same machine as the console app. There might be a slight difference in DB connectivity library. Maybe a Service Pack would fix the issue? I am on SP3 for SQL Server 2012. Might possibly be a .NET Framework issue, on your machine and/or the server running SQL Server. What Framework version is each of them running? – Solomon Rutzky Sep 09 '16 at 14:34
  • I identified the issue. This could not have been done without @srutzky helping eliminate the client-side code as the source of the problem. I have posted my solution but I am accepting this solution because of the crucial role in narrowing the problem down to the UDT specification itself. I upvoted the solution but it does not show because I do not have enough reputation points. – RonSanderson Sep 09 '16 at 14:43
  • @RonSanderson Glad it has been identified. It seems I was on the right track about focusing on the error message of not being able to find the `Read` method. I had no idea that it could be defined in a way that did not include `public`. Very interesting. – Solomon Rutzky Sep 09 '16 at 15:00
  • @RonSanderson Also, thanks for accepting this :). You should have enough rep now to up-vote answers from other people, but I don't think you can ever up-vote your own. – Solomon Rutzky Sep 09 '16 at 15:00
  • Bingo! I just needed one more reputation point to upvote. I have tried it again and it worked this time. – RonSanderson Sep 09 '16 at 15:33
1

I used an explicit interface notation on two methods in the UDT, like this.

void IBinarySerialize.Read( BinaryReader r )
{
}

void IBinarySerialize.Write( BinaryWriter w )
{
}

But they had to be defined like this:

public void Read( BinaryReader r )
{
}

public void Write( BinaryWriter w )
{
}

The difference was enough to keep SQL Server from identifying the correct method to use on the UDT during SqlBulkCopy and paramaterized INSERT when passed the complete MyType object.

The issue started when I used Visual Studio to add the stub routines that implemented the IBinarySerialize interface. I right-clicked on the interface name at the top of the struct definition and chose "Implement Interface Explicitly". I should have selected "Implement Interface", to generate the method stubs without the qualifiers.

  • I had no idea that you could use that other syntax. What is the benefit of implementing an interface explicitly? Could you just add the `public` keyword to the explicit implementation? And why does implementing it explicitly get around the requirement for it to be public? I tried compiling it yesterday, not explicitly, without the `public` and got an error, so I thought it was not possible to compile the UDT without the `Read` and `Write` methods being `public`, which is why I said in a comment or two that the `Read` method had to be there and be visible. – Solomon Rutzky Sep 09 '16 at 15:04
  • I really do not know the implications of defining an explicit interface. But once I had it, I did try adding `public` and it gave a compilation error. I need to learn this subtle area of C# a little better to see under what situations an explicit interface implementation is appropriate. I have used it before, but only because I liked having the interface's methods self-documenting by having the interface name qualifying the method name. It seems there are implications beyond that which I do know yet. – RonSanderson Sep 09 '16 at 21:27
  • Ok. Yes, I am curious about those "implications" and why one would choose to use the explicit declaration. Thanks for mentioning that adding `public` to the explicit declaration is not an option. – Solomon Rutzky Sep 09 '16 at 21:40