Wolverine 6.18.0
Wolverine 6.18.0
A security-relevant serialization fix, a startup-fatal codegen fix, a silently-dead-listener fix in RabbitMQ, the first F# saga codegen support of any persistence provider, and the CI split that makes "merge when green" mean something again.
If you use MassTransit interop over a durable listener, take this release. See the first section.
⚠️ Security-relevant: reserved envelope headers could be spoofed through the durable inbox
EnvelopeSerializer wrote the typed envelope properties to the wire format and then appended every Envelope.Headers entry verbatim, with no reserved-key filter — and the appended entries came last. Because the reader parses reserved keys straight back into typed properties, a Headers entry under a reserved key silently overwrote the real property on the next read.
A value in envelope.Headers["tenant-id"] is inert while the envelope is in memory. It stops being inert the moment the envelope crosses the serializer — any durable listener, the inbox/outbox, or the scheduled-message store:
- Something puts
tenant-idintoenvelope.Headers. - The durable inbox persists the envelope; the header is appended after the (null) typed property.
- On read back,
env.TenantIdis set from it.
saga-id reaches another saga's state, and id rewrites Envelope.Id — the inbox's dedupe identity.
This was live, not theoretical. MassTransitEnvelope.TransferData already copies every incoming MassTransit header into envelope.Headers unfiltered (and by assignment, not TryAdd). Any Wolverine app doing MassTransit interop over a durable listener has had this path open. If that describes you, this release is the one to take.
The fix filters reserved keys on the write side, so the typed property stays authoritative and a reserved key sitting in Headers becomes a no-op. causation-id is deliberately not filtered — DeliveryOptions intentionally carries it as a loose header for Wolverine.Marten's OutboxedSessionFactory, and it is never promoted by the reader.
Startup-fatal codegen fix
#3399 — fixed in #3406 — invalid generated class name for batched (array) message types. This one prevents the application from starting.
Fixes
- #3388 (#3400) — refuse a competing Marten daemon under Wolverine-managed event subscription distribution. A
DaemonMode.Solo/HotColddaemon alongside managed distribution is now an actionable startup exception instead of two schedulers quietly fighting over the same shards. - CritterWatch #698 (#3396) —
IAgentRuntime.ApplyRestrictionsAsyncpersisted the restriction and then never dispatched the commands it computed, so pausing an agent had no immediate effect. Reported by @erdtsieck against a live cluster. - #3385 (#3403) — a header-identified saga invoked over a gRPC hop failed with an opaque
Internalstatus. It now returns an actionable diagnostic telling you to put the saga identity on the request DTO. - #3398 (#3404) —
[AsParameters]now rejects unparseable values in collection query parameters, closing the gap left by the scalar fix in #3372. - #3365 (#3412) — the Polecat primary
IEventStorebridge registered twice, soGetServices<IEventStore>()returned the same store instance two times and anything iterating it double-counted. Polecat's ownAddPolecat()had started registeringIEventStoreand Wolverine was still bridging it as well. - #3391 (#3419) — RabbitMQ: a successful eager channel restart never re-consumed. A callback-exception restart could leave an open channel with zero consumers while reporting
State = Connected— a silently dead listener. The listener now defers toReconnectedAsync(), which re-declares and re-consumes. Also pins theConnectionMonitortracking invariant that #3370 fixed but nothing guarded.
OpenAPI
#3380 (#3418) — OpenAPI parameters are now derived from the full binding chain rather than the handler signature alone. Two real defects closed:
- Query/header values bound only by an
After/Finallypostprocessor were omitted from the operation entirely. - Route parameter types were read off resolved binding variables, so they degraded to the route constraint (or
string) whenever the description was assembled before those frames resolved — which is exactly the build-time OpenAPI /openapiCLI path, because ASP.NET caches the first ApiExplorer read.
More importantly, this ships the OpenAPI shape-test harness that was missing. Adding a shape assertion is now one endpoint plus one [Fact], which is why this class of omission kept shipping unnoticed.
New: Azure Service Bus emulator support
#3366 (#3409) — the docs told you to call UseAzureServiceBusTesting(), which only ever existed in Wolverine's own test suite. It is now a real, shipping API:
// standard emulator ports (AMQP 5672, management 5300)
opts.UseAzureServiceBusEmulator();
// or explicit, if you've mapped the emulator elsewhere
opts.UseAzureServiceBusEmulator(connectionString, managementConnectionString);The destructive delete-all-objects cleanup is strictly opt-in (DeleteAllExistingObjectsOnStartup()) — a mistaken call against a real namespace destroys nothing.
CI
#3350 (#3417) — the AWS and Polecat suites chronically blew the 20-minute job cap, so real failures were indistinguishable from timeouts and "merge when green" was unachievable. Both are now split across parallel shards; the slowest is 5m9s against an unchanged 20m cap, and CIPolecat's one-off 30-minute exemption is gone.
Community contributions
F# saga codegen for CosmosDB (#3410) by @thechucklingatom — the first GenerateFSharpCode saga-load support of any Wolverine persistence provider. Marten, RavenDb, and EF Core have none; Cosmos sets the precedent.
Note for F# saga authors generally: your saga type currently needs [<AllowNullLiteral>], because the generated null-guard emits isNull x and F#'s isNull carries a 'T : null constraint. That's a framework-wide constraint this PR was simply the first to surface, and it's tracked upstream in jasperfx#513.
Thanks
@erdtsieck — for #3388, CritterWatch #698, and a steady stream of production-grade bug reports out of a 512-tenant-database deployment that keep finding things our test suites can't.
Kafka record timestamps and headers (#3407, @jakub-petrylak-onerail) is still in review and lands in the next release.
Also in this release
- Marten pinned to 9.15.1 (#3401), which fixes a data-correctness regression in nested-tenant session identity-map sharing.
Everything in this release
Every issue closed and every pull request merged between V6.17.3 and V6.18.0.
| Issue | Pull request | Summary |
|---|---|---|
| #3408 | #3411 | |
| #3399 | #3406 | 🚨 Startup-fatal: invalid generated class name for batched (array) message types |
| #3391 | #3419 | RabbitMQ: a successful eager channel restart never re-consumed — open channel, zero consumers, State = Connected |
| #3388 | #3400 | Refuse a competing Marten daemon under Wolverine-managed subscription distribution |
| #3385 | #3403 | Actionable diagnostic for a header-identified saga over a gRPC hop |
| #3398 | #3404 | [AsParameters]: reject unparseable values in collection query parameters |
| #3380 | #3418 | OpenAPI: derive parameters from the full binding chain + a shape-test harness |
| #3366 | #3409 | Ship a real UseAzureServiceBusEmulator() API |
| #3365 | #3412 | Polecat primary IEventStore bridge registered twice |
| #3350 | #3417 | CI: split the CIAWS and CIPolecat suites across parallel shards |
| CritterWatch #698 | #3396 | ApplyRestrictionsAsync never dispatched the commands it computed |
| — | #3410 | F# saga codegen for CosmosDB (@thechucklingatom) |
| — | #3401 | Bump Marten to 9.15.1 |
Issues opened during this release cycle, not yet fixed
Several were found while fixing the above, and a few are more consequential than what they came out of:
| Issue | Why it's worth knowing about |
|---|---|
| #3421 | The AddOpenApi() pre-start freeze is only half-closed on hybrid hosts. The document goes from "empty" to "Wolverine routes present, every minimal-API route missing" — which looks populated and is therefore more deceptive |
| #3414 | CosmosDB saga persistence has no optimistic concurrency — a blind UpsertItemAsync silently loses updates under concurrent messages |
| #3415 | Every CosmosDB saga lands in one logical partition → 20GB / 10k RU/s ceiling for all saga state |
| #3416 | CosmosDB requires a camelCase serializer policy and nothing documented it |
| #3413 | Test suites pollute each other via the shared wolverine schema — and, underneath it, a real question about whether the durability agent should throw on an unresolvable transport |
| #3420 | OpenAPI: Marten-aggregate-bound route ids render as string, not uuid, on unconstrained routes |
| jasperfx#513 | isNull x codegen forces [<AllowNullLiteral>] on every F# saga, framework-wide |
Full changelog: V6.17.3...V6.18.0