Customize Application Time Zone with TimeProvider

June 17, 2024#Software Development
Article
Author image.

Scott DePouw, Senior Consultant

.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 existing appsettings.json file and AppSettings record
  • Update our DI registration (Autofac in this example) to use CustomTimeProvider instead of TimeProvider.System

We didn’t have to touch any of the application’s code because we already were using TimeProvider and properly injecting it. Awesome!


Copyright © 2024 NimblePros - All Rights Reserved