.NET’s TimeProvider
abstraction finally absolves us of the need to wrap DateTime
and friends with a custom interface. I’ve used it a lot for my date/time abstraction needs and have even written a blog post on this class. This post will detail how you can control and configure what time zone your application is in while relying on this abstraction.
TimeProvider’s Local Time Zone is Readonly
TimeProvider.LocalTimeZone
defines what time zone the TimeProvider
instance uses for its work. The default instance, which is accessible via TimeProvider.System
, uses the system time zone (wherever the application is running). Most of the time you’ll likely be working with UTC time (as you should), but it’s entirely possible you want to have TimeProvider
’s local time be a different time zone!
Your first instinct may be to try and set LocalTimeZone
to one of your choosing. You’ll run into a compiler error, however, as it’s a readonly item.
Create a Custom Time Provider to Set Local Time Zone
If you want to control the local time zone of your TimeProvider
abstraction, define a custom provider. We’ll keep whatever our time zone is in our application’s configuration, injecting it via the IOptions<TSettings>
abstraction.
using Ardalis.GuardClauses;
using Microsoft.Extensions.Options;
using System;
public class CustomTimeProvider : TimeProvider
{
public CustomTimeProvider(IOptions<AppSettings> settings)
{
string timeZoneId = settings.Value.TimeZoneId;
Guard.Against.NullOrWhiteSpace(timeZoneId);
if (!TimeZoneInfo.TryFindSystemTimeZoneById(timeZoneId, out TimeZoneInfo info)) throw new InvalidTimeZoneIdException(timeZoneId);
LocalTimeZone = info;
}
public override TimeZoneInfo LocalTimeZone { get; }
}
public class InvalidTimeZoneIdException(string timeZoneId) : Exception($"The Time Zone ID \"{timeZoneId}\" isn't valid");
public record AppSettings(string TimeZoneId);
Use a Default Time Zone Instead of Throwing
The above code assumes you’ll always have a time zone configured. You can optionally just use TimeProvider.System
’s LocalTimeZone
if the configuration isn’t available or isn’t valid.
public class CustomTimeProvider : TimeProvider
{
public CustomTimeProvider(IOptions<AppSettings> settings)
{
string timeZoneId = settings.Value.TimeZoneId;
Guard.Against.NullOrWhiteSpace(timeZoneId);
if (TimeZoneInfo.TryFindSystemTimeZoneById(timeZoneId, out TimeZoneInfo info))
{
LocalTimeZone = info;
}
else
{
LocalTimeZone = TimeProvider.System.LocalTimeZone;
}
}
public override TimeZoneInfo LocalTimeZone { get; }
}
Wiring Up Dependency Injection
As you set up DI within your application, instead of using TimeProvider.System
, you can register CustomTimeProvider
as the singleton instance. Here’s a demonstration of wiring it up using Autofac.
// AutoFac wire-up example, similar to using the System TimeProvider
public class ApplicationAutofacModule : Module
{
protected override void Load(ContainerBuilder _p_Builder)
{
// Any constructor with a TimePovider parameter will get the same CustomTimeProvider instance.
_p_Builder.RegisterType<CustomTimeProvider>().As<TimeProvider>().SingleInstance();
}
}
Using TimeProvider with the Customized Time Zone
With CustomTimeProvider
properly initialized and wired up, your application — without having to change any application code — now respects whatever time zone you have configured!
We’ll configure our application by updating appsettings.json
to have Eastern Standard Time. Here’s a list of time zone names that can be used. I’ve had success using abbreviations (like “EST”) as well. Were I to use the full name from the aforementioned resource, I’d use “Eastern Standard Time” instead.
{
"TimeZoneId": "EST"
}
Any existing code will now have a TimeProvider
whose local time zone is exactly that.
// Existing code already using TimeProvider.
public class SomeClass(TimeProvider timeProvider)
{
public void SomeMethod()
{
// Outputs "(UTC-05:00) GMT-05:00" for the Time Zone "EST"
Console.WriteLine(timeProvider.LocalTimeZone);
}
}
Lean Into Abstraction
That’s all there is to it. Since we made our time zone configurable, any deployments can be customized for whatever time zones they need to be for. The power of the TimeProvider
(or any) abstraction is that to implement this additional functionality, we didn’t have to touch the majority of our code. What did we have to do?
- Define a custom class,
CustomTimeProvider
- Add a configuration setting,
TimeZoneId
, to our existingappsettings.json
file andAppSettings
record - Update our DI registration (Autofac in this example) to use
CustomTimeProvider
instead ofTimeProvider.System
We didn’t have to touch any of the application’s code because we already were using TimeProvider
and properly injecting it. Awesome!