In this article, we are going to explore DateOnly and TimeOnly in C#, see what functionality exists for both, and help decide when to use one or the other.

When it comes to dates and times in C#, for the longest time we have been stuck with the DateTime struct, to serve both purposes. As with most software development decisions, when two requirements are solved by a single solution, often trade-offs are made. Usually, when it comes to DateTime, it meant that even though we set up a full DateTime instance, often we are only concerned with the date component or the time component, but rarely both.

For example:

var dateOfBirth = new DateTime(1984, 6, 13);
database.Save(dateOfBirth);

In this case, we probably aren’t interested in the time component of the date of birth, but we are left with no choice. We also probably have the field set to datetime in our database, unnecessarily persisting the time component (and having to read it back, when we select the record).

This means we’re dealing with unnecessary complexity and often storage. SQL Server for example already has separate date and time types, that we couldn’t make use of.

Well, this problem has now been solved. .NET 6/C# 10 has introduced two new structs to help with this problem, being DateOnly and TimeOnly

To download the source code for this article, you can visit our GitHub repository.

Let’s start with DateOnly.

DateOnly in C#

As the name suggests, we can use the new DateOnly struct when we want to represent only the date component. A good example might be some kind of historical recording, where we aren’t interested in the time of the event, but the fact is happened such as date of birth.

Let’s see how we can declare a new DateOnly instance:

var dateOnly = new DateOnly(2022, 1, 1);

As we expect, the parameters represent the year, month, and day for the date.

Let’s also create a normal DateTime instance with the same arguments, for comparison:

var dateTime = new DateTime(2022, 1, 1);

If we print them both out, we are going to see different results:

1/01/2022
1/01/2022 12:00:00 AM

The first example for DateOnly stores and prints exactly what we gave instructions for. However for DateTime, we see the time component of 12:00:00 AM being stored and presented, even though we didn’t instruct it to do so. Often we forget about this time component entirely, even storing it in the database and never questioning why there are so many records at 12:00 am. 

Internally, DateOnly stores its value as an integer, where 0 is the 1st of January 0001. 

Using the TryParse Method with DateOnly

With the DateTime struct, we had the ability to parse a DateTime from a string. This same functionality exists for DateOnly:

if (DateOnly.TryParse("2022/01/01", out DateOnly result))
{
    Console.WriteLine($"Parsed DateOnly: {result}");
}

As with DateTime, this works across various date formats, e.g American (MM/DD/YYYY), European (DD/MM/YYYY), and Universal (YYYY-MM-DD).

AddDays, AddMonths, AddYears Methods with DateOnly

Just like with DateTime, the Add<component> methods have been brought across to the DateOnly struct:

var addDays = dateOnly.AddDays(1);
var addMonths = dateOnly.AddMonths(1);
var addYears = dateOnly.AddYears(1);

Of course, AddHours , AddMinutes and AddSeconds aren’t available, as there is no time component. We’ll look at these in the next section when we discuss the TimeOnly struct.

TimeOnly in C#

When we are only interested in the time component, we can use the new TimeOnly struct. A good example here might be a repeating alarm event at 11 AM every day. The date itself is irrelevant, as it occurs every day.

Creating a new TimeOnly instance is very simple:

var elevenAM = new TimeOnly(11, 0);

The output is as we expect:

11:00 AM

There are a few different overloads for TimeOnly depending on the precision required. The hour component of TimeOnly is according to a 24-hour clock, so if we want 11 PM we would use the value 23 for the hour parameter.

Internally, TimeOnly stores its value as long, being the ticks since midnight.

AddHours and AddMinutes Methods with TimeOnly

We already know how the AddDays(), AddMonths() and AddYears() methods work with DateOnly. Similarily with TimeOnly, we have AddHours() and AddMinutes():

var addHours = oneAM.AddHours(1);
var addMinutes = oneAM.AddMinutes(5);

There is no AddSeconds() method, but we can use the generic Add()method, which takes a TimeSpan giving us full flexibility:

var addSeconds = oneAM.Add(TimeSpan.FromSeconds(1));

IsBetween Method with TimeOnly

A useful method that comes with the TimeOnly struct is the IsBetween method. As the name suggests, it helps us understand if a TimeOnly instance is between two other TimeOnly instances.

Let’s put it into action by modifying our previous code:

var sevenAM = new TimeOnly(7, 0);
var elevenAM = new TimeOnly(11, 0);
var onePM = new TimeOnly(13, 0);

Console.WriteLine(elevenAM.IsBetween(sevenAM, onePM)); //Returns True

It’s worth noting that because the hour component is on a 24-hour clock, IsBetween works across midnight, for example, the following code also prints True:

var elevenPM = new TimeOnly(23, 0);
var oneAM = new TimeOnly(1, 0);
var twoAM = new TimeOnly(2, 0);
Console.WriteLine(oneAM.IsBetween(elevenPM, twoAM));

IsBetween is a handy method to quickly understand if a time falls in a range.

Manipulations and Operations

In this section, we are going to explore some of the manipulations and operations we can do on the new DateOnly and TimeOnly structs.

Basic Operators

We can easily use comparison operators like < and > to compare two instances of DateOnly or TimeOnly:

var firstOfJan = new DateOnly(2022, 1, 1);
var secondOfJan = new DateOnly(2022, 1, 2);

if (secondOfJan > firstOfJan)
{
    Console.WriteLine($"{secondOfJan} is after {firstOfJan}");
}

var oneAm = new TimeOnly(1, 0);
var twoAm = new TimeOnly(2, 0);
if (oneAm < twoAm)
{
    Console.WriteLine($"{oneAm} is before {twoAm}");
}

FromDateTime

Because these structs are brand new in the framework, we’re bound to come across legacy DateTime instances in our app, or we might still have use cases for it (for example, “timestamps” in our database). We can easily convert to DateOnly and TimeOnly from these instances, with the FromDateTime method.

First, let’s set up a DateTime for the 1st of January 2022, 11:30 AM:

var dateTime = new DateTime(2022, 1, 1, 11, 30, 0);

We can then use the static FromDateTime method on the DateOnly and TimeOnly struct to create instances:

var dateOnly = DateOnly.FromDateTime(dateTime);
var timeOnly = TimeOnly.FromDateTime(dateTime);

If we print these out, we see:

1/01/2022
11:30 AM

This could be handy if we want to make use of the newer date/time components, but don’t want to change the type across our entire codebase. This gives us flexibility without doing a potentially breaking change.

Database Support

We mentioned earlier that many databases already support these new types (and most already did, before they were introduced in .NET), so it’s worth spending a moment on exploring that support.

For this article, let’s focus on SQL Server.

Let’s create a simple table to hold the types:

CREATE TABLE [dbo].[DateOnlyAndTimeOnlyTesting](
    [DateOnly] [date] NULL,
    [TimeOnly] [time] NULL
) ON [PRIMARY]
GO

INSERT INTO [DateOnlyAndTimeOnlyTesting]([DateOnly]) VALUES ('2022-01-01')
INSERT INTO [DateOnlyAndTimeOnlyTesting]([TimeOnly]) VALUES ('11:00')

SELECT * FROM [DateOnlyAndTimeOnlyTesting]

To store DateOnly in SQL, we can use the date type. To store TimeOnly in SQL, we can use the time type.

If we look at the query output, we see the records are saved exactly as we’d expect. Only the date or time is persisted, but not both.

It’s worth noting here that at the time of writing, popular ORM’s, like EF Core and Dapper, do not support mapping from the .NET types to the SQL Server types automatically, and custom solutions are required to map to the type.

When to use DateOnly and TimeOnly

The first obvious point here is around data storage. If we are only interested in either date or time, and not both, why store both? “Store” in this case could be:

  • Serialization (if we were building an API)
  • Persistence (writing to a file or database)

The other point worth calling out is being explicit about design decisions. In the past, if we had any problem that involved either date or time, we would automatically throw a DateTime instance at the problem and move on with our life. The new types mean we can spend a moment thinking about which one to use. The answer still might be DateTime, but we are now given more choices. We are being explicit about our choice, which helps other developers maintain the code base and understand our code better.

Conclusion

In this article, we’ve learned about the new DateOnly and TimeOnly types introduced in .NET 6. We now have better options to deal with dates and times in .NET, offering us more flexibility in our decisions.