Skip to content

Add StreamOne/StreamMany/StreamAggregate helpers for Wolverine.Http.Marten#2528

Merged
jeremydmiller merged 2 commits intomainfrom
feature/marten-http-streaming-1562
Apr 20, 2026
Merged

Add StreamOne/StreamMany/StreamAggregate helpers for Wolverine.Http.Marten#2528
jeremydmiller merged 2 commits intomainfrom
feature/marten-http-streaming-1562

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Closes #1562. Three new return-types that stream Marten JSON directly to the HTTP response, avoiding a deserialize/serialize round trip through .NET. Built on top of Marten.AspNetCore's existing JSON streaming helpers (WriteSingle/WriteArray/WriteLatest), with correct OpenAPI metadata contributed via IEndpointMetadataProvider.

API

// StreamOne<T> — single document via WriteSingle; 404 on no match
[WolverineGet(\"/invoices/{id}\")]
public static StreamOne<Invoice> Get(Guid id, IQuerySession session)
    => new(session.Query<Invoice>().Where(x => x.Id == id));

// StreamMany<T> — JSON array via WriteArray; empty array on no match
[WolverineGet(\"/invoices/approved\")]
public static StreamMany<Invoice> Approved(IQuerySession session)
    => new(session.Query<Invoice>().Where(x => x.Approved));

// StreamAggregate<T> — event-sourced latest aggregate via WriteLatest; 404 on unknown stream
[WolverineGet(\"/orders/{id}\")]
public static StreamAggregate<Order> Get(Guid id, IDocumentSession session)
    => new(session, id);

// Customize status code / content type
=> new(session.Query<Invoice>().Where(x => x.Id == cmd.InvoiceId))
   { OnFoundStatus = StatusCodes.Status201Created, ContentType = \"application/vnd.api+json\" };

Design notes

  • No codegen plumbing needed. All three types implement IResult; Wolverine.Http's existing ResultWriterPolicy dispatches them naturally.
  • OpenAPI via IEndpointMetadataProvider — Swashbuckle, NSwag, and the Minimal-API built-in OpenAPI generator all pick up correct 200: T/200: T[] + 404 responses without any extra registration.
  • Root namespace Wolverine.Http.Marten (matches existing CompiledQueryWriterPolicy) for discoverability.
  • StreamOne vs StreamAggregate explicitly distinguished in docs: StreamOne is for regular Marten documents (session.Query<T>()), StreamAggregate is for event-sourced aggregates (events.WriteLatest<T>, internally).

Scope boundaries

In scope: StreamOne<T>(IQueryable<T>), StreamMany<T>(IQueryable<T>), StreamAggregate<T>(IDocumentSession, Guid|string), OpenAPI metadata, custom status/content-type, tests, docs.

Deferred to follow-up: a parallel port for Polecat (Wolverine.Http.Polecat), to be opened as a separate PR once this lands.

Files

  • New: StreamOne.cs, StreamMany.cs, StreamAggregate.cs in Wolverine.Http.Marten
  • New: StreamingEndpoints.cs in WolverineWebApi/Marten (the Alba test host project)
  • New: streaming_endpoints.cs in Wolverine.Http.Tests/Marten (12 tests)
  • Modified: docs/guide/http/marten.md (new "Streaming JSON Responses" section with the StreamOne-vs-StreamAggregate distinction called out explicitly)

Test plan

  • StreamOne: returns document + 200 + Content-Length + Content-Type on hit
  • StreamOne: returns 404 on miss
  • StreamOne: respects custom OnFoundStatus (tested at 202)
  • StreamOne: respects custom ContentType (tested with vendor-specific type)
  • StreamMany: returns JSON array
  • StreamMany: returns empty array [] with 200 (not 404) on no match
  • StreamAggregate: returns latest aggregate JSON for a valid stream
  • StreamAggregate: returns 404 for unknown id
  • OpenAPI metadata: StreamOne<T> advertises Produces<T> + 404
  • OpenAPI metadata: StreamMany<T> advertises Produces<IReadOnlyList<T>>
  • OpenAPI metadata: StreamAggregate<T> advertises Produces<T> + 404
  • 20 existing Marten endpoint tests (compiled query writer, aggregate handler workflow) still pass

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits April 17, 2026 08:53
…arten (#1562)

Three new return-types that stream Marten JSON directly to the HTTP
response, avoiding a deserialize/serialize round trip through .NET.
Patterned after Marten.AspNetCore's WriteSingle/WriteArray/WriteLatest
helpers, with correct OpenAPI metadata contributed via
IEndpointMetadataProvider.

- StreamOne<T>(IQueryable<T>): single document via WriteSingle, 404 on miss
- StreamMany<T>(IQueryable<T>): JSON array via WriteArray, empty array on no match
- StreamAggregate<T>(IDocumentSession, id): latest event-sourced aggregate
  via WriteLatest, 404 on unknown stream. Supports both Guid and string ids.

All three implement IResult (picked up by Wolverine.Http's existing
ResultWriterPolicy, no codegen plumbing needed) and IEndpointMetadataProvider
so Swashbuckle / NSwag / Minimal-API OpenAPI all see the right response shape.
OnFoundStatus and ContentType are init-only properties so endpoints can
customize 200 → 201 (for create) or advertise a vendor-specific content type.

Follow-up: replicate this pattern in Wolverine.Http.Polecat (separate PR).

Closes #1562

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The streaming result types originally landed locally in Wolverine.Http.Marten
in this branch. They've since shipped upstream in Marten.AspNetCore 8.31.0
(via JasperFx/marten#4260 — the same types, lifted to live next to the
existing WriteSingle/WriteArray/WriteLatest helpers and usable from any
Minimal-API endpoint, not just Wolverine.Http).

This commit:
- Bumps Marten + Marten.AspNetCore 8.28.0 → 8.31.0 and JasperFx.Events
  1.27.0 → 1.28.1 in Directory.Packages.props
- Deletes the local StreamOne/StreamMany/StreamAggregate types from
  Wolverine.Http.Marten
- Switches the WolverineWebApi sample endpoints and the test file to use
  Marten.AspNetCore.StreamOne/StreamMany/StreamAggregate
- Updates docs/guide/http/marten.md to point at Marten.AspNetCore as the
  source of the types and call out that no Wolverine-specific code is
  required — the types implement IResult and Wolverine.Http dispatches
  them through its existing ResultWriterPolicy

The existing tests carry over unchanged — they pass against the upstream
types because the public surface is identical (same constructors, same
init-only OnFoundStatus/ContentType, same OpenAPI metadata via
IEndpointMetadataProvider).

Verified with `dotnet test`: all 12 streaming_endpoints tests + 20
related Marten tests (compiled_query_writer, using_aggregate_handler_workflow)
pass against the new packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

New helpers to stream Marten JSON with Wolverine.HTTP

1 participant