Skip to content

DateTime Assertions - #76

Merged
feO2x merged 4 commits into
mainfrom
75-datetime-assertions
Aug 2, 2026
Merged

DateTime Assertions#76
feO2x merged 4 commits into
mainfrom
75-datetime-assertions

Conversation

@feO2x

@feO2x feO2x commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #75

feO2x and others added 4 commits August 2, 2026 19:07
Plan IsUtc, IsLocal, and IsUnspecified for Check<DateTime>, following the
IsUuidV7 shape: three metadata-free error codes, three customizable
templates, and three built-in OpenAPI contracts. Named assertions rather
than one parameterized HasKind, matching how IsEmpty/IsNotEmpty and
IsNull/IsNotNull are split today.

State the contract as the kind and nothing more: IsUtc accepts exactly
those normalized values whose Kind is DateTimeKind.Utc. The predicate
cannot observe where a value came from, so the plan keeps that separate
from the System.Text.Json consequence, which is that a trailing Z is
required and 2026-08-02T10:00:00+00:00 arrives as Local and is rejected
despite denoting the same instant - measured on .NET 10, where the
converter resolves every explicit offset against the server's time zone
before the DTO exists. That consequence is real and belongs in the README
and the XML remarks, because it is the actionable form for an API
consumer, but it is false for a value arriving over gRPC or produced by
DateTime.UtcNow. The message reads "must be represented in UTC" for the
same reason: "must be in UTC" describes the instant and reads as already
satisfied on a +00:00 payload, while naming the Z encoding would claim
knowledge the assertion does not have.

Drop the Check<DateTimeOffset> overload the first draft shared the Utc
code with. DateTimeOffset has no kind, so the assertion would test
Offset == TimeSpan.Zero, which accepts +00:00 that the DateTime overload
rejects, and OpenAPI cannot expose the difference because both types map
to string/date-time. An unzoned wire value also arrives carrying the
server's own offset, which would make the verdict depend on the host's
time zone. Moved to #74 as a separate ZeroOffset candidate.

Correct two further claims the first draft got wrong. Check<T> is
constructed after the context-wide or per-check IValueNormalizer runs, so
these assertions inspect the normalized value; the default
TrimStringNormalizer preserves non-strings, but that is a default rather
than a guarantee. And there is no generator diagnostic for an
unregistered error code - document construction fails at runtime instead
- so the OpenAPI criterion has to be verified by generating a document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019vu3qgcqGWGr2TddYEpLpW
Cut the plan roughly in half, mostly by removing justification prose that
ai-plans/AGENTS.md asks plans not to carry: explanations of routine
implementation work and background a senior engineer already has. The
three-section structure, every acceptance criterion, and every decision
survive.

Restore four items the first pass dropped that are decisions rather than
prose. Chief among them, why the wire-format test is not redundant with
the 3x3 kind matrix: it walks the same ground, so without a stated reason
it reads as duplication and gets deleted, when it is really a regression
detector for System.Text.Json behavior that the whole feature rests on.
The 0072 plan needed the same note for its RFC-derived accepted set
against its version-by-variant matrix.

Also restore both rejected message wordings, since "must be represented
in UTC" took two review rounds to reach and half the reasoning made
reverting it to "must be in UTC" look like a simplification; why
TryGetStableMessageProvider applies here, so a later parameterized rule
in this family does not copy it blindly; and the note that the wire-format
table was measured rather than assumed, with only the kind being
host-independent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019vu3qgcqGWGr2TddYEpLpW
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
- Restore the indentation of the closing PackageReleaseNotes tag and use
  ASCII hyphens in the notes, matching the sibling packages.
- Say that DateTime.SpecifyKind and friends *can* produce Utc, as the XML
  remarks already do, instead of claiming they always do.
- Scope the JSON round-trip DTO to its file so it no longer occupies a
  name in the shared test namespace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019vu3qgcqGWGr2TddYEpLpW
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
@feO2x feO2x self-assigned this Aug 2, 2026
@feO2x feO2x added the enhancement New feature or request label Aug 2, 2026
@feO2x feO2x linked an issue Aug 2, 2026 that may be closed by this pull request
8 tasks
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Light.PortableResults 98% 94% 3219
Light.PortableResults.AspNetCore.MinimalApis 89% 75% 25
Light.PortableResults.AspNetCore.Mvc 89% 75% 25
Light.PortableResults.AspNetCore.OpenApi 94% 83% 505
Light.PortableResults.AspNetCore.Shared 100% 100% 28
Light.PortableResults.Validation 97% 89% 3050
Light.PortableResults.Validation.OpenApi 98% 91% 146
Light.PortableResults.Validation.OpenApi.SourceGeneration 92% 86% 1164
Summary 96% (14961 / 15551) 90% (6289 / 6956) 8162

Minimum allowed line rate is 60%

@feO2x
feO2x merged commit 2c62d14 into main Aug 2, 2026
2 checks passed
@feO2x
feO2x deleted the 75-datetime-assertions branch August 2, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DateTimeKind assertions (IsUtc, IsLocal, IsUnspecified) to the validation library

1 participant