agentsclimarketplace

Datetime and time handling

Skill Sarmkadan/dotnet-senior-skills/skills/datetime-and-time-handling

Senior-level .NET review rules for AI coding agents - Claude Code skills, Cursor rules, and Copilot instructions from one source

Install
npx -y skills add Sarmkadan/dotnet-senior-skills --skill datetime-and-time-handling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 26 days oldThe repository was created 26 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.9 KB, as published. Nobody here has run it

See also: globalization-and-culture for culture-sensitive parsing/formatting rules and string comparison guidelines.

DateTime and Time Handling

DateTimeOffset by default

DateTime carries a Kind flag that nothing enforces: a Kind.Unspecified value round-tripped through JSON, a database, or ToLocalTime() silently reinterprets the same ticks as a different instant. DateTimeOffset carries the offset in the value - comparisons and serialization are unambiguous.

// non-compiling: illustrative
// WRONG: is this UTC? Local? Depends on who wrote it and which driver read it back.
public DateTime CreatedAt { get; set; }
// RIGHT
public DateTimeOffset CreatedAt { get; set; }

Decision table:

  • Instants (created-at, expires-at, audit, logs, tokens): DateTimeOffset, stored as UTC. This is 90% of fields.
  • Calendar dates (birthday, invoice date, holiday): DateOnly. A birthday has no timezone; storing it as midnight DateTime shifts it a day for half the planet.
  • Wall-clock times (store opening hours): TimeOnly plus a timezone id stored separately.
  • Future local events (a meeting at "10:00 Sofia time" next March): store local time + IANA timezone id, convert at read time. Pre-converting to UTC bakes in today's offset rules; a DST law change makes the stored instant wrong.

Review flag: DateTime.Now anywhere in server code. Server-local time depends on the box's timezone; two instances in different regions disagree. DateTime.UtcNow is acceptable in legacy code; new code uses DateTimeOffset.UtcNow - via TimeProvider (below).

Rule: Data crossing a machine boundary (files, protocols, URLs, database strings, config) uses CultureInfo.InvariantCulture; text rendered for human eyes uses the user's culture.

For culture-aware parsing and formatting rules, including how to handle date/time parsing with explicit culture specification, see the globalization-and-culture skill.

TimeProvider: the clock is a dependency

Any logic that branches on "now" (expiry, grace periods, business-day rules) is untestable when it calls the static clock. .NET 8+ ships TimeProvider; inject it, register TimeProvider.System, and use FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in tests.

// non-compiling: illustrative
// WRONG: the test for "expires after 30 days" needs Thread.Sleep or a real month
if (DateTimeOffset.UtcNow > order.CreatedAt.AddDays(30)) { ... }
// RIGHT
public OrderService(TimeProvider clock) => _clock = clock;
if (_clock.GetUtcNow() > order.CreatedAt.AddDays(30)) { ... }

Multiple UtcNow reads inside one operation is a subtler bug: the value changes between reads, so "created" and "modified" timestamps of the same write differ. Read once at the top, pass the value down.

Timezone conversion

  • Convert at the presentation edge only. Storage, domain logic, and comparisons operate in UTC; the user's timezone applies exactly once, on display or on parsing user input.
  • Use IANA ids (Europe/Sofia) - TimeZoneInfo.FindSystemTimeZoneById accepts them cross-platform since .NET 8. Windows ids (FYRO Macedonia Standard Time) in config are a portability bug.
  • Never do arithmetic on local times: localTime.AddHours(24) across a DST transition is not "same time tomorrow". Convert to UTC, add, convert back - or use the date component and reattach the wall-clock time.
  • TimeZoneInfo.ConvertTime on an ambiguous/invalid local time (the DST fold and gap) picks an answer silently. Code parsing user-supplied local times around 2-3 a.m. must decide policy explicitly (IsAmbiguousTime/IsInvalidTime).

Durations and scheduling

  • Elapsed time measurement: Stopwatch (or TimeProvider.GetTimestamp()/GetElapsedTime), never subtracting two DateTime.Now reads - the wall clock jumps on NTP sync, producing negative or hour-long "durations".
  • TimeSpan for durations in APIs and options, not int timeoutSeconds - TimeSpan.FromSeconds(30) reads unambiguously, and misread units (ms vs s) are a classic 1000x incident.
  • Recurring jobs defined as "daily at 02:30" in a DST-observing zone either skip or double-fire once a year. Schedule in UTC, or use a scheduler (Quartz, Hangfire) that has an explicit DST policy - not a hand-rolled Task.Delay loop computing the next local occurrence.

Detection Fixtures

These fixtures provide concrete, compilable C# examples of anti-patterns that can be used by analysis tools to verify detection logic.

DST-boundary comparisons

public class DstExample
{
    public void ComparisonAcrossTransition()
    {
        // WRONG: Comparing DateTime.Now across possible DST transitions
        // can lead to unexpected behavior if the clock jumps.
        var now = DateTime.Now;
        var nextDay = now.AddDays(1);
        if (now > nextDay) { }
    }
}

Serialized DateTime without Kind information

using System.Text.Json;

public class SerializationExample
{
    public void RoundTrip()
    {
        var json = "{\"CreatedAt\":\"2026-07-17T10:00:00\"}";
        // WRONG: Deserializing into DateTime without Kind info loses timezone context
        var obj = JsonSerializer.Deserialize<MyModel>(json);
    }
}

public class MyModel
{
    public DateTime CreatedAt { get; set; }
}

Windows-only TimeZoneInfo lookup

public class TimeZoneExample
{
    public void Lookup()
    {
        // WRONG: Windows-only zone ID is a portability bug on Linux
        var zone = TimeZoneInfo.FindSystemTimeZoneById("FYRO Macedonia Standard Time");
    }
}

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.