From d86f4e031fd8d2595f9183045fba47f7b422adfa Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 7 Mar 2024 12:05:17 +0000 Subject: [PATCH] implement DateOnly/TimeOnly fix #1715 adds net6 TFM --- Dapper/Dapper.csproj | 2 +- Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt | 6 ++ .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 1 + Dapper/SqlMapper.cs | 55 +++++++++------ tests/Dapper.Tests/DateTimeOnlyTests.cs | 67 +++++++++++++++++++ tests/Dapper.Tests/MiscTests.cs | 2 +- 6 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt create mode 100644 Dapper/PublicAPI/net6.0/PublicAPI.Unshipped.txt create mode 100644 tests/Dapper.Tests/DateTimeOnlyTests.cs diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index 8fc9c65f..0ede9f79 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -5,7 +5,7 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0;net7.0 + net461;netstandard2.0;net5.0;net6.0;net7.0 enable true diff --git a/Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000..5da46e60 --- /dev/null +++ b/Dapper/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -0,0 +1,6 @@ +#nullable enable +Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! \ No newline at end of file diff --git a/Dapper/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net6.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..91b0e1a4 --- /dev/null +++ b/Dapper/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 57d84edc..5575a6f5 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -192,6 +192,7 @@ public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags) public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags; public static readonly TypeMapEntry DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None), + DoNotSetFieldValue = new((DbType)(-2), TypeMapEntryFlags.UseGetFieldValue), DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue), StringFieldValue = new(DbType.String, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue), BinaryFieldValue = new(DbType.Binary, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue); @@ -202,7 +203,11 @@ public static implicit operator TypeMapEntry(DbType dbType) static SqlMapper() { - typeMap = new Dictionary(41) + typeMap = new Dictionary(41 +#if NET6_0_OR_GREATER + + 4 // {Date|Time}Only[?] +#endif + ) { [typeof(byte)] = DbType.Byte, [typeof(sbyte)] = DbType.SByte, @@ -245,6 +250,12 @@ static SqlMapper() [typeof(SqlDecimal?)] = TypeMapEntry.DecimalFieldValue, [typeof(SqlMoney)] = TypeMapEntry.DecimalFieldValue, [typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue, +#if NET6_0_OR_GREATER + [typeof(DateOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(DateOnly?)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly?)] = TypeMapEntry.DoNotSetFieldValue, +#endif }; ResetTypeHandlers(false); } @@ -257,7 +268,7 @@ static SqlMapper() [MemberNotNull(nameof(typeHandlers))] private static void ResetTypeHandlers(bool clone) { - typeHandlers = new Dictionary(); + typeHandlers = []; AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone); AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone); AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone); @@ -370,10 +381,10 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clo var newCopy = clone ? new Dictionary(snapshot) : snapshot; #pragma warning disable 618 - typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler }); + typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); if (secondary is not null) { - typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler }); + typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); } #pragma warning restore 618 if (handler is null) @@ -1240,7 +1251,7 @@ internal enum Row SingleOrDefault = 3 } - private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = Array.Empty(); + private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = []; private static void ThrowMultipleRows(Row row) { _ = row switch @@ -2538,7 +2549,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql); } - var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true); var il = dm.GetILGenerator(); @@ -2909,7 +2920,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true { if (locals is null) { - locals = new Dictionary(); + locals = []; local = null; } else @@ -2946,14 +2957,14 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true { typeof(bool), typeof(sbyte), typeof(byte), typeof(ushort), typeof(short), typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(float), typeof(double), typeof(decimal) - }.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), new[] { typeof(IFormatProvider) })!); + }.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), [typeof(IFormatProvider)])!); private static MethodInfo? GetToString(TypeCode typeCode) { return toStrings.TryGetValue(typeCode, out MethodInfo? method) ? method : null; } - private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), new Type[] { typeof(string), typeof(string) })!, + private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!, InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!; private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action? paramReader) @@ -3117,7 +3128,7 @@ static Func ReadViaGetFieldValueFactory(Type type, int ind return factory(index); } // cache of ReadViaGetFieldValueFactory for per-value T - static readonly Hashtable s_ReadViaGetFieldValueCache = new(); + static readonly Hashtable s_ReadViaGetFieldValueCache = []; static Func UnderlyingReadViaGetFieldValueFactory(int index) => reader => reader.IsDBNull(index) ? null! : reader.GetFieldValue(index)!; @@ -3147,14 +3158,14 @@ private static T Parse(object? value) } private static readonly MethodInfo - enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), new Type[] { typeof(Type), typeof(string), typeof(bool) })!, + enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string), typeof(bool)])!, getItem = typeof(DbDataReader).GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int)) .Select(p => p.GetGetMethod()).First()!, getFieldValueT = typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFieldValue), - BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!, + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!, isDbNull = typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull), - BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!; + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!; /// /// Gets type-map for the given type @@ -3191,7 +3202,7 @@ public static ITypeMap GetTypeMap(Type type) } // use Hashtable to get free lockless reading - private static readonly Hashtable _typeMaps = new(); + private static readonly Hashtable _typeMaps = []; /// /// Set custom mapping for type deserializers @@ -3263,7 +3274,7 @@ public static Func GetTypeDeserializer( private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary? locals, Type type, bool initAndLoad) { if (type is null) throw new ArgumentNullException(nameof(type)); - locals ??= new Dictionary(); + locals ??= []; if (!locals.TryGetValue(type, out LocalBuilder? found)) { found = il.DeclareLocal(type); @@ -3294,7 +3305,7 @@ private static Func GetTypeDeserializerImpl( } var returnType = type.IsValueType ? typeof(object) : type; - var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(DbDataReader) }, type, true); + var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, [typeof(DbDataReader)], type, true); var il = dm.GetILGenerator(); if (IsValueTuple(type)) @@ -3403,7 +3414,7 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe if (nullableUnderlyingType is not null) { - var nullableTupleConstructor = valueTupleType.GetConstructor(new[] { nullableUnderlyingType }); + var nullableTupleConstructor = valueTupleType.GetConstructor([nullableUnderlyingType]); il.Emit(OpCodes.Newobj, nullableTupleConstructor!); } @@ -3668,7 +3679,7 @@ private static void LoadReaderValueViaGetFieldValue(ILGenerator il, int index, T if (underlyingType != memberType) { // Nullable; wrap it - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { underlyingType })!); // stack is now [...][T?] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([underlyingType])!); // stack is now [...][T?] } } @@ -3731,13 +3742,13 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind if (nullUnderlyingType is not null) { - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] } } else if (memberType.FullName == LinqBinary) { il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [...][byte-array] - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) })!);// stack is now [...][binary] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([typeof(byte[])])!);// stack is now [...][binary] } else { @@ -3762,7 +3773,7 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null); if (nullUnderlyingType is not null) { - il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value] + il.Emit(OpCodes.Newobj, unboxType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] } } } @@ -3846,7 +3857,7 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro il.Emit(OpCodes.Ldtoken, via ?? to); // stack is now [target][target][value][member-type-token] il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null); // stack is now [target][target][value][member-type] il.EmitCall(OpCodes.Call, InvariantCulture, null); // stack is now [target][target][value][member-type][culture] - il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), new Type[] { typeof(object), typeof(Type), typeof(IFormatProvider) })!, null); // stack is now [target][target][boxed-member-type-value] + il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), [typeof(object), typeof(Type), typeof(IFormatProvider)])!, null); // stack is now [target][target][boxed-member-type-value] il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value] } } diff --git a/tests/Dapper.Tests/DateTimeOnlyTests.cs b/tests/Dapper.Tests/DateTimeOnlyTests.cs new file mode 100644 index 00000000..015af2d5 --- /dev/null +++ b/tests/Dapper.Tests/DateTimeOnlyTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +#if NET6_0_OR_GREATER +namespace Dapper.Tests; + +/* we do **NOT** expect this to work against System.Data +[Collection("DateTimeOnlyTests")] +public sealed class SystemSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +*/ +#if MSSQLCLIENT +[Collection("DateTimeOnlyTests")] +public sealed class MicrosoftSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +#endif +public abstract class DateTimeOnlyTests : TestBase where TProvider : DatabaseProvider +{ + public class HazDateTimeOnly + { + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + } + + [Fact] + public void TypedInOut() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + }; + var row = connection.QuerySingle("select @date as [Date], @time as [Time]", args); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + } + + [Fact] + public async Task TypedInOutAsync() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + }; + var row = await connection.QuerySingleAsync("select @date as [Date], @time as [Time]", args); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + } + + [Fact] + public void UntypedInOut() + { + var now = DateTime.Now; + var args = new DynamicParameters(); + var date = DateOnly.FromDateTime(now); + var time = TimeOnly.FromDateTime(now); + args.Add("date", date); + args.Add("time", time); + var row = connection.QuerySingle("select @date as [Date], @time as [Time]", args); + // untyped, observation is that these come back as DateTime and TimeSpan + Assert.Equal(date, DateOnly.FromDateTime((DateTime)row.Date)); + Assert.Equal(time, TimeOnly.FromTimeSpan((TimeSpan)row.Time)); + } +} +#endif diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs index 7f584856..1bf4bbfb 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -1345,7 +1345,7 @@ public void Issue1164_OverflowExceptionForUInt64() private class Issue1164Object { - public T Value; + public T Value = default!; } internal record struct One(int OID);