Learn DateOnly & TimeOnly
Tags: csharp, dotnet, dotnetcore
Published: 2023-01-15 15:26:54

## Introduction

Every developer/coder at some point will work with dates. For the majority of those working with dates using [DateTime](https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-7.0) (or [DateTimeOffset](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset?view=net-7.0)) 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](https://learn.microsoft.com/en-us/dotnet/api/system.timespan?view=net-7.0) can also be cumbersome for specific task.

Also, when there is a need to work with json files, the same applies so [DateOnly](https://learn.microsoft.com/en-us/dotnet/api/system.dateonly?view=net-7.0) and [TimeOnly](https://learn.microsoft.com/en-us/dotnet/api/system.timeonly?view=net-7.0) 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.

:heavy_check_mark: DateOnly and TimeOnly were first introduced with .NET Core 6

{% cta https://github.com/karenpayneoregon/dateonly-primer %} GitHub repository {% endcta %}

> **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](https://github.com/dotnet/efcore/issues/29989).

## For advance programmers

Consider going right to the code samples.

- [Working with DateOnly primer with TimeOnly](https://github.com/karenpayneoregon/dateonly-primer)
- [EF Core: Working with TimeOnly](https://github.com/karenpayneoregon/ef-core-dateonly-timeonly)

## Basics

To create a DateOnly instance.

```csharp
DateOnly date = new DateOnly();
```

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

To create a meaningful DateOnly

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

Or with a specific calendar.

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

Creating a TimeOnly with hours, minutes and seconds

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

Creating a TimeOnly with hours, minutes, milliseconds and microseconds 

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

## SqlClient data provider

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

![Figure 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgqc5kvjww5gmfcut6hp.png)

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.

```csharp
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);
}
```

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

```csharp
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}";
}
```

And here is the code to read the data.

```csharp
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;

    }

}
```

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

```csharp
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;

}
```

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](https://learn.microsoft.com/en-us/dotnet/api/system.dateonly.fromdatetime?view=net-7.0) there really is nothing gained here reading from a DateTable.

```csharp
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")}");
}
```

### Note

There is a NuGet package [ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly](https://www.nuget.org/packages/ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly/) for working with the data provider.

This is the same author of [EF Power Tools](https://marketplace.visualstudio.com/items?itemName=ErikEJ.EFCorePowerTools) which I wrote a tutorial on found [here](https://dev.to/karenpayneoregon/ef-power-tools-tutorial-44d8).

## Bogus

[Bogus](https://github.com/bchavez/Bogus) is a simple fake data generator for .NET languages. 

Basic syntax for generating data

```csharp
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();
```

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.

```csharp
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() { }
        
}
```

We can generate data for BirthDate as per below.

```csharp
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);

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


![Figure 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sy501fnmxxy79kwrex23.png) 

## JSON.NET and DateOnly/TimeOnly support

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

[Code samples](https://github.com/karenpayneoregon/dateonly-primer/tree/master/NewtonsoftDateOnlyTimeOnlyApp) for Json.net which as of version 13.0.2 now supports [DateOnly](https://learn.microsoft.com/en-us/dotnet/api/system.dateonly?view=net-7.0) and [TimeOnly](https://learn.microsoft.com/en-us/dotnet/api/system.timeonly?view=net-7.0). Three code samples are used to show interactions with [Bogus](https://github.com/bchavez/Bogus) and [Microsoft.Data.SqlClient](https://github.com/dotnet/SqlClient/blob/main/release-notes/5.1/5.1.0-preview2.md) 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

```csharp
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}";
}
```
Code sample

```csharp
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();
    }
}
```


![results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/789i18f9epsep7ttgm1g.png)


## 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](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.storage.valueconversion.valueconverter?view=efcore-7.0).

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

For TimeOnly

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

**Setup in a DbContext**

Add the following method to your DbContext for DateOnly.

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

    base.ConfigureConventions(builder);
}
```

For TimeOnly

```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{

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

	base.ConfigureConventions(builder);

}
```

And for both DateOnly and TimeOnly

```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{

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

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

	base.ConfigureConventions(builder);

}
```
:bulb: 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bazdvkm536yu6dq0e0aj.png)

[Source code](https://github.com/karenpayneoregon/dateonly-primer/tree/master/EntityCoreTimeDateutilities)

**Simple example for ASP.NET Core Razor Pages**

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

```csharp
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; }
}
```

Code behind for the page

```csharp
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();
        }
    }
}
```

Front end for the page

```csharp
@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>
```


![Razor page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ausw3sbddvpu4l8eequw.png)

## Deconstruct DateOnly/TimeOnly

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

```csharp
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);
}
```

Usage

```csharp
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);
}
```

And we can use discards for values not needed.

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

## 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

- Microsoft docs - [DateOnly Struct](https://docs.microsoft.com/en-us/dotnet/api/system.dateonly?view=net-6.0)
- [Entity Framework Core working with dates](https://github.com/karenpayneoregon/ef-core-5-dates-csharp)
- [Working with DateTime]
(https://github.com/karenpayneoregon/working-with-datetime)
- [TimeOnly](https://learn.microsoft.com/en-us/dotnet/api/system.timeonly?view=net-7.0)
- [EF Core Value Conversions](https://github.com/karenpayneoregon/ef-core-transforming)
- [Microsoft.Data.SqlClient 5.1.0](https://github.com/dotnet/SqlClient/blob/main/release-notes/5.1/5.1.0.md) released 19 January 2023
 - Added support for <kbd>DateOnly</kbd> and <kbd>TimeOnly</kbd> for <kbd>SqlParameter</kbd> value and <kbd>GetFieldValue</kbd>. [#1813](https://github.com/dotnet/SqlClient/pull/1813)


## Source code

- [Working with DateOnly primer with TimeOnly](https://github.com/karenpayneoregon/dateonly-primer)
- [EF Core: Working with TimeOnly](https://github.com/karenpayneoregon/ef-core-dateonly-timeonly)