Updated on
DateOnly holds a calendar date with no time attached. TimeOnly holds a time of day with no date attached. Both have been in .NET since version 6, and both exist because a birth date and an opening hour are not timestamps and should never have been stored as one.
Converting between them and DateTime is a single static method in each direction, DateOnly.FromDateTime() on the way in and ToDateTime() on the way back, and that, along with getting today’s date, is what most code needs from these types.
For years, C# gave us one type for both jobs, the DateTime struct. Solving two requirements with a single type has a cost, and with DateTime the cost is familiar: we build a full instance even though we usually care about 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.
Since .NET 6 and C# 10, that problem is solved by two structs built for it, DateOnly and TimeOnly.
Let’s start with DateOnly.
What Is DateOnly in C#?
DateOnly is a struct that holds a calendar date and nothing else: a year, a month, and a day, with no time component and no time zone. It exists so a date of birth, an invoice date, or a public holiday can be stored as the thing it actually is.
It has been in the framework since .NET 6, so it is available on every supported version rather than being a new arrival to weigh up.
Creating one takes the year, month, and day, as new DateOnly(2022, 1, 1) does. Printing that under en-US gives 1/1/2022, where the equivalent DateTime prints 1/1/2022 12:00:00 AM, a midnight nobody asked for, which then travels into the database and back out again on every read.
Internally the value is a day count. DayNumber exposes it as the number of days since 1 January 0001, which makes the gap between two dates a subtraction of two integers rather than a TimeSpan.
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 under en-US, we are going to see different results:
1/1/2022 1/1/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.
The unwanted time component is only half of what we are rid of. Microsoft’s DateOnly and TimeOnly documentation names the other half: “DateOnly can’t be offset by a time zone, and it always represents the date that was set.”
Internally, DateOnly stores its value as an integer, where 0 is the 1st of January 0001.
How to Parse a String Into a DateOnly
Parsing a string into a DateTime has always been possible, and the 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).
When the format is known and fixed, ParseExact is the better tool, because it fails on anything that does not match rather than guessing:
var date = DateOnly.ParseExact("2022-01-01", "yyyy-MM-dd");
TryParse reads the current culture, so the same string can parse differently on two machines: 01/02/2022 is the 2nd of January under en-US and the 1st of February under en-GB, with no error either time. ParseExact with an explicit format does not guess, and the specifiers it takes are the same ones we use when formatting a DateTime with a format string.
AddDays, AddMonths, and AddYears 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.
What Is TimeOnly in C#?
TimeOnly is the other half: a time of day, running from 00:00:00 to 23:59:59.9999999, with no date attached and no time zone. It fits an opening hour, an alarm, or a shift start, anything that recurs every day and belongs to none of them in particular.
The hour runs on a 24-hour clock, so eleven at night is new TimeOnly(23, 0). Further overloads take a second, a millisecond, and a microsecond as precision demands, and one takes raw ticks.
Internally the value is a tick count since midnight, which is why Ticks, ToTimeSpan(), and FromTimeSpan() line up so neatly with TimeSpan.
The wrap-around behaviour is the part worth knowing before using it. A TimeOnly is a point on a circular clock rather than a position on a line, so adding two hours to 23:00 gives 01:00 rather than an error, and subtracting one time from another never gives a negative result.
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, which is why it converts so cleanly to and from TimeSpan and what it represents.
AddHours and AddMinutes With TimeOnly
We already know how the AddDays(), AddMonths() and AddYears() methods work with DateOnly. Similarly with TimeOnly, we have AddHours() and AddMinutes():
var oneAM = new TimeOnly(1, 0); var addHours = oneAM.AddHours(1); var addMinutes = oneAM.AddMinutes(5);
AddHours() wraps past midnight without complaining: two hours after 23:00 is 01:00, not an error. A second overload, AddHours(double, out int), returns that same 01:00 and reports how many days the value crossed, so it is the form to reach for whenever a day boundary matters.
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 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.
How Do We Convert Between DateTime, DateOnly, and TimeOnly?
DateOnly.FromDateTime() and TimeOnly.FromDateTime() take a DateTime and keep one half of it. That is the direction most code needs, because the DateTime usually arrives from a column, an API, or a library we do not control.
Going back is dateOnly.ToDateTime(), which takes the TimeOnly to pair with the date. For midnight, pass TimeOnly.MinValue; a second overload also takes a DateTimeKind, so the result is not left Unspecified for whatever reads it next.
Neither type has a Now or a Today property. Today’s date is DateOnly.FromDateTime(DateTime.Now), and the current time of day is TimeOnly.FromDateTime(DateTime.Now). That keeps the choice of clock in the caller’s hands, which matters, because whether the answer should come from the local clock or the UTC one is a decision neither struct can make.
There is no TimeOnly.ToDateTime() to match, and the asymmetry is the point: a time of day on its own does not name a day, so the join has to start from the date.
Splitting a DateTime takes one call per half. Rebuilding one takes a single call that needs both.

Whether today’s date should come from the local clock or the UTC one is its own decision, and we work through which of DateTime.Now and DateTime.UtcNow to use separately.
Converting a DateTime to a DateOnly or TimeOnly
We are bound to come across legacy DateTime instances in our app, or we might still have use cases for them (for example, “timestamps” in our database). We can easily convert to DateOnly and TimeOnly from these instances, with the FromDateTime method, which is also one of the other ways to strip the time off a DateTime.
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 under en-US, we see:
1/1/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.
Converting a DateOnly Back to a DateTime
The reverse trip needs a time to pair with the date, which is why it is an instance method on DateOnly rather than a static one:
var dateOnly = new DateOnly(2022, 1, 1); var timeOnly = new TimeOnly(11, 30); var combined = dateOnly.ToDateTime(timeOnly); var midnight = dateOnly.ToDateTime(TimeOnly.MinValue); var utc = dateOnly.ToDateTime(timeOnly, DateTimeKind.Utc);
The third call is the one to prefer whenever the result leaves the method, because the two-argument overload is the only way to say which clock the date came from. The first two both produce a DateTime whose Kind is Unspecified.
How Do We Get Today’s Date as a DateOnly?
DateOnly has no Today property and TimeOnly has no Now. The current date is:
var today = DateOnly.FromDateTime(DateTime.Now); var timeOfDay = TimeOnly.FromDateTime(DateTime.Now);
Use DateTime.UtcNow instead of DateTime.Now wherever the value is going to be stored or compared rather than shown.
Here is the whole conversion surface in one place, in both directions:
| I have | I want | The call |
|---|---|---|
DateTime | DateOnly | DateOnly.FromDateTime(dateTime) |
DateTime | TimeOnly | TimeOnly.FromDateTime(dateTime) |
DateOnly and a TimeOnly | DateTime | dateOnly.ToDateTime(timeOnly) |
DateOnly | DateTime at midnight | dateOnly.ToDateTime(TimeOnly.MinValue) |
DateOnly | DateTime with a known Kind | dateOnly.ToDateTime(timeOnly, DateTimeKind.Utc) |
| nothing | today's date | DateOnly.FromDateTime(DateTime.Now) |
| nothing | the current time of day | TimeOnly.FromDateTime(DateTime.Now) |
TimeSpan | TimeOnly | TimeOnly.FromTimeSpan(timeSpan) |
TimeOnly | TimeSpan since midnight | timeOnly.ToTimeSpan() |
a string | DateOnly | DateOnly.TryParse(text, out var date) |
a string in a fixed format | DateOnly | DateOnly.ParseExact(text, "yyyy-MM-dd") |
DateOnly | days since 1 January 0001 | dateOnly.DayNumber |
| a day count | DateOnly | DateOnly.FromDayNumber(days) |
How Do We Compare DateOnly and TimeOnly Values?
In this section, we are going to explore some of the manipulations and operations we can do on the DateOnly and TimeOnly structs.
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}");
}
Comparison works on both types, but the rest of the surface is not symmetrical, and the differences are where the surprises live:
DateOnly | TimeOnly |
|
|---|---|---|
| Add units | AddDays, AddMonths, AddYears | AddHours, AddMinutes, Add(TimeSpan) |
| Not available | AddHours, AddMinutes, AddSeconds | AddSeconds, AddDays |
| Current value | no Now or Today, use FromDateTime | no Now, use FromDateTime |
| Subtracting two values | no - operator; subtract DayNumber | - returns the elapsed TimeSpan, always positive |
| Range check | compare with < and > | IsBetween(start, end), and it wraps past midnight |
| Bounds | MinValue 0001-01-01, MaxValue 9999-12-31 | MinValue 00:00:00, MaxValue 23:59:59.9999999 |
| Parsing | Parse, TryParse, ParseExact, TryParseExact | the same four |
| Formatting | ToString(format), ToShortDateString(), ToLongDateString() | ToString(format), ToShortTimeString(), ToLongTimeString() |
| Underlying value | DayNumber, days since 0001-01-01 | Ticks, ticks since midnight |
How Do DateOnly and TimeOnly Map to SQL and JSON?
We mentioned earlier that many databases already support these 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.
SQL Server has had matching column types all along: date for a DateOnly and time for a TimeOnly. A datetime column full of midnight timestamps is what happens when neither side of the boundary has a narrower type to offer.
The ORM story has moved on since this article first ran. EF Core 8 added DateOnly and TimeOnly mapping for SQL Server, so a DateOnly property lands in a date column with no value converter written by hand; the SQLite, MySQL and PostgreSQL providers have supported them since .NET 6, and earlier EF Core versions on SQL Server did need a converter.
System.Text.Json has serialized both natively since .NET 7, writing 2022-01-01 and 11:30:00 rather than a full timestamp with a midnight glued to it. Newtonsoft.Json 13.0.4 produces the same two strings.
The rule that survives every provider: choose the narrower column type and the narrower .NET type together. Doing one without the other is how the midnight timestamps got there in the first place.
Mapping these types through EF Core, value converters, custom comparers, and what it costs, has its own walkthrough in How to Map DateOnly and TimeOnly Types to SQL.
When Should We 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 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.
Tested with .NET 10.0.10.

Thank you. Nice article. What about using TimeOnly in Entity Framework code first? Does it map correctly to Time type of Sql Server?
Hi Eris, thanks for reading and commenting! Unfortunately EF still doesn’t support these types OOTB, as explained in this GH issue for SqlClient (which EF uses under the hood).
There are a few workarounds possible, such as using a custom converter. So you’ll need to use one of those, or wait until EF adds support.
Hope that helps.