DateOnly TimeOnly NET6 - lobodava/artisan-orm GitHub Wiki
.NET 6 introduced DateOnly and TimeOnly — value types for "date without time" and "time without date". Artisan.Orm has typed parameter and reader helpers for both, so you don't have to hand-convert through DateTime / TimeSpan at every call site.
Available on net8.0 and net10.0 targets only — guarded by #if NET6_0_OR_GREATER.
Added in v4 — see What's New in v4.
Parameter helpers
// SQL date <-> DateOnly
public static void AddDateParam(this SqlCommand cmd, string name, DateOnly value);
public static void AddDateParam(this SqlCommand cmd, string name, DateOnly? value);
// SQL time <-> TimeOnly
public static void AddTimeParam(this SqlCommand cmd, string name, TimeOnly value);
public static void AddTimeParam(this SqlCommand cmd, string name, TimeOnly? value);
Internally these convert to DateTime / TimeSpan once and reuse the existing AddDateParam(DateTime) / AddTimeParam(TimeSpan) overloads, so the SQL-side type is plain date / time(7).
Reader helpers
public static DateOnly GetDateOnly(this SqlDataReader dr, int ordinal);
public static DateOnly? GetDateOnlyNullable(this SqlDataReader dr, int ordinal);
public static DateOnly GetDateOnly(this SqlDataReader dr, int ordinal, DateOnly defaultValue);
public static TimeOnly GetTimeOnly(this SqlDataReader dr, int ordinal);
public static TimeOnly? GetTimeOnlyNullable(this SqlDataReader dr, int ordinal);
public static TimeOnly GetTimeOnly(this SqlDataReader dr, int ordinal, TimeOnly defaultValue);
The default-value overloads return defaultValue when the column is NULL — handy when the column has a logical default that matters to the domain (e.g. 00:00:00 for "all day").
End-to-end example
create procedure dbo.SaveAppointment
@PersonId int,
@Date date,
@Time time(7)
as
begin
set nocount on;
insert into dbo.Appointments (PersonId, [Date], [Time])
values (@PersonId, @Date, @Time);
end;
public class AppointmentRepository : RepositoryBase
{
public AppointmentRepository(string connStr) : base(connStr) { }
public void Save(int personId, DateOnly date, TimeOnly time)
{
ExecuteCommand(cmd =>
{
cmd.UseProcedure("dbo.SaveAppointment");
cmd.AddIntParam ("@PersonId", personId);
cmd.AddDateParam("@Date", date);
cmd.AddTimeParam("@Time", time);
});
}
public Appointment GetByPersonAndDate(int personId, DateOnly date)
{
return GetByCommand(cmd =>
{
cmd.UseSql("select PersonId, [Date], [Time] " +
"from dbo.Appointments " +
"where PersonId = @PersonId and [Date] = @Date");
cmd.AddIntParam ("@PersonId", personId);
cmd.AddDateParam("@Date", date);
return cmd.ReadTo(dr => new Appointment(
PersonId: dr.GetInt32 (0),
Date: dr.GetDateOnly(1),
Time: dr.GetTimeOnly(2)));
});
}
}
public record Appointment(int PersonId, DateOnly Date, TimeOnly Time);
Auto-mapping support
ReadAs<T> / ReadAsList<T> recognise DateOnly and TimeOnly properties on T and use the same conversion under the hood, so no inline mapper is needed:
public record Appointment
{
public int PersonId { get; set; }
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
}
IList<Appointment> list = ReadAsList<Appointment>(
"select PersonId, [Date], [Time] from dbo.Appointments");
Caveats
-
The SQL side needs
date/timecolumns — notdatetime/datetime2— for the conversion to be lossless. Reading adatetimecolumn withGetDateOnlydiscards the time; reading adatetime2withGetTimeOnlydiscards the date. That's usually what you want when the column is named with intent (AppointmentDate,OpeningTime), but there is no static check. -
DateOnly.MinValueis0001-01-01, which T-SQL accepts asdate '0001-01-01'. If you hit "date out of range" errors, check whether the column issmalldatetime(range starts at 1900) rather thandate.
See also: