feat(#356): ETag / If-None-Match (304) for StreamOne + StreamAggregate (marten#5015 parity) - #360
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #356.
Ports Marten's HTTP conditional-request support (JasperFx/marten#5015) to Polecat.
StreamOne<T>andStreamAggregate<T>now supportETag/If-None-Match→304 Not Modifiedso polling clients skip re-downloading unchanged docs/aggregates. On by default via a newEmitETaginit-only property (defaulttrue);EmitETag = falserestores the exact pre-ETag behavior.HTTP layer (identical to Marten)
ETagHelpersported near-verbatim:Format(Guid)/Format(long)→ quoted opaque ETag;IfNoneMatchMatcheshandles the*wildcard, comma-separatedIf-None-Matchlists, and stripsW/weak validators (weak comparison per RFC 7232 §3.2).ETagheader. On matchingIf-None-Match→304+ empty body +ETag. Both advertise304in endpoint metadata.StreamMany<T>is intentionally out of scope.SQL Server version source (the part that differs)
StreamOne<T>reads the document JSON and itsversioncolumn in one round trip (newExecuteJsonFirstWithVersionAsync/ToJsonFirstWithVersionAsync) — no follow-up metadata query, i.e. the fixed shape from marten#5027 — and now streams the raw persisted JSON. ETag = the bigint documentversion.StreamAggregate<T>reads the stream version cheaply viaFetchStreamStateAsyncbefore folding, so a304skips aggregation entirely. ETag = the streamlongversion.versioncolumn, so a document ETag is always available whenEmitETagis on (no "no-version-column" gap to guard); the only suppression isEmitETag = false. Noted in the docs.Tests
ETagHelpersunit facts:*wildcard,W/weak validator, multi-value comma list, exact/mismatch/no-header,Formatoutput.StreamOneandStreamAggregate: ETag on200,304on match (empty body + ETag echoed), full200on stale,EmitETag = falsesuppression, the404path, and ETag-changes-after-append.docs/documents/aspnetcore.md.20/20 new + existing streaming regression 6/6 green on net10.
🤖 Generated with Claude Code