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