Skip to content

fix: TimeZoneNotFoundException on old windows#20

Merged
arika0093 merged 8 commits intomainfrom
fix/issue_19
Sep 6, 2025
Merged

fix: TimeZoneNotFoundException on old windows#20
arika0093 merged 8 commits intomainfrom
fix/issue_19

Conversation

@arika0093
Copy link
Owner

@arika0093 arika0093 commented Sep 6, 2025

This pull request introduces support for environments where ICU (International Components for Unicode) is unavailable, such as Windows Server 2016, by allowing conversion from IANA to Windows time zones through a configurable converter. It also refactors service registration to use a new configuration class, improves dependency injection, and adds targeted tests for NLS mode. The changes enhance flexibility and reliability of time zone detection and conversion in Blazor applications.

close #19

Summary by CodeRabbit

  • New Features

    • Configurable options: selectable TimeProvider and optional IANA→Windows converter; new service registration overload to configure options.
    • Improved browser time-zone resolution with ICU/NLS detection and clearer guidance when conversion is required.
  • Tests

    • Added NLS-focused tests and updated test utilities to exercise converter and platform behaviors.
  • Chores

    • Updated solution/project layout and CI/test matrix; expanded test build configuration and internal visibility for testing.

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

Walkthrough

Adds a configuration object and DI overloads, makes BlazorLocalTimeProvider ICU-aware and optionally converts IANA→Windows IDs via a pluggable converter, internalizes/refactors local-time service/interface, adds NLS-specific tests and test project, updates solution structure and CI matrix, and adjusts test mocks and DI helpers.

Changes

Cohort / File(s) Summary
Solution structure
BlazorLocalTime.sln
Adds VS17 metadata, new project entries (src/sample/tests), nested project mappings, SolutionGuid, HideSolutionNode, and updated ProjectConfigurationPlatforms.
Configuration & DI
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs, src/BlazorLocalTime/BlazorLocalTimeExtension.cs
Adds public BlazorLocalTimeConfiguration (TimeProvider, Func<string,string>? IanaToWindows) and a new IServiceCollection overload accepting Action; existing overloads delegate to it.
Provider ICU/NLS handling
src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs
Detects ICU at runtime; when ICU is not enabled, uses Configuration.IanaToWindows to convert IANA IDs to Windows IDs (throws TimeZoneNotFoundException with guidance if converter missing) before resolving TimeZoneInfo and updating service.
Service & interface refactor
src/BlazorLocalTime/ILocalTimeService.cs, src/BlazorLocalTime/LocalTimeService.cs
Introduces new ILocalTimeService shape with default interface method implementations and internal state indicators; adds internal LocalTimeService that accepts BlazorLocalTimeConfiguration, raises LocalTimeZoneChanged, and uses configuration.TimeProvider for Now.
Internals visibility
src/Directory.Build.props
Adds InternalsVisibleTo for BlazorLocalTimeTest.Nls.
NLS test project & imports
tests/BlazorLocalTimeTest.Nls/*
Adds BlazorLocalTimeTest.Nls project targeting net8/net9, enables NLS (UseNls=true), references TimeZoneConverter, and adds tests/_Imports asserting behavior with/without converter.
Test updates & mocks
tests/BlazorLocalTimeTest/*
Refactors tests and mocks to use BlazorLocalTimeConfiguration-based mock services (LocalTimeMockService/LocalTimeEmptyMockService), new AddLocalTimeMockService overloads, makes TestInitializer.JavaScriptInitializer public, and updates DI wiring.
Approvals & snapshots
tests/BlazorLocalTimeTest/Approvals/*
Updates public API approval snapshots to include BlazorLocalTimeConfiguration and new InternalsVisibleTo entry.
Tests build props & CI
tests/Directory.Build.props, .github/workflows/test.yaml
Adds centralized test package/using entries and switches CI tests to an OS matrix (ubuntu/macos/windows).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Browser as Browser JS
    participant Provider as BlazorLocalTimeProvider
    participant Config as BlazorLocalTimeConfiguration
    participant TZ as TimeZoneInfo API
    participant Service as LocalTimeService

    Browser->>Provider: getBrowserTimeZone() -> IANA id
    Provider->>Provider: IsIcuEnabled()?
    alt ICU enabled
        Provider->>TZ: FindSystemTimeZoneById(IANA)
    else ICU not enabled
        alt IanaToWindows provided
            Provider->>Config: IanaToWindows(IANA)
            Config-->>Provider: WindowsId
            Provider->>TZ: FindSystemTimeZoneById(WindowsId)
        else No converter
            Provider-->>Provider: throw TimeZoneNotFoundException (guidance)
        end
    end
    TZ-->>Provider: TimeZoneInfo
    Provider->>Service: SetBrowserTimeZoneInfo(TimeZoneInfo)
    Service-->>Service: Raise LocalTimeZoneChanged (if effective TZ changed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Handle IANA IDs on Windows/NLS by converting to Windows IDs using a pluggable mechanism (#19)
Prevent TimeZoneNotFoundException during provider initialization on Windows Server 2016/IIS when browser returns IANA ID (#19)
Provide guidance/path to use TimeZoneConverter for mapping (#19)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Broad refactor of ILocalTimeService to include default interface implementations and internal state (src/BlazorLocalTime/ILocalTimeService.cs) Large API/design change beyond the specific IANA→Windows conversion objective.
DI/API registration refactor to configuration object overloads (src/BlazorLocalTime/BlazorLocalTimeExtension.cs) Changes registration pattern from simple TimeProvider parameter to options delegate; useful but not strictly required for the linked issue.
Test mock/service restructuring and new AddLocalTimeMockService overloads (tests/BlazorLocalTimeTest/LocalTimeMockService.cs and multiple tests) Extensive test harness refactor unrelated to runtime conversion logic; alters test architecture.

Possibly related PRs

"I nibbled on zones from Reykjavik to Rio,
Found IANA whispers Windows didn’t know.
With a twitch and a mapper in tow,
I hopped past NLS where cold winds blow.
Now clocks align—tick-tock, let’s go! 🐇"


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 48d84f8 and 31d28db.

📒 Files selected for processing (1)
  • .github/workflows/test.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/test.yaml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (windows-latest)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/issue_19

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/BlazorLocalTime/LocalTimeService.cs (1)

16-16: Make BrowserTimeZoneInfo setter private and update tests
The setter is currently internal but tests in tests/BlazorLocalTimeTest/LocalTimeMockService.cs (lines 16, 25) assign it directly. Change the setter to private and modify those tests to call SetBrowserTimeZoneInfo instead of assigning the property directly.

🧹 Nitpick comments (28)
tests/Directory.Build.props (1)

18-21: Add a global using for Bunit (tests convenience)

Most test files will reference Bunit types; add a global using to reduce per-file imports.

   <ItemGroup>
     <Using Include="Xunit" />
     <Using Include="Shouldly" />
+    <Using Include="Bunit" />
   </ItemGroup>
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (2)

6-6: Consider sealing the configuration type

Sealing prevents unintended inheritance and makes the intent (a simple options bag) explicit.

-public class BlazorLocalTimeConfiguration
+public sealed class BlazorLocalTimeConfiguration

8-19: Tighten XML docs: fix “Gets” vs “Gets or sets”, wording, and links

Minor doc polish for accuracy and readability.

-    /// <summary>
-    /// Gets the <see cref="TimeProvider"/> used to supply the current time.
-    /// </summary>
+    /// <summary>
+    /// Gets or sets the <see cref="TimeProvider"/> used as the current time source.
+    /// </summary>
@@
-	/// <summary>
-	/// Specifies a function to convert IANA time zones to Windows time zones.
-	/// For example, `TZConvert.IanaToWindows` from TimeZoneConverter(https://github.com/mattjohnsonpint/TimeZoneConverter) can be used.
-	/// This is required on operating systems where ICU is unavailable(such as Windows Server 2016), but need not be specified on others.
-	/// For details, see https://github.com/arika0093/BlazorLocalTime/issues/19
-	/// </summary>
+	/// <summary>
+	/// Specifies a function to convert IANA time zone IDs to Windows time zone IDs.
+	/// For example, you can pass <c>TZConvert.IanaToWindows</c> from the
+	/// <see href="https://github.com/mattjohnsonpint/TimeZoneConverter">TimeZoneConverter</see> package.
+	/// Required on operating systems where ICU is unavailable (such as Windows Server 2016); optional otherwise.
+	/// See <see href="https://github.com/arika0093/BlazorLocalTime/issues/19" /> for details.
+	/// </summary>
tests/BlazorLocalTimeTest/LocalTimeTest.razor (1)

75-76: Minor naming nit

Rename tms to mock or service for readability in tests.

-        var tms = new LocalTimeEmptyMockService(new() { TimeProvider = TimeProvider.System });
-        Services.AddLocalTimeMockService(tms);
+        var mock = new LocalTimeEmptyMockService(new() { TimeProvider = TimeProvider.System });
+        Services.AddLocalTimeMockService(mock);
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (1)

15-17: Avoid test-to-test project reference; extract shared helpers instead.

Referencing the base test project pulls all its tests as a dependency and couples test runs. Prefer a small shared test-utilities project referenced by both test projects.

-  <ItemGroup>
-    <ProjectReference Include="..\..\src\BlazorLocalTime\BlazorLocalTime.csproj" />
-    <ProjectReference Include="..\BlazorLocalTimeTest\BlazorLocalTimeTest.csproj" />
-  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\BlazorLocalTime\BlazorLocalTime.csproj" />
+    <!-- New shared helpers project: move TestInitializer, mocks, etc. here -->
+    <ProjectReference Include="..\BlazorLocalTimeTest.Common\BlazorLocalTimeTest.Common.csproj" />
+  </ItemGroup>

If you want, I can sketch the Common project layout and file moves.

tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs (1)

19-19: Reduce platform variance by avoiding IANA IDs in FindSystemTimeZoneById.

On non-ICU Windows, “America/New_York” isn’t resolvable. Since this test doesn’t exercise ICU/NLS behavior, prefer a Windows-friendly ID (e.g., “Tokyo Standard Time”) or use the already-resolved Asia/Tokyo from elsewhere for consistency.

-        var timeZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
+        // Prefer a cross-platform-friendly ID to avoid ICU/NLS variance in this test
+        var timeZone = TimeZoneInfo.FindSystemTimeZoneById(
+            OperatingSystem.IsWindows() ? "Tokyo Standard Time" : "Asia/Tokyo");
tests/BlazorLocalTimeTest/LocalTimeFormTest.razor (1)

152-153: Nit: fix “Blazil” → “Brazil”.

Tiny spelling tweak in comments.

-        // Change timezone to Blazil (UTC-3)
+        // Change timezone to Brazil (UTC-3)

Also applies to: 178-179

tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor (2)

31-35: Nit: wording.

Minor comment fix.

-        // can convertable
+        // convertible

1-35: Optional: add a Windows-NLS job to CI for this test.

To actually exercise the failure path, run tests on windows-latest with DOTNET_SYSTEM_GLOBALIZATION_USENLS=1; keep Linux/macOS to cover ICU.

src/BlazorLocalTime/BlazorLocalTimeExtension.cs (1)

55-61: Prefer TryAddSingleton and guard null configuration.

Prevents accidental duplicate registrations and null delegates.

-        services.AddSingleton<BlazorLocalTimeConfiguration>(_ => {
-            var config = new BlazorLocalTimeConfiguration();
-            configuration(config);
-            return config;
-		});
+        if (configuration is null) throw new ArgumentNullException(nameof(configuration));
+        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ =>
+        {
+            var cfg = new BlazorLocalTimeConfiguration();
+            configuration(cfg);
+            return cfg;
+        });
src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (2)

41-53: Improve message and resilience when converter is missing.

  • Make the exception actionable (include the offending ID and setup hint).
  • Minor grammar tweak in the comment.
-                if(!ICUMode()) {
-					// On Windows with NLS mode, IANA time zone are must be converted to Windows time zone.
+                if (!ICUMode()) {
+					// On Windows with NLS mode, IANA time zones must be converted to Windows time zones.
 					var converter = Configuration.IanaToWindows;
-                    if(converter == null) {
-                        var message = """
-                        In older Windows environments, IANA time zones (such as “Asia/Tokyo”) cannot be used directly.
-                        For details, see https://github.com/arika0093/BlazorLocalTime/issues/19.
-                        """;
-                        throw new TimeZoneNotFoundException(message);
+                    if (converter == null) {
+                        var message =
+                            $"IANA time zone '{timeZoneString}' cannot be used on this platform without a converter. " +
+                            "Set BlazorLocalTimeConfiguration.IanaToWindows (e.g., TZConvert.IanaToWindows). " +
+                            "See https://github.com/arika0093/BlazorLocalTime/issues/19.";
+                        throw new TimeZoneNotFoundException(message);
 					}
-                    timeZoneString = converter(timeZoneString);
+                    timeZoneString = converter(timeZoneString);
 				}
 				timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneString);

Alternative (more robust across platforms): try resolving the IANA ID first, and only fall back to conversion on TimeZoneNotFoundException. This avoids relying on ICU detection.

-                var timeZoneString = await module.InvokeAsync<string>("getBrowserTimeZone");
-                if(!ICUMode()) {
-                    ...
-                }
-                timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneString);
+                var timeZoneString = await module.InvokeAsync<string>("getBrowserTimeZone");
+                try
+                {
+                    timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneString);
+                }
+                catch (TimeZoneNotFoundException)
+                {
+                    var converter = Configuration.IanaToWindows;
+                    if (converter is null)
+                    {
+                        throw new TimeZoneNotFoundException(
+                            $"IANA time zone '{timeZoneString}' cannot be used on this platform without a converter. " +
+                            "Set BlazorLocalTimeConfiguration.IanaToWindows (e.g., TZConvert.IanaToWindows). " +
+                            "See https://github.com/arika0093/BlazorLocalTime/issues/19.");
+                    }
+                    var windowsId = converter(timeZoneString);
+                    timeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsId);
+                }

79-86: Nit: cache ICU detection result.

ICU mode won’t change at runtime; consider caching to a static readonly bool.

-    private static bool ICUMode()
+    private static readonly bool s_isIcu = ComputeIcuMode();
+    private static bool ICUMode() => s_isIcu;
+    private static bool ComputeIcuMode()
tests/BlazorLocalTimeTest/LocalTimeTotalTest.razor (4)

9-10: Drop redundant TimeProvider registration

The options-based AddBlazorLocalTimeService already conveys the TimeProvider; the extra singleton can cause ambiguity and is unused. Remove it.

-        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);
-        Services.AddSingleton<TimeProvider>(timeProvider);
+        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);

22-22: Unnecessary DI entry for TimeProvider

This singleton isn’t required with the new configuration-based registration.

-        Services.AddSingleton<TimeProvider>(TimeProvider.System);

36-37: Same here: remove the extra TimeProvider singleton

Keep only the AddBlazorLocalTimeService configuration.

-        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);
-        Services.AddSingleton<TimeProvider>(timeProvider);
+        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);

49-50: Remove redundant singleton registration

Avoid duplicate TimeProvider entries.

-        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);
-        Services.AddSingleton<TimeProvider>(timeProvider);
+        Services.AddBlazorLocalTimeService(option => option.TimeProvider = timeProvider);
tests/BlazorLocalTimeTest/LocalTimeMockService.cs (4)

9-17: Make Config immutable

Declare Config as readonly to prevent accidental reassignment during tests.

-    internal BlazorLocalTimeConfiguration Config;
+    internal readonly BlazorLocalTimeConfiguration Config;

40-45: Use AddSingleton for instance-backed registrations and bind the exact config

Returning the same instance from a scoped factory is unusual; register it as a singleton. Also prefer AddSingleton over TryAddSingleton for the config to avoid mismatches when another config was added earlier.

-        services.AddScoped<ILocalTimeService>(_ => instance);
-        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ => instance.Config);
+        services.AddSingleton<ILocalTimeService>(instance);
+        services.AddSingleton<BlazorLocalTimeConfiguration>(_ => instance.Config);

47-54: Ensure deterministic config resolution

When passing a MockTimeProvider, prefer AddSingleton to guarantee this config is used even if prior registrations exist.

-        services.AddScoped<ILocalTimeService, LocalTimeMockService>();
-        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ => new() {
+        services.AddScoped<ILocalTimeService, LocalTimeMockService>();
+        services.AddSingleton<BlazorLocalTimeConfiguration>(_ => new() {
             TimeProvider = instance
         });

40-45: Optional: add an overload that accepts Action

For flexibility in tests (e.g., setting IanaToWindows), consider an overload mirroring the production extension to configure the mock service via an action.

I can draft it if helpful.

Also applies to: 47-54

src/BlazorLocalTime/LocalTimeService.cs (4)

1-1: Remove unused using

System.Diagnostics.CodeAnalysis isn’t used in this file.

-using System.Diagnostics.CodeAnalysis;
+

8-8: Guard against null configuration in primary constructor

Defensive null-check avoids hard-to-trace NREs from DI misconfiguration.

-internal class LocalTimeService(BlazorLocalTimeConfiguration configuration) : ILocalTimeService
+internal class LocalTimeService(BlazorLocalTimeConfiguration configuration) : ILocalTimeService
+{
+    public LocalTimeService : this(configuration ?? throw new ArgumentNullException(nameof(configuration))) {}
+}

Alternatively, switch to a traditional ctor and throw on null.


19-38: Avoid raising change event when the effective time zone didn’t change

If override switches to a value equal to the current effective zone, we still fire an event. Short-circuit to reduce redundant renders.

             var previousTimeZone = TimeZoneInfo;
             _overrideTimeZoneInfo = value;
             var currentTimeZone = TimeZoneInfo;

-            // Fire both events for backward compatibility
-            LocalTimeZoneChanged.Invoke(this, new(previousTimeZone, currentTimeZone));
+            // Raise event only when the effective time zone actually changed
+            if (!(previousTimeZone is null ? currentTimeZone is null : previousTimeZone.Equals(currentTimeZone)))
+            {
+                LocalTimeZoneChanged.Invoke(this, new(previousTimeZone, currentTimeZone));
+            }

Also, the comment about “both events” is stale—only one event exists now.


52-64: Avoid event spam when there’s no effective change; handle null==null early

When setting the same browser TZ or when both are null, we still raise. Also if an override is in effect, changing the browser TZ might not change the effective TZ.

 public void SetBrowserTimeZoneInfo(TimeZoneInfo? timeZoneInfo)
 {
-    if (BrowserTimeZoneInfo != null && BrowserTimeZoneInfo.Equals(timeZoneInfo))
+    if ((BrowserTimeZoneInfo is null && timeZoneInfo is null) ||
+        (BrowserTimeZoneInfo is not null && BrowserTimeZoneInfo.Equals(timeZoneInfo)))
     {
         return;
     }
     var previousTimeZone = TimeZoneInfo;
     BrowserTimeZoneInfo = timeZoneInfo;
     var currentTimeZone = TimeZoneInfo;

-    // Fire both events for backward compatibility
-    LocalTimeZoneChanged.Invoke(this, new(previousTimeZone, currentTimeZone));
+    // Raise event only when the effective time zone actually changed
+    if (!(previousTimeZone is null ? currentTimeZone is null : previousTimeZone.Equals(currentTimeZone)))
+    {
+        LocalTimeZoneChanged.Invoke(this, new(previousTimeZone, currentTimeZone));
+    }
 }
src/BlazorLocalTime/ILocalTimeService.cs (4)

48-53: Limit external mutation surface

These members are internal, which is good. Consider documenting that consumers should not call them directly; all updates should flow via the provider.


79-92: Prefer BCL conversions to construct DateTimeOffset

Use TimeZoneInfo.ConvertTime overloads to avoid manual pairing and edge cases around DST/ambiguous times.

-    public DateTimeOffset ToLocalTimeOffset(DateTime utcDateTime)
-    {
-        return new(ToLocalTime(utcDateTime), GetBrowserTimeZone().GetUtcOffset(utcDateTime));
-    }
+    public DateTimeOffset ToLocalTimeOffset(DateTime utcDateTime)
+    {
+        var tz = GetBrowserTimeZone();
+        return TimeZoneInfo.ConvertTime(new DateTimeOffset(utcDateTime, TimeSpan.Zero), tz);
+    }
@@
-    public DateTimeOffset ToLocalTimeOffset(DateTimeOffset dateTimeOffset)
-    {
-        return new(ToLocalTime(dateTimeOffset), GetBrowserTimeZone().GetUtcOffset(dateTimeOffset));
-    }
+    public DateTimeOffset ToLocalTimeOffset(DateTimeOffset dateTimeOffset)
+    {
+        return TimeZoneInfo.ConvertTime(dateTimeOffset, GetBrowserTimeZone());
+    }

95-116: Clarify method purpose (name vs behavior mismatch)

GetBrowserTimeZone returns the effective TZ (override-aware), not strictly the browser TZ. Update docs to prevent misuse; consider a future alias for clarity.

-    /// Gets the browser's time zone information.
+    /// Gets the current effective time zone (override takes precedence over the browser zone).
@@
-        if (!IsTimeZoneInfoAvailable)
+        if (!IsTimeZoneInfoAvailable)
         {
             throw new InvalidOperationException(
                 """
-                Failed to obtain the browser's time zone information.
+                Failed to obtain local time zone information.
                 Possible causes:
                 1) The `<BlazorLocalTimeProvider />` component has not been added.
                     In this case, please add `<BlazorLocalTimeProvider />` to a root component such as `Routes.razor`.
                 2) You are trying to use `ILocalTimeService` in `OnInitialized(Async)`.
-                    In this case, you need to subscribe to the `ILocalTimeService.OnLocalTimeZoneChanged` event
+                    In this case, subscribe to the `ILocalTimeService.LocalTimeZoneChanged` event
                     and perform processing after the time zone information has been set.
                 """
             );
         }
         return TimeZoneInfo;

Optionally, introduce a non-breaking alias:

  • Add TimeZoneInfo GetLocalTimeZone() that delegates to GetBrowserTimeZone(), and update docs to recommend it going forward.

46-47: Naming nit: IsSuccessLoadBrowserTimeZone

Consider IsBrowserTimeZoneLoadedSuccessfully or IsBrowserTimeZoneLoaded for readability. Since it’s internal, this is low-risk.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 91ad1fb and 41f9a4b.

📒 Files selected for processing (23)
  • BlazorLocalTime.sln (2 hunks)
  • src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1 hunks)
  • src/BlazorLocalTime/BlazorLocalTimeExtension.cs (1 hunks)
  • src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (4 hunks)
  • src/BlazorLocalTime/ILocalTimeService.cs (1 hunks)
  • src/BlazorLocalTime/LocalTimeService.cs (2 hunks)
  • src/Directory.Build.props (1 hunks)
  • tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (1 hunks)
  • tests/BlazorLocalTimeTest.Nls/_Imports.razor (1 hunks)
  • tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net8.approved.txt (1 hunks)
  • tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net9.approved.txt (1 hunks)
  • tests/BlazorLocalTimeTest/BlazorLocalTimeProviderTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest/BlazorLocalTimeTest.csproj (0 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeChangeEventTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeFormDefaultTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeFormTest.razor (2 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeMockService.cs (2 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs (2 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeTotalTest.razor (4 hunks)
  • tests/BlazorLocalTimeTest/TestInitializer.cs (1 hunks)
  • tests/Directory.Build.props (1 hunks)
💤 Files with no reviewable changes (1)
  • tests/BlazorLocalTimeTest/BlazorLocalTimeTest.csproj
🧰 Additional context used
🧬 Code graph analysis (18)
tests/BlazorLocalTimeTest.Nls/_Imports.razor (4)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (3)
  • PublicApiCheckTest (8-30)
  • Fact (10-22)
  • c (17-21)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (3)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
  • Fact (18-29)
src/BlazorLocalTime/LocalTimeZoneOverwrite.cs (1)
  • LocalTimeZoneOverwrite (18-125)
src/BlazorLocalTime/Components/LocalTimeText.razor.cs (1)
  • LocalTimeText (8-59)
tests/Directory.Build.props (1)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (3)
  • c (17-21)
  • PublicApiCheckTest (8-30)
  • Fact (10-22)
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor (1)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (6)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • Fact (46-59)
  • Fact (31-44)
  • Fact (18-29)
  • BlazorLocalTimeProviderCodeTest (12-16)
  • Fact (61-69)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (4)
src/BlazorLocalTime/Components/LocalTimeZone.razor.cs (1)
  • LocalTimeZone (8-47)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (12-16)
  • BlazorLocalTimeProviderCodeTest (10-70)
src/BlazorLocalTime/Components/LocalTimeText.razor.cs (1)
  • LocalTimeText (8-59)
src/BlazorLocalTime/Components/LocalTime.razor.cs (1)
  • LocalTime (8-55)
tests/BlazorLocalTimeTest/LocalTimeFormDefaultTest.razor (2)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (1)
  • BlazorLocalTimeProviderCodeTest (12-16)
tests/BlazorLocalTimeTest/MockTimeProvider.cs (1)
  • MockTimeProvider (3-6)
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (3)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (3)
  • PublicApiCheckTest (8-30)
  • c (17-21)
  • Fact (10-22)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
src/BlazorLocalTime/LocalTimeZoneOverwrite.cs (1)
  • LocalTimeZoneOverwrite (18-125)
tests/BlazorLocalTimeTest/TestInitializer.cs (1)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (4)
  • Fact (18-29)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • Fact (31-44)
  • Fact (46-59)
tests/BlazorLocalTimeTest/LocalTimeFormTest.razor (1)
tests/BlazorLocalTimeTest/MockTimeProvider.cs (2)
  • MockTimeProvider (3-6)
  • DateTimeOffset (5-5)
tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs (1)
src/BlazorLocalTime/LocalTimeService.cs (1)
  • LocalTimeService (8-65)
src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (4)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/Components/LocalTimeForm.razor.cs (4)
  • Task (168-195)
  • Task (197-216)
  • Task (218-238)
  • Task (240-260)
src/BlazorLocalTime/ILocalTimeService.cs (1)
  • TimeZoneInfo (98-115)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (1)
  • BlazorLocalTimeProviderCodeTest (10-70)
src/BlazorLocalTime/ILocalTimeService.cs (4)
src/BlazorLocalTime/LocalTimeService.cs (2)
  • TimeZoneChangedEventArgs (75-87)
  • SetBrowserTimeZoneInfo (52-64)
src/BlazorLocalTime/Components/LocalTimeZone.razor.cs (1)
  • LocalTimeZone (8-47)
src/BlazorLocalTime/Components/LocalTime.razor.cs (1)
  • LocalTime (8-55)
src/BlazorLocalTime/Components/LocalTimeText.razor.cs (1)
  • LocalTimeText (8-59)
src/BlazorLocalTime/BlazorLocalTimeExtension.cs (3)
tests/BlazorLocalTimeTest/LocalTimeMockService.cs (3)
  • IServiceCollection (31-38)
  • IServiceCollection (40-45)
  • IServiceCollection (47-54)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/LocalTimeService.cs (1)
  • LocalTimeService (8-65)
tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net8.approved.txt (2)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (2)
  • PublicApiCheckTest (8-30)
  • Fact (10-22)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
tests/BlazorLocalTimeTest/LocalTimeTotalTest.razor (1)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (4)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
  • Fact (61-69)
  • Fact (18-29)
tests/BlazorLocalTimeTest/LocalTimeMockService.cs (4)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/ILocalTimeService.cs (1)
  • TimeZoneInfo (98-115)
src/BlazorLocalTime/BlazorLocalTimeExtension.cs (3)
  • IServiceCollection (18-21)
  • IServiceCollection (29-41)
  • IServiceCollection (49-61)
tests/BlazorLocalTimeTest/MockTimeProvider.cs (1)
  • MockTimeProvider (3-6)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderTest.razor (1)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • Fact (18-29)
tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net9.approved.txt (2)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (1)
  • PublicApiCheckTest (8-30)
src/BlazorLocalTime/LocalTimeService.cs (5)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/Components/LocalTimeZone.razor.cs (1)
  • LocalTimeZone (8-47)
src/BlazorLocalTime/Components/LocalTime.razor.cs (1)
  • LocalTime (8-55)
src/BlazorLocalTime/Components/LocalTimeForm.razor.cs (1)
  • OnLocalTimeZoneChangedDetailed (56-91)
src/BlazorLocalTime/Components/LocalTimeText.razor.cs (1)
  • LocalTimeText (8-59)
🪛 GitHub Actions: .NET Build and Test
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor

[error] 12-12: Shouldly.ShouldAssertException: '// cannot convert timezone from IANA to Windows Render( @ );' should throw System.TimeZoneNotFoundException but did not. Stack Trace: /home/runner/work/BlazorLocalTime/BlazorLocalTime/tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor(12,0)

🔇 Additional comments (18)
tests/Directory.Build.props (1)

11-11: Isolate bUnit package as test-only

  • Apply:
-    <PackageReference Include="bunit" Version="1.40.0" />
+    <PackageReference Include="bunit" Version="1.40.0">
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
  • Compatibility verified: bUnit 1.40.0 targets net9.0 and works with .NET 9 and Microsoft.NET.Test.Sdk 17.14.1; xunit.runner.visualstudio 3.1.3 supports xUnit 2.x.
  • Optionally pin exact versions in Directory.Packages.props for reproducible builds; mirror for other test-only dependencies.
tests/BlazorLocalTimeTest/TestInitializer.cs (1)

20-24: Making JavaScriptInitializer public is appropriate for cross-project reuse

Visibility increase aligns with its usage from tests; no concerns.

src/Directory.Build.props (1)

39-41: InternalsVisibleTo entry verified; assembly name matches

  • AssemblyName matches the InternalsVisibleTo entry.
  • For future strong-naming, include the public key token in this attribute.
tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net9.approved.txt (1)

3-12: Approved baseline updates look correct

  • InternalsVisibleTo includes Nls tests.
  • New public BlazorLocalTimeConfiguration with expected properties is captured.
tests/BlazorLocalTimeTest/LocalTimeFormDefaultTest.razor (1)

12-12: Good move: centralize mock wiring via AddLocalTimeMockService.

This keeps tests consistent with the new configuration-based DI and ensures the MockTimeProvider flows into BlazorLocalTimeConfiguration.

tests/BlazorLocalTimeTest.Nls/_Imports.razor (1)

1-8: Usings set looks right for bUnit + DI/JS interop.

Nothing to change.

tests/BlazorLocalTimeTest/Approvals/PublicApiCheckTest.Run.net8.approved.txt (1)

3-3: Public API baselines updated for .NET 8 and .NET 9

tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs (1)

11-11: Target-typed new() for configuration is clean and matches ctor expectations.

Nice alignment with the new LocalTimeService(BlazorLocalTimeConfiguration) signature.

Also applies to: 19-19, 31-31

tests/BlazorLocalTimeTest/LocalTimeFormTest.razor (2)

97-99: Good: deterministic time with explicit MockTimeProvider.

This makes the test stable and intention-revealing.


120-122: Good: consistent mock injection for time-only path.

Keeps both date-only and time-only tests aligned.

BlazorLocalTime.sln (1)

12-19: LGTM: adds NLS test project and solution folders.

No concerns from solution metadata changes.

tests/BlazorLocalTimeTest/LocalTimeChangeEventTest.razor (1)

10-10: Good migration to config-driven mocks

Using LocalTimeEmptyMockService with AddLocalTimeMockService aligns with the new API and keeps tests focused. LGTM.

Also applies to: 14-14

tests/BlazorLocalTimeTest/BlazorLocalTimeProviderTest.razor (1)

8-9: Config-based mock wiring looks correct

Constructing LocalTimeEmptyMockService with TimeProvider.System and registering via AddLocalTimeMockService is consistent with the new DI surface. LGTM.

tests/BlazorLocalTimeTest/LocalTimeMockService.cs (3)

20-26: LGTM

Empty mock correctly nulls BrowserTimeZoneInfo, letting the provider populate it via JS.


34-36: Default mock registration OK

TryAddSingleton with a default config is fine here since this helper is usually called first in tests.


9-17: Ignore redundant TimeProvider singleton suggestion: a search across the codebase shows no other AddSingleton<TimeProvider>(…) registrations, so there are no duplicates to remove.

Likely an incorrect or invalid review comment.

src/BlazorLocalTime/LocalTimeService.cs (1)

45-46: Now may throw before TZ is available

Now calls ToLocalTimeOffset(...) which throws until the time zone is set. Ensure call sites check IsTimeZoneInfoAvailable or handle exceptions; consider documenting this explicitly in XML docs on the interface.

src/BlazorLocalTime/ILocalTimeService.cs (1)

35-40: Good use of MemberNotNullWhen on availability flag

Nice usability improvement for callers and analyzers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (1)

13-14: Gate UseNls to Windows to avoid false behavior on Linux/macOS.

Apply the Windows-only condition so the NLS runtime switch is emitted only on Windows builds.

-  <ItemGroup>
-    <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
-  </ItemGroup>
+  <ItemGroup Condition="'$(OS)'=='Windows_NT'">
+    <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
+  </ItemGroup>

Run to confirm Windows-only test gating or skips exist:

#!/bin/bash
# Verify NLS tests are Windows-gated or skippable
rg -nC2 -g 'tests/BlazorLocalTimeTest.Nls/**' -P '(SkippableFact|Skip\.If|OperatingSystem\.IsWindows|RuntimeInformation\.IsOSPlatform)'
🧹 Nitpick comments (4)
tests/BlazorLocalTimeTest/LocalTimeMockService.cs (4)

9-9: Make Config immutable

This is assigned once; mark it readonly to prevent accidental mutation in tests.

-internal BlazorLocalTimeConfiguration Config;
+internal readonly BlazorLocalTimeConfiguration Config;

34-38: Deterministic override of test configuration

TryAddSingleton won’t replace an already-registered config (e.g., if AddBlazorLocalTimeService was called earlier), leading to surprising behavior. Replace to guarantee the mock’s config wins in tests.

-        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ =>
-            new() { TimeProvider = TimeProvider.System }
-        );
+        services.Replace(
+            ServiceDescriptor.Singleton<BlazorLocalTimeConfiguration>(
+                new BlazorLocalTimeConfiguration { TimeProvider = TimeProvider.System }
+            )
+        );

50-58: Minor: clarify parameter name and consolidate registration

Use a descriptive name and consider delegating to the instance-based overload to reduce duplication.

-    public static IServiceCollection AddLocalTimeMockService(
-        this IServiceCollection services,
-        MockTimeProvider instance
-    )
+    public static IServiceCollection AddLocalTimeMockService(
+        this IServiceCollection services,
+        MockTimeProvider timeProvider
+    )
     {
-        services.AddScoped<ILocalTimeService, LocalTimeMockService>();
-        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ =>
-            new() { TimeProvider = instance }
-        );
-        return services;
+        return services.AddLocalTimeMockService(
+            new LocalTimeMockService(
+                new BlazorLocalTimeConfiguration { TimeProvider = timeProvider }
+            )
+        );
     }

40-48: Use Singleton registration for externally created instance

Registering a prebuilt instance as Scoped can cause lifetime and disposal quirks; switch to Singleton:

-        services.AddScoped<ILocalTimeService>(_ => instance);
-        services.TryAddSingleton<BlazorLocalTimeConfiguration>(_ => instance.Config);
+        services.AddSingleton<ILocalTimeService>(instance);
+        services.Replace(
+            ServiceDescriptor.Singleton<BlazorLocalTimeConfiguration>(instance.Config)
+        );

LocalTimeService does not implement IDisposable or IAsyncDisposable, so Singleton disposal is safe.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 41f9a4b and 48d84f8.

📒 Files selected for processing (8)
  • .github/workflows/test.yaml (1 hunks)
  • src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1 hunks)
  • src/BlazorLocalTime/BlazorLocalTimeExtension.cs (1 hunks)
  • src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (4 hunks)
  • tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor (1 hunks)
  • tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (1 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeMockService.cs (2 hunks)
  • tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeProviderTest.razor
  • src/BlazorLocalTime/BlazorLocalTimeExtension.cs
  • tests/BlazorLocalTimeTest/LocalTimeServiceTest.cs
  • src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs
🧰 Additional context used
🧬 Code graph analysis (3)
tests/BlazorLocalTimeTest.Nls/BlazorLocalTimeTest.Nls.csproj (3)
tests/BlazorLocalTimeTest/PublicApiCheckTest.cs (3)
  • PublicApiCheckTest (8-30)
  • c (17-21)
  • Fact (10-22)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
src/BlazorLocalTime/LocalTimeZoneOverwrite.cs (1)
  • LocalTimeZoneOverwrite (18-125)
tests/BlazorLocalTimeTest/LocalTimeMockService.cs (4)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/BlazorLocalTimeExtension.cs (3)
  • IServiceCollection (18-21)
  • IServiceCollection (29-41)
  • IServiceCollection (49-62)
tests/BlazorLocalTimeTest/MockTimeProvider.cs (1)
  • MockTimeProvider (3-6)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (2)
  • BlazorLocalTimeProviderCodeTest (10-70)
  • BlazorLocalTimeProviderCodeTest (12-16)
src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (3)
src/BlazorLocalTime/BlazorLocalTimeConfiguration.cs (1)
  • BlazorLocalTimeConfiguration (6-20)
src/BlazorLocalTime/LocalTimeService.cs (2)
  • ILocalTimeService (8-116)
  • LocalTimeService (121-178)
tests/BlazorLocalTimeTest/BlazorLocalTimeProviderCodeTest.cs (1)
  • BlazorLocalTimeProviderCodeTest (10-70)
🔇 Additional comments (3)
.github/workflows/test.yaml (1)

14-17: Matrix build LGTM.

Multi-OS coverage is appropriate for ICU/NLS differences. No changes requested.

src/BlazorLocalTime/Components/BlazorLocalTimeProvider.razor.cs (1)

81-88: ICU detection approach LGTM.

Matches Microsoft’s recommended detection pattern for ICU usage. (learn.microsoft.com)

tests/BlazorLocalTimeTest/LocalTimeMockService.cs (1)

20-27: LGTM: empty mock behavior is clear

Nulling BrowserTimeZoneInfo is appropriate for “no browser TZ” scenarios.

@arika0093 arika0093 merged commit 3024920 into main Sep 6, 2025
5 checks passed
@arika0093 arika0093 deleted the fix/issue_19 branch September 6, 2025 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: System.TimeZoneNotFoundException: The time zone ID 'America/Sao_Paulo' was not found on the local computer

1 participant