Skip to content

Bump Marten.AspNetCore, WolverineFx.Http.Marten and WolverineFx.Marten#12

Open
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/multi-4c9d68b9e2
Open

Bump Marten.AspNetCore, WolverineFx.Http.Marten and WolverineFx.Marten#12
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/multi-4c9d68b9e2

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jul 18, 2026

Copy link
Copy Markdown
Contributor

Updated Marten.AspNetCore from 9.12.0 to 9.16.0.

Release notes

Sourced from Marten.AspNetCore's releases.

9.16.0

Lot of CritterWatch, couple bug fixes too

What's Changed

Full Changelog: JasperFx/marten@v9.15.4...V9.16.0

9.15.4

What's Changed

New Contributors

Full Changelog: JasperFx/marten@v9.15.3...v9.15.4

9.15.3

This addresses a potential vulnerability from SQL injection via non-string constant in a LINQ Select projection

Not a common usage, but still.

What's Changed

Full Changelog: JasperFx/marten@9.15.2...v9.15.3

9.15.2

Marten 9.15.2

A patch release. Both fixes come out of the same 512-tenant-database production deployment, reported by @​erdtsieck, and both turned out to be worse than the reports described.

Bulk event insert ran a full schema apply on every batch

#​4946fixed in #​4949

The batch BulkInsertEventsAsync overloads opened with Storage.ApplyAllConfiguredChangesToDatabaseAsync() on every call.

That is not a cheap check. It calls Tenancy.BuildDatabases() and runs a full schema delta — partition introspection plus information_schema sweeps — across every database in the store. So a sharded store paid one apply per database, per batch. On the reporting deployment, each ~1,000-event batch was triggering 512 schema applies.

The measured effect: import throughput collapsed to ~17 events/s, against >3,000/s for the streaming overload. A 686k-event tenant projected to roughly 11 hours. The connection pool filled with ~370 backends whose last statement was Weasel's partition-introspection query, which fed directly into the server-wide connection pressure that deployment was already fighting.

That the streaming overload BulkInsertEventStreamAsync has no such call and is fine is the tell: the schema apply was never part of the contract. It was a leftover.

The apply is now:

  • skipped entirely when the effective AutoCreate is None — it is a no-op there by contract, so all that remained was the introspection cost; and
  • otherwise run at most once per database the import actually touches, memoized on IMartenDatabase.Identifier.

One subtlety worth recording, because it is the kind of thing that bites later: the memoized apply deliberately does not take a caller's CancellationToken. The first caller to arrive owns the single in-flight task that every concurrent caller for that database awaits — so binding that shared task to one caller's token would let a single cancelled batch fail sibling batches that were never cancelled. Each caller applies its own token at the await site instead. A schema apply is short and idempotent, so letting it run to completion is the cheaper trade.

Under AutoCreate.None, the event storage must already exist before import. That is the documented contract and it matches the streaming overload — but if you were previously relying on the per-call apply to create it for you under a non-None store, note the change.

The document bulk-insert path (BulkInsertAsync / BulkInsertDocumentsAsync) is unaffected. It routes through the ordinary per-feature EnsureStorageExistsAsync that Weasel already memoizes, not a full-store delta.

Tenant provisioning silently under-provisioned partitions

#​4944fixed in #​4950

AddPartitionToAllTables, and the tenant-provisioning paths built on it, walked the calling store's StoreOptions to decide which tables needed a list partition for a new tenant.

So any tool or host that provisions tenants from a store which doesn't register every document type silently under-provisioned. Document types unknown to the caller never got their partitions — and the tenant then failed with a Postgres 23514 check-constraint violation on first write to the missing partition. Nothing failed at provisioning time; the damage surfaced later, somewhere else.

The workaround was "the provisioning tool must register all document types," which re-creates schema knowledge in a second place and drifts as document types are added.

The sweep is now database-driven: it enumerates tenant list-partitioned tables from the Postgres catalog, so a partially-registered store still provisions every partitioned table it finds.

Scoping is enforced inside the catalog query rather than filtered in memory afterward:

  • Schema — the store's own AllSchemaNames() only. Foreign partitioned tables in a shared database are never touched.
  • Partition shape — LIST strategy, exactly one key column, and that column named tenant_id. This is the filter that matters most, and it is what keeps the sweep off Marten's own non-tenant list partitioning: UseArchivedStreamPartitioning keys mt_events on is_archived, and ByList() keys on its own field. Without it, a "helpful" sweep would start adding tenant partitions to tables partitioned on something else entirely.
  • External management — tables marked ByExternallyManagedListPartitions() are subtracted.

Opt out with SweepPartitionedTablesFromDatabase (default on). No Weasel change was required.

Known limitation, and it is a real one: a document type registered into a schema the calling store has never heard of stays invisible to the schema filter — a store cannot own a schema it does not know exists. Single-schema stores (the default, and the reporting deployment's shape) are fully covered. Closing this properly would need a persisted table list alongside mt_tenant_partitions.


... (truncated)

9.15.1

Patch release for a silent data-correctness regression. If you use ForTenant() on an identity-mapped or dirty-tracked session, upgrade.

Fixed

  • #​4947ForTenant() on an identity session stopped returning tenancy-neutral documents (reported by @​dervagabund, with a repro — thank you). A ForTenant() view of an identity- or dirty-tracked session no longer saw global (tenancy-neutral) documents tracked by the parent session. Since a global document has exactly one row per id for the whole database, LoadAsync through the ForTenant view missed the identity map, went to the database, and returned null for a document that is there. A silent wrong answer, not an error.

    Affected: 9.13.0, 9.14.x, 9.15.0. Introduced by the fix for #​4801, which tenant-scoped the identity map and version tracker for ForTenant sessions. That was correct for conjoined documents — where the same id means a different document per tenant — but it was applied per session rather than per document type, so it also isolated document types that are tenancy-neutral and must be shared.

    Sharing is now decided per document type. A nested ForTenant session shares the parent's identity-map and version-tracker entry for a type only when the storage is identity-mapped, the type is not Conjoined, and the nested session's database is the same instance as the parent's (under database-per-tenant, the same id in another tenant's database is a different document even for a tenancy-neutral type). The isolation introduced by #​4801 is preserved exactly — the Bug_4801 suite still passes, and the new tests include guard rails asserting conjoined documents stay isolated.

Full changelog: JasperFx/marten@9.15.0...9.15.1

9.15.0

Closed issues

  • #​4942 — sharded tenancy: auto-assign never repaired half-provisioned tenants (PR #​4945). findOrAssignTenantDatabaseAsync returned early on an existing assignment row, skipping createPartitionsForTenant + per-tenant event-sequence provisioning — so a tenant whose provisioning was interrupted (assignment committed, partitions missing) failed every write with 23514 forever. Both early-return paths (including a second race-window hole under the advisory lock) now run the same idempotent repair the explicit AddTenantToShardAsync(tenantId, databaseId) overload always ran, guarded to once per process per tenant via the resolution cache.
  • #​4941 — two-day silent projection outage (closed with full mapping). Root cause was #​4942; the invisibility was JasperFx/jasperfx#​506/#​507, fixed in JasperFx 2.27.0 which this release consumes.

Also in this release

  • Bundles the fixed JasperFx.Events.SourceGenerator analyzer (JasperFx/jasperfx#​505) — CS1061 compile break for no-parameterless-ctor aggregates with instance Apply returning the aggregate.
  • Follow-up enhancement filed as #​4944 (database-driven partition sweep via pg_inherits) for the #​4943 provisioning-tool scenario.

Verified against Wolverine (full solution + CoreTests/MartenTests/distribution/Http suites, zero failures) and CritterWatch before publishing. Thanks to @​erdtsieck for the dump-verified root-cause analysis.

9.14.1

Marten 9.14.1 is a patch release focused on a substantial round of LINQ query-translation improvements, plus event-store partitioning, high-water, and AoT fixes, and refreshed Weasel/JasperFx dependencies.

LINQ query translation

This release significantly expands what the LINQ provider can push down to PostgreSQL instead of falling back to slower strategies or throwing:

  • Collection Any(predicate) filters now translate to JSONPath and OR-of-containment strategies, and the old explode/ctid fallback has been replaced by a correlated EXISTS strategy. All() shapes and duplicated array fields moved onto the same EXISTS strategy. The net effect is correct, index-friendlier SQL for nested-collection predicates.
  • Indexing into complex child collections inside Where() clauses is now supported (e.g. x.Children[0].Name == "...").
  • Aggregates over collectionsSum/Min/Max/Average — can now be used inside Where() clauses.
  • Regex.IsMatch() is translated in Where() clauses.
  • IComparable.CompareTo() now works for non-string comparables such as Guid (#​4920), alongside broader CompareTo() coverage, string IsOneOf via the ?| operator, and CollectionIsEmpty via ICollectionAware.
  • GinIndexJsonDataMember() was added for member-scoped expression GIN indexes.

#​4916 — subclass queries now use duplicated fields and the base id

Querying a document subclass and filtering on a Duplicate()'d field or the base-class id previously emitted a JSONB filter (CAST(d.data ->> 'FarmId' as uuid)) instead of the real column, missing the duplicated column and the primary-key index:

o.Schema.For<Animal>().AddSubClass<Cow>().Duplicate(x => x.FarmId);

Query<Cow>().Where(x => x.FarmId == id)  // now: d.farm_id = :p0     (was: CAST(d.data ->> 'FarmId' ...))
Query<Cow>().Where(x => x.Id == id)      // now: d.id = :p0          (was: CAST(d.data ->> 'Id' ...))

A subclass shares its parent's table, so the parent's column-backed members (duplicated fields, the id, the soft-delete flag) are now inherited by the subclass's query member resolution. Querying the parent type was already correct and is unchanged.

Event store, partitioning & daemon

  • #​4924 — hyphenated / GUID tenant ids under UseTenantPartitionedEvents. Registering a tenant whose partition suffix contains a - (so every GUID tenant id) made ApplyAllConfiguredChangesToDatabaseAsync() throw 42601 because the per-tenant CREATE SEQUENCE / DROP SEQUENCE DDL emitted the identifier unquoted. The schema-apply statements are now quoted (matching the quick-append function and the imperative provisioning path), so hyphenated tenants migrate cleanly. Quote — not sanitize — so the append function can still resolve the sequence by its raw suffix.
  • #​4915 — projection coordinator shutdown. The projection coordinator now drains on disposal, and via the Weasel 9.16.3 bump the advisory-lock ObjectDisposedException path latches-and-rethrows so a HotCold cold node's leadership loop terminates instead of re-polling a disposed data source during shutdown.
  • #​4913 — high-water scan under partitioning (JasperFx 2.26.0). Under UseTenantPartitionedEvents the store-global high-water agent was continuously running select max(seq_id) from mt_events, an unfiltered scan that fans out across every tenant partition on every poll. That store-global mark is not used to advance tenant projections (they advance per-tenant), so the recurring scan is now skipped under partitioning; tenant high water is driven by the per-tenant coordinator and poll timer.
  • jasperfx#​502 (#​4922)GetProjectionStatusesAsync now resolves the correct named database.

AoT / trimming

  • #​4917 — corrected AoT annotations in the event graph.
  • The AddEventType / QueryRawEventDataOnly generic-constraint tightening was reversed, and event-mapping construction now routes through the cached GenericFactoryCache while preserving the trimming root (#​4930).

Dependencies

  • Weasel 9.16.3 (#​4932) — advisory-lock disposed-pool fix (marten#​4915).
  • JasperFx 2.26.0 — the #​4913 high-water fix, plus 2.25.0's ShardState.DatabaseIdentifier (jasperfx#​501).

Closed issues

#​4913, #​4915, #​4916, #​4917, #​4924, and jasperfx#​502.

9.14.0

Marten 9.14.0 is the recommended upgrade for all 9.x users. It combines the LINQ SQL-injection security fix (first shipped in 9.13.0) with the fix for the projection-coordinator shutdown race in #​4874 and the accompanying dependency updates.

Beyond the LINQ updates, this made the new Per-Tenant Event Partitioning much more robust as we're testing that in conjunction with a JasperFx client for ludicrous scalability.

🔒 Security — SQL injection in the LINQ provider (GHSA-rfx3-98h7-v3xp)

Several LINQ / tenant-management code paths interpolated a runtime, potentially attacker-influenced value into generated SQL as a single-quoted literal without escaping or parameterization. A value containing a single quote could break out of the literal and inject SQL. The primary vector — a Dictionary<,> indexer key in a Where filter (a common "filter by attribute name" / EAV pattern) — was reported privately with an executed proof-of-concept and enabled filter / multi-tenant authorization bypass and blind data exfiltration.

Fixed sinks (#​4911):

  • DictionaryItemMember — dictionary indexer key, e.g. Where(x => x.Attributes[key] == v)
  • DictionaryContainsKeyFilterDictionary.ContainsKey(key) (Newtonsoft serializer + the Enum branch, which bypass System.Text.Json's quote escaping)
  • SelectParser — a constant string projected through Select(x => new { L = runtimeString })
  • DeleteAllForTenant — tenant id reaching per-tenant projection teardown (now parameterized)
  • DatabaseScopedTenantPartitions — tenant id inlined into partition DDL
  • EventLoader — per-tenant partition-pruning literal (defense-in-depth)

Each sink now escapes embedded single quotes or binds the value as a parameter; regression tests lock down every vector, and a follow-up LINQ-wide audit cleared the rest of the query hot path (full-text search, string-method translations, comparisons, IsOneOf/Contains/subset operators, and patching paths). Affected versions: 7.0.0 – 9.12.0. Also patched in 8.37.4 (8.x line) and 9.13.0.

Reported responsibly by @​svenclaesson — thank you. See advisory GHSA-rfx3-98h7-v3xp (CVE pending assignment).

🛠️ Reliability — projection-coordinator shutdown drain race (#​4874)

On host shutdown, the native HotCold projection coordinator could abort with ObjectDisposedException: 'Npgsql.PoolingDataSource' — the coordinator's leadership poll issued an OpenAsync against an already-disposed data source while tenancy was tearing down. This is the "case B" ordering storm reported against #​4874 (distinct from the async-tenancy foundation laid in #​4907, which did not resolve it).

The fix ships through the dependency updates below, with a Marten-side regression test (Bug_4874_coordinator_drain_ordering, #​4912):

  • JasperFx 2.24.1 (jasperfx#​499/#​500) — ProjectionCoordinatorBase terminates the leadership loop on a disposed data source / wrapped cancellation instead of re-polling.
  • Weasel 9.16.2 (weasel#​349/#​350) — AdvisoryLock guards against a disposed NpgsqlDataSource during shutdown (short-circuits while disposing and treats a disposed-pool ObjectDisposedException as a non-acquire rather than propagating).

⬆️ Dependency updates

  • JasperFx 2.24.0 → 2.24.1
  • Weasel 9.16.1 → 9.16.2
  • Weasel.EntityFrameworkCore 9.2.1 → 9.16.2 (released from its prior version hold now that the Weasel line is published)

Full changelog since 9.13.0

  • #​4911 — SQL injection fix in the LINQ provider (carried into this release)
  • #​4912 — regression test for the #​4874 case-B coordinator-drain ordering storm
  • JasperFx 2.24.1 / Weasel 9.16.2 / EFCore 9.16.2 bump (#​4874 shutdown-race fix)

9.13.0

Security release. Fixes SQL injection in the LINQ provider via unescaped string literals (#​4911).

Several LINQ / tenant-management code paths interpolated a runtime, potentially attacker-influenced value into generated SQL as a single-quoted literal without escaping or parameterization; a value containing a single quote could break out and inject SQL. The primary vector — a Dictionary<,> indexer key in a Where filter — was reported privately with an executed proof-of-concept (filter / multi-tenant authorization bypass, blind exfiltration).

Fixed sinks:

  • DictionaryItemMember — dictionary indexer key
  • DictionaryContainsKeyFilterContainsKey key (Newtonsoft serializer + Enum branch)
  • SelectParser — constant string projected via Select(...)
  • DeleteAllForTenant — tenant id in per-tenant projection teardown (now parameterized)
  • DatabaseScopedTenantPartitions — tenant id in partition DDL
  • EventLoader — per-tenant partition-pruning literal (defense-in-depth)

All 9.x users should upgrade. The 8.x line is fixed in 8.37.4. See advisory GHSA-rfx3-98h7-v3xp.

Commits viewable in compare view.

Updated WolverineFx.Http.Marten from 6.16.0 to 6.20.0.

Release notes

Sourced from WolverineFx.Http.Marten's releases.

6.20.0

Wolverine 6.20.0

Dependency upgrades (critter stack)

  • Marten 9.16.0, Polecat 5.2.0, JasperFx 2.29.0, Weasel 9.17.0 (aligned across the stack).

Multi-tenancy & connection footprint

  • Scope tenant scheduled-job polling to the owning node (GH-3376) — under Wolverine-managed distribution, nodes no longer open a scheduled-job polling connection to every tenant database; the footprint scales with databases, not nodes × databases. Plus daemon tracker-subscription leak hygiene.
  • Polecat primary-store database-per-tenant (GH-3445) — IntegrateWithWolverine() now honors a database-per-tenant Polecat store and reads MainDatabaseConnectionString.
  • DatabaseServerId sourced from DatabaseDescriptor.Port instead of re-parsing (jasperfx#​514).

CritterWatch / connection state

  • Degrade-only connection state for Azure Service Bus, GCP Pub/Sub (GH-3237) and Kafka (GH-3454) listeners — heuristics only degrade to Reconnecting/Disconnected from real SDK callbacks; they never synthesize Connected, and resting state is Unknown.

Kafka

  • Read record timestamps into SentAt and expose record headers on raw-JSON listeners (GH-3407).
  • JsonSerializerOptions on raw-JSON endpoints now actually applies; PublishRawJson mapper registration fixed.
  • Bounded the listener shutdown drain instead of awaiting forever (GH-3434).

Sagas

  • SagaConcurrencyException now inherits JasperFx.ConcurrencyException (GH-3444) — existing OnException<ConcurrencyException>() policies now catch saga concurrency failures.
  • Lightweight RDBMS saga provider treated as a catch-all so Marten/EF Core win precedence in mixed-storage hosts (GH-3443).

Other

  • Ancillary Marten store now registers the event-subscription family interface aliases (GH-3438).
  • F# code generation improvements (#​3437); TrackedSession not-tracked vs not-routed fix (#​3435).
  • Buffered circuit-breaker tests re-scoped to the transport's non-durable contract (GH-3137); durability docs corrections.

6.19.0

CosmosDB

  • Add optimistic concurrency (ETag compare-and-swap) to saga persistence (GH-3414) @​mysticmind
  • CosmosDbConfiguration.PartitionSagasById(): opt-in, saga id becomes the document partition key (GH-3415) @​mysticmind
  • Refuse a CosmosClient whose serializer would drop a saga's id at host start; document the camelCase requirement (GH-3416) @​mysticmind

HTTP / OpenAPI

  • Describe the whole route table on a pre-start ApiExplorer read; minimal API and MVC endpoints were silently omitted (GH-3421) @​mysticmind
  • Type a Marten/Polecat aggregate-id route parameter as uuid instead of falling back to string (GH-3420) @​mysticmind

Durability / persistence

  • Dead letter recovered envelopes whose transport can't be resolved, instead of losing the rest of the batch and rethrowing forever (GH-3413) @​mysticmind
  • Measure and surface per-server database connection budgets: OTel gauges + IWolverineObserver.ConnectionBudget (GH-3397) @​jeremydmiller

Test infrastructure only

  • Stop IntegrationContext from disposing a class fixture it doesn't own; pin ApplicationAssembly in the CoreTests harness (GH-3423) @​jeremydmiller
  • Give the modular monolith fixture its own message storage schema (GH-3413) @​mysticmind
  • Stop using_dynamic_multi_tenancy from poisoning its own next run @​mysticmind

Milestone: https://github.com/JasperFx/wolverine/issues?q=is%3Aissue%20state%3Aclosed%20milestone%3A6.19.0
Full Changelog: JasperFx/wolverine@V6.18.0...V6.19.0

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

#​3408fixed in #​3411

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:

  1. Something puts tenant-id into envelope.Headers.
  2. The durable inbox persists the envelope; the header is appended after the (null) typed property.
  3. On read back, env.TenantId is 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

#​3399fixed 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/HotCold daemon alongside managed distribution is now an actionable startup exception instead of two schedulers quietly fighting over the same shards.
  • CritterWatch #​698 (#​3396) — IAgentRuntime.ApplyRestrictionsAsync persisted 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 Internal status. 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 IEventStore bridge registered twice, so GetServices<IEventStore>() returned the same store instance two times and anything iterating it double-counted. Polecat's own AddPolecat() had started registering IEventStore and 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 to ReconnectedAsync(), which re-declares and re-consumes. Also pins the ConnectionMonitor tracking 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/Finally postprocessor 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 / openapi CLI 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:

... (truncated)

6.17.3

Bug-fix and scale release, following the 6.17.2 community sweep. Every item below came from a community report or a review finding — thank you all.

Closed issues

  • #​3375 — durability metrics polling pinned a connection per database per node (PR #​3384 by @​erdtsieck). Each durability agent ran its own in-phase PeriodicTimer, so at high database counts the metrics polling itself became significant connection pressure. Agents now register their store with a node-wide sequential sweeper that walks the node's databases one at a time across the UpdateMetricsPeriod window — at most one metrics connection in flight per node, regardless of database count. The registration set is re-read every pass, so databases join and leave the sweep as agents start and stop without a restart.
  • #​3332 — CIAWS was disabled by a broken SNS per-tenant LocalStack setup (PR #​3364 by @​Steve-XYZ). Test-only; re-enables the AWS CI job. Partially addresses #​3350 (the CIPolecat half remains open).
  • RabbitMQ listeners ghosted after a broker restart on a channel callback exception (PR #​3370 by @​kconfesor). The agent stayed latched after a channel-only shutdown and was never rebuilt, so a listener came back "connected" but dead. Reviewed specifically against the #​3171/#​3187 channel-only-shutdown work to confirm it does not reintroduce the latched-Disconnected state that #​3187 fixed. Two follow-ups are tracked in #​3391.
  • ProductSupport#​33 — CritterWatch telemetry was caught by tracked-session waits (PR #​3390, reported by @​uniquelau). A monitored host publishes telemetry that a TrackedSession would pick up and then sit waiting on messages the test never sent. The default ignore rule now covers all of INotToBeRouted (agent commands and framework telemetry), with a deliberate carve-out for Acknowledgement / FailureAcknowledgement, which the session's own acknowledgement APIs depend on. If you are on an older version, IgnoreMessagesMatchingType(t => t.CanBeCastTo<INotToBeRouted>()) is the workaround.

Fixes from review

  • Metrics sweeper: unregistration race and a hot-spin guard (PR #​3393, follow-ups from the #​3384 post-merge review). The sweeper removed a registration by URI alone, so when an agent for a database stopped after a replacement agent for the same database had registered — exactly what agent redistribution does — the stopping agent evicted the live registration, and that database silently stopped being polled until the node restarted. Unregistration now removes only the exact registration instance it created. Separately, UpdateMetricsPeriod = TimeSpan.Zero would hot-spin the sweep loop (the pre-#​3384 PeriodicTimer threw); it is now rejected at configuration time, with DurabilityMetricsEnabled = false as the way to turn polling off.

Marten test-helper: PauseThenCatchUpOnMartenDaemonActivity

  • #​3388 — cold first catch-up appeared to stall (PR #​3394, reported by @​uniquelau). Investigated in depth. The reported mechanism — that coordinator.ResumeAsync() does not start never-started shards — does not hold: under Wolverine-managed distribution the coordinator is WolverineProjectionCoordinator, whose ResumeAsync builds the daemon lazily and starts every shard, bypassing agent assignment entirely. The cold path works, and there are now four tests proving it (including with a second subscription-agent consumer sharing the agent family).

    The real defect was a timeout mismatch, and it explains the reported symptom exactly. The stage runs inside a child TrackedSession whose token cancels at TrackedSession.Timeout5 seconds by default — while the catch-up ignored that token and waited on an internal 60-second budget. The session gave up first and left the catch-up envelope started-but-never-finished, which reads as a hang. This is a genuine 6.16 → 6.17 behavior change: the old active ForceAll finished inside 5 seconds; resume-and-wait on a cold daemon or a busy machine does not. The catch-up now honors the session's token and raises an actionable TimeoutException naming the store and pointing at TrackActivity().Timeout(...).

    If you hit this on 6.17.0–6.17.2, raising the tracked-session timeout is the fix.

Docs

  • New page: gRPC + Sagas (PR #​3389, following @​erikshafer's coverage in PR #​3386). gRPC services can start and continue sagas with no gRPC-specific code — the saga identity must be on the message body, because a gRPC method is a thin shim in front of IMessageBus.InvokeAsync<T> and the chain that runs is the handler's. The header-identified gap is tracked as #​3385, with a clear diagnostic planned.
  • Testing guide (PR #​3395): tracked sessions ignore framework telemetry by default as of this release, and — the trap behind #​3388 — Timeout() bounds the whole session including its stages, so a slow stage like PauseThenCatchUpOnMartenDaemonActivity() is capped by the session's 5-second default, not by any budget internal to the stage.

Full changelog: JasperFx/wolverine@V6.17.2...V6.17.3

6.17.2

Community-issue sweep release. Every fix below shipped same-day from issues filed by the community — thank you all.

Closed issues

  • #​3371 — any ApiExplorer read before server start permanently emptied every OpenAPI document (PR #​3373 by @​uniquelau). WolverineApiDescriptionProvider now enumerates the HttpGraph (complete when MapWolverineEndpoints() returns) instead of the start-time EndpointDataSource, so ASP.NET's version-keyed cache can never freeze an empty first read. If you monitor Wolverine hosts with CritterWatch and expose OpenAPI, upgrade to this release (see JasperFx/CritterWatch#​689).
  • #​3374 — [AsParameters] + compound-handler LoadAsync binding the same route variable generated uncompilable code (CS0136/CS0841, host failed at startup) (PR #​3381). Binding frames are now emitted once per chain and re-homed so any second consumer reuses them; both the [FromRoute] and [AsParameters]-parameter variants are covered. The related OpenAPI gap (route params bound only by LoadAsync missing from the operation) is tracked as #​3380.
  • #​3372 — [AsParameters] query binder silently ignored unparseable values (PR #​3379). New opt-in WolverineHttpOptions.RejectUnparseableQueryValues: a present-but-unparseable query value short-circuits with a 400 ProblemDetails naming the parameter, matching ASP.NET minimal APIs; missing values keep their initializer in both modes. The default flips to strict in Wolverine 7.0.
  • #​3368 — gRPC server-side tenant-id detection (PRs #​3369 by @​erikshafer + #​3382). The server now reads back what the client interceptor stamps: envelope propagation onto the scoped IMessageContext, plus a full ITenantDetectionPolicies-style mirror (opts.TenantId.IsRequestHeaderValue(...), IsClaimTypeNamed(...), DetectWith<T>()) that sets the codegen tenant variable Marten/Polecat session frames consume — with a zero-config default when the client stamps tenant-id. New docs page: gRPC multi-tenancy.
  • #​3375 (docs half) — documented DurabilityMetricsEnabled = false and raising UpdateMetricsPeriod as mitigations for metrics-polling connection load at high tenant-database counts (PR #​3378). The per-node sweeper implementation is in progress on the issue.

Dependency bumps

  • Marten 9.15.0 (sharded-tenancy provisioning repair, marten#​4942) and JasperFx 2.27.0 (daemon block observability, jasperfx#​506/#​507) — the fixes from the marten#​4941 silent-outage incident (PR #​3383).

6.17.1

Wolverine 6.17.1 is a bug-fix release covering EF Core outbox enlistment gaps in Wolverine.Http, persistence provider resolution, HTTP route parameter binding, multi-tenancy message store roles, and a RavenDB startup race. It also upgrades the Marten dependency to 9.14.1.

EF Core & persistence

  • HTTP endpoints that inject a DbContext and cascade messages only through a tuple return are now enlisted in the EF Core outbox, so cascaded messages are no longer sent before the transaction commits when using Lightweight mode (#​3358, #​3362)
  • Wolverine.Http endpoints that persist entities through storage actions (IStorageAction<T> / storage side effects) are likewise enlisted in the EF Core outbox in Lightweight mode (#​3353, #​3357)
  • When both EF Core and Marten (or another catch-all provider like RavenDb) are registered, the selective EF Core persistence provider is now evaluated first regardless of registration order, so DbContext-based handlers get the correct transactional middleware (#​3359, #​3361)
  • MessageStoreRole.Ancillary is now honored for tenanted message stores (static tenants and master-table tenancy) instead of silently reporting Main (#​3351), with the registration behavior now covered by tests across PostgreSQL, SQL Server, SQLite, MySQL, and Oracle

HTTP

  • [FromRoute(Name = "...")] is now honored on plain endpoint method parameters (previously only inside [AsParameters] types), enabling route segments like {journey-id} that aren't valid C# identifiers (#​3356 — thanks to @​outofrange-consulting!)

RavenDB

  • Fixed a node sequence startup race that could cause duplicate node assignments when multiple nodes started concurrently (#​3352)

Dependencies

  • Marten upgraded to 9.14.1, which brings a substantial round of LINQ query-translation improvements plus event-store partitioning, high-water, and AoT fixes (#​3363)

Documentation

  • Corrected the HTTP QUERY verb documentation: transactional middleware is applied based on a chain's dependencies (e.g. taking an IDocumentSession or DbContext), not the HTTP verb (#​3355, #​3360)

6.17.0

Why is this such a big release? Because @​jeremydmiller went on a 3 night vacation and the community decided to throw in issues and pull requests left and right!

A big theme was filling in the remaining gaps of "Name Broker" and "Broker per Tenant" support in every external messaging transport where it made sense to add that rather than just being Rabbit MQ, Azure Service Bus, and hit and miss everywhere else. We also added HTTP QUERY support.

What's Changed

Commits viewable in compare view.

Updated WolverineFx.Marten from 6.16.0 to 6.20.0.

Release notes

Sourced from WolverineFx.Marten's releases.

6.20.0

Wolverine 6.20.0

Dependency upgrades (critter stack)

  • Marten 9.16.0, Polecat 5.2.0, JasperFx 2.29.0, Weasel 9.17.0 (aligned across the stack).

Multi-tenancy & connection footprint

  • Scope tenant scheduled-job polling to the owning node (GH-3376) — under Wolverine-managed distribution, nodes no longer open a scheduled-job polling connection to every tenant database; the footprint scales with databases, not nodes × databases. Plus daemon tracker-subscription leak hygiene.
  • Polecat primary-store database-per-tenant (GH-3445) — IntegrateWithWolverine() now honors a database-per-tenant Polecat store and reads MainDatabaseConnectionString.
  • DatabaseServerId sourced from DatabaseDescriptor.Port instead of re-parsing (jasperfx#​514).

CritterWatch / connection state

  • Degrade-only connection state for Azure Service Bus, GCP Pub/Sub (GH-3237) and Kafka (GH-3454) listeners — heuristics only degrade to Reconnecting/Disconnected from real SDK callbacks; they never synthesize Connected, and resting state is Unknown.

Kafka

  • Read record timestamps into SentAt and expose record headers on raw-JSON listeners (GH-3407).
  • JsonSerializerOptions on raw-JSON endpoints now actually applies; PublishRawJson mapper registration fixed.
  • Bounded the listener shutdown drain instead of awaiting forever (GH-3434).

Sagas

  • SagaConcurrencyException now inherits JasperFx.ConcurrencyException (GH-3444) — existing OnException<ConcurrencyException>() policies now catch saga concurrency failures.
  • Lightweight RDBMS saga provider treated as a catch-all so Marten/EF Core win precedence in mixed-storage hosts (GH-3443).

Other

  • Ancillary Marten store now registers the event-subscription family interface aliases (GH-3438).
  • F# code generation improvements (#​3437); TrackedSession not-tracked vs not-routed fix (#​3435).
  • Buffered circuit-breaker tests re-scoped to the transport's non-durable contract (GH-3137); durability docs corrections.

6.19.0

CosmosDB

  • Add optimistic concurrency (ETag compare-and-swap) to saga persistence (GH-3414) @​mysticmind
  • CosmosDbConfiguration.PartitionSagasById(): opt-in, saga id becomes the document partition key (GH-3415) @​mysticmind
  • Refuse a CosmosClient whose serializer would drop a saga's id at host start; document the camelCase requirement (GH-3416) @​mysticmind

HTTP / OpenAPI

  • Describe the whole route table on a pre-start ApiExplorer read; minimal API and MVC endpoints were silently omitted (GH-3421) @​mysticmind
  • Type a Marten/Polecat aggregate-id route parameter as uuid instead of falling back to string (GH-3420) @​mysticmind

Durability / persistence

  • Dead letter recovered envelopes whose transport can't be resolved, instead of losing the rest of the batch and rethrowing forever (GH-3413) @​mysticmind
  • Measure and surface per-server database connection budgets: OTel gauges + IWolverineObserver.ConnectionBudget (GH-3397) @​jeremydmiller

Test infrastructure only

  • Stop IntegrationContext from disposing a class fixture it doesn't own; pin ApplicationAssembly in the CoreTests harness (GH-3423) @​jeremydmiller
  • Give the modular monolith fixture its own message storage schema (GH-3413) @​mysticmind
  • Stop using_dynamic_multi_tenancy from poisoning its own next run @​mysticmind

Milestone: https://github.com/JasperFx/wolverine/issues?q=is%3Aissue%20state%3Aclosed%20milestone%3A6.19.0
Full Changelog: JasperFx/wolverine@V6.18.0...V6.19.0

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

#​3408fixed in #​3411

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:

  1. Something puts tenant-id into envelope.Headers.
  2. The durable inbox persists the envelope; the header is appended after the (null) typed property.
  3. On read back, env.TenantId is 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

#​3399fixed 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/HotCold daemon alongside managed distribution is now an actionable startup exception instead of two schedulers quietly fighting over the same shards.
  • CritterWatch #​698 (#​3396) — IAgentRuntime.ApplyRestrictionsAsync persisted 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 Internal status. 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 IEventStore bridge registered twice, so GetServices<IEventStore>() returned the same store instance two times and anything iterating it double-counted. Polecat's own AddPolecat() had started registering IEventStore and 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 to ReconnectedAsync(), which re-declares and re-consumes. Also pins the ConnectionMonitor tracking 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/Finally postprocessor 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 / openapi CLI 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:

... (truncated)

6.17.3

Bug-fix and scale release, following the 6.17.2 community sweep. Every item below came from a community report or a review finding — thank you all.

Closed issues

  • #​3375 — durability metrics polling pinned a connection per database per node (PR #​3384 by @​erdtsieck). Each durability agent ran its own in-phase PeriodicTimer, so at high database counts the metrics polling itself became significant connection pressure. Agents now register their store with a node-wide sequential sweeper that walks the node's databases one at a time across the UpdateMetricsPeriod window — at most one metrics connection in flight per node, regardless of database count. The registration set is re-read every pass, so databases join and leave the sweep as agents start and stop without a restart.
  • #​3332 — CIAWS was disabled by a broken SNS per-tenant LocalStack setup (PR #​3364 by @​Steve-XYZ). Test-only; re-enables the AWS CI job. Partially addresses #​3350 (the CIPolecat half remains open).
  • RabbitMQ listeners ghosted after a broker restart on a channel callback exception (PR #​3370 by @​kconfesor). The agent stayed latched after a channel-only shutdown and was never rebuilt, so a listener came back "connected" but dead. Reviewed specifically against the #​3171/#​3187 channel-only-shutdown work to confirm it does not reintroduce the latched-Disconnected state that #​3187 fixed. Two follow-ups are tracked in #​3391.
  • ProductSupport#​33 — CritterWatch telemetry was caught by tracked-session waits (PR #​3390, reported by @​uniquelau). A monitored host publishes telemetry that a TrackedSession would pick up and then sit waiting on messages the test never sent. The default ignore rule now covers all of INotToBeRouted (agent commands and framework telemetry), with a deliberate carve-out for Acknowledgement / FailureAcknowledgement, which the session's own acknowledgement APIs depend on. If you are on an older version, IgnoreMessagesMatchingType(t => t.CanBeCastTo<INotToBeRouted>()) is the workaround.

Fixes from review

  • Metrics sweeper: unregistration race and a hot-spin guard (PR #​3393, follow-ups from the #​3384 post-merge review). The sweeper removed a registration by URI alone, so when an agent for a database stopped after a replacement agent for the same database had registered — exactly what agent redistribution does — the stopping agent evicted the live registration, and that database silently stopped being polled until the node restarted. Unregistration now removes only the exact registration instance it created. Separately, UpdateMetricsPeriod = TimeSpan.Zero would hot-spin the sweep loop (the pre-#​3384 PeriodicTimer threw); it is now rejected at configuration time, with DurabilityMetricsEnabled = false as the way to turn polling off.

Marten test-helper: PauseThenCatchUpOnMartenDaemonActivity

  • #​3388 — cold first catch-up appeared to stall (PR #​3394, reported by @​uniquelau). Investigated in depth. The reported mechanism — that coordinator.ResumeAsync() does not start never-started shards — does not hold: under Wolverine-managed distribution the coordinator is WolverineProjectionCoordinator, whose ResumeAsync builds the daemon lazily and starts every shard, bypassing agent assignment entirely. The cold path works, and there are now four tests proving it (including with a second subscription-agent consumer sharing the agent family).

    The real defect was a timeout mismatch, and it explains the reported symptom exactly. The stage runs inside a child TrackedSession whose token cancels at TrackedSession.Timeout5 seconds by default — while the catch-up ignored that token and waited on an internal 60-second budget. The session gave up first and left the catch-up envelope started-but-never-finished, which reads as a hang. This is a genuine 6.16 → 6.17 behavior change: the old active ForceAll finished inside 5 seconds; resume-and-wait on a cold daemon or a busy machine does not. The catch-up now honors the session's token and raises an actionable TimeoutException naming the store and pointing at TrackActivity().Timeout(...).

    If you hit this on 6.17.0–6.17.2, raising the tracked-session timeout is the fix.

Docs

  • New page: gRPC + Sagas (PR #​3389, following @​erikshafer's coverage in PR #​3386). gRPC services can start and continue sagas with no gRPC-specific code — the saga identity must be on the message body, because a gRPC method is a thin shim in front of IMessageBus.InvokeAsync<T> and the chain that runs is the handler's. The header-identified gap is tracked as #​3385, with a clear diagnostic planned.
  • Testing guide (PR #​3395): tracked sessions ignore framework telemetry by default as of this release, and — the trap behind #​3388 — Timeout() bounds the whole session including its stages, so a slow stage like PauseThenCatchUpOnMartenDaemonActivity() is capped by the session's 5-second default, not by any budget internal to the stage.

Full changelog: JasperFx/wolverine@V6.17.2...V6.17.3

6.17.2

Community-issue sweep release. Every fix below shipped same-day from issues filed by the community — thank you all.

Closed issues

  • #​3371 — any ApiExplorer read before server start permanently emptied every OpenAPI document (PR #​3373 by @​uniquelau). WolverineApiDescriptionProvider now enumerates the HttpGraph (complete when MapWolverineEndpoints() returns) instead of the start-time EndpointDataSource, so ASP.NET's version-keyed cache can never freeze an empty first read. If you monitor Wolverine hosts with CritterWatch and expose OpenAPI, upgrade to this release (see JasperFx/CritterWatch#​689).
  • #​3374 — [AsParameters] + compound-handler LoadAsync binding the same route variable generated uncompilable code (CS0136/CS0841, host failed at startup) (PR #​3381). Binding frames are now emitted once per chain and re-homed so any second consumer reuses them; both the [FromRoute] and [AsParameters]-parameter variants are covered. The related OpenAPI gap (route params bound only by LoadAsync missing from the operation) is tracked as #​3380.
  • **#​3372 — [AsParameters] query binder s.....

Description has been truncated

Bumps Marten.AspNetCore from 9.12.0 to 9.16.0
Bumps WolverineFx.Http.Marten from 6.16.0 to 6.20.0
Bumps WolverineFx.Marten from 6.16.0 to 6.20.0

---
updated-dependencies:
- dependency-name: Marten.AspNetCore
  dependency-version: 9.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: WolverineFx.Http.Marten
  dependency-version: 6.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: WolverineFx.Marten
  dependency-version: 6.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added .NET Pull requests that update .NET code dependencies Pull requests that update a dependency file labels Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file .NET Pull requests that update .NET code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants