diff --git a/src/content/docs/dotnet-next/whats-new.mdx b/src/content/docs/dotnet-next/whats-new.mdx index 48652b1..735f0da 100644 --- a/src/content/docs/dotnet-next/whats-new.mdx +++ b/src/content/docs/dotnet-next/whats-new.mdx @@ -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( + "payments", + builder => builder + .AddEventHandler() + .AddEventHandler() // 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(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())); +``` + +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(), ownsHandler: false); + +builder.AddCompositionEventHandler( + sp => sp.GetRequiredService(), + handler => new PollyEventHandler(handler, retryPolicy), + ownsInnerHandler: false +); +``` + +To have the container create and own the handler instead, use the `AddEventHandler()` 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. @@ -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)` 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` — 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> 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(subscriptionId)`. Handlers registered by type still are. +