Skip to content

feat(#356): ETag / If-None-Match (304) for StreamOne + StreamAggregate (marten#5015 parity) - #360

Merged
jeremydmiller merged 2 commits into
mainfrom
feat/356-etag
Jul 22, 2026
Merged

feat(#356): ETag / If-None-Match (304) for StreamOne + StreamAggregate (marten#5015 parity)#360
jeremydmiller merged 2 commits into
mainfrom
feat/356-etag

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #356.

Ports Marten's HTTP conditional-request support (JasperFx/marten#5015) to Polecat. StreamOne<T> and StreamAggregate<T> now support ETag / If-None-Match304 Not Modified so polling clients skip re-downloading unchanged docs/aggregates. On by default via a new EmitETag init-only property (default true); EmitETag = false restores the exact pre-ETag behavior.

HTTP layer (identical to Marten)

  • ETagHelpers ported near-verbatim: Format(Guid)/Format(long) → quoted opaque ETag; IfNoneMatchMatches handles the * wildcard, comma-separated If-None-Match lists, and strips W/ weak validators (weak comparison per RFC 7232 §3.2).
  • On a hit → ETag header. On matching If-None-Match304 + empty body + ETag. Both advertise 304 in endpoint metadata. StreamMany<T> is intentionally out of scope.

SQL Server version source (the part that differs)

  • StreamOne<T> reads the document JSON and its version column in one round trip (new ExecuteJsonFirstWithVersionAsync / ToJsonFirstWithVersionAsync) — no follow-up metadata query, i.e. the fixed shape from marten#5027 — and now streams the raw persisted JSON. ETag = the bigint document version.
  • StreamAggregate<T> reads the stream version cheaply via FetchStreamStateAsync before folding, so a 304 skips aggregation entirely. ETag = the stream long version.
  • Polecat document tables always carry a version column, so a document ETag is always available when EmitETag is on (no "no-version-column" gap to guard); the only suppression is EmitETag = false. Noted in the docs.

Tests

  • 9 ETagHelpers unit facts: * wildcard, W/ weak validator, multi-value comma list, exact/mismatch/no-header, Format output.
  • 11 Alba scenarios covering, for both StreamOne and StreamAggregate: ETag on 200, 304 on match (empty body + ETag echoed), full 200 on stale, EmitETag = false suppression, the 404 path, and ETag-changes-after-append.
  • Docs added to docs/documents/aspnetcore.md.

20/20 new + existing streaming regression 6/6 green on net10.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits July 22, 2026 11:18
…Aggregate (marten#5015 parity)

Adds HTTP conditional-request support so polling clients skip re-downloading unchanged
documents/aggregates. On by default via a new `EmitETag` init-only property (default true)
on both result types; `EmitETag = false` restores the exact pre-ETag behavior.

- ETagHelpers (ported near-verbatim from Marten): Format(Guid)/Format(long) → quoted opaque
  ETag; IfNoneMatchMatches handles the `*` wildcard, comma-separated If-None-Match lists, and
  strips `W/` weak validators (weak comparison per RFC 7232 §3.2).
- StreamOne<T>: reads the document JSON *and* its `version` column in one round trip
  (new ExecuteJsonFirstWithVersionAsync / ToJsonFirstWithVersionAsync — no follow-up metadata
  query, the fixed shape from marten#5027) and now streams the raw persisted JSON. ETag = the
  bigint document version.
- StreamAggregate<T>: reads the stream version cheaply via FetchStreamStateAsync *before*
  folding, so a 304 skips the aggregation entirely. ETag = the stream version.
- Both advertise 304 in endpoint metadata. StreamMany stays out of scope by design.

SQL Server version-source note: Polecat document tables always carry a `version` bigint
column, so a document ETag is always available when EmitETag is on (no "no-version" gap to
guard); the only suppression is EmitETag = false. Aggregate ETag uses the stream `long`
version, same as Marten conceptually.

Tests: 9 ETagHelpers unit facts (wildcard, W/ weak validator, multi-value list, exact/mismatch/
no-header) + 11 Alba scenarios covering, for BOTH StreamOne and StreamAggregate: ETag on 200,
304 on match (empty body + ETag), full 200 on stale, EmitETag=false suppression, the 404 path,
and stream-version-changes-after-append. Docs added to aspnetcore.md. 20/20 new + existing
streaming regression 6/6 green on net10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Alba streaming/ETag test classes each spin up a host against the single shared SQL Server
test database and mutate the same StreamingIssue/aggregate tables (clean-all + seed). xUnit
runs test classes in parallel by default, so classes cross-contaminate that shared state.
Disable assembly-level parallelization so the integration classes run serially.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 81bfb45 into main Jul 22, 2026
7 of 8 checks passed
@jeremydmiller
jeremydmiller deleted the feat/356-etag branch July 22, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port ETag / If-None-Match (304) support from Marten #5015 (SQL Server version-source differs)

1 participant