LinQ to SQL and CLR User Defined Types

2019-05-27 03:53发布

问题:

I have created a User Defined Type in .Net 3.5 as per my blog entry at :

http://jwsadlerdesign.blogspot.com/2009/04/this-is-how-you-register.html

This works fine when using SQL with technologies like nHibernate.

However, when I try to map my LinQ to SQL class to use this UDT (with attribute defintions not XML), and I setup the property as the enumeration. I cannot get LinQ to map to this type. I have tried Image, Binary, varchar and integer all of which seem to issue Invalid Cast errors.

In particular I get the error 'Unable to cast object of type 'ISTD.InstallManager.Common.Classes.SQLUDTTargetType' to type 'System.Byte[]' any ideas or help would be much appreciated.

James.

回答1:

UPDATE: I ran into this myself recently and found that the previous solution wasn't quite complete. Despite what all of the documentation says, it is possible to do this, but somewhat painful.

The first step, for your own convenience, is to implement some conversion operators:

public class MyUDT : INullable, IBinarySerialize
{
    // Class implementation would go here
    // ...

    public static explicit operator MyUDT(byte[] data)
    {
        using (MemoryStream stream = new MemoryStream(data))
        {
            using (BinaryReader reader = new BinaryReader(stream))
            {
                MyUDT result = new MyUDT();
                result.Read(reader);
                return result;
            }
        }
    }

    public static explicit operator byte[](MyUDT x)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            using (BinaryWriter writer = new BinaryWriter(ms))
            {
                x.Write(writer);
            }
            return ms.ToArray();
        }
    }
}

Linq to SQL will still flat-out refuse to give you the UDT field, no matter how you declare the property. So you have to give it a binary field instead. You don't need a stored procedure or any custom SQL for this, just add a computed column to your table:

ALTER TABLE MyTable
ADD UDTField_Data AS CAST(UDTField AS varbinary(len))

Where len is whatever your UDT defines in the MaxByteSize attribute.

Now you can finally get access to the column data. You might be tempted to use your UDT as the return type of the new property, thinking that Linq to SQL will find your conversion operator and automatically convert from the byte array; don't bother. Linq to SQL will decide that it's actually a serialized .NET object and spit out a message to the effect of "input stream is not a valid binary format." Instead, you need another layer of indirection:

private MyUDT udtField;

[Column(Name = "UDTField_Data", DbType = "varbinary(len)")]
private byte[] UdtFieldData
{
    get { return (byte[])udtField; }
    set { udtField = (MyUDT)value; }
}

public MyUDT UdtProperty
{
    get { return udtField; }
    set { udtField = value; }
}

A few notes to make it clear what's going on here:

  • The actual field data (udtField) is declared as the UDT itself, not a byte array. The reason for this is that we only want the conversion to happen when loading from or saving to the database. If you had to convert the byte array to the UDT every time you accessed it, it would not only hurt performance, but it would cause inconsistencies if the UDT declares any mutable fields.
  • The raw byte[] property (UdtFieldData) is declared private, so consumers only see the UDT itself. Linq to SQL will still read it as long as it has the [Column] attribute.
  • The UdtFieldData property does not declare a storage property. This is critical; if you try to use the UDT field as the storage property, you'll just get the same type conversion error.
  • Finally, the UdtProperty property is how consumers actually get to access the data. To them it looks like any other property.

It's unfortunate that you have to jump through so many hoops to get this to work, but it does work. You'll probably have difficulties doing this kind of massaging through the Linq surface designer, which is just one of several reasons why I don't use it; better to write the classes yourself and use SqlMetal to help you along if necessary.