diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index f2438ca4d..36cdc1c78 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -1,7 +1,7 @@ #nullable enable abstract Dapper.SqlMapper.StringTypeHandler.Format(T xml) -> string! abstract Dapper.SqlMapper.StringTypeHandler.Parse(string! xml) -> T -abstract Dapper.SqlMapper.TypeHandler.Parse(object? value) -> T? +abstract Dapper.SqlMapper.TypeHandler.Parse(object! value) -> T? abstract Dapper.SqlMapper.TypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void const Dapper.DbString.DefaultLength = 4000 -> int Dapper.CommandDefinition @@ -130,8 +130,8 @@ Dapper.SqlMapper.IParameterCallbacks.OnCompleted() -> void Dapper.SqlMapper.IParameterLookup Dapper.SqlMapper.IParameterLookup.this[string! name].get -> object? Dapper.SqlMapper.ITypeHandler -Dapper.SqlMapper.ITypeHandler.Parse(System.Type! destinationType, object? value) -> object? -Dapper.SqlMapper.ITypeHandler.SetValue(System.Data.IDbDataParameter! parameter, object? value) -> void +Dapper.SqlMapper.ITypeHandler.Parse(System.Type! destinationType, object! value) -> object? +Dapper.SqlMapper.ITypeHandler.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void Dapper.SqlMapper.ITypeMap Dapper.SqlMapper.ITypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo? Dapper.SqlMapper.ITypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo? @@ -149,7 +149,7 @@ override Dapper.DbString.ToString() -> string! override Dapper.SqlMapper.Identity.Equals(object? obj) -> bool override Dapper.SqlMapper.Identity.GetHashCode() -> int override Dapper.SqlMapper.Identity.ToString() -> string! -override Dapper.SqlMapper.StringTypeHandler.Parse(object? value) -> T +override Dapper.SqlMapper.StringTypeHandler.Parse(object! value) -> T override Dapper.SqlMapper.StringTypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void readonly Dapper.SqlMapper.Identity.commandType -> System.Data.CommandType? readonly Dapper.SqlMapper.Identity.connectionString -> string! diff --git a/Dapper/SqlDataRecordHandler.cs b/Dapper/SqlDataRecordHandler.cs index ed113dc02..2d9976338 100644 --- a/Dapper/SqlDataRecordHandler.cs +++ b/Dapper/SqlDataRecordHandler.cs @@ -7,12 +7,12 @@ namespace Dapper internal sealed class SqlDataRecordHandler : SqlMapper.ITypeHandler where T : IDataRecord { - public object Parse(Type destinationType, object? value) + public object Parse(Type destinationType, object value) { throw new NotSupportedException(); } - public void SetValue(IDbDataParameter parameter, object? value) + public void SetValue(IDbDataParameter parameter, object value) { SqlDataRecordListTVPParameter.Set(parameter, value as IEnumerable, null); } diff --git a/Dapper/SqlMapper.ITypeHandler.cs b/Dapper/SqlMapper.ITypeHandler.cs index 5cd4a4306..57a4d2153 100644 --- a/Dapper/SqlMapper.ITypeHandler.cs +++ b/Dapper/SqlMapper.ITypeHandler.cs @@ -15,7 +15,7 @@ public interface ITypeHandler /// /// The parameter to configure /// Parameter value - void SetValue(IDbDataParameter parameter, object? value); + void SetValue(IDbDataParameter parameter, object value); /// /// Parse a database value back to a typed value @@ -23,7 +23,7 @@ public interface ITypeHandler /// The value from the database /// The type to parse to /// The typed value - object? Parse(Type destinationType, object? value); + object? Parse(Type destinationType, object value); } } } diff --git a/Dapper/SqlMapper.TypeHandler.cs b/Dapper/SqlMapper.TypeHandler.cs index 4e12bb812..c36a60b4b 100644 --- a/Dapper/SqlMapper.TypeHandler.cs +++ b/Dapper/SqlMapper.TypeHandler.cs @@ -23,9 +23,9 @@ public abstract class TypeHandler : ITypeHandler /// /// The value from the database /// The typed value - public abstract T? Parse(object? value); + public abstract T? Parse(object value); - void ITypeHandler.SetValue(IDbDataParameter parameter, object? value) + void ITypeHandler.SetValue(IDbDataParameter parameter, object value) { if (value is DBNull) { @@ -37,7 +37,7 @@ void ITypeHandler.SetValue(IDbDataParameter parameter, object? value) } } - object? ITypeHandler.Parse(Type destinationType, object? value) + object? ITypeHandler.Parse(Type destinationType, object value) { return Parse(value); } @@ -76,7 +76,7 @@ public override void SetValue(IDbDataParameter parameter, T? value) /// /// The value from the database /// The typed value - public override T Parse(object? value) + public override T Parse(object value) { if (value is null || value is DBNull) return default!; return Parse((string)value); diff --git a/Dapper/UdtTypeHandler.cs b/Dapper/UdtTypeHandler.cs index 9bb80b61d..503485927 100644 --- a/Dapper/UdtTypeHandler.cs +++ b/Dapper/UdtTypeHandler.cs @@ -22,12 +22,12 @@ public UdtTypeHandler(string udtTypeName) this.udtTypeName = udtTypeName; } - object? ITypeHandler.Parse(Type destinationType, object? value) + object? ITypeHandler.Parse(Type destinationType, object value) { return value is DBNull ? null : value; } - void ITypeHandler.SetValue(IDbDataParameter parameter, object? value) + void ITypeHandler.SetValue(IDbDataParameter parameter, object value) { #pragma warning disable 0618 parameter.Value = SanitizeParameterValue(value); diff --git a/docs/index.md b/docs/index.md index e2273bb74..15d68faff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,6 +25,7 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command (note: new PRs will not be merged until they add release note wording here) - add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell) +- tweak NRT annotations on type-handler API (#1960 via @mgravell, fixes #1959) ### 2.1.1 diff --git a/tests/Dapper.Tests/DecimalTests.cs b/tests/Dapper.Tests/DecimalTests.cs index 9e19f4ec0..fc0b0fce5 100644 --- a/tests/Dapper.Tests/DecimalTests.cs +++ b/tests/Dapper.Tests/DecimalTests.cs @@ -20,7 +20,7 @@ public void Issue261_Decimals() parameters.Add("c", dbType: DbType.Decimal, direction: ParameterDirection.Output, precision: 10, scale: 5); connection.Execute("create proc #Issue261 @c decimal(10,5) OUTPUT as begin set @c=11.884 end"); connection.Execute("#Issue261", parameters, commandType: CommandType.StoredProcedure); - var c = parameters.Get("c"); + var c = parameters.Get("c"); Assert.Equal(11.884M, c); } diff --git a/tests/Dapper.Tests/TypeHandlerTests.cs b/tests/Dapper.Tests/TypeHandlerTests.cs index 759cf6c48..2e754ca46 100644 --- a/tests/Dapper.Tests/TypeHandlerTests.cs +++ b/tests/Dapper.Tests/TypeHandlerTests.cs @@ -374,7 +374,7 @@ private RatingValueHandler() { } - public static readonly RatingValueHandler Default = new RatingValueHandler(); + public static readonly RatingValueHandler Default = new(); public override RatingValue Parse(object? value) { @@ -431,7 +431,7 @@ private StringListTypeHandler() { } - public static readonly StringListTypeHandler Default = new StringListTypeHandler(); + public static readonly StringListTypeHandler Default = new(); //Just a simple List type handler implementation public override void SetValue(IDbDataParameter parameter, List? value) { @@ -739,5 +739,130 @@ public Issue461_ParameterisedTypeConstructor(int id, string someValue, Blarg som 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(); + } } }