From 95e811e18b8206788d106df6b0e3c8648f5f4035 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:47:06 +0100 Subject: [PATCH 01/10] Expand code coverage for UDT serialization --- .../Microsoft.Data.SqlClient.UnitTests.csproj | 1 + .../InvalidSerializationTest.cs | 41 +++ .../NativeSerializationTest.cs | 268 ++++++++++++++++++ .../UdtSerialization/SerializedTypes.cs | 207 ++++++++++++++ .../UserDefinedSerializationTest.cs | 97 +++++++ 5 files changed, 614 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/SerializedTypes.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/UserDefinedSerializationTest.cs 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..2a8c60f4ce 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 @@ -25,6 +25,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..cba44991b2 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs @@ -0,0 +1,41 @@ +// 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; + +public class InvalidSerializationTest +{ + [Fact] + public void RequiresSqlUserDefinedTypeAttribute() + { + using MemoryStream stream = new MemoryStream(); + + InvalidUdtException exception = Assert.Throws( + () => SerializationHelperSql9.Serialize(stream, new ClassMissingSqlUserDefinedTypeAttribute())); + + Assert.Equal($"'{typeof(ClassMissingSqlUserDefinedTypeAttribute).FullName}' is an invalid user defined type, reason: no UDT attribute.", exception.Message); + } + + [Fact] + public void CannotSerializeUnknownFormattedType() + { + using MemoryStream stream = new MemoryStream(); + + ArgumentOutOfRangeException exception = Assert.Throws("Format", + () => SerializationHelperSql9.Serialize(stream, new UnknownFormattedClass())); + +#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..6b399700dd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/NativeSerializationTest.cs @@ -0,0 +1,268 @@ +// 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 class NativeSerializationTest +{ + public static IEnumerable SerializedNonNullPrimitiveTypeValues() + { + yield return [new BoolWrapperStruct { Field1 = true }, + new byte[] { 0x01 }]; + yield return [new ByteWrapperStruct { Field1 = 0x20 }, + new byte[] { 0x20 }]; + yield return [new SByteWrapperStruct { Field1 = -0x1 }, + new byte[] { 0x7F }]; + yield return [new UShortWrapperStruct { Field1 = 0x8000 }, + new byte[] { 0x80, 0x00 }]; + yield return [new ShortWrapperStruct { Field1 = 0x1234 }, + new byte[] { 0x92, 0x34 }]; + yield return [new UIntWrapperStruct { Field1 = 0xFFFFFFFF }, + new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }]; + yield return [new IntWrapperStruct { Field1 = -0x12345678 }, + new byte[] { 0x6D, 0xCB, 0xA9, 0x88 }]; + yield return [new ULongWrapperStruct { Field1 = ulong.MaxValue }, + new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }]; + yield return [new LongWrapperStruct { Field1 = long.MinValue }, + new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }]; + yield return [new FloatWrapperStruct { Field1 = -0 }, + new byte[] { 0x80, 0x00, 0x00, 0x00 }]; + yield return [new DoubleWrapperStruct { Field1 = Math.PI }, + new byte[] { 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }]; + yield return [new SqlByteWrapperStruct { Field1 = 0x20 }, + new byte[] { 0x01, 0x20 }]; + yield return [new SqlInt16WrapperStruct { Field1 = 0x1234 }, + new byte[] { 0x01, 0x92, 0x34 }]; + yield return [new SqlInt32WrapperStruct { Field1 = -0x12345678 }, + new byte[] { 0x01, 0x6D, 0xCB, 0xA9, 0x88 }]; + yield return [new SqlInt64WrapperStruct { Field1 = long.MinValue }, + new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }]; + yield return [new SqlBooleanWrapperStruct { Field1 = false }, + new byte[] { 0x01 }]; + yield return [new SqlSingleWrapperStruct { Field1 = -1 }, + new byte[] { 0x01, 0x40, 0x7F, 0xFF, 0xFF }]; + yield return [new SqlDoubleWrapperStruct { Field1 = -Math.PI }, + new byte[] { 0x01, 0x3F, 0xF6, 0xDE, 0x04, 0xAB, 0xBB, 0xD2, 0xE7 }]; + yield return [new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500) }, + new byte[] { 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6 }]; + yield return [new SqlMoneyWrapperStruct { Field1 = 1.10m }, + new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8 }]; + } + + public static IEnumerable SerializedNestedNonNullPrimitiveTypeValues() + { + yield return [new NestedBoolWrapperStruct { Field1 = true, Field2 = new BoolWrapperStruct { Field1 = false } }, + new byte[] { 0x01, + 0x00 }]; + yield return [new NestedByteWrapperStruct { Field1 = 0x20, Field2 = new ByteWrapperStruct { Field1 = 0x30 } }, + new byte[] { 0x20, + 0x30 }]; + yield return [new NestedSByteWrapperStruct { Field1 = -0x01, Field2 = new SByteWrapperStruct { Field1 = 0x01 } }, + new byte[] { 0x7F, + 0x81 }]; + yield return [new NestedUShortWrapperStruct { Field1 = 0x8000, Field2 = new UShortWrapperStruct { Field1 = 0x8014 } }, + new byte[] { 0x80, 0x00, + 0x80, 0x14 }]; + yield return [new NestedShortWrapperStruct { Field1 = 0x1234, Field2 = new ShortWrapperStruct { Field1 = 0x4321 } }, + new byte[] { 0x92, 0x34, + 0xC3, 0x21 }]; + yield return [new NestedUIntWrapperStruct { Field1 = 0xFFFFFFFF, Field2 = new UIntWrapperStruct { Field1 = 0x00000000 } }, + new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00 }]; + yield return [new NestedIntWrapperStruct { Field1 = -0x12345678, Field2 = new IntWrapperStruct { Field1 = 0x12345678 } }, + new byte[] { 0x6D, 0xCB, 0xA9, 0x88, + 0x92, 0x34, 0x56, 0x78 }]; + yield return [new NestedULongWrapperStruct { Field1 = ulong.MaxValue, Field2 = new ULongWrapperStruct { Field1 = long.MaxValue } }, + new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }]; + yield return [new NestedLongWrapperStruct { Field1 = long.MinValue, Field2 = new LongWrapperStruct { Field1 = long.MaxValue } }, + new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }]; + yield return [new NestedFloatWrapperStruct { Field1 = -0, Field2 = new FloatWrapperStruct { Field1 = +0 } }, + new byte[] { 0x80, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00 }]; + yield return [new NestedDoubleWrapperStruct { Field1 = Math.PI, Field2 = new DoubleWrapperStruct { Field1 = Math.PI } }, + new byte[] { 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18, + 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }]; + yield return [new NestedSqlByteWrapperStruct { Field1 = 0x20, Field2 = new SqlByteWrapperStruct { Field1 = 0x30 } }, + new byte[] { 0x01, 0x20, + 0x01, 0x30 }]; + yield return [new NestedSqlInt16WrapperStruct { Field1 = 0x1234, Field2 = new SqlInt16WrapperStruct { Field1 = 0x4321 } }, + new byte[] { 0x01, 0x92, 0x34, + 0x01, 0xC3, 0x21 }]; + yield return [new NestedSqlInt32WrapperStruct { Field1 = -0x12345678, Field2 = new SqlInt32WrapperStruct { Field1 = 0x12345678 } }, + new byte[] { 0x01, 0x6D, 0xCB, 0xA9, 0x88, + 0x01, 0x92, 0x34, 0x56, 0x78 }]; + yield return [new NestedSqlInt64WrapperStruct { Field1 = long.MinValue, Field2 = new SqlInt64WrapperStruct { Field1 = long.MaxValue } }, + new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }]; + yield return [new NestedSqlBooleanWrapperStruct { Field1 = false, Field2 = new SqlBooleanWrapperStruct { Field1 = true } }, + new byte[] { 0x01, + 0x02 }]; + yield return [new NestedSqlSingleWrapperStruct { Field1 = -0, Field2 = new SqlSingleWrapperStruct { Field1 = +0 } }, + new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, + 0x01, 0x80, 0x00, 0x00, 0x00 }]; + yield return [new NestedSqlDoubleWrapperStruct { Field1 = Math.PI, Field2 = new SqlDoubleWrapperStruct { Field1 = Math.PI } }, + new byte[] { 0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18, + 0x01, 0xC0, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18 }]; + yield return [new NestedSqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1, 12, 34, 56, 500), Field2 = new SqlDateTimeWrapperStruct { Field1 = new DateTime(2000, 1, 1) } }, + new byte[] { 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0xCF, 0x59, 0xD6, + 0x01, 0x80, 0x00, 0x8E, 0xAC, 0x80, 0x00, 0x00, 0x00 }]; + yield return [new NestedSqlMoneyWrapperStruct { Field1 = 1.10m, Field2 = new SqlMoneyWrapperStruct { Field1 = -2.55m } }, + new byte[] { 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xF8, + 0x01, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0x64 }]; + } + + public static IEnumerable SerializedNullPrimitiveTypeValues() + { + yield return [new SqlByteWrapperStruct { Field1 = SqlByte.Null }, + new byte[] { 0x00, 0x00 }]; + yield return [new SqlInt16WrapperStruct { Field1 = SqlInt16.Null }, + new byte[] { 0x00, 0x80, 0x00 }]; + yield return [new SqlInt32WrapperStruct { Field1 = SqlInt32.Null }, + new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }]; + yield return [new SqlInt64WrapperStruct { Field1 = SqlInt64.Null }, + new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }]; + yield return [new SqlBooleanWrapperStruct { Field1 = SqlBoolean.Null }, + new byte[] { 0x00 }]; + yield return [new SqlSingleWrapperStruct { Field1 = SqlSingle.Null }, + new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00 }]; + yield return [new SqlDoubleWrapperStruct { Field1 = SqlDouble.Null }, + new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }]; + yield return [new SqlDateTimeWrapperStruct { Field1 = SqlDateTime.Null }, + new byte[] { 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00 }]; + yield return [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 SerializePrimitiveType(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 SerializeNestedPrimitiveType(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 SerializeNullPrimitiveType(object primitive, byte[] expectedValue) + => RoundtripType(primitive, expectedValue); + + /// + /// Attempts to serializes an instance of a class. + /// + /// + [Fact] + public void CanSerializeTopLevelClass() + { + NestedBoolWrapperClass validWrapper = new NestedBoolWrapperClass() + { + Field1 = true, + Field2 = new BoolWrapperStruct() { Field1 = true } + }; + using MemoryStream stream = new MemoryStream(); + + SerializationHelperSql9.Serialize(stream, validWrapper); + } + + /// + /// Attempts to serializes a field referring to an instance of a class. + /// Verifies that this succeeds, and that Native format serialization only operates with primitive types + /// and value types containing these. + /// + /// + [Fact] + public void CannotSerializeNestedClass() + { + InvalidNestedBoolWrapperClass invalidWrapper = new InvalidNestedBoolWrapperClass() + { + Field1 = true, + Field2 = new BoolWrapperClass() { Field1 = true } + }; + using MemoryStream stream = new MemoryStream(); + + Exception 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 CannotSerializeNonPrimitiveType() + { + InvalidIntPtrAndByteWrapperStruct invalidWrapper = new InvalidIntPtrAndByteWrapperStruct() + { + Field1 = 1, + Field2 = IntPtr.Zero + }; + using MemoryStream stream = new MemoryStream(); + + Exception 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 static void RoundtripType(object inputValue, byte[] expectedValue) + { + using MemoryStream stream = new MemoryStream(); + object readPrimitive; + 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); + 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..df59f7556b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/SerializedTypes.cs @@ -0,0 +1,207 @@ +// 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)] public struct BoolWrapperStruct { public bool Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ByteWrapperStruct { public byte Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SByteWrapperStruct { public sbyte Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct UShortWrapperStruct { public ushort Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ShortWrapperStruct { public short Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct UIntWrapperStruct { public uint Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct IntWrapperStruct { public int Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ULongWrapperStruct { public ulong Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct LongWrapperStruct { public long Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct FloatWrapperStruct { public float Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct DoubleWrapperStruct { public double Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlByteWrapperStruct { public SqlByte Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt16WrapperStruct { public SqlInt16 Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt32WrapperStruct { public SqlInt32 Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt64WrapperStruct { public SqlInt64 Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlBooleanWrapperStruct { public SqlBoolean Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlSingleWrapperStruct { public SqlSingle Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlDoubleWrapperStruct { public SqlDouble Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlMoneyWrapperStruct { public SqlMoney Field1; } + +// Success case: a class containing one of the designated primitive types +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public class BoolWrapperClass { public bool Field1; } + +// Success case: a struct containing one designated primitive type and one nested struct +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public 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)] public 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)] public 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)] public struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; } + +// Success case: a struct or a class implementing IBinarySerialize which would not otherwise be serializable +public interface IFormattingProgress +{ + bool ParameterlessConstructorInvoked { get; } + bool ReadInvoked { get; } + bool WriteInvoked { get; } +} + +[SqlUserDefinedType(Format.UserDefined, MaxByteSize = 11)] +public 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)] +public 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)] +public class UserDefinedMissingPublicConstructor : IBinarySerialize +{ + public UserDefinedMissingPublicConstructor(bool _) { } + + public void Read(BinaryReader r) { } + + public void Write(BinaryWriter w) { } +} + +[SqlUserDefinedType(Format.UserDefined)] +public class UserDefinedDoesNotImplementIBinarySerialize +{ + public UserDefinedDoesNotImplementIBinarySerialize() { } +} + +public class ClassMissingSqlUserDefinedTypeAttribute +{ +} + +[SqlUserDefinedType(Format.Unknown)] +public 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..b33e18819c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/UserDefinedSerializationTest.cs @@ -0,0 +1,97 @@ +// 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 class UserDefinedSerializationTest +{ + [Fact] + public void CanSerializeStruct() + => RoundtripType(new UserDefinedFormattedStruct((IntPtr)0x12345678)); + + [Fact] + public void CanSerializeClass() + => RoundtripType(new UserDefinedFormattedClass((IntPtr)0x12345678)); + + [Fact] + public void RequiresPublicParameterlessConstructor() + { + using MemoryStream stream = new MemoryStream(); + + SerializationHelperSql9.Serialize(stream, new UserDefinedMissingPublicConstructor(true)); + stream.Seek(0, SeekOrigin.Begin); + + Assert.Throws( + () => SerializationHelperSql9.Deserialize(stream, typeof(UserDefinedMissingPublicConstructor))); + } + + [Fact] + public void RequiresIBinarySerializeImplementation() + { + using MemoryStream stream = new MemoryStream(); + + Assert.Throws( + () => SerializationHelperSql9.Serialize(stream, new UserDefinedDoesNotImplementIBinarySerialize())); + } + + private static void RoundtripType(T userObject) + where T : IFormattingProgress + { + using MemoryStream stream = new MemoryStream(); + byte[] serializedValue; + T readInstance; + int typeSize = SerializationHelperSql9.SizeInBytes(userObject.GetType()); + int objectSize = SerializationHelperSql9.SizeInBytes(userObject); + int maxTypeSize = SerializationHelperSql9.GetUdtMaxLength(userObject.GetType()); + + SerializationHelperSql9.Serialize(stream, userObject); + serializedValue = stream.ToArray(); + stream.Seek(0, SeekOrigin.Begin); + 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(8, typeSize); + Assert.Equal(8, 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); + } +} From 21e5fd4ea26c0b86fd1e07d40e0a93f1f0519819 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 19 Jun 2025 05:24:43 +0100 Subject: [PATCH 02/10] Address test failures on 32-bit OSes --- .../UdtSerialization/UserDefinedSerializationTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b33e18819c..34f8ab660a 100644 --- 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 @@ -67,8 +67,8 @@ private static void RoundtripType(T userObject) Assert.True(userObject.WriteInvoked); } - Assert.Equal(8, typeSize); - Assert.Equal(8, objectSize); + Assert.Equal(IntPtr.Size, typeSize); + Assert.Equal(IntPtr.Size, objectSize); Assert.Equal(11, maxTypeSize); Assert.Equal(IntPtr.Size, serializedValue.Length); From 01f711fcd4ade535d3bf2de16f72fb0fdb807ad6 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 19:30:57 +0100 Subject: [PATCH 03/10] Make Microsoft.SqlServer.Server a PackageReference --- .../tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2a8c60f4ce..f12807a403 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 @@ -25,7 +25,7 @@ all - + From 4db8883ae144e38a65bc970f00e85a1b143adafb Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 19:34:59 +0100 Subject: [PATCH 04/10] Reformat SerializedTypes.cs --- .../UdtSerialization/SerializedTypes.cs | 216 ++++++++++++++---- 1 file changed, 172 insertions(+), 44 deletions(-) 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 index df59f7556b..d6538f3584 100644 --- 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 @@ -11,60 +11,188 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes; // Simple cases: a struct containing one of the designated primitive types -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct BoolWrapperStruct { public bool Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ByteWrapperStruct { public byte Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SByteWrapperStruct { public sbyte Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct UShortWrapperStruct { public ushort Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ShortWrapperStruct { public short Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct UIntWrapperStruct { public uint Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct IntWrapperStruct { public int Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct ULongWrapperStruct { public ulong Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct LongWrapperStruct { public long Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct FloatWrapperStruct { public float Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct DoubleWrapperStruct { public double Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlByteWrapperStruct { public SqlByte Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt16WrapperStruct { public SqlInt16 Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt32WrapperStruct { public SqlInt32 Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlInt64WrapperStruct { public SqlInt64 Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlBooleanWrapperStruct { public SqlBoolean Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlSingleWrapperStruct { public SqlSingle Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlDoubleWrapperStruct { public SqlDouble Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct SqlMoneyWrapperStruct { public SqlMoney Field1; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct BoolWrapperStruct { public bool Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct ByteWrapperStruct { public byte Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SByteWrapperStruct { public sbyte Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct UShortWrapperStruct { public ushort Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct ShortWrapperStruct { public short Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct UIntWrapperStruct { public uint Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct IntWrapperStruct { public int Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct ULongWrapperStruct { public ulong Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct LongWrapperStruct { public long Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct FloatWrapperStruct { public float Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct DoubleWrapperStruct { public double Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlByteWrapperStruct { public SqlByte Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlInt16WrapperStruct { public SqlInt16 Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlInt32WrapperStruct { public SqlInt32 Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlInt64WrapperStruct { public SqlInt64 Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlBooleanWrapperStruct { public SqlBoolean Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlSingleWrapperStruct { public SqlSingle Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlDoubleWrapperStruct { public SqlDouble Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct SqlMoneyWrapperStruct { public SqlMoney Field1; } + // Success case: a class containing one of the designated primitive types -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public class BoolWrapperClass { public bool Field1; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public class BoolWrapperClass { public bool Field1; } // Success case: a struct containing one designated primitive type and one nested struct -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; } -[SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public struct NestedSqlMoneyWrapperStruct { public SqlMoney Field1; public SqlMoneyWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; } + +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public 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)] public class NestedBoolWrapperClass { public bool Field1; public BoolWrapperStruct Field2; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public 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)] public class InvalidNestedBoolWrapperClass { public bool Field1; public BoolWrapperClass Field2; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public 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)] public struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; } +[SqlUserDefinedType(Format.Native)] +[StructLayout(LayoutKind.Sequential)] +public struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; } // Success case: a struct or a class implementing IBinarySerialize which would not otherwise be serializable public interface IFormattingProgress From bd495149b1d81a949af2323e96d7f43e7b32c05d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:09:56 +0100 Subject: [PATCH 05/10] Switch to implicit object creation Also enforce this via editorconfig --- .../tests/UnitTests/.editorconfig | 11 +++++++++++ .../InvalidSerializationTest.cs | 8 ++++---- .../NativeSerializationTest.cs | 18 +++++++++--------- .../UserDefinedSerializationTest.cs | 6 +++--- 4 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/.editorconfig 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/UdtSerialization/InvalidSerializationTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/UdtSerialization/InvalidSerializationTest.cs index cba44991b2..6036253304 100644 --- 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 @@ -16,9 +16,9 @@ public class InvalidSerializationTest [Fact] public void RequiresSqlUserDefinedTypeAttribute() { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); - InvalidUdtException exception = Assert.Throws( + var exception = Assert.Throws( () => SerializationHelperSql9.Serialize(stream, new ClassMissingSqlUserDefinedTypeAttribute())); Assert.Equal($"'{typeof(ClassMissingSqlUserDefinedTypeAttribute).FullName}' is an invalid user defined type, reason: no UDT attribute.", exception.Message); @@ -27,9 +27,9 @@ public void RequiresSqlUserDefinedTypeAttribute() [Fact] public void CannotSerializeUnknownFormattedType() { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); - ArgumentOutOfRangeException exception = Assert.Throws("Format", + var exception = Assert.Throws("Format", () => SerializationHelperSql9.Serialize(stream, new UnknownFormattedClass())); #if NET 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 index 6b399700dd..ae5523579a 100644 --- 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 @@ -188,12 +188,12 @@ public void SerializeNullPrimitiveType(object primitive, byte[] expectedValue) [Fact] public void CanSerializeTopLevelClass() { - NestedBoolWrapperClass validWrapper = new NestedBoolWrapperClass() + NestedBoolWrapperClass validWrapper = new() { Field1 = true, Field2 = new BoolWrapperStruct() { Field1 = true } }; - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); SerializationHelperSql9.Serialize(stream, validWrapper); } @@ -207,14 +207,14 @@ public void CanSerializeTopLevelClass() [Fact] public void CannotSerializeNestedClass() { - InvalidNestedBoolWrapperClass invalidWrapper = new InvalidNestedBoolWrapperClass() + InvalidNestedBoolWrapperClass invalidWrapper = new() { Field1 = true, Field2 = new BoolWrapperClass() { Field1 = true } }; - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); - Exception ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); + var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName); Assert.Equal(expectedException, ex.Message); @@ -227,14 +227,14 @@ public void CannotSerializeNestedClass() [Fact] public void CannotSerializeNonPrimitiveType() { - InvalidIntPtrAndByteWrapperStruct invalidWrapper = new InvalidIntPtrAndByteWrapperStruct() + InvalidIntPtrAndByteWrapperStruct invalidWrapper = new() { Field1 = 1, Field2 = IntPtr.Zero }; - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); - Exception ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); + var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName); Assert.Equal(expectedException, ex.Message); @@ -247,7 +247,7 @@ public void CannotSerializeNonPrimitiveType() /// Expected serialization output. private static void RoundtripType(object inputValue, byte[] expectedValue) { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); object readPrimitive; int typeSize = SerializationHelperSql9.SizeInBytes(inputValue.GetType()); int objectSize = SerializationHelperSql9.SizeInBytes(inputValue); 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 index 34f8ab660a..fef8425121 100644 --- 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 @@ -27,7 +27,7 @@ public void CanSerializeClass() [Fact] public void RequiresPublicParameterlessConstructor() { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); SerializationHelperSql9.Serialize(stream, new UserDefinedMissingPublicConstructor(true)); stream.Seek(0, SeekOrigin.Begin); @@ -39,7 +39,7 @@ public void RequiresPublicParameterlessConstructor() [Fact] public void RequiresIBinarySerializeImplementation() { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); Assert.Throws( () => SerializationHelperSql9.Serialize(stream, new UserDefinedDoesNotImplementIBinarySerialize())); @@ -48,7 +48,7 @@ public void RequiresIBinarySerializeImplementation() private static void RoundtripType(T userObject) where T : IFormattingProgress { - using MemoryStream stream = new MemoryStream(); + using MemoryStream stream = new(); byte[] serializedValue; T readInstance; int typeSize = SerializationHelperSql9.SizeInBytes(userObject.GetType()); From 9f195c991a6b29899e5d0ede322f0a0961a2caf4 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:35:53 +0100 Subject: [PATCH 06/10] Add and correct comments Also mandating this by generating a documentation XML file --- .../tests/UnitTests/InternalsVisibleToTest.cs | 6 ++ .../Microsoft.Data.SqlClient.UnitTests.csproj | 1 + .../ChannelDbConnectionPoolTest.cs | 2 + .../InvalidSerializationTest.cs | 10 ++ .../NativeSerializationTest.cs | 22 +++- .../UdtSerialization/SerializedTypes.cs | 102 +++++++++--------- .../UserDefinedSerializationTest.cs | 16 +++ 7 files changed, 103 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs index eb924990db..36621fcb51 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs @@ -4,8 +4,14 @@ namespace Microsoft.Data.SqlClient.UnitTests { + /// + /// Tests proving that the InternalsVisibleTo attribute works correctly. + /// public class InternalsVisibleToTest { + /// + /// Creates an instance of an internal class. Verifies that this compiles. + /// [Fact] public void TestInternalsVisible() { 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 f12807a403..4493b00b4d 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 + true diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs index 2dcfe476fe..671c1428dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPoolTest.cs @@ -11,6 +11,7 @@ namespace Microsoft.Data.SqlClient.UnitTests { +#pragma warning disable CS1591 // Test classes do not require XML documentation comments public class ChannelDbConnectionPoolTest { private readonly ChannelDbConnectionPool _pool; @@ -152,4 +153,5 @@ public void TestTryGetConnection() Assert.Throws(() => _pool.TryGetConnection(null!, null!, null!, out _)); } } +#pragma warning restore CS1591 } 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 index 6036253304..0aeb910356 100644 --- 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 @@ -11,8 +11,14 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization; +/// +/// Attempts to serialize types which do not meet the requirements for either user-defined or native serialization. +/// public class InvalidSerializationTest { + /// + /// Attempts to serialize a class that does not have the SqlUserDefinedType attribute. Verifies that this fails. + /// [Fact] public void RequiresSqlUserDefinedTypeAttribute() { @@ -24,6 +30,10 @@ public void RequiresSqlUserDefinedTypeAttribute() 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 CannotSerializeUnknownFormattedType() { 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 index ae5523579a..f88b1707a1 100644 --- 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 @@ -18,6 +18,11 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization; /// public class NativeSerializationTest { + /// + /// Provides a collection of test data representing non-null primitive type values and their corresponding + /// serialized byte arrays. + /// + /// public static IEnumerable SerializedNonNullPrimitiveTypeValues() { yield return [new BoolWrapperStruct { Field1 = true }, @@ -62,6 +67,10 @@ public static IEnumerable SerializedNonNullPrimitiveTypeValues() 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 IEnumerable SerializedNestedNonNullPrimitiveTypeValues() { yield return [new NestedBoolWrapperStruct { Field1 = true, Field2 = new BoolWrapperStruct { Field1 = false } }, @@ -126,6 +135,10 @@ public static IEnumerable SerializedNestedNonNullPrimitiveTypeValues() 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 IEnumerable SerializedNullPrimitiveTypeValues() { yield return [new SqlByteWrapperStruct { Field1 = SqlByte.Null }, @@ -182,7 +195,7 @@ public void SerializeNullPrimitiveType(object primitive, byte[] expectedValue) => RoundtripType(primitive, expectedValue); /// - /// Attempts to serializes an instance of a class. + /// Attempts to serialize an instance of a class. /// /// [Fact] @@ -199,11 +212,10 @@ public void CanSerializeTopLevelClass() } /// - /// Attempts to serializes a field referring to an instance of a class. - /// Verifies that this succeeds, and that Native format serialization only operates with primitive types - /// and value types containing these. + /// 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 CannotSerializeNestedClass() { 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 index d6538f3584..5929958959 100644 --- 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 @@ -13,189 +13,189 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization.SerializedTypes; // Simple cases: a struct containing one of the designated primitive types [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct BoolWrapperStruct { public bool Field1; } +internal struct BoolWrapperStruct { public bool Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct ByteWrapperStruct { public byte Field1; } +internal struct ByteWrapperStruct { public byte Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SByteWrapperStruct { public sbyte Field1; } +internal struct SByteWrapperStruct { public sbyte Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct UShortWrapperStruct { public ushort Field1; } +internal struct UShortWrapperStruct { public ushort Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct ShortWrapperStruct { public short Field1; } +internal struct ShortWrapperStruct { public short Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct UIntWrapperStruct { public uint Field1; } +internal struct UIntWrapperStruct { public uint Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct IntWrapperStruct { public int Field1; } +internal struct IntWrapperStruct { public int Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct ULongWrapperStruct { public ulong Field1; } +internal struct ULongWrapperStruct { public ulong Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct LongWrapperStruct { public long Field1; } +internal struct LongWrapperStruct { public long Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct FloatWrapperStruct { public float Field1; } +internal struct FloatWrapperStruct { public float Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct DoubleWrapperStruct { public double Field1; } +internal struct DoubleWrapperStruct { public double Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlByteWrapperStruct { public SqlByte Field1; } +internal struct SqlByteWrapperStruct { public SqlByte Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlInt16WrapperStruct { public SqlInt16 Field1; } +internal struct SqlInt16WrapperStruct { public SqlInt16 Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlInt32WrapperStruct { public SqlInt32 Field1; } +internal struct SqlInt32WrapperStruct { public SqlInt32 Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlInt64WrapperStruct { public SqlInt64 Field1; } +internal struct SqlInt64WrapperStruct { public SqlInt64 Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlBooleanWrapperStruct { public SqlBoolean Field1; } +internal struct SqlBooleanWrapperStruct { public SqlBoolean Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlSingleWrapperStruct { public SqlSingle Field1; } +internal struct SqlSingleWrapperStruct { public SqlSingle Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlDoubleWrapperStruct { public SqlDouble Field1; } +internal struct SqlDoubleWrapperStruct { public SqlDouble Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; } +internal struct SqlDateTimeWrapperStruct { public SqlDateTime Field1; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct SqlMoneyWrapperStruct { public SqlMoney Field1; } +internal struct SqlMoneyWrapperStruct { public SqlMoney Field1; } // Success case: a class containing one of the designated primitive types [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public class BoolWrapperClass { public bool Field1; } +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)] -public struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; } +internal struct NestedBoolWrapperStruct { public bool Field1; public BoolWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; } +internal struct NestedByteWrapperStruct { public byte Field1; public ByteWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; } +internal struct NestedSByteWrapperStruct { public sbyte Field1; public SByteWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; } +internal struct NestedUShortWrapperStruct { public ushort Field1; public UShortWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; } +internal struct NestedShortWrapperStruct { public short Field1; public ShortWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; } +internal struct NestedUIntWrapperStruct { public uint Field1; public UIntWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; } +internal struct NestedIntWrapperStruct { public int Field1; public IntWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; } +internal struct NestedULongWrapperStruct { public ulong Field1; public ULongWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; } +internal struct NestedLongWrapperStruct { public long Field1; public LongWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; } +internal struct NestedFloatWrapperStruct { public float Field1; public FloatWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; } +internal struct NestedDoubleWrapperStruct { public double Field1; public DoubleWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; } +internal struct NestedSqlByteWrapperStruct { public SqlByte Field1; public SqlByteWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; } +internal struct NestedSqlInt16WrapperStruct { public SqlInt16 Field1; public SqlInt16WrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; } +internal struct NestedSqlInt32WrapperStruct { public SqlInt32 Field1; public SqlInt32WrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; } +internal struct NestedSqlInt64WrapperStruct { public SqlInt64 Field1; public SqlInt64WrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; } +internal struct NestedSqlBooleanWrapperStruct { public SqlBoolean Field1; public SqlBooleanWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; } +internal struct NestedSqlSingleWrapperStruct { public SqlSingle Field1; public SqlSingleWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; } +internal struct NestedSqlDoubleWrapperStruct { public SqlDouble Field1; public SqlDoubleWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; } +internal struct NestedSqlDateTimeWrapperStruct { public SqlDateTime Field1; public SqlDateTimeWrapperStruct Field2; } [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] -public struct NestedSqlMoneyWrapperStruct { public SqlMoney Field1; public SqlMoneyWrapperStruct Field2; } +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)] -public class NestedBoolWrapperClass { public bool Field1; public BoolWrapperStruct Field2; } +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)] -public class InvalidNestedBoolWrapperClass { public bool Field1; public BoolWrapperClass Field2; } +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)] -public struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; } +internal struct InvalidIntPtrAndByteWrapperStruct { public byte Field1; public IntPtr Field2; } // Success case: a struct or a class implementing IBinarySerialize which would not otherwise be serializable -public interface IFormattingProgress +internal interface IFormattingProgress { bool ParameterlessConstructorInvoked { get; } bool ReadInvoked { get; } @@ -203,7 +203,7 @@ public interface IFormattingProgress } [SqlUserDefinedType(Format.UserDefined, MaxByteSize = 11)] -public struct UserDefinedFormattedStruct : IBinarySerialize, IFormattingProgress, IEquatable +internal struct UserDefinedFormattedStruct : IBinarySerialize, IFormattingProgress, IEquatable { public IntPtr Field1; public bool ParameterlessConstructorInvoked { get; } @@ -255,7 +255,7 @@ public bool Equals(UserDefinedFormattedStruct other) } [SqlUserDefinedType(Format.UserDefined, MaxByteSize = 11)] -public class UserDefinedFormattedClass : IBinarySerialize, IFormattingProgress, IEquatable +internal class UserDefinedFormattedClass : IBinarySerialize, IFormattingProgress, IEquatable { public IntPtr Field1; public bool ParameterlessConstructorInvoked { get; } @@ -310,7 +310,7 @@ public bool Equals(UserDefinedFormattedClass other) // or has a SqlUserDefinedType attribute with a Format of Unknown. [SqlUserDefinedType(Format.UserDefined)] -public class UserDefinedMissingPublicConstructor : IBinarySerialize +internal class UserDefinedMissingPublicConstructor : IBinarySerialize { public UserDefinedMissingPublicConstructor(bool _) { } @@ -320,16 +320,16 @@ public void Write(BinaryWriter w) { } } [SqlUserDefinedType(Format.UserDefined)] -public class UserDefinedDoesNotImplementIBinarySerialize +internal class UserDefinedDoesNotImplementIBinarySerialize { public UserDefinedDoesNotImplementIBinarySerialize() { } } -public class ClassMissingSqlUserDefinedTypeAttribute +internal class ClassMissingSqlUserDefinedTypeAttribute { } [SqlUserDefinedType(Format.Unknown)] -public class UnknownFormattedClass +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 index fef8425121..a944f48d81 100644 --- 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 @@ -16,14 +16,26 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization; /// public class UserDefinedSerializationTest { + /// + /// Attempts to serialize and deserialize an instance of a struct with a user-defined serialization method. + /// + /// [Fact] public void CanSerializeStruct() => RoundtripType(new UserDefinedFormattedStruct((IntPtr)0x12345678)); + /// + /// Attempts to serialize and deserialize an instance of a class with a user-defined serialization method. + /// + /// [Fact] public void CanSerializeClass() => 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 RequiresPublicParameterlessConstructor() { @@ -36,6 +48,10 @@ public void RequiresPublicParameterlessConstructor() () => SerializationHelperSql9.Deserialize(stream, typeof(UserDefinedMissingPublicConstructor))); } + /// + /// 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 RequiresIBinarySerializeImplementation() { From 62edb700162fefafdb87437b520f8b92c15f2a72 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:38:26 +0100 Subject: [PATCH 07/10] Enable nullable for project --- .../tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj | 1 + .../Data/SqlClient/UdtSerialization/SerializedTypes.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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 4493b00b4d..cb9ea86243 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 @@ -8,6 +8,7 @@ $(BinFolder)$(Configuration).$(Platform).$(AssemblyName) true true + enable 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 index 5929958959..a02c213e01 100644 --- 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 @@ -187,7 +187,7 @@ internal class NestedBoolWrapperClass { public bool Field1; public BoolWrapperSt // 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; } +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)] @@ -302,7 +302,7 @@ public void Write(BinaryWriter w) WriteInvoked = true; } - public bool Equals(UserDefinedFormattedClass other) + public bool Equals(UserDefinedFormattedClass? other) => other is not null && other.Field1 == Field1; } From f9136f9a111417cba3ac9106c3032d3fbf403662 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:54:19 +0100 Subject: [PATCH 08/10] Declare and assign variables together --- .../SqlClient/UdtSerialization/NativeSerializationTest.cs | 3 +-- .../UdtSerialization/UserDefinedSerializationTest.cs | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) 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 index f88b1707a1..987a1cb6e5 100644 --- 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 @@ -260,14 +260,13 @@ public void CannotSerializeNonPrimitiveType() private static void RoundtripType(object inputValue, byte[] expectedValue) { using MemoryStream stream = new(); - object readPrimitive; 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); - readPrimitive = SerializationHelperSql9.Deserialize(stream, inputValue.GetType()); + 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); 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 index a944f48d81..e338a90628 100644 --- 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 @@ -65,16 +65,14 @@ private static void RoundtripType(T userObject) where T : IFormattingProgress { using MemoryStream stream = new(); - byte[] serializedValue; - T readInstance; int typeSize = SerializationHelperSql9.SizeInBytes(userObject.GetType()); int objectSize = SerializationHelperSql9.SizeInBytes(userObject); int maxTypeSize = SerializationHelperSql9.GetUdtMaxLength(userObject.GetType()); SerializationHelperSql9.Serialize(stream, userObject); - serializedValue = stream.ToArray(); stream.Seek(0, SeekOrigin.Begin); - readInstance = (T)SerializationHelperSql9.Deserialize(stream, userObject.GetType()); + 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 From 6e80cc54c8b46d9ab55a0c2b11a4d5850c915d5c Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:41:37 +0100 Subject: [PATCH 09/10] Moved MemoryStream to an instance variable --- .../InvalidSerializationTest.cs | 25 ++++++++---- .../NativeSerializationTest.cs | 37 +++++++++++------ .../UserDefinedSerializationTest.cs | 40 ++++++++++++------- 3 files changed, 67 insertions(+), 35 deletions(-) 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 index 0aeb910356..bee1db5838 100644 --- 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 @@ -14,18 +14,31 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization; /// /// Attempts to serialize types which do not meet the requirements for either user-defined or native serialization. /// -public class InvalidSerializationTest +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 RequiresSqlUserDefinedTypeAttribute() { - using MemoryStream stream = new(); - var exception = Assert.Throws( - () => SerializationHelperSql9.Serialize(stream, new ClassMissingSqlUserDefinedTypeAttribute())); + () => SerializationHelperSql9.Serialize(_stream, new ClassMissingSqlUserDefinedTypeAttribute())); Assert.Equal($"'{typeof(ClassMissingSqlUserDefinedTypeAttribute).FullName}' is an invalid user defined type, reason: no UDT attribute.", exception.Message); } @@ -37,10 +50,8 @@ public void RequiresSqlUserDefinedTypeAttribute() [Fact] public void CannotSerializeUnknownFormattedType() { - using MemoryStream stream = new(); - var exception = Assert.Throws("Format", - () => SerializationHelperSql9.Serialize(stream, new UnknownFormattedClass())); + () => SerializationHelperSql9.Serialize(_stream, new UnknownFormattedClass())); #if NET Assert.Equal("The Format enumeration value, 0, is not supported by the format method. (Parameter 'Format')", exception.Message); 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 index 987a1cb6e5..e37e862e99 100644 --- 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 @@ -16,8 +16,23 @@ 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 class NativeSerializationTest +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. @@ -206,9 +221,8 @@ public void CanSerializeTopLevelClass() Field1 = true, Field2 = new BoolWrapperStruct() { Field1 = true } }; - using MemoryStream stream = new(); - SerializationHelperSql9.Serialize(stream, validWrapper); + SerializationHelperSql9.Serialize(_stream, validWrapper); } /// @@ -224,9 +238,8 @@ public void CannotSerializeNestedClass() Field1 = true, Field2 = new BoolWrapperClass() { Field1 = true } }; - using MemoryStream stream = new(); - var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); + var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(_stream, invalidWrapper)); string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName); Assert.Equal(expectedException, ex.Message); @@ -244,9 +257,8 @@ public void CannotSerializeNonPrimitiveType() Field1 = 1, Field2 = IntPtr.Zero }; - using MemoryStream stream = new(); - var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(stream, invalidWrapper)); + var ex = Assert.Throws(() => SerializationHelperSql9.Serialize(_stream, invalidWrapper)); string expectedException = StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, invalidWrapper.Field2.GetType().FullName); Assert.Equal(expectedException, ex.Message); @@ -257,23 +269,22 @@ public void CannotSerializeNonPrimitiveType() /// /// Object to serialize. /// Expected serialization output. - private static void RoundtripType(object inputValue, byte[] expectedValue) + private void RoundtripType(object inputValue, byte[] expectedValue) { - using MemoryStream stream = new(); 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()); + 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(expectedValue, _stream.ToArray()); Assert.Equal(inputValue, readPrimitive); } } 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 index e338a90628..da3f47f5ee 100644 --- 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 @@ -14,8 +14,23 @@ namespace Microsoft.Data.SqlClient.UnitTests.UdtSerialization; /// /// Tests the user-defined UDT serialization method. Verifies that custom types round-trip. /// -public class UserDefinedSerializationTest +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. /// @@ -39,13 +54,11 @@ public void CanSerializeClass() [Fact] public void RequiresPublicParameterlessConstructor() { - using MemoryStream stream = new(); - - SerializationHelperSql9.Serialize(stream, new UserDefinedMissingPublicConstructor(true)); - stream.Seek(0, SeekOrigin.Begin); + SerializationHelperSql9.Serialize(_stream, new UserDefinedMissingPublicConstructor(true)); + _stream.Seek(0, SeekOrigin.Begin); Assert.Throws( - () => SerializationHelperSql9.Deserialize(stream, typeof(UserDefinedMissingPublicConstructor))); + () => SerializationHelperSql9.Deserialize(_stream, typeof(UserDefinedMissingPublicConstructor))); } /// @@ -55,24 +68,21 @@ public void RequiresPublicParameterlessConstructor() [Fact] public void RequiresIBinarySerializeImplementation() { - using MemoryStream stream = new(); - Assert.Throws( - () => SerializationHelperSql9.Serialize(stream, new UserDefinedDoesNotImplementIBinarySerialize())); + () => SerializationHelperSql9.Serialize(_stream, new UserDefinedDoesNotImplementIBinarySerialize())); } - private static void RoundtripType(T userObject) + private void RoundtripType(T userObject) where T : IFormattingProgress { - using MemoryStream stream = new(); 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()); + 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 From fa46855ed26f35b6620e3b6cebe3f10e0ebabc6d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:47:13 +0100 Subject: [PATCH 10/10] Post-merge build corrections --- .../tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs | 4 ++++ src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs index faf44a4e29..52d22b54b3 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlJsonTest.cs @@ -12,6 +12,8 @@ namespace Microsoft.Data.SqlClient.UnitTests; +#pragma warning disable CS1591 // Test classes do not require XML documentation comments + public class SqlJsonTest { #region Private Fields @@ -155,3 +157,5 @@ private static JsonDocument GenerateRandomJson() #endregion } + +#pragma warning restore CS1591 diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs index 3390d95c02..3206c64881 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SqlVectorTest.cs @@ -11,6 +11,8 @@ namespace Microsoft.Data.SqlClient.Tests; +#pragma warning disable CS1591 // Test classes do not require XML documentation comments + public class SqlVectorTest { #region Tests @@ -243,3 +245,5 @@ private byte[] MakeTdsPayload(byte[] header, ReadOnlyMemory values) #endregion } + +#pragma warning restore CS1591