Learn DateOnly & TimeOnly

Karen Payne - Jan 15 '23 - - Dev Community

Introduction

Every developer/coder at some point will work with dates. For the majority of those working with dates using DateTime (or DateTimeOffset) will suffice for most task while for others with needs such as interacting with a database which has column type of date it can be awkward transposing from DateTime to date and then it’s now a string. Similarly, working with time via TimeSpan can also be cumbersome for specific task.

Also, when there is a need to work with json files, the same applies so DateOnly and TimeOnly is a welcome addition to the .NET Framework.

In this article learn how to work with DateOnly and TimeOnly with SqlClient data provider, Newtonsoft Json.net and Entity Framework Core 7.

✔️ DateOnly and TimeOnly were first introduced with .NET Core 6

GitHub repository

Note
04/2024 updated several projects to .NET Core 8, added a Dapper example.

EF Core 8

Will support DateOnly and TimeOnly as per the following.

For advance programmers

Consider going right to the code samples.

Basics

To create a DateOnly instance.



DateOnly date = new DateOnly();


Enter fullscreen mode Exit fullscreen mode

Which will have a value of 1/1/0001.

To create a meaningful DateOnly



DateOnly date = new DateOnly(2023,1,14);


Enter fullscreen mode Exit fullscreen mode

Or with a specific calendar.



DateOnly date = new DateOnly(2023,1,14, new PersianCalendar());


Enter fullscreen mode Exit fullscreen mode

Creating a TimeOnly with hours, minutes and seconds



TimeOnly time = new TimeOnly(13,15,45);


Enter fullscreen mode Exit fullscreen mode

Creating a TimeOnly with hours, minutes, milliseconds and microseconds



TimeOnly time = new TimeOnly(13,15,45,11,55);


Enter fullscreen mode Exit fullscreen mode

SqlClient data provider

In this example we will use the following table schema, a date and two time(7) columns.

Figure 1

Next, to keep code clean, two language extension methods, one get get a DateOnly by indexing into the data reader while the second one gets a TimeOnly by indexing into the data reader.



internal static class Extensions
{

    public static DateOnly GetDateOnly(this SqlDataReader reader, int index)
        => reader.GetFieldValue<DateOnly>(index);
    public static TimeOnly ToTimeOnly(this TimeSpan sender)
        => TimeOnly.FromTimeSpan(sender);

    public static TimeOnly GetTimeOnly(this SqlDataReader reader, int index)
        => reader.GetFieldValue<TimeOnly>(index);
}


Enter fullscreen mode Exit fullscreen mode

The following model is for returning read data from the database.



public class VisitorLog
{

    public DateOnly VisitOn { get; set; }
    public TimeOnly EnteredTime { get; set; }
    public TimeOnly ExitedTime { get; set; }

    public override string ToString() 
        => $"{VisitOn,-10}{EnteredTime,-10}{ExitedTime}";
}


Enter fullscreen mode Exit fullscreen mode

And here is the code to read the data.



internal class DataOperations
{
    public static async Task<List<VisitorLog>> DataReaderLoopExample()
    {

        List<VisitorLog> list = new();
        var statement = """
            SELECT VL.VisitOn, VL.EnteredTime, VL.ExitedTime 
            FROM Visitor AS V  
            INNER JOIN VisitorLog AS VL ON V.VisitorIdentifier = VL.VisitorIdentifier 
            """;

        await using var cn = new SqlConnection(ConfigurationHelper.ConnectionString());
        await using var cmd = new SqlCommand { Connection = cn, CommandText = statement };

        await cn.OpenAsync();
        await using var reader = await cmd.ExecuteReaderAsync();



        while (reader.Read())
        {
            list.Add(new VisitorLog()
            {
                VisitOn = reader.GetDateOnly(0),
                EnteredTime = reader.GetTimeOnly(1),
                ExitedTime = reader.GetTimeOnly(2)
            });
        }

        return list;

    }

}


Enter fullscreen mode Exit fullscreen mode

What if the task is to read the data above into a DataTable?



public static async Task<DataTable> DataTableExample()
{

    var statement = """
        SELECT VL.VisitOn, VL.EnteredTime, VL.ExitedTime 
        FROM Visitor AS V  
        INNER JOIN VisitorLog AS VL ON V.VisitorIdentifier = VL.VisitorIdentifier 
        """;

    await using var cn = new SqlConnection(ConfigurationHelper.ConnectionString());
    await using var cmd = new SqlCommand { Connection = cn, CommandText = statement };

    await cn.OpenAsync();

    DataTable dataTable = new DataTable();

    dataTable.Load(await cmd.ExecuteReaderAsync());
    return dataTable;

}


Enter fullscreen mode Exit fullscreen mode

The code reads the data but the date column will be seen as a DateTime. Although the DateTime can be converted to a DateOnly using DateOnly.FromDateTime there really is nothing gained here reading from a DateTable.



var tableResult = await DataOperations.DataTableExample();

foreach (DataRow row in tableResult.Rows)
{
    Console.WriteLine(
        $"{DateOnly.FromDateTime(row.Field<DateTime>("VisitOn")).ToString("MM/dd/yyyy"),-12}" + 
        $"{row.Field<TimeSpan>("EnteredTime").ToTimeOnly().ToString("hh:mm:ss tt"),-15}" + 
        $"{row.Field<TimeSpan>("ExitedTime").ToTimeOnly().ToString("hh:mm:ss tt")}");
}


Enter fullscreen mode Exit fullscreen mode

Note

There is a NuGet package ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly for working with the data provider.

This is the same author of EF Power Tools which I wrote a tutorial on found here.

Bogus

Bogus is a simple fake data generator for .NET languages.

Basic syntax for generating data



Randomizer.Seed = new Random(1338);
var orderIds = 0;
var orderFaker = new Faker<Order>()
    .RuleFor(o => o.OrderId, f => orderIds++)
    .RuleFor(o => o.Item, f => f.Commerce.Product())
    .RuleFor(o => o.Quantity, f => f.Random.Number(1, 5));

orderFaker.Generate(5).Dump();


Enter fullscreen mode Exit fullscreen mode

What is interesting is Bogus has not updated their documentation to include that it provides the ability to work with DateOnly.

Given the following model.



public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }

    public Person(int identifier)
    {
        Id = identifier;
    }

    public Person() { }

}


Enter fullscreen mode Exit fullscreen mode

We can generate data for BirthDate as per below.



public static List<Person> People(int count = 10)
{
    int identifier = 1;

    Faker<Person> fakePerson = new Faker<Person>()
            .CustomInstantiator(f => new Person(identifier++))
            .RuleFor(p => p.FirstName, f => f.Person.FirstName)
            .RuleFor(p => p.LastName, f => f.Person.LastName)
            .RuleFor(p => p.BirthDate, f => 
                f.Date.BetweenDateOnly(
                    new DateOnly(2000, 1, 1), 
                    new DateOnly(2022, 12, 1)))
        ;


    return fakePerson.Generate(count);

}


Enter fullscreen mode Exit fullscreen mode

There are a few more via Intellisense as there is no documentation.

Figure 2

JSON.NET and DateOnly/TimeOnly support

There is really nothing special to show here, instead check out the sample code.

Code samples for Json.net which as of version 13.0.2 now supports DateOnly and TimeOnly. Three code samples are used to show interactions with Bogus and Microsoft.Data.SqlClient which is most likely used to work with json data.

Sample Description
Sample1 Created a list of mocked people, serialize then deserialize with Json.net
Sample2 Same as Sample1 but uses Bogus NuGet package to create a list. Bogus just began support for DateOnly and TimeOnly with Json.net

System.Text.Json

For System.Text.Json basic example

Model



public class VisitorLog
{

    public DateOnly VisitOn { get; set; }
    public TimeOnly EnteredTime { get; set; }
    public TimeOnly ExitedTime { get; set; }

    public override string ToString()
        => $"{VisitOn,-10}{EnteredTime,-10}{ExitedTime}";
}


Enter fullscreen mode Exit fullscreen mode

Code sample



using System.Text.Json;
using DateOnlyTimeOnlySysJsonApp.Models;
using Spectre.Console.Json;

namespace DateOnlyTimeOnlySysJsonApp;

internal partial class Program
{
    static void Main(string[] args)
    {
        VisitorLog log = new()
        {
            VisitOn = new DateOnly(2023,1,12), 
            EnteredTime = new TimeOnly(13,15,15), 
            ExitedTime = new TimeOnly(13,45,0)
        };


        string jsonString = JsonSerializer.Serialize(log, 
            new JsonSerializerOptions { WriteIndented = true });

        var json = new JsonText(jsonString)
            .BracketColor(Color.Green)
            .ColonColor(Color.Blue)
            .CommaColor(Color.Red)
            .StringColor(Color.Green)
            .NumberColor(Color.Blue)
            .BooleanColor(Color.Red)
            .NullColor(Color.Green);

        AnsiConsole.Write(
            new Panel(json)
                .Header("VisitorLog serialized")
                .Collapse()
                .BorderColor(Color.White));

        Console.WriteLine();

        var deserializedLog = JsonSerializer.Deserialize<VisitorLog>(jsonString);
        AnsiConsole.MarkupLine("[white]Deserialize[/]");
        AnsiConsole.MarkupLine($"[yellow]Visited[/] {deserializedLog.VisitOn,-15}" + 
                               $"[yellow]Entered[/] {deserializedLog.EnteredTime, -15}" + 
                               $"[yellow]Exit[/] {deserializedLog.ExitedTime, -15}");

        Console.ReadLine();
    }
}


Enter fullscreen mode Exit fullscreen mode

results

EF Core

EF Core is fairly simple once you see the code. In this case all DateOnly properties in all models will be converted from DateTime to DateOnly using the following ValueConverter.



internal class DateOnlyConverter : ValueConverter<DateOnly, DateTime>
{
    public DateOnlyConverter()
        : base(d => d.ToDateTime(TimeOnly.MinValue),
            d => DateOnly.FromDateTime(d)) { }
}


Enter fullscreen mode Exit fullscreen mode

For TimeOnly



public class TimeOnlyConverter : ValueConverter<TimeOnly, TimeSpan>
{
    public TimeOnlyConverter() : base(timeOnly => 
            timeOnly.ToTimeSpan(), 
        timeSpan => TimeOnly.FromTimeSpan(timeSpan)) { }
}


Enter fullscreen mode Exit fullscreen mode

Setup in a DbContext

Add the following method to your DbContext for DateOnly.



protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
    builder.Properties<DateOnly>()
        .HaveConversion<DateOnlyConverter>()
        .HaveColumnType("date");

    base.ConfigureConventions(builder);
}


Enter fullscreen mode Exit fullscreen mode

For TimeOnly



protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{

    builder.Properties<TimeOnly>()
        .HaveConversion<TimeOnlyConverter>()
        .HaveColumnType("time");

    base.ConfigureConventions(builder);

}


Enter fullscreen mode Exit fullscreen mode

And for both DateOnly and TimeOnly



protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{

    builder.Properties<DateOnly>()
        .HaveConversion<DateOnlyConverter>()
        .HaveColumnType("date");

    builder.Properties<TimeOnly>()
        .HaveConversion<TimeOnlyConverter>()
        .HaveColumnType("time");

    base.ConfigureConventions(builder);

}


Enter fullscreen mode Exit fullscreen mode

💡 If you plan on working with DateOnly and TimeOnly in more than one project, consider creating a class project with the above converters.

class project

Source code

Simple example for ASP.NET Core Razor Pages

The value converter for DateOnly has been setup in the DbContext. Next here is the model.



public partial class Person
{
    public int PersonId { get; set; }
    [Display(Name = "First")]
    public string FirstName { get; set; }
    [Display(Name = "Last")]
    public string LastName { get; set; }
    [Display(Name = "Birth Date")]
    public DateOnly? BirthDate { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

Code behind for the page



public class ViewPeopleModel : PageModel
{
    private readonly Data.Context _context;

    public ViewPeopleModel(Data.Context context)
    {
        _context = context;
    }

    public IList<Person> Person { get;set; } = default!;

    public async Task OnGetAsync()
    {
        if (_context.Person != null)
        {
            Person = await _context.Person.ToListAsync();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Front end for the page



@page
@model DateOnlyApp1.Pages.ViewPeopleModel

@{
    ViewData["Title"] = "ViewPeople";
}



<table class="table table-striped">
    <thead class="table-primary">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Person[0].FirstName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Person[0].LastName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Person[0].BirthDate)
            </th>

        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Person)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.BirthDate)
                </td>

            </tr>
        }
    </tbody>
</table>


Enter fullscreen mode Exit fullscreen mode

Razor page

Deconstruct DateOnly/TimeOnly

In some cases one may want an elegant way to get individual parts for DateOnly and/or TimeOnly as per below.



internal static class Helpers
{
    public static void Deconstruct(this DateOnly date, out int day, out int month, out int year) =>
        (day, month, year) = (date.Day, date.Month, date.Year);

    public static void Deconstruct(this TimeOnly time, out int hour, out int minutes, out int seconds, out int milliseconds)
        => (hour, minutes, seconds, milliseconds) = (time.Hour, time.Minute, time.Second, time.Microsecond);

    public static void Deconstruct(this TimeOnly time, out int hour, out int minutes, out int seconds)
        => (hour, minutes, seconds) = (time.Hour, time.Minute, time.Second);
}


Enter fullscreen mode Exit fullscreen mode

Usage



internal partial class Program
{
    static void Main(string[] args)
    {
        var (day, month, year) = Sample1();
        AnsiConsole.MarkupLine($"{month}  {day}  {year}");
        var (hour, minutes, seconds) = Sample2();
        AnsiConsole.MarkupLine($"{hour}  {minutes}  {seconds}");

        Console.ReadLine();
    }

    static DateOnly Sample1() => new(2023, 7, 11);
    static TimeOnly Sample2() => new(13,15, 15);
}


Enter fullscreen mode Exit fullscreen mode

And we can use discards for values not needed.



var (hour, minutes, _ ) = Sample2();
AnsiConsole.MarkupLine($"{hour} {minutes}");

Enter fullscreen mode Exit fullscreen mode




Summary

This article and accompanying source code provides what a developer needs to get started working with DateOnly and TimeOnly in their applications.

Notes, the code for data provider uses a preview version and expect and non-preview version to be release in the near future. For EF Core, there is talks about having native support in EF Core 8 for DateOnly and TimeOnly.

References

Source code

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .