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
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();
Which will have a value of 1/1/0001.
To create a meaningful DateOnly
DateOnly date = new DateOnly(2023,1,14);
Or with a specific calendar.
DateOnly date = new DateOnly(2023,1,14, new PersianCalendar());
Creating a TimeOnly with hours, minutes and seconds
TimeOnly time = new TimeOnly(13,15,45);
Creating a TimeOnly with hours, minutes, milliseconds and microseconds
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.
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);
}
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}";
}
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;
}
}
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;
}
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")}");
}
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();
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() { }
}
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);
}
There are a few more via Intellisense as there is no documentation.
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}";
}
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();
}
}
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)) { }
}
For TimeOnly
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.
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
builder.Properties<DateOnly>()
.HaveConversion<DateOnlyConverter>()
.HaveColumnType("date");
base.ConfigureConventions(builder);
}
For TimeOnly
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
builder.Properties<TimeOnly>()
.HaveConversion<TimeOnlyConverter>()
.HaveColumnType("time");
base.ConfigureConventions(builder);
}
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);
}
💡 If you plan on working with DateOnly and TimeOnly in more than one project, consider creating a class project with the above converters.
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; }
}
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();
}
}
}
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>
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);
}
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);
}
And we can use discards for values not needed.
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
- Entity Framework Core working with dates
- Working with DateTime
- TimeOnly
- EF Core Value Conversions
-
Microsoft.Data.SqlClient 5.1.0 released 19 January 2023
- Added support for DateOnly and TimeOnly for SqlParameter value and GetFieldValue. #1813