diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/.editorconfig b/src/Microsoft.Data.SqlClient/tests/UnitTests/.editorconfig
new file mode 100644
index 0000000000..c75dd30b68
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/.editorconfig
@@ -0,0 +1,11 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = false
+
+[*.cs]
+
+csharp_style_var_when_type_is_apparent = false:refactor
+# IDE0090: Use 'new(...)'
+csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
+
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj
index 2f0e12c922..fcef431b36 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj
@@ -7,6 +7,7 @@
$(ObjFolder)$(Configuration).$(Platform).$(AssemblyName)
$(BinFolder)$(Configuration).$(Platform).$(AssemblyName)
true
+ enable
@@ -25,6 +26,7 @@
all
+
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs
new file mode 100644
index 0000000000..246d9a73f0
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using Microsoft.Data.SqlClient.Server;
+using Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
+using Microsoft.SqlServer.Server;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization;
+
+///
+/// Attempts to serialize types which do not meet the requirements for either user-defined or native serialization.
+///
+public sealed class InvalidSerializationTest : IDisposable
+{
+ private readonly MemoryStream _stream;
+
+ ///
+ /// Initializes the MemoryStream used for all tests in this class.
+ ///
+ public InvalidSerializationTest()
+ {
+ _stream = new MemoryStream();
+ }
+
+ void IDisposable.Dispose()
+ {
+ _stream.Dispose();
+ }
+
+ ///
+ /// Attempts to serialize a class that does not have the SqlUserDefinedType attribute. Verifies that this fails.
+ ///
+ [Fact]
+ public void Serialize_MissingSqlUserDefinedTypeAttribute_Throws()
+ {
+ Action serialize = () => SerializationHelperSql9.Serialize(_stream, new ClassMissingSqlUserDefinedTypeAttribute());
+ var exception = Assert.Throws(serialize);
+
+ Assert.Equal($"'{typeof(ClassMissingSqlUserDefinedTypeAttribute).FullName}' is an invalid user defined type, reason: no UDT attribute.", exception.Message);
+ }
+
+ ///
+ /// Attempts to serialize a class that has a SqlUserDefinedType attribute, but specifies a Format enumeration value of
+ /// Unknown. Verifies that this fails.
+ ///
+ [Fact]
+ public void Serialize_UnknownFormattedType_Throws()
+ {
+ Action serialize = () => SerializationHelperSql9.Serialize(_stream, new UnknownFormattedClass());
+ var exception = Assert.Throws("Format", serialize);
+
+#if NET
+ Assert.Equal("The Format enumeration value, 0, is not supported by the format method. (Parameter 'Format')", exception.Message);
+#else
+ Assert.Equal("The Format enumeration value, Unknown, is not supported by the format method.\r\nParameter name: Format", exception.Message);
+#endif
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs
new file mode 100644
index 0000000000..3369a66985
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs
@@ -0,0 +1,491 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Data.SqlClient.Server;
+using Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
+using System;
+using System.Collections.Generic;
+using System.Data.SqlTypes;
+using System.IO;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization;
+
+///
+/// Tests the serialization method defined by MS-SSCLRT. Ensures that combinations of primitives and custom types round-trip.
+///
+///
+public sealed class NativeSerializationTest : IDisposable
+{
+ private readonly MemoryStream _stream;
+
+ ///
+ /// Initializes the MemoryStream used for all tests in this class.
+ ///
+ public NativeSerializationTest()
+ {
+ _stream = new MemoryStream();
+ }
+
+ void IDisposable.Dispose()
+ {
+ _stream.Dispose();
+ }
+
+ ///
+ /// Provides a collection of test data representing non-null primitive type values and their corresponding
+ /// serialized byte arrays.
+ ///
+ ///
+ public static TheoryData SerializedNonNullPrimitiveTypeValues() =>
+ new()
+ {
+ {
+ new BoolWrapperStruct { Field1 = true },
+ new byte[] { 0x01 }
+ },
+ {
+ new ByteWrapperStruct { Field1 = 0x20 },
+ new byte[] { 0x20 }
+ },
+ {
+ new SByteWrapperStruct { Field1 = -0x1 },
+ new byte[] { 0x7F }
+ },
+ {
+ new UShortWrapperStruct { Field1 = 0x8000 },
+ new byte[] { 0x80, 0x00 }
+ },
+ {
+ new ShortWrapperStruct { Field1 = 0x1234 },
+ new byte[] { 0x92, 0x34 }
+ },
+ {
+ new UIntWrapperStruct { Field1 = 0xFFFFFFFF },
+ new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }
+ },
+ {
+ new IntWrapperStruct { Field1 = -0x12345678 },
+ new byte[] { 0x6D, 0xCB, 0xA9, 0x88 }
+ },
+ {
+ new ULongWrapperStruct { Field1 = ulong.MaxValue },
+ new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }
+ },
+ {
+ new LongWrapperStruct { Field1 = long.MinValue },
+ new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
+ },
+ {
+ new FloatWrapperStruct { Field1 = -0 },
+ new byte[] { 0x80, 0x00, 0x00, 0x00 }
+ },
+ {
+ new DoubleWrapperStruct { Field1 = Math.PI },
+ new byte[] { 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }
+ },
+ {
+ new SqlByteWrapperStruct { Field1 = 0x20 },
+ new byte[] { 0x01, 0x20 }
+ },
+ {
+ new SqlInt16WrapperStruct { Field1 = 0x1234 },
+ new byte[] { 0x01, 0x92, 0x34 }
+ },
+ {
+ new SqlInt32WrapperStruct { Field1 = -0x12345678 },
+ new byte[] { 0x01, 0x6D, 0xCB, 0xA9, 0x88 }
+ },
+ {
+ new SqlInt64WrapperStruct { Field1 = long.MinValue },
+ new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlBooleanWrapperStruct { Field1 = false },
+ new byte[] { 0x01 }
+ },
+ {
+ new SqlSingleWrapperStruct { Field1 = -1 },
+ new byte[] { 0x01, 0x40, 0x7F, 0xFF, 0xFF }
+ },
+ {
+ new SqlDoubleWrapperStruct { Field1 = -Math.PI },
+ new byte[] { 0x01, 0x3F, 0xF6, 0xDE, 0x04, 0xAB, 0xBB, 0xD2, 0xE7 }
+ },
+ {
+ new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500) },
+ new byte[] { 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6 }
+ },
+ {
+ new SqlMoneyWrapperStruct { Field1 = 1.10m },
+ new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8 }
+ }
+ };
+
+ ///
+ /// Provides a collection of test data representing serialized values of nested non-null primitive types.
+ ///
+ ///
+ public static TheoryData SerializedNestedNonNullPrimitiveTypeValues() =>
+ new()
+ {
+ {
+ new NestedBoolWrapperStruct { Field1 = true, Field2 = new BoolWrapperStruct { Field1 = false } },
+ new byte[]
+ {
+ // Field1
+ 0x01,
+ // Field2
+ 0x00
+ }
+ },
+ {
+ new NestedByteWrapperStruct { Field1 = 0x20, Field2 = new ByteWrapperStruct { Field1 = 0x30 } },
+ new byte[]
+ {
+ // Field1
+ 0x20,
+ // Field2
+ 0x30
+ }
+ },
+ {
+ new NestedSByteWrapperStruct { Field1 = -0x01, Field2 = new SByteWrapperStruct { Field1 = 0x01 } },
+ new byte[]
+ {
+ // Field1
+ 0x7F,
+ // Field2
+ 0x81
+ }
+ },
+ {
+ new NestedUShortWrapperStruct { Field1 = 0x8000, Field2 = new UShortWrapperStruct { Field1 = 0x8014 } },
+ new byte[]
+ {
+ // Field1
+ 0x80, 0x00,
+ // Field2.Field1
+ 0x80, 0x14
+ }
+ },
+ {
+ new NestedShortWrapperStruct { Field1 = 0x1234, Field2 = new ShortWrapperStruct { Field1 = 0x4321 } },
+ new byte[]
+ {
+ // Field1
+ 0x92, 0x34,
+ // Field2.Field1
+ 0xC3, 0x21
+ }
+ },
+ {
+ new NestedUIntWrapperStruct { Field1 = 0xFFFFFFFF, Field2 = new UIntWrapperStruct { Field1 = 0x00000000 } },
+ new byte[]
+ {
+ // Field1
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ // Field2.Field1
+ 0x00, 0x00, 0x00, 0x00
+ }
+ },
+ {
+ new NestedIntWrapperStruct { Field1 = -0x12345678, Field2 = new IntWrapperStruct { Field1 = 0x12345678 } },
+ new byte[]
+ {
+ /// Field1
+ 0x6D, 0xCB, 0xA9, 0x88,
+ // Field2.Field1
+ 0x92, 0x34, 0x56, 0x78
+ }
+ },
+ {
+ new NestedULongWrapperStruct { Field1 = ulong.MaxValue, Field2 = new ULongWrapperStruct { Field1 = long.MaxValue } },
+ new byte[]
+ {
+ // Field1
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ // Field2.Field1
+ 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+ }
+ },
+ {
+ new NestedLongWrapperStruct { Field1 = long.MinValue, Field2 = new LongWrapperStruct { Field1 = long.MaxValue } },
+ new byte[]
+ {
+ // Field1
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // Field2.Field1
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+ }
+ },
+ {
+ new NestedFloatWrapperStruct { Field1 = -0, Field2 = new FloatWrapperStruct { Field1 = +0 } },
+ new byte[]
+ {
+ // Field1
+ 0x80, 0x00, 0x00, 0x00,
+ // Field2.Field1
+ 0x80, 0x00, 0x00, 0x00
+ }
+ },
+ {
+ new NestedDoubleWrapperStruct { Field1 = Math.PI, Field2 = new DoubleWrapperStruct { Field1 = Math.PI } },
+ new byte[]
+ {
+ // Field1
+ 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18,
+ // Field2.Field1
+ 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18
+ }
+ },
+ {
+ new NestedSqlByteWrapperStruct { Field1 = 0x20, Field2 = new SqlByteWrapperStruct { Field1 = 0x30 } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x20,
+ // Field2.Field1
+ 0x01, 0x30
+ }
+ },
+ {
+ new NestedSqlInt16WrapperStruct { Field1 = 0x1234, Field2 = new SqlInt16WrapperStruct { Field1 = 0x4321 } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x92, 0x34,
+ // Field2.Field1
+ 0x01, 0xC3, 0x21
+ }
+ },
+ {
+ new NestedSqlInt32WrapperStruct { Field1 = -0x12345678, Field2 = new SqlInt32WrapperStruct { Field1 = 0x12345678 } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x6D, 0xCB, 0xA9, 0x88,
+ // Field2.Field1
+ 0x01, 0x92, 0x34, 0x56, 0x78
+ }
+ },
+ {
+ new NestedSqlInt64WrapperStruct { Field1 = long.MinValue, Field2 = new SqlInt64WrapperStruct { Field1 = long.MaxValue } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // Field2.Field1
+ 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+ }
+ },
+ {
+ new NestedSqlBooleanWrapperStruct { Field1 = false, Field2 = new SqlBooleanWrapperStruct { Field1 = true } },
+ new byte[]
+ {
+ // Field1
+ 0x01,
+ // Field2.Field1
+ 0x02
+ }
+ },
+ {
+ new NestedSqlSingleWrapperStruct { Field1 = -0, Field2 = new SqlSingleWrapperStruct { Field1 = +0 } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x80, 0x00, 0x00, 0x00,
+ // Field2.Field1
+ 0x01, 0x80, 0x00, 0x00, 0x00
+ }
+ },
+ {
+ new NestedSqlDoubleWrapperStruct { Field1 = Math.PI, Field2 = new SqlDoubleWrapperStruct { Field1 = Math.PI } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18,
+ // Field2.Field1
+ 0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18
+ }
+ },
+ {
+ new NestedSqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500), Field2 = new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1) } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6,
+ // Field2.Field1
+ 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0x00, 0x00, 0x00
+ }
+ },
+ {
+ new NestedSqlMoneyWrapperStruct { Field1 = 1.10m, Field2 = new SqlMoneyWrapperStruct { Field1 = -2.55m } },
+ new byte[]
+ {
+ // Field1
+ 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8,
+ // Field2.Field1
+ 0x01, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0x64
+ }
+ }
+ };
+
+ ///
+ /// Provides a collection of test data representing serialized null values for various primitive types.
+ ///
+ ///
+ public static TheoryData SerializedNullPrimitiveTypeValues() =>
+ new()
+ {
+ {
+ new SqlByteWrapperStruct { Field1 = SqlByte.Null },
+ new byte[] { 0x00, 0x00 }
+ },
+ {
+ new SqlInt16WrapperStruct { Field1 = SqlInt16.Null },
+ new byte[] { 0x00, 0x80, 0x00 }
+ },
+ {
+ new SqlInt32WrapperStruct { Field1 = SqlInt32.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlInt64WrapperStruct { Field1 = SqlInt64.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlBooleanWrapperStruct { Field1 = SqlBoolean.Null },
+ new byte[] { 0x00 }
+ },
+ {
+ new SqlSingleWrapperStruct { Field1 = SqlSingle.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlDoubleWrapperStruct { Field1 = SqlDouble.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlDateTimeWrapperStruct { Field1 = SqlDateTime.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00 }
+ },
+ {
+ new SqlMoneyWrapperStruct { Field1 = SqlMoney.Null },
+ new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
+ }
+ };
+
+ ///
+ /// Attempts to serialize various structs containing non-null primitive types.
+ /// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
+ ///
+ /// Primitive to serialize and to compare against.
+ /// Expected byte output.
+ [Theory]
+ [MemberData(nameof(SerializedNonNullPrimitiveTypeValues))]
+ public void Serialize_PrimitiveType_Roundtrips(object primitive, byte[] expectedValue) =>
+ RoundtripType(primitive, expectedValue);
+
+ ///
+ /// Attempts to serialize a nested struct hierarchy containing non-null primitive types.
+ /// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
+ ///
+ /// Primitive to serialize and to compare against.
+ /// Expected byte output.
+ [Theory]
+ [MemberData(nameof(SerializedNestedNonNullPrimitiveTypeValues))]
+ public void Serialize_NestedPrimitiveType_Roundtrips(object primitive, byte[] expectedValue) =>
+ RoundtripType(primitive, expectedValue);
+
+ ///
+ /// Attempts to serialize various structs containing null-valued primitive types.
+ /// Verifies that the method does not throw, that serialized byte output is correct, and that the value round-trips.
+ ///
+ /// Primitive to serialize and to compare against.
+ /// Expected byte output.
+ [Theory]
+ [MemberData(nameof(SerializedNullPrimitiveTypeValues))]
+ public void Serialize_NullPrimitiveType_Roundtrips(object primitive, byte[] expectedValue) =>
+ RoundtripType(primitive, expectedValue);
+
+ ///
+ /// Attempts to serialize an instance of a class.
+ ///
+ ///
+ [Fact]
+ public void Serialize_TopLevelClass_Succeeds()
+ {
+ NestedBoolWrapperClass validWrapper = new()
+ {
+ Field1 = true,
+ Field2 = new BoolWrapperStruct() { Field1 = true }
+ };
+
+ SerializationHelperSql9.Serialize(_stream, validWrapper);
+ }
+
+ ///
+ /// Attempts to serialize a field referring to an instance of a class.
+ /// Verifies that this fails, and that Native format serialization only operates with primitive types and value types containing these.
+ ///
+ ///
+ [Fact]
+ public void Serialize_NestedClass_Throws()
+ {
+ InvalidNestedBoolWrapperClass invalidWrapper = new()
+ {
+ Field1 = true,
+ Field2 = new BoolWrapperClass() { Field1 = true }
+ };
+
+ var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(_stream, invalidWrapper));
+ string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName);
+
+ Assert.Equal(expectedException, ex.Message);
+ }
+
+ ///
+ /// Attempts to serialize a struct containing non-primitive value types.
+ /// Verifies that this fails.
+ ///
+ [Fact]
+ public void Serialize_NonPrimitiveType_Throws()
+ {
+ InvalidIntPtrAndByteWrapperStruct invalidWrapper = new()
+ {
+ Field1 = 1,
+ Field2 = IntPtr.Zero
+ };
+
+ var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(_stream, invalidWrapper));
+ string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName);
+
+ Assert.Equal(expectedException, ex.Message);
+ }
+
+ ///
+ /// Serializes an object, verifies the value and the size of the object, then roundtrips it and verifies the result is identical.
+ ///
+ /// Object to serialize.
+ /// Expected serialization output.
+ private void RoundtripType(object inputValue, byte[] expectedValue)
+ {
+ int typeSize = SerializationHelperSql9.SizeInBytes(inputValue.GetType());
+ int objectSize = SerializationHelperSql9.SizeInBytes(inputValue);
+ int maxTypeSize = SerializationHelperSql9.GetUdtMaxLength(inputValue.GetType());
+
+ SerializationHelperSql9.Serialize(_stream, inputValue);
+ _stream.Seek(0, SeekOrigin.Begin);
+ object readPrimitive = SerializationHelperSql9.Deserialize(_stream, inputValue.GetType());
+
+ // For native formatting, the type size, the object size and the maximum object size will always be identical
+ Assert.Equal(typeSize, objectSize);
+ Assert.Equal(expectedValue.Length, typeSize);
+ Assert.Equal(typeSize, maxTypeSize);
+
+ Assert.Equal(expectedValue, _stream.ToArray());
+ Assert.Equal(inputValue, readPrimitive);
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/SerializedTypes.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/SerializedTypes.cs
new file mode 100644
index 0000000000..a02c213e01
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/SerializedTypes.cs
@@ -0,0 +1,335 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.SqlServer.Server;
+using System;
+using System.Data.SqlTypes;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
+
+// Simple cases: a struct containing one of the designated primitive types
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct BoolWrapperStruct { public bool Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct ByteWrapperStruct { public byte Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SByteWrapperStruct { public sbyte Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct UShortWrapperStruct { public ushort Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct ShortWrapperStruct { public short Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct UIntWrapperStruct { public uint Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct IntWrapperStruct { public int Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct ULongWrapperStruct { public ulong Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct LongWrapperStruct { public long Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct FloatWrapperStruct { public float Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct DoubleWrapperStruct { public double Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlByteWrapperStruct { public SqlByte Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlInt16WrapperStruct { public SqlInt16 Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlInt32WrapperStruct { public SqlInt32 Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlInt64WrapperStruct { public SqlInt64 Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlBooleanWrapperStruct { public SqlBoolean Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlSingleWrapperStruct { public SqlSingle Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlDoubleWrapperStruct { public SqlDouble Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct SqlMoneyWrapperStruct { public SqlMoney Field1; }
+
+
+// Success case: a class containing one of the designated primitive types
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal class BoolWrapperClass { public bool Field1; }
+
+// Success case: a struct containing one designated primitive type and one nested struct
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; }
+
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct NestedSqlMoneyWrapperStruct { public SqlMoney Field1; public SqlMoneyWrapperStruct Field2; }
+
+
+// Success case: a class containing one designated primitive type and a nested struct
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal class NestedBoolWrapperClass { public bool Field1; public BoolWrapperStruct Field2; }
+
+// Failure case: a struct or a class containing one designated primitive type and a nested class
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal class InvalidNestedBoolWrapperClass { public bool Field1; public BoolWrapperClass? Field2; }
+
+// Failure case: a struct or a class containing a field which is not a designated primitive type
+[SqlUserDefinedType(Format.Native)]
+[StructLayout(LayoutKind.Sequential)]
+internal struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; }
+
+// Success case: a struct or a class implementing IBinarySerialize which would not otherwise be serializable
+internal interface IFormattingProgress
+{
+ bool ParameterlessConstructorInvoked { get; }
+ bool ReadInvoked { get; }
+ bool WriteInvoked { get; }
+}
+
+[SqlUserDefinedType(Format.UserDefined, MaxByteSize = 11)]
+internal struct UserDefinedFormattedStruct : IBinarySerialize, IFormattingProgress, IEquatable
+{
+ public IntPtr Field1;
+ public bool ParameterlessConstructorInvoked { get; }
+ public bool ReadInvoked { get; private set; }
+ public bool WriteInvoked { get; private set; }
+
+ public UserDefinedFormattedStruct()
+ {
+ ParameterlessConstructorInvoked = true;
+ }
+
+ public UserDefinedFormattedStruct(IntPtr field1)
+ {
+ Field1 = field1;
+ }
+
+ public void Read(BinaryReader r)
+ {
+ Field1 = IntPtr.Size switch
+ {
+ sizeof(uint) => (IntPtr)r.ReadUInt32(),
+ sizeof(ulong) => (IntPtr)r.ReadUInt64(),
+ _ => throw new Exception("Invalid IntPtr size")
+ };
+
+ ReadInvoked = true;
+ }
+
+ public void Write(BinaryWriter w)
+ {
+ if (IntPtr.Size == sizeof(uint))
+ {
+ w.Write((uint)Field1);
+ }
+ else if (IntPtr.Size == sizeof(ulong))
+ {
+ w.Write((ulong)Field1);
+ }
+ else
+ {
+ throw new Exception("Invalid IntPtr size");
+ }
+
+ WriteInvoked = true;
+ }
+
+ public bool Equals(UserDefinedFormattedStruct other)
+ => other.Field1 == Field1;
+}
+
+[SqlUserDefinedType(Format.UserDefined, MaxByteSize = 11)]
+internal class UserDefinedFormattedClass : IBinarySerialize, IFormattingProgress, IEquatable
+{
+ public IntPtr Field1;
+ public bool ParameterlessConstructorInvoked { get; }
+ public bool ReadInvoked { get; private set; }
+ public bool WriteInvoked { get; private set; }
+
+ public UserDefinedFormattedClass()
+ {
+ ParameterlessConstructorInvoked = true;
+ }
+
+ public UserDefinedFormattedClass(IntPtr field1)
+ {
+ Field1 = field1;
+ }
+
+ public void Read(BinaryReader r)
+ {
+ Field1 = IntPtr.Size switch
+ {
+ sizeof(uint) => (IntPtr)r.ReadUInt32(),
+ sizeof(ulong) => (IntPtr)r.ReadUInt64(),
+ _ => throw new Exception("Invalid IntPtr size")
+ };
+
+ ReadInvoked = true;
+ }
+
+ public void Write(BinaryWriter w)
+ {
+ if (IntPtr.Size == sizeof(uint))
+ {
+ w.Write((uint)Field1);
+ }
+ else if (IntPtr.Size == sizeof(ulong))
+ {
+ w.Write((ulong)Field1);
+ }
+ else
+ {
+ throw new Exception("Invalid IntPtr size");
+ }
+
+ WriteInvoked = true;
+ }
+
+ public bool Equals(UserDefinedFormattedClass? other)
+ => other is not null && other.Field1 == Field1;
+}
+
+// Failure cases: type does not have a public constructor, does not implement IBinarySerialize, does not have a SqlUserDefinedType attribute,
+// or has a SqlUserDefinedType attribute with a Format of Unknown.
+
+[SqlUserDefinedType(Format.UserDefined)]
+internal class UserDefinedMissingPublicConstructor : IBinarySerialize
+{
+ public UserDefinedMissingPublicConstructor(bool _) { }
+
+ public void Read(BinaryReader r) { }
+
+ public void Write(BinaryWriter w) { }
+}
+
+[SqlUserDefinedType(Format.UserDefined)]
+internal class UserDefinedDoesNotImplementIBinarySerialize
+{
+ public UserDefinedDoesNotImplementIBinarySerialize() { }
+}
+
+internal class ClassMissingSqlUserDefinedTypeAttribute
+{
+}
+
+[SqlUserDefinedType(Format.Unknown)]
+internal class UnknownFormattedClass
+{
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/UserDefinedSerializationTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/UserDefinedSerializationTest.cs
new file mode 100644
index 0000000000..4d7ea799fd
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/UserDefinedSerializationTest.cs
@@ -0,0 +1,123 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Data.SqlClient.Server;
+using Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization;
+
+///
+/// Tests the user-defined UDT serialization method. Verifies that custom types round-trip.
+///
+public sealed class UserDefinedSerializationTest : IDisposable
+{
+ private readonly MemoryStream _stream;
+
+ ///
+ /// Initializes the MemoryStream used for all tests in this class.
+ ///
+ public UserDefinedSerializationTest()
+ {
+ _stream = new MemoryStream();
+ }
+
+ void IDisposable.Dispose()
+ {
+ _stream.Dispose();
+ }
+
+ ///
+ /// Attempts to serialize and deserialize an instance of a struct with a user-defined serialization method.
+ ///
+ ///
+ [Fact]
+ public void Serialize_Struct_Roundtrips() =>
+ RoundtripType(new UserDefinedFormattedStruct((IntPtr)0x12345678));
+
+ ///
+ /// Attempts to serialize and deserialize an instance of a class with a user-defined serialization method.
+ ///
+ ///
+ [Fact]
+ public void Serialize_Class_Roundtrips() =>
+ RoundtripType(new UserDefinedFormattedClass((IntPtr)0x12345678));
+
+ ///
+ /// Attempts to deserialize an instance of a type with a user-defined serialization method but without a public
+ /// parameterless constructor. Verifies that this fails.
+ ///
+ [Fact]
+ public void Deserialize_MissingPublicParameterlessConstructor_Throws()
+ {
+ SerializationHelperSql9.Serialize(_stream, new UserDefinedMissingPublicConstructor(true));
+ _stream.Seek(0, SeekOrigin.Begin);
+
+ Action deserialize = () => SerializationHelperSql9.Deserialize(_stream, typeof(UserDefinedMissingPublicConstructor));
+
+ Assert.Throws(deserialize);
+ }
+
+ ///
+ /// Attempts to deserialize an instance of a type with a user-defined serialization method but which does not,
+ /// implement IBinarySerialize. Verifies that this fails.
+ ///
+ [Fact]
+ public void Serialize_DoesNotImplementIBinarySerialize_Throws()
+ {
+ Action serialize = () => SerializationHelperSql9.Serialize(_stream, new UserDefinedDoesNotImplementIBinarySerialize());
+
+ Assert.Throws(serialize);
+ }
+
+ private void RoundtripType(T userObject)
+ where T : IFormattingProgress
+ {
+ int typeSize = SerializationHelperSql9.SizeInBytes(userObject.GetType());
+ int objectSize = SerializationHelperSql9.SizeInBytes(userObject);
+ int maxTypeSize = SerializationHelperSql9.GetUdtMaxLength(userObject.GetType());
+
+ SerializationHelperSql9.Serialize(_stream, userObject);
+ _stream.Seek(0, SeekOrigin.Begin);
+ byte[] serializedValue = _stream.ToArray();
+ T readInstance = (T)SerializationHelperSql9.Deserialize(_stream, userObject.GetType());
+
+ // If this is a struct, it will have been copied by value and the write to WriteInvoked will have been made
+ // to another copy of our object
+ if (!typeof(T).IsValueType)
+ {
+ Assert.True(userObject.WriteInvoked);
+ }
+
+ Assert.Equal(IntPtr.Size, typeSize);
+ Assert.Equal(IntPtr.Size, objectSize);
+ Assert.Equal(11, maxTypeSize);
+
+ Assert.Equal(IntPtr.Size, serializedValue.Length);
+ if (IntPtr.Size == 8)
+ {
+ Assert.Equal([0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00], serializedValue);
+ }
+ else if (IntPtr.Size == 4)
+ {
+ Assert.Equal([0x78, 0x56, 0x34, 0x12], serializedValue);
+ }
+ else
+ {
+ Assert.Fail("Invalid IntPtr size.");
+ }
+
+ // In .NET Framework, Activator.CreateInstance does not invoke a struct's parameterless constructor
+#if NET
+ Assert.NotEqual(userObject.ParameterlessConstructorInvoked, readInstance.ParameterlessConstructorInvoked);
+ Assert.True(readInstance.ParameterlessConstructorInvoked);
+#endif
+ Assert.True(readInstance.ReadInvoked);
+
+ Assert.Equal(userObject, readInstance);
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs
index f7cd1811ed..cf97f21a39 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlTypeWorkaroundsTests.cs
@@ -20,7 +20,7 @@ public class SqlTypeWorkaroundsTests
#region SqlBinary
public static TheoryData ByteArrayToSqlBinary_NonNullInput_Data =>
- new TheoryData
+ new()
{
Array.Empty(),
new byte[] { 1, 2, 3, 4},
@@ -53,7 +53,7 @@ public void ByteArrayToSqlBinary_NullInput()
#region SqlDecimal
public static TheoryData SqlDecimalWriteTdsValue_NonNullInput_Data =>
- new TheoryData
+ new()
{
SqlDecimal.MinValue,
new SqlDecimal(-1.2345678),
@@ -102,7 +102,7 @@ public void SqlDecimalWriteTdsValue_NullInput()
#region SqlGuid
public static TheoryData ByteArrayToSqlGuid_InvalidInput_Data =>
- new TheoryData
+ new()
{
null,
Array.Empty(),
@@ -122,7 +122,7 @@ public void ByteArrayToSqlGuid_InvalidInput(byte[]? input)
}
public static TheoryData ByteArrayToSqlGuid_ValidInput_Data =>
- new TheoryData
+ new()
{
new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }
@@ -145,7 +145,7 @@ public void ByteArrayToSqlGuid_ValidInput(byte[] input)
#region SqlMoney
public static TheoryData LongToSqlMoney_Data =>
- new TheoryData
+ new()
{
{ long.MinValue, SqlMoney.MinValue },
{ (long)((decimal)-123000000 / 10000), new SqlMoney(-1.23) },
@@ -166,7 +166,7 @@ public void LongToSqlMoney(long input, SqlMoney expected)
}
public static TheoryData SqlMoneyToLong_NonNullInput_Data =>
- new TheoryData
+ new()
{
{ SqlMoney.MinValue, long.MinValue },
{ new SqlMoney(-1.23), (long)(new SqlMoney(-1.23).ToDecimal() * 10000) },
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs
index f0b3729d6f..17a4ad8b5a 100644
--- a/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/TdsParserInternalsTest.cs
@@ -92,7 +92,7 @@ public void WriteUserAgentFeatureRequest_WriteTrue_AppendsOnlyExtensionBytes()
bufferAfter[start + 5]);
// slice into the existing buffer
- ReadOnlySpan writtenSpan = new ReadOnlySpan(
+ ReadOnlySpan writtenSpan = new(
bufferAfter,
start + 6,
appended - 6);