using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Reflection; using Xunit; namespace Dapper.Tests { [Collection(NonParallelDefinition.Name)] public sealed class SystemSqlClientTypeHandlerTests : TypeHandlerTests { } #if MSSQLCLIENT [Collection(NonParallelDefinition.Name)] public sealed class MicrosoftSqlClientTypeHandlerTests : TypeHandlerTests { } #endif public abstract class TypeHandlerTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestChangingDefaultStringTypeMappingToAnsiString() { const string sql = "SELECT SQL_VARIANT_PROPERTY(CONVERT(sql_variant, @testParam),'BaseType') AS BaseType"; var param = new { testParam = "TestString" }; var result01 = connection.Query(sql, param).FirstOrDefault(); Assert.Equal("nvarchar", result01); SqlMapper.PurgeQueryCache(); SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString try { var result02 = connection.Query(sql, param).FirstOrDefault(); Assert.Equal("varchar", result02); SqlMapper.PurgeQueryCache(); } finally { SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String } } [Fact] public void TestChangingDefaultStringTypeMappingToAnsiStringFirstOrDefault() { const string sql = "SELECT SQL_VARIANT_PROPERTY(CONVERT(sql_variant, @testParam),'BaseType') AS BaseType"; var param = new { testParam = "TestString" }; var result01 = connection.QueryFirstOrDefault(sql, param); Assert.Equal("nvarchar", result01); SqlMapper.PurgeQueryCache(); SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString try { var result02 = connection.QueryFirstOrDefault(sql, param); Assert.Equal("varchar", result02); SqlMapper.PurgeQueryCache(); } finally { SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String } } [Fact] public void TestCustomTypeMap() { // default mapping var item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single(); Assert.Equal("AVal", item.A); Assert.Equal("BVal", item.B); // custom mapping var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName)!); SqlMapper.SetTypeMap(typeof(TypeWithMapping), map); item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single(); Assert.Equal("BVal", item.A); Assert.Equal("AVal", item.B); // reset to default SqlMapper.SetTypeMap(typeof(TypeWithMapping), null); item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single(); Assert.Equal("AVal", item.A); Assert.Equal("BVal", item.B); } private static string? GetDescriptionFromAttribute(MemberInfo member) { if (member == null) return null; var attrib = (DescriptionAttribute?)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false); return attrib?.Description; } public class TypeWithMapping { [Description("B")] public string? A { get; set; } [Description("A")] public string? B { get; set; } } [Fact] public void Issue136_ValueTypeHandlers() { SqlMapper.ResetTypeHandlers(); SqlMapper.AddTypeHandler(typeof(LocalDate), LocalDateHandler.Default); var param = new LocalDateResult { NotNullable = new LocalDate { Year = 2014, Month = 7, Day = 25 }, NullableNotNull = new LocalDate { Year = 2014, Month = 7, Day = 26 }, NullableIsNull = null, }; var result = connection.Query("SELECT @NotNullable AS NotNullable, @NullableNotNull AS NullableNotNull, @NullableIsNull AS NullableIsNull", param).Single(); SqlMapper.ResetTypeHandlers(); SqlMapper.AddTypeHandler(typeof(LocalDate?), LocalDateHandler.Default); result = connection.Query("SELECT @NotNullable AS NotNullable, @NullableNotNull AS NullableNotNull, @NullableIsNull AS NullableIsNull", param).Single(); } public class LocalDateHandler : SqlMapper.TypeHandler { private LocalDateHandler() { /* private constructor */ } // Make the field type ITypeHandler to ensure it cannot be used with SqlMapper.AddTypeHandler(TypeHandler) // by mistake. public static readonly SqlMapper.ITypeHandler Default = new LocalDateHandler(); public override LocalDate Parse(object? value) { var date = (DateTime)value!; return new LocalDate { Year = date.Year, Month = date.Month, Day = date.Day }; } public override void SetValue(IDbDataParameter parameter, LocalDate value) { parameter.DbType = DbType.DateTime; parameter.Value = new DateTime(value.Year, value.Month, value.Day); } } public struct LocalDate { public int Year { get; set; } public int Month { get; set; } public int Day { get; set; } } public class LocalDateResult { public LocalDate NotNullable { get; set; } public LocalDate? NullableNotNull { get; set; } public LocalDate? NullableIsNull { get; set; } } public class LotsOfNumerics { public enum E_Byte : byte { A = 0, B = 1 } public enum E_SByte : sbyte { A = 0, B = 1 } public enum E_Short : short { A = 0, B = 1 } public enum E_UShort : ushort { A = 0, B = 1 } public enum E_Int : int { A = 0, B = 1 } public enum E_UInt : uint { A = 0, B = 1 } public enum E_Long : long { A = 0, B = 1 } public enum E_ULong : ulong { A = 0, B = 1 } public E_Byte P_Byte { get; set; } public E_SByte P_SByte { get; set; } public E_Short P_Short { get; set; } public E_UShort P_UShort { get; set; } public E_Int P_Int { get; set; } public E_UInt P_UInt { get; set; } public E_Long P_Long { get; set; } public E_ULong P_ULong { get; set; } public bool N_Bool { get; set; } public byte N_Byte { get; set; } public sbyte N_SByte { get; set; } public short N_Short { get; set; } public ushort N_UShort { get; set; } public int N_Int { get; set; } public uint N_UInt { get; set; } public long N_Long { get; set; } public ulong N_ULong { get; set; } public float N_Float { get; set; } public double N_Double { get; set; } public decimal N_Decimal { get; set; } public E_Byte? N_P_Byte { get; set; } public E_SByte? N_P_SByte { get; set; } public E_Short? N_P_Short { get; set; } public E_UShort? N_P_UShort { get; set; } public E_Int? N_P_Int { get; set; } public E_UInt? N_P_UInt { get; set; } public E_Long? N_P_Long { get; set; } public E_ULong? N_P_ULong { get; set; } public bool? N_N_Bool { get; set; } public byte? N_N_Byte { get; set; } public sbyte? N_N_SByte { get; set; } public short? N_N_Short { get; set; } public ushort? N_N_UShort { get; set; } public int? N_N_Int { get; set; } public uint? N_N_UInt { get; set; } public long? N_N_Long { get; set; } public ulong? N_N_ULong { get; set; } public float? N_N_Float { get; set; } public double? N_N_Double { get; set; } public decimal? N_N_Decimal { get; set; } } [Fact] public void TestBigIntForEverythingWorks() { TestBigIntForEverythingWorks_ByDataType("bigint"); TestBigIntForEverythingWorks_ByDataType("int"); TestBigIntForEverythingWorks_ByDataType("tinyint"); TestBigIntForEverythingWorks_ByDataType("smallint"); TestBigIntForEverythingWorks_ByDataType("bit"); TestBigIntForEverythingWorks_ByDataType("float(24)"); TestBigIntForEverythingWorks_ByDataType("float(53)"); } private void TestBigIntForEverythingWorks_ByDataType(string dbType) { using (var reader = connection.ExecuteReader("select cast(1 as " + dbType + ")")) { Assert.True(reader.Read()); reader.GetFieldType(0).Equals(typeof(T)); Assert.False(reader.Read()); Assert.False(reader.NextResult()); } string sql = "select " + string.Join(",", typeof(LotsOfNumerics).GetProperties().Select( x => "cast (1 as " + dbType + ") as [" + x.Name + "]")); var row = connection.Query(sql).Single(); Assert.True(row.N_Bool); Assert.Equal((sbyte)1, row.N_SByte); Assert.Equal((byte)1, row.N_Byte); Assert.Equal((int)1, row.N_Int); Assert.Equal((uint)1, row.N_UInt); Assert.Equal((short)1, row.N_Short); Assert.Equal((ushort)1, row.N_UShort); Assert.Equal((long)1, row.N_Long); Assert.Equal((ulong)1, row.N_ULong); Assert.Equal((float)1, row.N_Float); Assert.Equal((double)1, row.N_Double); Assert.Equal((decimal)1, row.N_Decimal); Assert.Equal(LotsOfNumerics.E_Byte.B, row.P_Byte); Assert.Equal(LotsOfNumerics.E_SByte.B, row.P_SByte); Assert.Equal(LotsOfNumerics.E_Short.B, row.P_Short); Assert.Equal(LotsOfNumerics.E_UShort.B, row.P_UShort); Assert.Equal(LotsOfNumerics.E_Int.B, row.P_Int); Assert.Equal(LotsOfNumerics.E_UInt.B, row.P_UInt); Assert.Equal(LotsOfNumerics.E_Long.B, row.P_Long); Assert.Equal(LotsOfNumerics.E_ULong.B, row.P_ULong); Assert.True(row.N_N_Bool!.Value); Assert.Equal((sbyte)1, row.N_N_SByte!.Value); Assert.Equal((byte)1, row.N_N_Byte!.Value); Assert.Equal((int)1, row.N_N_Int!.Value); Assert.Equal((uint)1, row.N_N_UInt!.Value); Assert.Equal((short)1, row.N_N_Short!.Value); Assert.Equal((ushort)1, row.N_N_UShort!.Value); Assert.Equal((long)1, row.N_N_Long!.Value); Assert.Equal((ulong)1, row.N_N_ULong!.Value); Assert.Equal((float)1, row.N_N_Float!.Value); Assert.Equal((double)1, row.N_N_Double!.Value); Assert.Equal((decimal)1, row.N_N_Decimal); Assert.Equal(LotsOfNumerics.E_Byte.B, row.N_P_Byte!.Value); Assert.Equal(LotsOfNumerics.E_SByte.B, row.N_P_SByte!.Value); Assert.Equal(LotsOfNumerics.E_Short.B, row.N_P_Short!.Value); Assert.Equal(LotsOfNumerics.E_UShort.B, row.N_P_UShort!.Value); Assert.Equal(LotsOfNumerics.E_Int.B, row.N_P_Int!.Value); Assert.Equal(LotsOfNumerics.E_UInt.B, row.N_P_UInt!.Value); Assert.Equal(LotsOfNumerics.E_Long.B, row.N_P_Long!.Value); Assert.Equal(LotsOfNumerics.E_ULong.B, row.N_P_ULong!.Value); TestBigIntForEverythingWorksGeneric(true, dbType); TestBigIntForEverythingWorksGeneric((sbyte)1, dbType); TestBigIntForEverythingWorksGeneric((byte)1, dbType); TestBigIntForEverythingWorksGeneric((int)1, dbType); TestBigIntForEverythingWorksGeneric((uint)1, dbType); TestBigIntForEverythingWorksGeneric((short)1, dbType); TestBigIntForEverythingWorksGeneric((ushort)1, dbType); TestBigIntForEverythingWorksGeneric((long)1, dbType); TestBigIntForEverythingWorksGeneric((ulong)1, dbType); TestBigIntForEverythingWorksGeneric((float)1, dbType); TestBigIntForEverythingWorksGeneric((double)1, dbType); TestBigIntForEverythingWorksGeneric((decimal)1, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Byte.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_SByte.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Int.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_UInt.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Short.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_UShort.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Long.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_ULong.B, dbType); TestBigIntForEverythingWorksGeneric(true, dbType); TestBigIntForEverythingWorksGeneric((sbyte)1, dbType); TestBigIntForEverythingWorksGeneric((byte)1, dbType); TestBigIntForEverythingWorksGeneric((int)1, dbType); TestBigIntForEverythingWorksGeneric((uint)1, dbType); TestBigIntForEverythingWorksGeneric((short)1, dbType); TestBigIntForEverythingWorksGeneric((ushort)1, dbType); TestBigIntForEverythingWorksGeneric((long)1, dbType); TestBigIntForEverythingWorksGeneric((ulong)1, dbType); TestBigIntForEverythingWorksGeneric((float)1, dbType); TestBigIntForEverythingWorksGeneric((double)1, dbType); TestBigIntForEverythingWorksGeneric((decimal)1, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Byte.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_SByte.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Int.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_UInt.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Short.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_UShort.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_Long.B, dbType); TestBigIntForEverythingWorksGeneric(LotsOfNumerics.E_ULong.B, dbType); } private void TestBigIntForEverythingWorksGeneric(T expected, string dbType) { var query = connection.Query("select cast(1 as " + dbType + ")").Single(); Assert.Equal(query, expected); var scalar = connection.ExecuteScalar("select cast(1 as " + dbType + ")"); Assert.Equal(scalar, expected); } [Fact] public void TestSubsequentQueriesSuccess() { var data0 = connection.Query("select 1 as [Id] where 1 = 0").ToList(); Assert.Empty(data0); var data1 = connection.Query(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)).ToList(); Assert.Empty(data1); var data2 = connection.Query(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)).ToList(); Assert.Empty(data2); data0 = connection.Query("select 1 as [Id] where 1 = 0").ToList(); Assert.Empty(data0); data1 = connection.Query(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.Buffered)).ToList(); Assert.Empty(data1); data2 = connection.Query(new CommandDefinition("select 1 as [Id] where 1 = 0", flags: CommandFlags.None)).ToList(); Assert.Empty(data2); } private class Fooz0 { public int Id { get; set; } } private class Fooz1 { public int Id { get; set; } } private class Fooz2 { public int Id { get; set; } } public class RatingValueHandler : SqlMapper.TypeHandler { private RatingValueHandler() { } public static readonly RatingValueHandler Default = new(); public override RatingValue Parse(object? value) { if (value is int i) { return new RatingValue() { Value = i }; } throw new FormatException("Invalid conversion to RatingValue"); } public override void SetValue(IDbDataParameter parameter, RatingValue? value) { // ... null, range checks etc ... parameter.DbType = System.Data.DbType.Int32; parameter.Value = value?.Value; } } public class RatingValue { public int Value { get; set; } // ... some other properties etc ... } public class MyResult { public string? CategoryName { get; set; } public RatingValue? CategoryRating { get; set; } } [Fact] public void SO24740733_TestCustomValueHandler() { SqlMapper.AddTypeHandler(RatingValueHandler.Default); var foo = connection.Query("SELECT 'Foo' AS CategoryName, 200 AS CategoryRating").Single(); Assert.Equal("Foo", foo.CategoryName); Assert.Equal(200, foo.CategoryRating?.Value); } [Fact] public void SO24740733_TestCustomValueSingleColumn() { SqlMapper.AddTypeHandler(RatingValueHandler.Default); var foo = connection.Query("SELECT 200 AS CategoryRating").Single(); Assert.Equal(200, foo.Value); } public class StringListTypeHandler : SqlMapper.TypeHandler> { private StringListTypeHandler() { } public static readonly StringListTypeHandler Default = new(); //Just a simple List type handler implementation public override void SetValue(IDbDataParameter parameter, List? value) { parameter.Value = string.Join(",", value ?? new()); } public override List Parse(object? value) { return ((value as string) ?? "").Split(',').ToList(); } } public class MyObjectWithStringList { public List? Names { get; set; } } [Fact] public void Issue253_TestIEnumerableTypeHandlerParsing() { SqlMapper.ResetTypeHandlers(); SqlMapper.AddTypeHandler(StringListTypeHandler.Default); var foo = connection.Query("SELECT 'Sam,Kyro' AS Names").Single(); Assert.Equal(new[] { "Sam", "Kyro" }, foo.Names); } [Fact] public void Issue253_TestIEnumerableTypeHandlerSetParameterValue() { SqlMapper.ResetTypeHandlers(); SqlMapper.AddTypeHandler(StringListTypeHandler.Default); connection.Execute("CREATE TABLE #Issue253 (Names VARCHAR(50) NOT NULL);"); try { const string names = "Sam,Kyro"; List names_list = names.Split(',').ToList(); var foo = connection.Query("INSERT INTO #Issue253 (Names) VALUES (@Names); SELECT Names FROM #Issue253;", new { Names = names_list }).Single(); Assert.Equal(names, foo); } finally { connection.Execute("DROP TABLE #Issue253;"); } } public class RecordingTypeHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, T? value) { SetValueWasCalled = true; parameter.Value = value; } public override T Parse(object? value) { ParseWasCalled = true; return (T)value!; } public bool SetValueWasCalled { get; set; } public bool ParseWasCalled { get; set; } } [Fact] public void Test_RemoveTypeMap() { SqlMapper.ResetTypeHandlers(); SqlMapper.RemoveTypeMap(typeof(DateTime)); var dateTimeHandler = new RecordingTypeHandler(); SqlMapper.AddTypeHandler(dateTimeHandler); connection.Execute("CREATE TABLE #Test_RemoveTypeMap (x datetime NOT NULL);"); try { connection.Execute("INSERT INTO #Test_RemoveTypeMap VALUES (@Now)", new { DateTime.Now }); connection.Query("SELECT * FROM #Test_RemoveTypeMap"); Assert.True(dateTimeHandler.ParseWasCalled); Assert.True(dateTimeHandler.SetValueWasCalled); } finally { connection.Execute("DROP TABLE #Test_RemoveTypeMap"); SqlMapper.AddTypeMap(typeof(DateTime), DbType.DateTime); // or an option to reset type map? } } [Fact] public void TestReaderWhenResultsChange() { try { connection.Execute("create table #ResultsChange (X int);create table #ResultsChange2 (Y int);insert #ResultsChange (X) values(1);insert #ResultsChange2 (Y) values(1);"); var obj1 = connection.Query("select * from #ResultsChange").Single(); Assert.Equal(1, obj1.X); Assert.Equal(0, obj1.Y); Assert.Equal(0, obj1.Z); var obj2 = connection.Query("select * from #ResultsChange rc inner join #ResultsChange2 rc2 on rc2.Y=rc.X").Single(); Assert.Equal(1, obj2.X); Assert.Equal(1, obj2.Y); Assert.Equal(0, obj2.Z); connection.Execute("alter table #ResultsChange add Z int null"); connection.Execute("update #ResultsChange set Z = 2"); var obj3 = connection.Query("select * from #ResultsChange").Single(); Assert.Equal(1, obj3.X); Assert.Equal(0, obj3.Y); Assert.Equal(2, obj3.Z); var obj4 = connection.Query("select * from #ResultsChange rc inner join #ResultsChange2 rc2 on rc2.Y=rc.X").Single(); Assert.Equal(1, obj4.X); Assert.Equal(1, obj4.Y); Assert.Equal(2, obj4.Z); } finally { connection.Execute("drop table #ResultsChange;drop table #ResultsChange2;"); } } private class ResultsChangeType { public int X { get; set; } public int Y { get; set; } public int Z { get; set; } } public class WrongTypes { public int A { get; set; } public double B { get; set; } public long C { get; set; } public bool D { get; set; } } [Fact] public void TestWrongTypes_WithRightTypes() { var item = connection.Query("select 1 as A, cast(2.0 as float) as B, cast(3 as bigint) as C, cast(1 as bit) as D").Single(); Assert.Equal(1, item.A); Assert.Equal(2.0, item.B); Assert.Equal(3L, item.C); Assert.True(item.D); } [Fact] public void TestWrongTypes_WithWrongTypes() { var item = connection.Query("select cast(1.0 as float) as A, 2 as B, 3 as C, cast(1 as bigint) as D").Single(); Assert.Equal(1, item.A); Assert.Equal(2.0, item.B); Assert.Equal(3L, item.C); Assert.True(item.D); } [Fact] public void TestTreatIntAsABool() { Assert.True(connection.Query("select CAST(1 AS BIT)").Single()); Assert.True(connection.Query("select 1").Single()); } [Fact] public void SO24607639_NullableBools() { var obj = connection.Query( @"declare @vals table (A bit null, B bit null, C bit null); insert @vals (A,B,C) values (1,0,null); select * from @vals").Single(); Assert.NotNull(obj); Assert.True(obj.A.HasValue); Assert.True(obj.A.Value); Assert.True(obj.B.HasValue); Assert.False(obj.B.Value); Assert.Null(obj.C); } private class HazBools { public bool? A { get; set; } public bool? B { get; set; } public bool? C { get; set; } } [Fact] public void Issue130_IConvertible() { dynamic row = connection.Query("select 1 as [a], '2' as [b]").Single(); int a = row.a; string b = row.b; Assert.Equal(1, a); Assert.Equal("2", b); row = connection.Query("select 3 as [a], '4' as [b]").Single(); a = row.a; b = row.b; Assert.Equal(3, a); Assert.Equal("4", b); } [Fact] public void Issue149_TypeMismatch_SequentialAccess() { Guid guid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); var ex = Assert.ThrowsAny(() => connection.Query("select @guid as Id", new { guid }).First()); Assert.Equal("Error parsing column 0 (Id=cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e - Guid)", ex.Message); } public class Issue149_Person { public string? Id { get; set; } } [Fact] public void Issue295_NullableDateTime_SqlServer() => Common.TestDateTime(connection); [Fact] public void SO29343103_UtcDates() { const string sql = "select @date"; var date = DateTime.UtcNow; var returned = connection.Query(sql, new { date }).Single(); var delta = returned - date; Assert.True(delta.TotalMilliseconds >= -10 && delta.TotalMilliseconds <= 10); } [Fact] public void Issue461_TypeHandlerWorksInConstructor() { SqlMapper.AddTypeHandler(new Issue461_BlargHandler()); connection.Execute(@"CREATE TABLE #Issue461 ( Id int not null IDENTITY(1,1), SomeValue nvarchar(50), SomeBlargValue nvarchar(200), )"); const string Expected = "abc123def"; var blarg = new Blarg(Expected); connection.Execute( "INSERT INTO #Issue461 (SomeValue, SomeBlargValue) VALUES (@value, @blarg)", new { value = "what up?", blarg }); // test: without constructor var parameterlessWorks = connection.QuerySingle("SELECT * FROM #Issue461"); Assert.Equal(1, parameterlessWorks.Id); Assert.Equal("what up?", parameterlessWorks.SomeValue); Assert.Equal(Expected, parameterlessWorks.SomeBlargValue?.Value); // test: via constructor var parameterDoesNot = connection.QuerySingle("SELECT * FROM #Issue461"); Assert.Equal(1, parameterDoesNot.Id); Assert.Equal("what up?", parameterDoesNot.SomeValue); Assert.Equal(Expected, parameterDoesNot.SomeBlargValue?.Value); } // I would usually expect this to be a struct; using a class // so that we can't pass unexpectedly due to forcing an unsafe cast - want // to see an InvalidCastException if it is wrong private class Blarg { public Blarg(string? value) { Value = value; } public string? Value { get; } public override string ToString() { return Value!; } } private class Issue461_BlargHandler : SqlMapper.TypeHandler { public override void SetValue(IDbDataParameter parameter, Blarg? value) { parameter.Value = ((object?)value?.Value) ?? DBNull.Value; } public override Blarg? Parse(object? value) { string? s = (value == null || value is DBNull) ? null : Convert.ToString(value); return new Blarg(s); } } private class Issue461_ParameterlessTypeConstructor { public int Id { get; set; } public string? SomeValue { get; set; } public Blarg? SomeBlargValue { get; set; } } private class Issue461_ParameterisedTypeConstructor { public Issue461_ParameterisedTypeConstructor(int id, string someValue, Blarg someBlargValue) { Id = id; SomeValue = someValue; SomeBlargValue = someBlargValue; } public int Id { get; } public string SomeValue { get; } public Blarg SomeBlargValue { get; } } [Theory] [InlineData(true)] [InlineData(false)] public void Issue1959_TypeHandlerNullability_Subclass(bool isNull) { Issue1959_Subclass_Handler.Register(); Issue1959_Subclass? when = isNull ? null : new(DateTime.Today); var whenNotNull = when ?? new(new DateTime(1753, 1, 1)); var args = new HazIssue1959_Subclass { Id = 42, Nullable = when, NonNullable = whenNotNull }; var row = connection.QuerySingle( "select @Id as [Id], @NonNullable as [NonNullable], @Nullable as [Nullable]", args); Assert.NotNull(row); Assert.Equal(42, row.Id); Assert.Equal(when, row.Nullable); Assert.Equal(whenNotNull, row.NonNullable); } [Theory] [InlineData(true)] [InlineData(false)] public void Issue1959_TypeHandlerNullability_Raw(bool isNull) { Issue1959_Raw_Handler.Register(); Issue1959_Raw? when = isNull ? null : new(DateTime.Today); var whenNotNull = when ?? new(new DateTime(1753, 1, 1)); var args = new HazIssue1959_Raw { Id = 42, Nullable = when, NonNullable = whenNotNull }; var row = connection.QuerySingle( "select @Id as [Id], @NonNullable as [NonNullable], @Nullable as [Nullable]", args); Assert.NotNull(row); Assert.Equal(42, row.Id); Assert.Equal(when, row.Nullable); Assert.Equal(whenNotNull, row.NonNullable); } public class HazIssue1959_Subclass { public int Id { get; set; } public Issue1959_Subclass NonNullable { get; set; } public Issue1959_Subclass? Nullable { get; set; } } public class HazIssue1959_Raw { public int Id { get; set; } public Issue1959_Raw NonNullable { get; set; } public Issue1959_Raw? Nullable { get; set; } } public class Issue1959_Subclass_Handler : SqlMapper.TypeHandler { public static void Register() => SqlMapper.AddTypeHandler(Instance); private Issue1959_Subclass_Handler() { } private static readonly Issue1959_Subclass_Handler Instance = new(); public override Issue1959_Subclass Parse(object value) { Assert.NotNull(value); Assert.IsType(value); // checking not DbNull etc return new Issue1959_Subclass((DateTime)value); } public override void SetValue(IDbDataParameter parameter, TypeHandlerTests.Issue1959_Subclass value) => parameter.Value = value.Value; } public class Issue1959_Raw_Handler : SqlMapper.ITypeHandler { public static void Register() => SqlMapper.AddTypeHandler(typeof(Issue1959_Raw), Instance); private Issue1959_Raw_Handler() { } private static readonly Issue1959_Raw_Handler Instance = new(); void SqlMapper.ITypeHandler.SetValue(IDbDataParameter parameter, object value) { Assert.NotNull(value); if (value is DBNull) { parameter.Value = value; } else { Assert.IsType(value); // checking not DbNull etc parameter.Value = ((Issue1959_Raw)value).Value; } } object? SqlMapper.ITypeHandler.Parse(Type destinationType, object value) { Assert.NotNull(value); Assert.IsType(value); // checking not DbNull etc return new Issue1959_Raw((DateTime)value); } } #pragma warning disable CA2231 // Overload operator equals on overriding value type Equals public readonly struct Issue1959_Subclass : IEquatable #pragma warning restore CA2231 // Overload operator equals on overriding value type Equals { public Issue1959_Subclass(DateTime value) => Value = value; public readonly DateTime Value; public override int GetHashCode() => Value.GetHashCode(); public override bool Equals(object? obj) => obj is Issue1959_Subclass other && Equals(other); public bool Equals(Issue1959_Subclass other) => other.Value == Value; public override string ToString() => Value.ToString(); } #pragma warning disable CA2231 // Overload operator equals on overriding value type Equals public readonly struct Issue1959_Raw : IEquatable #pragma warning restore CA2231 // Overload operator equals on overriding value type Equals { public Issue1959_Raw(DateTime value) => Value = value; public readonly DateTime Value; public override int GetHashCode() => Value.GetHashCode(); public override bool Equals(object? obj) => obj is Issue1959_Raw other && Equals(other); public bool Equals(Issue1959_Raw other) => other.Value == Value; public override string ToString() => Value.ToString(); } } }