Troubleshooting
Practical problem/solution guide for Pragmatic.Temporal. Each section covers a common issue, the likely causes, and the fix.
Incorrect Time After Timezone Conversion
Section titled “Incorrect Time After Timezone Conversion”A DateTimeOffset or ZonedDateTime shows the wrong local time after converting between timezones.
Checklist
Section titled “Checklist”-
Is the source value in UTC? The safest conversion path is always UTC -> target zone. If the source has a non-UTC offset, convert to UTC first:
var utc = sourceValue.ToUniversalTime();var target = ZonedDateTime.FromUtc(utc, "Europe/Rome"); -
Is the timezone ID correct?
"CET"is not a valid IANA timezone (it is an abbreviation). Use"Europe/Rome","Europe/Berlin", etc. Validate with:TimeZoneResolver.IsValidTimezone("CET") // falseTimeZoneResolver.IsValidTimezone("Europe/Rome") // true -
Is DST being accounted for? If you use
TimeZoneInfo.GetUtcOffset()directly, the offset changes between standard and daylight time.ZonedDateTimehandles this automatically. -
Are you comparing local times across timezones? Two
DateTimevalues from different timezones cannot be meaningfully compared. Convert both to UTC first or useZonedDateTime(which compares UTC instants).
TimeZoneNotFoundException at Runtime
Section titled “TimeZoneNotFoundException at Runtime”TimeZoneResolver.GetTimeZone() throws TimeZoneNotFoundException.
Checklist
Section titled “Checklist”-
Check the timezone ID format. Use IANA IDs (
Europe/Rome) for portability. Windows IDs (Central European Standard Time) work but are not available on Linux/macOS natively. -
Check for typos. Common mistakes:
Europe/Roma(should beEurope/Rome),US/Eastern(should beAmerica/New_York). -
Use TryGetTimeZone for user input. If the timezone ID comes from a client or external system, use the Try pattern:
if (!TimeZoneResolver.TryGetTimeZone(userInput, out var zone)){// Handle invalid timezone -- fall back to defaultzone = TimeZoneInfo.Utc;} -
Check the platform. On Linux containers, timezone data comes from the OS
tzdatapackage; see Timezone data on Linux containers below. -
Check TemporalOptions.ThrowOnInvalidTimeZone. When set to
false(default), invalid timezone IDs fall back toDefaultTimeZonewith a logged warning instead of throwing.
Timezone Data on Linux Containers
Section titled “Timezone Data on Linux Containers”Pragmatic.Temporal resolves zones through the operating system’s TimeZoneInfo (unlike
NodaTime, which embeds its own tzdb copy). This keeps the library dependency-free, but it
means the OS must ship timezone data.
Symptom: every TimeZoneResolver.GetTimeZone("Europe/Rome") call throws
TimeZoneNotFoundException in the container while the same code works on your machine.
Cause: slim images (Alpine, distroless, some -slim variants) do not include the
tzdata package, so the OS has no IANA database to resolve against.
Fix by installing tzdata in the image:
# Debian/Ubuntu-based images (e.g. mcr.microsoft.com/dotnet/aspnet)RUN apt-get update && apt-get install -y --no-install-recommends tzdata && rm -rf /var/lib/apt/lists/*
# Alpine-based imagesRUN apk add --no-cache tzdataFor distroless images, copy /usr/share/zoneinfo from a build stage or switch to a base
image that includes it. .NET also honors the TZ environment variable for the local
zone, but named-zone lookups always need the zoneinfo files.
Keep it updated: timezone rules change several times a year (governments move DST dates at short notice). Because the data comes from the OS, you pick up new rules by rebuilding the image against an updated base, not by updating a NuGet package. If your app does date math far in the future across political timezone changes, schedule regular base-image rebuilds.
Business Day Calculation Returns Unexpected Date
Section titled “Business Day Calculation Returns Unexpected Date”AddBusinessDays or CountBusinessDays returns a date that does not match expectations.
Checklist
Section titled “Checklist”-
Did you pass a country code? Without a country code, only weekends are excluded. Holidays are only considered when you call the
(from, days, countryCode)overload. -
Is the holiday provider registered? Check that
IHolidayProvideris registered in DI and that it contains holidays for the correct year and country:var holidays = provider.GetHolidays(2026, "IT");// Verify Christmas, St. Stephen's, etc. are present -
Is the holiday in the correct year?
StaticHolidayProviderstores holidays by(year, countryCode). If you added holidays for 2025 but are querying for 2026, they will not be found. -
Is the direction correct?
AddBusinessDays(date, -5)subtracts business days (goes backward). Verify the sign of thedaysparameter. -
Is
CountBusinessDaysusing the expected range? The range is[from, to)–fromis inclusive,tois exclusive. Iffrom >= to, the result is 0.
TestClock Not Affecting Other Services
Section titled “TestClock Not Affecting Other Services”You set up a TestClock in tests, but services still use the real system time.
Checklist
Section titled “Checklist”-
Did you register the TestClock as IClock in DI?
var clock = new TestClock();services.AddSingleton<IClock>(clock); -
Did you register it before building the service provider?
TryAddSingleton(used byAddPragmaticTemporal) does not replace an existing registration. Register yourTestClockfirst, or useUseClock():services.AddPragmaticTemporal();services.UseClock(clock); // Replaces the default SystemClock -
Is TimeProvider also updated? Some .NET APIs use
TimeProviderinstead ofIClock.UseClock()updates both registrations. If you registeredIClockmanually, also registerTimeProvider:services.AddSingleton<IClock>(clock);services.AddSingleton<TimeProvider>(clock.GetTimeProvider()); -
Are you using the clock from the same DI scope?
TemporalContextis scoped. In integration tests, ensure the test clock is resolved from the same scope as the context.
DST Gap or Overlap Exception in Production
Section titled “DST Gap or Overlap Exception in Production”NonExistentTimeException or AmbiguousTimeException thrown in production when converting user input.
Checklist
Section titled “Checklist”-
Are you using ThrowException policies? Check if
ZonedDateTime.FromLocal()orZonedDateTime.FromLocalStrict()is being called withThrowExceptionpolicies. For user-facing input, preferShiftForward/UseStandardTime:var zoned = ZonedDateTime.FromLocal(localDateTime, zone,nonExistentPolicy: NonExistentTimePolicy.ShiftForward,ambiguousPolicy: AmbiguousTimePolicy.UseStandardTime); -
Are you validating user input before conversion? Check if the user submitted a time that falls in a DST gap:
var zone = TimeZoneResolver.GetTimeZone(timezoneId);var dt = DateTime.SpecifyKind(userDateTime, DateTimeKind.Unspecified);if (zone.IsInvalidTime(dt))return Error("This time does not exist due to daylight saving.");if (zone.IsAmbiguousTime(dt))return Error("This time is ambiguous. Please specify AM/PM or offset."); -
Check TemporalOptions defaults.
TemporalOptions.NonExistentTimeHandlingandAmbiguousTimeHandlingprovide application-wide defaults. Set them to safe values:services.AddPragmaticTemporal(o =>{// These are already the defaults, but being explicit helpso.NonExistentTimeHandling = NonExistentTimePolicy.ShiftForward;o.AmbiguousTimeHandling = AmbiguousTimePolicy.UseStandardTime;});
CronExpression Returns No Occurrences
Section titled “CronExpression Returns No Occurrences”GetNextOccurrence returns null or GetOccurrences yields no results.
Checklist
Section titled “Checklist”-
Is the expression valid? Use
TryParseto check:if (!CronExpression.TryParse(expression, out var cron))// Invalid expression -
Is the
fromtime before the expected occurrence?GetNextOccurrencefinds the next match afterfrom. Iffromis already past the expected time, it will find the next cycle. -
Does the expression match any dates in the search window? The scheduler searches up to 4 years ahead. An expression like
0 0 31 2 *(midnight on February 31) will never match. -
Is a DST gap swallowing the occurrence? If the scheduled time falls in a DST gap, that occurrence is skipped. The scheduler advances to the next valid minute:
// "Every day at 2:30 AM" in Europe/Rome// On March 29, 2026 (spring forward), 2:30 AM does not exist// The scheduler skips to the next valid occurrencevar cron = CronExpression.Parse("30 2 * * *");var next = cron.GetNextOccurrence(beforeSpringForward, TimeZoneResolver.GetTimeZone("Europe/Rome")); -
Check semantics (Unix vs Quartz). For expressions with both day-of-month and day-of-week specified, Unix semantics use OR logic (matches if either field matches), Quartz uses AND logic (both must match):
var unixCron = CronExpression.Parse("0 9 15 * 1", CronSemantics.Unix); // 15th OR Mondayvar quartzCron = CronExpression.Parse("0 9 15 * 1", CronSemantics.Quartz); // 15th AND Monday
TemporalContext Shows Wrong Client Timezone
Section titled “TemporalContext Shows Wrong Client Timezone”The TemporalContext.ClientTimeZone does not match the expected user timezone.
Checklist
Section titled “Checklist”-
Is the middleware registered? The ASP.NET Core middleware must be in the pipeline:
app.UsePragmaticTemporal(); -
Is the client sending timezone information? Check the detection strategies (evaluated in priority order):
Priority Strategy Where to Check 50 Query string ?tz=Europe/Romein the URL100 Header X-Timezone: Europe/Romerequest header200 JWT claim timezoneclaim in the access token300 Cookie tzcookie -
Is the timezone ID valid? If the client sends an invalid timezone ID and
ThrowOnInvalidTimeZoneisfalse(default), the middleware silently falls back toTemporalOptions.DefaultTimeZone. -
Is the default timezone configured? If no detection strategy matches,
TemporalContext.ClientTimeZoneusesTemporalOptions.DefaultTimeZone, which defaults to UTC.
Diagnostics Reference
Section titled “Diagnostics Reference”| ID | Severity | Cause | Fix | IDE quick-fix |
|---|---|---|---|---|
| PRAG0900 | Warning | DateTime.Now, .UtcNow, DateTimeOffset.Now, .UtcNow used directly |
Inject and use IClock |
Replaces with the equivalent read from an IClock member already on the type (e.g. DateTime.UtcNow → _clock.UtcNow.UtcDateTime); offered only when a clock is in scope |
| PRAG0901 | Warning | DateTime.Today used directly |
Use IClock.Today or IClock.UtcToday |
Replaces with _clock.Now.LocalDateTime.Date (local midnight, Kind preserved) when a clock is in scope |
| PRAG0902 | Warning | new DateTime() without DateTimeKind parameter |
Specify DateTimeKind or use LocalDateTime |
Appends DateTimeKind.Utc / Local / Unspecified (date-only constructors expand to the 7-argument overload) |
| PRAG0903 | Warning | DateTimeOffset relational comparison (<, >) without .UtcDateTime |
Compare .UtcDateTime properties or use ZonedDateTime |
Wraps both operands with .UtcDateTime (only when both are DateTimeOffset) |
| PRAG0904 | Info | DateTime.Now/.UtcNow in test code |
Use TestClock for deterministic tests |
Same clock-member replacement as PRAG0900 when the test class exposes an IClock |
The quick-fixes ship in Pragmatic.Temporal.CodeFixers, packaged alongside the analyzers: they appear as lightbulb actions in the IDE and support Fix All. Check the Error List window in Visual Studio or the build output for diagnostic details and the affected source location.
Should I use LocalDate or DateOnly?
Section titled “Should I use LocalDate or DateOnly?”Use LocalDate. It wraps DateOnly but integrates with the Pragmatic ecosystem: Period arithmetic, DateRange operations, ITemporalCalculator, and extension methods like Next(DayOfWeek). Implicit conversions exist between LocalDate and DateOnly for interop.
When should I use ZonedDateTime vs DateTimeOffset?
Section titled “When should I use ZonedDateTime vs DateTimeOffset?”Use ZonedDateTime when the timezone is semantically meaningful (scheduling, display, conversion). Use DateTimeOffset for storage and transport (databases, APIs). Convert at boundaries: ZonedDateTime.ToUtc() for storage, ZonedDateTime.FromUtc(dto, zone) for display.
How do I handle “every day at 2:30 AM” across DST?
Section titled “How do I handle “every day at 2:30 AM” across DST?”Use CronExpression with a timezone:
var cron = CronExpression.Parse("30 2 * * *");var next = cron.GetNextOccurrence(now, TimeZoneResolver.GetTimeZone("Europe/Rome"));// Automatically skips non-existent times during spring forwardCan I use Pragmatic.Temporal without DI?
Section titled “Can I use Pragmatic.Temporal without DI?”Yes. All types (LocalDate, ZonedDateTime, Duration, etc.) are plain value types with no DI dependency. TemporalCalculator can be instantiated directly. IClock and TemporalContext are the only DI-oriented APIs.
How does Pragmatic.Temporal differ from NodaTime?
Section titled “How does Pragmatic.Temporal differ from NodaTime?”Pragmatic.Temporal uses .NET BCL timezone data (always current via OS updates), focuses on business day calculations and cron scheduling, and provides Roslyn analyzers. NodaTime ships its own TZDB data, supports multiple calendar systems, and has a larger API surface. See the comparison table in the README for details.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/pragmatic-design/Pragmatic.Design/issues
- Showcase Examples: See the
Showcaseproject for working temporal usage across business logic. - Core Types Guide: See core-types.md for all types, storage formats, and conversion rules.
- DST Handling Guide: See dst-handling.md for detailed DST policy examples.
- Testing Guide: See testing.md for TestClock, TestTemporalContext, and test patterns.
