Skip to content

Commit 402f20c

Browse files
authored
implement DateOnly/TimeOnly (#2051)
fix #1715 adds net6 TFM
1 parent 87eb033 commit 402f20c

File tree

6 files changed

+109
-24
lines changed

6 files changed

+109
-24
lines changed

Dapper/Dapper.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<PackageTags>orm;sql;micro-orm</PackageTags>
66
<Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..</Description>
77
<Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
8-
<TargetFrameworks>net461;netstandard2.0;net5.0;net7.0</TargetFrameworks>
8+
<TargetFrameworks>net461;netstandard2.0;net5.0;net6.0;net7.0</TargetFrameworks>
99
<Nullable>enable</Nullable>
1010
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1111
</PropertyGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#nullable enable
2+
Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask
3+
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable<dynamic!>!
4+
Dapper.SqlMapper.GridReader.ReadUnbufferedAsync<T>() -> System.Collections.Generic.IAsyncEnumerable<T>!
5+
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<dynamic!>!
6+
static Dapper.SqlMapper.QueryUnbufferedAsync<T>(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<T>!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#nullable enable

Dapper/SqlMapper.cs

+33-22
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags)
192192
public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags;
193193
public static readonly TypeMapEntry
194194
DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None),
195+
DoNotSetFieldValue = new((DbType)(-2), TypeMapEntryFlags.UseGetFieldValue),
195196
DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue),
196197
StringFieldValue = new(DbType.String, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue),
197198
BinaryFieldValue = new(DbType.Binary, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue);
@@ -202,7 +203,11 @@ public static implicit operator TypeMapEntry(DbType dbType)
202203

203204
static SqlMapper()
204205
{
205-
typeMap = new Dictionary<Type, TypeMapEntry>(41)
206+
typeMap = new Dictionary<Type, TypeMapEntry>(41
207+
#if NET6_0_OR_GREATER
208+
+ 4 // {Date|Time}Only[?]
209+
#endif
210+
)
206211
{
207212
[typeof(byte)] = DbType.Byte,
208213
[typeof(sbyte)] = DbType.SByte,
@@ -245,6 +250,12 @@ static SqlMapper()
245250
[typeof(SqlDecimal?)] = TypeMapEntry.DecimalFieldValue,
246251
[typeof(SqlMoney)] = TypeMapEntry.DecimalFieldValue,
247252
[typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue,
253+
#if NET6_0_OR_GREATER
254+
[typeof(DateOnly)] = TypeMapEntry.DoNotSetFieldValue,
255+
[typeof(TimeOnly)] = TypeMapEntry.DoNotSetFieldValue,
256+
[typeof(DateOnly?)] = TypeMapEntry.DoNotSetFieldValue,
257+
[typeof(TimeOnly?)] = TypeMapEntry.DoNotSetFieldValue,
258+
#endif
248259
};
249260
ResetTypeHandlers(false);
250261
}
@@ -257,7 +268,7 @@ static SqlMapper()
257268
[MemberNotNull(nameof(typeHandlers))]
258269
private static void ResetTypeHandlers(bool clone)
259270
{
260-
typeHandlers = new Dictionary<Type, ITypeHandler>();
271+
typeHandlers = [];
261272
AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone);
262273
AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone);
263274
AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone);
@@ -370,10 +381,10 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clo
370381
var newCopy = clone ? new Dictionary<Type, ITypeHandler>(snapshot) : snapshot;
371382

372383
#pragma warning disable 618
373-
typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler });
384+
typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]);
374385
if (secondary is not null)
375386
{
376-
typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, new object?[] { handler });
387+
typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache<int>.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]);
377388
}
378389
#pragma warning restore 618
379390
if (handler is null)
@@ -1240,7 +1251,7 @@ internal enum Row
12401251
SingleOrDefault = 3
12411252
}
12421253

1243-
private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = Array.Empty<int>();
1254+
private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = [];
12441255
private static void ThrowMultipleRows(Row row)
12451256
{
12461257
_ = row switch
@@ -2538,7 +2549,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
25382549
filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql);
25392550
}
25402551

2541-
var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true);
2552+
var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true);
25422553

25432554
var il = dm.GetILGenerator();
25442555

@@ -2909,7 +2920,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
29092920
{
29102921
if (locals is null)
29112922
{
2912-
locals = new Dictionary<Type, LocalBuilder>();
2923+
locals = [];
29132924
local = null;
29142925
}
29152926
else
@@ -2946,14 +2957,14 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
29462957
{
29472958
typeof(bool), typeof(sbyte), typeof(byte), typeof(ushort), typeof(short),
29482959
typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(float), typeof(double), typeof(decimal)
2949-
}.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), new[] { typeof(IFormatProvider) })!);
2960+
}.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), [typeof(IFormatProvider)])!);
29502961

29512962
private static MethodInfo? GetToString(TypeCode typeCode)
29522963
{
29532964
return toStrings.TryGetValue(typeCode, out MethodInfo? method) ? method : null;
29542965
}
29552966

2956-
private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), new Type[] { typeof(string), typeof(string) })!,
2967+
private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!,
29572968
InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!;
29582969

29592970
private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action<IDbCommand, object?>? paramReader)
@@ -3117,7 +3128,7 @@ static Func<DbDataReader, object> ReadViaGetFieldValueFactory(Type type, int ind
31173128
return factory(index);
31183129
}
31193130
// cache of ReadViaGetFieldValueFactory<T> for per-value T
3120-
static readonly Hashtable s_ReadViaGetFieldValueCache = new();
3131+
static readonly Hashtable s_ReadViaGetFieldValueCache = [];
31213132

31223133
static Func<DbDataReader, object> UnderlyingReadViaGetFieldValueFactory<T>(int index)
31233134
=> reader => reader.IsDBNull(index) ? null! : reader.GetFieldValue<T>(index)!;
@@ -3147,14 +3158,14 @@ private static T Parse<T>(object? value)
31473158
}
31483159

31493160
private static readonly MethodInfo
3150-
enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), new Type[] { typeof(Type), typeof(string), typeof(bool) })!,
3161+
enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string), typeof(bool)])!,
31513162
getItem = typeof(DbDataReader).GetProperties(BindingFlags.Instance | BindingFlags.Public)
31523163
.Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int))
31533164
.Select(p => p.GetGetMethod()).First()!,
31543165
getFieldValueT = typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFieldValue),
3155-
BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!,
3166+
BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!,
31563167
isDbNull = typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull),
3157-
BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(int) }, null)!;
3168+
BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!;
31583169

31593170
/// <summary>
31603171
/// Gets type-map for the given type
@@ -3191,7 +3202,7 @@ public static ITypeMap GetTypeMap(Type type)
31913202
}
31923203

31933204
// use Hashtable to get free lockless reading
3194-
private static readonly Hashtable _typeMaps = new();
3205+
private static readonly Hashtable _typeMaps = [];
31953206

31963207
/// <summary>
31973208
/// Set custom mapping for type deserializers
@@ -3263,7 +3274,7 @@ public static Func<DbDataReader, object> GetTypeDeserializer(
32633274
private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary<Type, LocalBuilder>? locals, Type type, bool initAndLoad)
32643275
{
32653276
if (type is null) throw new ArgumentNullException(nameof(type));
3266-
locals ??= new Dictionary<Type, LocalBuilder>();
3277+
locals ??= [];
32673278
if (!locals.TryGetValue(type, out LocalBuilder? found))
32683279
{
32693280
found = il.DeclareLocal(type);
@@ -3294,7 +3305,7 @@ private static Func<DbDataReader, object> GetTypeDeserializerImpl(
32943305
}
32953306

32963307
var returnType = type.IsValueType ? typeof(object) : type;
3297-
var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(DbDataReader) }, type, true);
3308+
var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, [typeof(DbDataReader)], type, true);
32983309
var il = dm.GetILGenerator();
32993310

33003311
if (IsValueTuple(type))
@@ -3403,7 +3414,7 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe
34033414

34043415
if (nullableUnderlyingType is not null)
34053416
{
3406-
var nullableTupleConstructor = valueTupleType.GetConstructor(new[] { nullableUnderlyingType });
3417+
var nullableTupleConstructor = valueTupleType.GetConstructor([nullableUnderlyingType]);
34073418

34083419
il.Emit(OpCodes.Newobj, nullableTupleConstructor!);
34093420
}
@@ -3668,7 +3679,7 @@ private static void LoadReaderValueViaGetFieldValue(ILGenerator il, int index, T
36683679
if (underlyingType != memberType)
36693680
{
36703681
// Nullable<T>; wrap it
3671-
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { underlyingType })!); // stack is now [...][T?]
3682+
il.Emit(OpCodes.Newobj, memberType.GetConstructor([underlyingType])!); // stack is now [...][T?]
36723683
}
36733684
}
36743685

@@ -3731,13 +3742,13 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind
37313742

37323743
if (nullUnderlyingType is not null)
37333744
{
3734-
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value]
3745+
il.Emit(OpCodes.Newobj, memberType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value]
37353746
}
37363747
}
37373748
else if (memberType.FullName == LinqBinary)
37383749
{
37393750
il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [...][byte-array]
3740-
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) })!);// stack is now [...][binary]
3751+
il.Emit(OpCodes.Newobj, memberType.GetConstructor([typeof(byte[])])!);// stack is now [...][binary]
37413752
}
37423753
else
37433754
{
@@ -3762,7 +3773,7 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind
37623773
FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null);
37633774
if (nullUnderlyingType is not null)
37643775
{
3765-
il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })!); // stack is now [...][typed-value]
3776+
il.Emit(OpCodes.Newobj, unboxType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value]
37663777
}
37673778
}
37683779
}
@@ -3846,7 +3857,7 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro
38463857
il.Emit(OpCodes.Ldtoken, via ?? to); // stack is now [target][target][value][member-type-token]
38473858
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null); // stack is now [target][target][value][member-type]
38483859
il.EmitCall(OpCodes.Call, InvariantCulture, null); // stack is now [target][target][value][member-type][culture]
3849-
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]
3860+
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]
38503861
il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value]
38513862
}
38523863
}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
5+
#if NET6_0_OR_GREATER
6+
namespace Dapper.Tests;
7+
8+
/* we do **NOT** expect this to work against System.Data
9+
[Collection("DateTimeOnlyTests")]
10+
public sealed class SystemSqlClientDateTimeOnlyTests : DateTimeOnlyTests<SystemSqlClientProvider> { }
11+
*/
12+
#if MSSQLCLIENT
13+
[Collection("DateTimeOnlyTests")]
14+
public sealed class MicrosoftSqlClientDateTimeOnlyTests : DateTimeOnlyTests<MicrosoftSqlClientProvider> { }
15+
#endif
16+
public abstract class DateTimeOnlyTests<TProvider> : TestBase<TProvider> where TProvider : DatabaseProvider
17+
{
18+
public class HazDateTimeOnly
19+
{
20+
public DateOnly Date { get; set; }
21+
public TimeOnly Time { get; set; }
22+
}
23+
24+
[Fact]
25+
public void TypedInOut()
26+
{
27+
var now = DateTime.Now;
28+
var args = new HazDateTimeOnly
29+
{
30+
Date = DateOnly.FromDateTime(now),
31+
Time = TimeOnly.FromDateTime(now),
32+
};
33+
var row = connection.QuerySingle<HazDateTimeOnly>("select @date as [Date], @time as [Time]", args);
34+
Assert.Equal(args.Date, row.Date);
35+
Assert.Equal(args.Time, row.Time);
36+
}
37+
38+
[Fact]
39+
public async Task TypedInOutAsync()
40+
{
41+
var now = DateTime.Now;
42+
var args = new HazDateTimeOnly
43+
{
44+
Date = DateOnly.FromDateTime(now),
45+
Time = TimeOnly.FromDateTime(now),
46+
};
47+
var row = await connection.QuerySingleAsync<HazDateTimeOnly>("select @date as [Date], @time as [Time]", args);
48+
Assert.Equal(args.Date, row.Date);
49+
Assert.Equal(args.Time, row.Time);
50+
}
51+
52+
[Fact]
53+
public void UntypedInOut()
54+
{
55+
var now = DateTime.Now;
56+
var args = new DynamicParameters();
57+
var date = DateOnly.FromDateTime(now);
58+
var time = TimeOnly.FromDateTime(now);
59+
args.Add("date", date);
60+
args.Add("time", time);
61+
var row = connection.QuerySingle<dynamic>("select @date as [Date], @time as [Time]", args);
62+
// untyped, observation is that these come back as DateTime and TimeSpan
63+
Assert.Equal(date, DateOnly.FromDateTime((DateTime)row.Date));
64+
Assert.Equal(time, TimeOnly.FromTimeSpan((TimeSpan)row.Time));
65+
}
66+
}
67+
#endif

tests/Dapper.Tests/MiscTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1345,7 +1345,7 @@ public void Issue1164_OverflowExceptionForUInt64()
13451345

13461346
private class Issue1164Object<T>
13471347
{
1348-
public T Value;
1348+
public T Value = default!;
13491349
}
13501350

13511351
internal record struct One(int OID);

0 commit comments

Comments
 (0)