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 / time columns — not datetime / datetime2 — for the conversion to be lossless. Reading a datetime column with GetDateOnly discards the time; reading a datetime2 with GetTimeOnly discards the date. That's usually what you want when the column is named with intent (AppointmentDate, OpeningTime), but there is no static check.

  • DateOnly.MinValue is 0001-01-01, which T-SQL accepts as date '0001-01-01'. If you hit "date out of range" errors, check whether the column is smalldatetime (range starts at 1900) rather than date.


See also: