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