Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e26224d
chore: update packages
iancooper Jul 9, 2026
4824f09
docs: Add streaming query results spec and ADR 0019
iancooper Jul 9, 2026
0d7ee81
chore: Ignore .DS_Store and .junie/
iancooper Jul 9, 2026
ab306f8
docs: Add task breakdown for streaming query results (spec 012)
iancooper Jul 9, 2026
98334a0
chore: Approve tasks phase for streaming query results (spec 012)
iancooper Jul 9, 2026
c0e5c93
feat: Add structural foundations for streaming query pipeline (T001-T…
iancooper Jul 9, 2026
ece5400
feat: Add StreamQueryHandlerRegistry resolving stream handlers by que…
iancooper Jul 9, 2026
642774d
test: Verify RegisterFromAssemblies scans only IStreamQueryHandler im…
iancooper Jul 9, 2026
0ae74f6
feat: Thread IStreamQueryHandlerRegistry through configuration, build…
iancooper Jul 9, 2026
6f0d37e
feat: Implement PipelineBuilder.BuildStream and IQueryProcessor.Execu…
iancooper Jul 9, 2026
58bdbf5
test: Verify ExecuteStream end-to-end wiring with async factories and…
iancooper Jul 9, 2026
b7f55c0
test: Verify ExecuteStream items are produced lazily with no eager bu…
iancooper Jul 12, 2026
d50117f
test: Verify mid-stream cancellation stops enumeration and propagates…
iancooper Jul 12, 2026
465b216
test: Verify mid-stream exceptions surface unwrapped without TargetIn…
iancooper Jul 12, 2026
1a1148e
test: Verify early stream break releases handler and ends span exactl…
iancooper Jul 12, 2026
7f7e4d0
test: Verify stream query passed to ExecuteAsync throws Configuration…
iancooper Jul 12, 2026
8f926aa
test: Verify ConfigurationException is deferred to first MoveNextAsyn…
iancooper Jul 12, 2026
61d819f
feat: Add stream decorator pipeline support ordered by Step descendin…
iancooper Jul 12, 2026
15e498a
feat: Reject mismatched decorator attributes between stream and sync/…
iancooper Jul 12, 2026
5be4251
test: Document that re-enumeration re-executes handler with fresh pip…
iancooper Jul 12, 2026
82dbc53
feat: Add stream logging decorator wrapping lifecycle with item count…
iancooper Jul 12, 2026
263d51a
feat: Record enumeration faults in stream logging decorator at Error …
iancooper Jul 12, 2026
4187232
test: Document that BuildStream writes a span event per pipeline step…
iancooper Jul 13, 2026
a30d4e4
refactor: Add UseResiliencePipelineStreamAttribute and placeholder ha…
iancooper Jul 13, 2026
ce653fd
feat: Implement UseResiliencePipelineStreamHandler happy path — estab…
iancooper Jul 13, 2026
c47f430
test: Document that retry before first item uses fresh stream with no…
iancooper Jul 13, 2026
334a5c9
test: Document that post-first-item faults propagate without retry (T…
iancooper Jul 13, 2026
64502d7
chore: Mark T020-T024 complete in tasks.md
iancooper Jul 13, 2026
cdcf640
test: Document that each failed establishment attempt disposes its en…
iancooper Jul 13, 2026
e641de6
chore: Mark T025 complete in tasks.md
iancooper Jul 13, 2026
09e7043
test: Document that fallback strategy substitutes an alternate stream…
iancooper Jul 13, 2026
0c692de
chore: Mark T026 complete in tasks.md
iancooper Jul 13, 2026
7a342cb
feat: wire stream handler registry into DI assembly scan (T027)
iancooper Jul 13, 2026
4a02945
feat: add AddStreamHandlers explicit registration to DI builder (T028)
iancooper Jul 13, 2026
de7567f
test: document QueryProcessorBuilder stream registry wiring (T029)
iancooper Jul 13, 2026
155bf32
chore: mark T027-T030 complete in tasks.md
iancooper Jul 13, 2026
060fb69
docs: add streaming queries documentation (T031)
iancooper Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,9 @@ paket-files/

# Benchmark.Net
BenchmarkDotNet.Artifacts/

# macOS
.DS_Store

# JetBrains Junie
.junie/
7 changes: 4 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="10.0.9" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageVersion Include="OpenTelemetry" Version="1.11.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" />
<PackageVersion Include="OpenTelemetry" Version="1.16.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.16.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" />
<PackageVersion Include="Polly" Version="8.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
Expand Down
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,108 @@ IQueryProcessor queryProcessor = QueryProcessorBuilder.With()
Instead of `Activator.CreateInstance`, you can pass any factory `Func<Type, object>` to construct handlers and decorators.

> **Note:** The `Paramore.Darker.SimpleInjector` and `Paramore.Darker.LightInject` packages have been removed as of V5. If you use a third-party DI container, use its built-in adapter for `Microsoft.Extensions.DependencyInjection` and integrate with Darker via the `Paramore.Darker.Extensions.DependencyInjection` package instead.

## Streaming Queries

Darker supports streaming queries that yield results incrementally as `IAsyncEnumerable<TResult>`,
so large result sets or real-time feeds are produced on demand rather than buffered into memory.

### Define a stream query and handler

```csharp
using Paramore.Darker;
using System.Collections.Generic;
using System.Threading;

// TResult is the item type, not the enumerable.
public sealed class GetOrdersStream : IStreamQuery<Order>
{
public string CustomerId { get; }
public GetOrdersStream(string customerId) => CustomerId = customerId;
}

public sealed class GetOrdersStreamHandler : StreamQueryHandler<GetOrdersStream, Order>
{
public override async IAsyncEnumerable<Order> ExecuteAsync(
GetOrdersStream query,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamByCustomerAsync(query.CustomerId, cancellationToken))
yield return order;
}
}
```

### Execute with `await foreach`

```csharp
await foreach (var order in queryProcessor.ExecuteStream(new GetOrdersStream("C42"), cancellationToken))
{
// items arrive as the handler produces them — no buffering
Process(order);
}
```

### Registration with DI (assembly scan)

`AddHandlersFromAssemblies` picks up `IStreamQueryHandler<,>` implementations automatically alongside
sync and async handlers:

```csharp
services.AddDarker()
.AddHandlersFromAssemblies(typeof(GetOrdersStreamHandler).Assembly);
```

### Registration with DI (explicit)

```csharp
services.AddDarker()
.AddStreamHandlers(r => r.Register<GetOrdersStream, Order, GetOrdersStreamHandler>());
```

### Registration without DI

```csharp
var streamRegistry = new StreamQueryHandlerRegistry();
streamRegistry.Register<GetOrdersStream, Order, GetOrdersStreamHandler>();

IQueryProcessor queryProcessor = QueryProcessorBuilder.With()
.Handlers(new HandlerConfiguration(
syncRegistry, handlerFactory, decoratorRegistry, decoratorFactory,
asyncRegistry, handlerFactory, decoratorRegistry, decoratorFactory,
streamRegistry))
.InMemoryQueryContextFactory()
.Build();
```

### Resilience (Polly v8)

Use `[UseResiliencePipelineStream]` — **not** `[RetryableQuery]` or `[FallbackPolicy]`, which apply
only to single-result handlers and throw a `ConfigurationException` on a stream handler:

```csharp
public sealed class GetOrdersStreamHandler : StreamQueryHandler<GetOrdersStream, Order>
{
[UseResiliencePipelineStream(1, "MyRetryPipeline")]
public override async IAsyncEnumerable<Order> ExecuteAsync(GetOrdersStream query,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{ ... }
}
```

Resilience covers **stream establishment and the first item only**. Once the first item has been
yielded to the caller, the pipeline has exited and subsequent faults propagate un-retried. A `Timeout`
strategy therefore bounds *getting the stream started*, not total enumeration time. `Hedging` is not
supported for streams. These are intentional semantics, not limitations to be worked around.

### Documented semantics

| Behaviour | Detail |
|---|---|
| **Laziness** | The framework never buffers the sequence; items are produced on demand. Custom decorators must also avoid buffering (e.g. `ToListAsync`). |
| **Cancellation** | Pass a `CancellationToken` to `ExecuteStream`; cancelling mid-stream stops enumeration and propagates `OperationCanceledException`. |
| **Exceptions mid-stream** | Faults during enumeration propagate with their original stack trace — no `TargetInvocationException` wrapper. |
| **Configuration errors** | A missing or mismatched handler surfaces as `ConfigurationException` from the caller's **first `await foreach` iteration**, not from the `ExecuteStream` call itself (deliberate — resolving eagerly would leak the handler if the caller never enumerates). |
| **Re-enumeration** | Each `await foreach` over the same `IAsyncEnumerable` re-executes the handler with a fresh pipeline. The stream is cold, not cached. To iterate twice over the same data, buffer it yourself (`await ToListAsync()`). |
| **Caller-supplied context** | A `queryContext` passed to `ExecuteStream` is scoped to a **single enumeration**. Concurrent or repeated enumeration is only safe when the processor creates the context (pass `null`). |
| **Legacy attributes** | `[RetryableQuery]` and `[FallbackPolicy]` do **not** apply to streams. Use `[UseResiliencePipelineStream]` for stream resilience. Applying a mismatched attribute throws `ConfigurationException`. |
Loading
Loading