This post is part of a series on .NET 6 and C# 10 features. Use the following links to navigate to other articles in the series and build up your .NET 6/C# 10 knowledge! While the articles are seperated into .NET 6 and C# 10 changes, these days the lines are very blurred so don’t read too much into it.
.NET 6
Minimal API Framework
DateOnly and TimeOnly Types
LINQ OrDefault Enhancements
Implicit Using Statements
IEnumerable Chunk
SOCKS Proxy Support
Priority Queue
MaxBy/MinBy
C# 10
Global Using Statements
File Scoped Namespaces
For a long time now (Actually, almost since forever), we have been stuck with the DateTime type in .NET. By “stuck”, it hasn’t been like it’s always been a problem, and you can go months, if not years of using DateTime without any issue. But every now and again, those gremlins creep in when trying to use DateTime as either only a Date, or only a Time.
In the case of using only a Date, this is extremely common when storing things such as birthdays, anniversary dates, or really any calendar date that doesn’t have a specific time associated with it. But in .NET, you were forced to use a DateTime object, often with a time portion of midnight. While this worked, often the time portion would get in the way when comparing dates or wanting to manipulate a date without really worrying about the time. It was especially weird when working with a SQL database that has the concept of a “date” type (with no time), but when loading the data into C#, you had to deal with a DateTime.
If you only wanted to store a time, then you were really in trouble. Imagine wanting to set an alarm for a recurring time, every day. What type would you use? Your best bet was of course to use the “TimeSpan” type, but this was actually designed to store elapsed time, not a particular clock time. As an example, a TimeSpan can have a value of 25 hours because it’s a measure of time, not the actual time of day. Another problem was time wrapping. If something started at 11PM, and went for 2 hours. How would you know to “wrap” the end time around to be 1AM? This often meant using some sort of rinky dink DateTime object that now ignored the date portion, ugh.
And that’s why .NET 6 has introduced DateOnly and TimeOnly!
Getting Setup With .NET 6 Preview
At the time of writing, .NET 6 is in preview, and is not currently available in general release. That doesn’t mean it’s hard to set up, it just means that generally you’re not going to have it already installed on your machine if you haven’t already been playing with some of the latest fandangle features.
To get set up using .NET 6, you can go and read out guide here : https://dotnetcoretutorials.com/2021/03/13/getting-setup-with-net-6-preview/
Remember, this feature is *only* available in .NET 6. Not .NET 5, not .NET Core 3.1, or any other version you can think of. 6.
Using DateOnly
Using DateOnly is actually pretty easy. I mean.. Check the following code out :
DateOnly date = DateOnly.MinValue; Console.WriteLine(date); //Outputs 01/01/0001 (With no Time)
An important distinction to make is that a DateOnly object never has a Timezone component. After all, if your birthday is on the 10th of May, it’s the 10th of May no matter where in the world you are. While this might seem insignificant, there are things like DateTimeKind and DateTimeOffset dedicated to solving timezone problems, of which, when we are using DateOnly we are uninterested in.
The thing to note here is that even if you didn’t intend to deal with timezones previously, you might have been conned into it. For example :
DateTime date = DateTime.Now; Console.WriteLine(date.Kind); //Outputs "Local"
The “Kind” of our DateTime object is “Local”. While this may seem insignificant, it could actually become very painful when converting the DateTime, or even serializing it.
All in all, DateOnly gives us a way to give a very precise meaning to a date, without being confused about timezones or time itself.
Using TimeOnly
While DateOnly had fairly simple examples, TimeOnly actually has some nifty features that suddenly make this all the more a needed feature.
But first, a simple example :
TimeOnly time = TimeOnly.MinValue; Console.WriteLine(time); //Outputs 12:00 AM
Nice and simple right! What about if we use our example from above, and we check how time is wrapped. For example :
TimeOnly startTime = TimeOnly.Parse("11:00 PM"); var hoursWorked = 2; var endTime = startTime.AddHours(hoursWorked); Console.WriteLine(endTime); //Outputs 1:00 AM
As we can see, we no longer have to account for “overflows” like we would when using a TimeSpan object.
Another really cool feature is the “IsBetween” method on a TimeOnly object. As an example :
TimeOnly startTime = TimeOnly.Parse("11:00 PM"); var hoursWorked = 2; var endTime = startTime.AddHours(hoursWorked); var isBetween = TimeOnly.Parse("12:00 AM").IsBetween(startTime, endTime); //Returns true.
This is actually very very significant. In a recent project I was working on, we needed to send emails only in a select few hours in the evening. To check whether the current time was between two other datetimes is actually significantly harder than you may think. What would often happen is that we would have to take the time, and assign it to an arbitrary date (For example, 01/01/0001), and then do the comparison. But then the midnight wrap around would always choke us in the end.
In my view DateOnly and TimeOnly are significant additions to .NET, and ones that I think will be used right from release. On top of this, it doesn’t mean DateTime will be a relic of the past, instead, DateTime can be used only when you want to point to a specific date and time past, present or future, and not one or the other.
Can you go into how you would use this in EF Core. I know it is not going to be ready until EF Core 7, but you can write custom converters right now.
https://github.com/dotnet/efcore/issues/24507
https://github.com/dotnet/SqlClient/issues/1009
Thanks!
Yeah I was thinking of doing a post on the custom converter. The only annoying thing about that is when I write about things that are stopgaps until the new thing comes out, once the real fix comes out, I get inundated with emails/comments saying I’m dumb and you can just do it XYZ way :p
Lol I hear that. That makes sense. I just wish the EF Core team would address it in the docs. There isn’t a single place besides those github issues where it’s even mentioned how to use the new types with EF Core.
Is the TimeOnly type going to work with am/pm time formats? Or does it work with the 24 hour format too?
In terms of parsing? Nope also works with 24 hour too. For example :
The timeonly between functionality would have been valuable 15 years ago when I was doing store hours for an IVR and some stores were open from 7:00 am to 2:00 am. Trying to do all they rollovers in C was a pain.
IsBetween Always should take dates into account, because for 11pm is between 10pm and 2 am could return true or false depends on dates.
Also how TimeOnly works with day light saving days, it would be new source of bugs.
But for both of those, it is usecase based. But just to pick on your Daylight Savings example, that’s also not true in the majority of cases. If you start at 9AM for work, when daylight savings hits, you still start at 9AM. DST is irrelevant for that example.
IsBetween doesn’t need the date to make that logic work though. You can just put in a bit of logic checking if EndTime < StartTime and adjust as appropriate e.g. if it were just an int of the hour you would add 24 to the EndTime in the between comparison.
DST and other date time complexities are a design decision. Its often better to just use local time and date for software that doesn't cross time zones. It cuts out a lot of bugs if 09:00 is always 09:00 and it doesn't move when DST changes.