From 195cca898651060eda0d7f7fc499b9c3e38ca05a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 20 Aug 2026 05:28:28 +0100 Subject: [PATCH] CommandUtils.As: accept a DateOnly source (Npgsql 10 'date' columns) Npgsql 10 changed reader.GetValue() for a 'date' column to return DateOnly rather than DateTime; DateOnly does not implement IConvertible, so the As fallback through Convert.ToDateTime threw InvalidCastException. Bisected empirically: the DateOnlyTimeOnly Postgres tests pass on Npgsql 9.0.2 and fail on 10.0.2, which arrived with #174 - unseen because the Postgres integration tests need local Docker. As now converts via ToDateTime(TimeOnly.MinValue), and As answers default for a date - the same answer a zero-time DateTime gives, which is the contract the integration test documents. New docker-free unit tests cover the matrix directly, so this coverage no longer depends on a live container. Fixes #202 --- src/Dapper.AOT/Internal/CommandUtils.cs | 26 ++++++++++++ .../CommandUtilsDateOnlyTests.cs | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/Dapper.AOT.Test/CommandUtilsDateOnlyTests.cs diff --git a/src/Dapper.AOT/Internal/CommandUtils.cs b/src/Dapper.AOT/Internal/CommandUtils.cs index 997274b7..19d5e2fa 100644 --- a/src/Dapper.AOT/Internal/CommandUtils.cs +++ b/src/Dapper.AOT/Internal/CommandUtils.cs @@ -157,11 +157,26 @@ internal static T As(object? value) } else if (typeof(T) == typeof(DateTime)) { +#if NET6_0_OR_GREATER + // Npgsql 10 hands back DateOnly for "date" columns; not IConvertible + if (value is DateOnly dateOnlySource) + { + DateTime fromDateOnly = dateOnlySource.ToDateTime(TimeOnly.MinValue); + return Unsafe.As(ref fromDateOnly); + } +#endif DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); return Unsafe.As(ref t); } else if (typeof(T) == typeof(DateTime?)) { +#if NET6_0_OR_GREATER + if (value is DateOnly dateOnlySource) + { + DateTime? fromDateOnly = dateOnlySource.ToDateTime(TimeOnly.MinValue); + return Unsafe.As(ref fromDateOnly); + } +#endif DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); return Unsafe.As(ref t); } @@ -187,6 +202,12 @@ internal static T As(object? value) var fromSpan = TimeOnly.FromTimeSpan(timeSpan); return Unsafe.As(ref fromSpan); } + if (value is DateOnly) + { + // a date has no time component; same answer a zero-time DateTime gives + TimeOnly zero = default; + return Unsafe.As(ref zero); + } DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); var timeOnly = TimeOnly.FromDateTime(t); @@ -199,6 +220,11 @@ internal static T As(object? value) var fromSpan = TimeOnly.FromTimeSpan(timeSpan); return Unsafe.As(ref fromSpan); } + if (value is DateOnly) + { + TimeOnly? zero = default(TimeOnly); + return Unsafe.As(ref zero); + } DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture); TimeOnly? timeOnly = t is null ? null : TimeOnly.FromDateTime(t.Value); diff --git a/test/Dapper.AOT.Test/CommandUtilsDateOnlyTests.cs b/test/Dapper.AOT.Test/CommandUtilsDateOnlyTests.cs new file mode 100644 index 00000000..f3cce1c9 --- /dev/null +++ b/test/Dapper.AOT.Test/CommandUtilsDateOnlyTests.cs @@ -0,0 +1,40 @@ +#if NET6_0_OR_GREATER +using System; +using Dapper.Internal; +using Xunit; + +namespace Dapper.AOT.Test +{ + // Npgsql 10 hands back DateOnly (not DateTime) for "date" columns; these are the + // docker-free half of DateOnlyTimeOnlyPostgreSqlTests, so the coverage does not + // depend on a live container (see #202) + public class CommandUtilsDateOnlyTests + { + private static readonly object BoxedDateOnly = new DateOnly(2021, 1, 2); + + [Fact] + public void As_DateOnlyToDateTime() + => Assert.Equal(new DateTime(2021, 1, 2), CommandUtils.As(BoxedDateOnly)); + + [Fact] + public void As_DateOnlyToNullableDateTime() + => Assert.Equal(new DateTime(2021, 1, 2), CommandUtils.As(BoxedDateOnly)); + + [Fact] // a date has no time component; same answer a zero-time DateTime gives + public void As_DateOnlyToTimeOnly() + => Assert.Equal(default, CommandUtils.As(BoxedDateOnly)); + + [Fact] + public void As_DateOnlyToNullableTimeOnly() + => Assert.Equal(default(TimeOnly), CommandUtils.As(BoxedDateOnly)); + + [Fact] // pass-through, unchanged + public void As_DateOnlyToDateOnly() + => Assert.Equal(new DateOnly(2021, 1, 2), CommandUtils.As(BoxedDateOnly)); + + [Fact] // the pre-Npgsql-10 shape, unchanged + public void As_DateTimeToDateOnly() + => Assert.Equal(new DateOnly(2021, 1, 2), CommandUtils.As((object)new DateTime(2021, 1, 2))); + } +} +#endif