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 sourceFrom the repository description

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.

One thing to look at

  • 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, ~1.3k tokens by cl100k_base, 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");
    }
}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. 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.