What problem would this solve?
Stream queries currently map to one wire shape: an incrementally-written JSON array (MapStreamQuery). That fits exports and large result sets, but not live feeds — progress of a long-running job, tail-style updates — where the natural consumer is a browser EventSource. SSE gives that consumer automatic reconnection, Last-Event-ID resume semantics, and zero client libraries, but wiring a CQRS stream handler to a text/event-stream response is boilerplate every app writes itself today. The producer side already exists in Postie (IStreamQuery/IStreamQueryHandler/IStreamEndpointDispatcher); only the response shaping is missing.
Proposed API or behavior
// same handler shape as MapStreamQuery — no new mediator concepts
public record TailImportProgress(Guid JobId) : IStreamQuery<ImportProgressEvent>;
app.MapSseQuery<TailImportProgress, ImportProgressEvent>("/imports/{jobId}/progress");
// -> text/event-stream; each yielded item is one SSE event (JSON data payload);
// stream ends when the handler completes; binding/verb semantics match MapStreamQuery
// (QueryMethod method = Get, RequestBinding? binding = null)
Dispatch goes through IStreamEndpointDispatcher, so it is mediator-agnostic like every other mapping. Same pipeline behaviors and enumeration-time activity semantics as MapStreamQuery.
TFM note: net10.0 can use the framework's TypedResults.ServerSentEvents; net8.0/net9.0 would write the (simple) SSE wire format via the downlevel System.Net.ServerSentEvents formatter so the API surface is identical on all TFMs.
Alternatives considered
MapStreamQuery (JSON array): works for progressive data, but has no reconnection/resume story and browsers can't consume it with EventSource; a dropped connection mid-feed loses the stream.
- Chaining on the returned
RouteHandlerBuilder: can't express this — the response shaping happens inside the handler delegate.
- Hand-rolled minimal API endpoint injecting
IStreamQueryDispatcher: the escape hatch that works today, at the cost of re-implementing binding, verb selection and the SSE plumbing per app.
- SignalR/WebSockets: bidirectional machinery for a unidirectional feed; SSE is the right-sized primitive.
Contribution
What problem would this solve?
Stream queries currently map to one wire shape: an incrementally-written JSON array (
MapStreamQuery). That fits exports and large result sets, but not live feeds — progress of a long-running job, tail-style updates — where the natural consumer is a browserEventSource. SSE gives that consumer automatic reconnection,Last-Event-IDresume semantics, and zero client libraries, but wiring a CQRS stream handler to atext/event-streamresponse is boilerplate every app writes itself today. The producer side already exists in Postie (IStreamQuery/IStreamQueryHandler/IStreamEndpointDispatcher); only the response shaping is missing.Proposed API or behavior
Dispatch goes through
IStreamEndpointDispatcher, so it is mediator-agnostic like every other mapping. Same pipeline behaviors and enumeration-time activity semantics asMapStreamQuery.TFM note: net10.0 can use the framework's
TypedResults.ServerSentEvents; net8.0/net9.0 would write the (simple) SSE wire format via the downlevelSystem.Net.ServerSentEventsformatter so the API surface is identical on all TFMs.Alternatives considered
MapStreamQuery(JSON array): works for progressive data, but has no reconnection/resume story and browsers can't consume it withEventSource; a dropped connection mid-feed loses the stream.RouteHandlerBuilder: can't express this — the response shaping happens inside the handler delegate.IStreamQueryDispatcher: the escape hatch that works today, at the cost of re-implementing binding, verb selection and the SSE plumbing per app.Contribution