Skip to content
Merged
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions src/content/docs/dotnet-next/whats-new.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,55 @@ sidebar:

This page tracks changes for the next release of Eventuous.

## Breaking changes

### One handler type per subscription

Adding the same handler type twice to a single subscription now throws `ArgumentException` when the subscription is registered:

```csharp
services.AddSubscription<StreamSubscription, StreamSubscriptionOptions>(
"payments",
builder => builder
.AddEventHandler<PaymentsProjection>()
.AddEventHandler<PaymentsProjection>() // throws
);
```

Both registrations claimed the same container slot, keyed by handler type and subscription id, so the subscription dispatched every event to the same instance twice — silently. The same applies to `AddCompositionEventHandler<THandler, TWrappingHandler>(getWrappingHandler)`, which resolves its inner handler from that slot.

When a subscription genuinely needs two handlers of the same type, register them with the factory or the instance overload, which keep one handler per registration.

## New features

### Subscriptions dispose the handlers their factories build

A handler registered with a factory is no longer in the container, so the subscription owns it and disposes it:

```csharp
builder.AddEventHandler(sp => new ProjectionHandler(sp.GetRequiredService<NpgsqlDataSource>()));
```

If the handler implements `IDisposable` or `IAsyncDisposable`, it is disposed when the subscription is disposed, after the consume pipe filters have drained the messages in flight, with `IAsyncDisposable` preferred when both are implemented. The same applies to an inner handler built by `AddCompositionEventHandler(getInnerHandler, ...)`.

A factory is free to return a handler it didn't create, and disposing that would break the other components using it. New overloads decline ownership for that case:

```csharp
// Owned by the container and possibly shared elsewhere
builder.AddEventHandler(sp => sp.GetRequiredService<SharedHandler>(), ownsHandler: false);

builder.AddCompositionEventHandler<SharedHandler, PollyEventHandler>(
sp => sp.GetRequiredService<SharedHandler>(),
handler => new PollyEventHandler(handler, retryPolicy),
ownsInnerHandler: false
);
```

To have the container create and own the handler instead, use the `AddEventHandler<THandler>()` overload, which registers it keyed by subscription id as before.

Handlers registered by type are created and disposed by the container as before, and handlers passed as an instance stay the caller's to dispose. A wrapping handler produced by `AddCompositionEventHandler` decorates the inner one and is never disposed.


### Multi-stream append

The `IEventWriter` interface now supports appending events to multiple streams in a single operation. Each append specifies a target stream, expected version, and events. Atomicity depends on the store implementation — relational stores use a single transaction, KurrentDB 25.1+ uses native multi-stream append, and the default fallback writes sequentially.
Expand Down Expand Up @@ -109,3 +156,26 @@ await client.SubscribeTyped("Order-123", fromPosition: 0)

See the [SignalR documentation](../infra/signalr) for full details including auto-reconnect behavior and custom hubs.

## Bug fixes

### Handlers registered with a factory no longer collapse into one

`AddEventHandler(Func<IServiceProvider, THandler>)` registered the handler in the container keyed by the subscription id, then resolved it back by `THandler`. When several handlers were added to one subscription through this overload and `THandler` inferred to the same type for each call — which happens naturally when the factories are held in a collection typed `Func<IServiceProvider, IEventHandler>` — every registration after the first silently did nothing, and the subscription ran several copies of the first handler and none of the others:

```csharp
IReadOnlyList<Func<IServiceProvider, IEventHandler>> projectors = [
_ => new FactProjector(),
_ => new VectorProjector(),
_ => new ClusterProjector()
];

// Before: three copies of FactProjector. Now: one of each.
foreach (var projector in projectors) builder.AddEventHandler(projector);
```

Nothing failed and nothing was logged, so with idempotent projectors the duplicate dispatch was invisible and the missing handlers only showed up as missing data.

Factory-created handlers are now kept by the subscription rather than the container, so each registration keeps its own handler. The same fix applies to the `AddCompositionEventHandler` overloads that build the inner handler from a factory.

As a consequence, a handler registered with a factory is no longer resolvable from the container with `GetRequiredKeyedService<THandler>(subscriptionId)`. Handlers registered by type still are.