Skip to content

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

Description

@jeremydmiller

Port ETag / If-None-Match (304) support from Marten

Marten added HTTP conditional-request support (ETag / If-None-Match304 Not Modified) to StreamOne<T> and StreamAggregate<T> so polling clients skip re-downloading unchanged docs/aggregates. Polecat should match it. Marten issue: JasperFx/marten#5010. Reference PR: JasperFx/marten#5015.

Behavior to mirror (identical at the HTTP layer)

  • New EmitETag init-only property on both result types, default true.
  • On a normal hit: set an ETag response header.
  • On an incoming If-None-Match that matches the current version: respond 304 with an empty body (ContentLength = 0) and the ETag header, skip the body.
  • EmitETag = false restores the pre-ETag behavior exactly (no header, no conditional handling) — backwards-compatible opt-out.
  • StreamMany<T> is intentionally out of scope (a cheap collection-wide ETag is hard to derive).

Marten implementation to point at

Piece Marten file / symbol
ETag format + If-None-Match matching src/Marten.AspNetCore/ETagHelpers.csFormat(Guid), Format(long), IfNoneMatchMatches(HttpContext, etag)
StreamOne wiring src/Marten.AspNetCore/StreamOne.cs + QueryableExtensions.WriteSingle<T>
StreamAggregate wiring src/Marten.AspNetCore/StreamAggregate.cs
Endpoint metadata both add ProducesResponseTypeMetadata(304, typeof(void), …)
Sample endpoints src/IssueService/StreamingMinimalEndpoints.cs
Docs docs/documents/aspnetcore.md → "ETag / Conditional Requests"

ETagHelpers is directly portable — it's pure HTTP logic: handles the * wildcard, comma-separated If-None-Match lists, and strips W/ weak validators (weak comparison is the correct function for If-None-Match per RFC 7232 §3.2). Port it close to verbatim.

SQL Server mechanics differ — this is the part to design, not copy

The HTTP layer is identical; the version source differs:

  • StreamAggregate (event stream version): same as Marten conceptually — read the stream's long version cheaply before folding so a 304 skips the aggregation. Use Polecat's FetchStreamStateAsync equivalent. Low divergence.
  • StreamOne (document version): Marten reads mt_version (a Guid) and formats a quoted-GUID ETag. On SQL Server you have a better-fitting option: a native rowversion/timestamp column is a monotonic 8-byte value that auto-changes on every update — purpose-built for change detection. If Polecat's document tables carry (or can carry) a rowversion, formatting that as the ETag is cheaper and more natural than a GUID version. Decide whether to (a) mirror Marten's mt_version GUID exactly for cross-store envelope parity, or (b) use rowversion for efficiency and accept a different opaque ETag value. Either is HTTP-valid (ETags are opaque); pick one deliberately.
  • Avoid Marten's current inefficiency: Marten's StreamOne presently does a second query (MetadataForAsync) to read the version — this is being fixed to an inline read (StreamOne<T> ETag: inline mt_version (eliminate 2nd round trip) + reverse where T:notnull + ETag test hardening (pre-9.18, follow-up to #5015) marten#5027). Port the fixed shape: read the version inline in the same round trip as the document (SQL Server: SELECT data, <version> FROM …, or FOR JSON plus a version column), not via a follow-up query.
  • Version-metadata-disabled guard: if a doc type has no version column, emit no ETag rather than a constant one (which would risk false 304s).

Test plan to mirror

Marten's Alba tests (src/Marten.AspNetCore.Testing/streaming_result_types_tests.cs) cover, for both StreamOne and StreamAggregate:

  1. ETag header present on a normal 200 hit
  2. 304 (empty body) when If-None-Match matches the current version
  3. Full 200 body when If-None-Match is stale
  4. EmitETag = false suppresses the header/behavior entirely

Port all four for each type. Additionally add (gaps we're also backfilling on the Marten side):

  • ETagHelpers unit coverage for the * wildcard, W/ weak validator, and multi-value comma lists.
  • The 404 path (missing document / missing stream).
  • The version-metadata-disabled / no-version-column case → no ETag emitted.

Filed from the Marten repo as a parity hand-off during review of #5015. Note the SQL Server version-source difference above is deliberate, not an oversight to copy around.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions