Convert DateTime to user's time zone with Blazor in .NET 8

 
 
  • Gérald Barré

This post is an update of the original post Convert DateTime to user's time zone with server-side Blazor to take advantage of new .NET 8 features. It is inspired by the following pull request in dotnet/aspire

When you display DateTime data to a user, you may want to convert the value to the user's time zone. With server-side Blazor, the code is executed on the server, so DateTime.Now corresponds to the time zone of the server instead of the user's time zone. At the end of this post, you'll be able to use the following component to display the date in the user's time zone:

Razor
<LocalTime DateTime="DateTime.UtcNow" />

Starting with .NET 8, you can use the new time abstraction TimeProvider. By using the standard abstraction, all components can use the right time zone. The idea is to override this class to set the LocalTimeZone property with the data provided by the user's browser.

First, we need to add a JavaScript file to be able to get the time zone from the client. Add a JavaScript file named wwwroot/timezone.js with the following content:

wwwroot/timezone.js (JavaScript)
export function getBrowserTimeZone() {
    const options = Intl.DateTimeFormat().resolvedOptions();
    return options.timeZone;
}

Then, you can create the BrowserTimeProvider class that inherits from TimeProvider and overrides the LocalTimeZone property. This class also has an event LocalTimeZoneChanged which is raised when the time zone changes. This is important because the JS is executed asynchronously, so the timezone may not be correct when the page first renders. A component can subscribe to the event and redraw itself if needed when the local time zone changes.

C#
internal sealed class BrowserTimeProvider : TimeProvider
{
    private TimeZoneInfo? _browserLocalTimeZone;

    // Notify when the local time zone changes
    public event EventHandler? LocalTimeZoneChanged;

    public override TimeZoneInfo LocalTimeZone
        => _browserLocalTimeZone ?? base.LocalTimeZone;

    internal bool IsLocalTimeZoneSet => _browserLocalTimeZone != null;

    // Set the local time zone
    public void SetBrowserTimeZone(string timeZone)
    {
        if (!TimeZoneInfo.TryFindSystemTimeZoneById(timeZone, out var timeZoneInfo))
        {
            timeZoneInfo = null;
        }

        if (timeZoneInfo != LocalTimeZone)
        {
            _browserLocalTimeZone = timeZoneInfo;
            LocalTimeZoneChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}

Then, you must register the service to the DI container to use the BrowserTimeProvider instead of the default TimeProvider. Add a new extension class to register the service:

C#
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddBrowserTimeProvider(this IServiceCollection services)
        => services.AddScoped<TimeProvider, BrowserTimeProvider>();
}

Then, you can edit the Program.cs file to add the service to the DI container:

Program.cs (C#)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBrowserTimeProvider();

Now, you can create a component to execute the JavaScript function and set the time zone:

C#
public sealed class InitializeTimeZone : ComponentBase
{
    [Inject] public TimeProvider TimeProvider { get; set; } = default!;
    [Inject] public IJSRuntime JSRuntime { get; set; } = default!;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && TimeProvider is BrowserTimeProvider browserTimeProvider && !browserTimeProvider.IsLocalTimeZoneSet)
        {
            try
            {
                await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./timezone.js");
                var timeZone = await module.InvokeAsync<string>("getBrowserTimeZone");
                browserTimeProvider.SetBrowserTimeZone(timeZone);
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}

You can add this component to the Home.razor file or any component that is rendered when the page is loaded. Note that you may need to set the render mode to InteractiveServer to execute the JavaScript in the OnAfterRenderAsync method. Otherwise, the JavaScript may not be executed.

Components/Pages/Home.razor (Razor)
@page "/"
@rendermode InteractiveServer
<InitializeTimeZone />

Finally, you can create an extension method to convert the DateTime to the user's time zone and a component to easily display the date in the user's time zone:

C#
public static class TimeProviderExtensions
{
    public static DateTime ToLocalDateTime(this TimeProvider timeProvider, DateTime dateTime)
    {
        return dateTime.Kind switch
        {
            DateTimeKind.Unspecified => throw new InvalidOperationException("Unable to convert unspecified DateTime to local time"),
            DateTimeKind.Local => dateTime,
            _ => DateTime.SpecifyKind(TimeZoneInfo.ConvertTimeFromUtc(dateTime, timeProvider.LocalTimeZone), DateTimeKind.Local),
        };
    }

    public static DateTime ToLocalDateTime(this TimeProvider timeProvider, DateTimeOffset dateTime)
    {
        var local = TimeZoneInfo.ConvertTimeFromUtc(dateTime.UtcDateTime, timeProvider.LocalTimeZone);
        local = DateTime.SpecifyKind(local, DateTimeKind.Local);
        return local;
    }
}

public sealed class LocalTime : ComponentBase, IDisposable
{
    [Inject]
    public TimeProvider TimeProvider { get; set; } = default!;

    [Parameter]
    public DateTime? DateTime { get; set; }

    protected override void OnInitialized()
    {
        if (TimeProvider is BrowserTimeProvider browserTimeProvider)
        {
            browserTimeProvider.LocalTimeZoneChanged += LocalTimeZoneChanged;
        }
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (DateTime != null)
        {
            builder.AddContent(0, TimeProvider.ToLocalDateTime(DateTime.Value));
        }
    }

    public void Dispose()
    {
        if (TimeProvider is BrowserTimeProvider browserTimeProvider)
        {
            browserTimeProvider.LocalTimeZoneChanged -= LocalTimeZoneChanged;
        }
    }

    private void LocalTimeZoneChanged(object? sender, EventArgs e)
    {
        _ = InvokeAsync(StateHasChanged);
    }
}

You can now use the new component in your code:

Home.razor (Razor)
<LocalTime DateTime="DateTime.UtcNow" />

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub