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
tzdatapackage. If the container image does not include it, no timezone can be resolved. Installtzdatain your Dockerfile:RUN apt-get update && apt-get install -y tzdata -
Check TemporalOptions.ThrowOnInvalidTimeZone. When set to
false(default), invalid timezone IDs fall back toDefaultTimeZonewith a logged warning instead of throwing.
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:app.UseTemporal(temporal =>{// These are already the defaults, but being explicit helpstemporal.UseDefaultTimeZone("Europe/Rome");});
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.UseTemporalContext(); -
Is the client sending timezone information? Check the detection strategies (evaluated in priority order):
Priority Strategy Where to Check 10 Query string ?tz=Europe/Romein the URL20 Header X-Timezone: Europe/Romerequest header30 JWT claim timezoneclaim in the access token40 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 |
|---|---|---|---|
| PRAG0900 | Warning | DateTime.Now, .UtcNow, DateTimeOffset.Now, .UtcNow used directly | Inject and use IClock |
| PRAG0901 | Warning | DateTime.Today used directly | Use IClock.Today or IClock.UtcToday |
| PRAG0902 | Warning | new DateTime() without DateTimeKind parameter | Specify DateTimeKind or use LocalDateTime |
| PRAG0903 | Warning | DateTimeOffset relational comparison (<, >) without .UtcDateTime | Compare .UtcDateTime properties or use ZonedDateTime |
| PRAG0904 | Info | DateTime.Now/.UtcNow in test code | Use TestClock for deterministic tests |
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/nicola-pragmatic/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.