Skip to content

Bump WolverineFx from 5.24.0 to 6.44.0 - #757

Open
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/src/DataFacade/WolverineFx-6.44.0
Open

dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/nuget/src/DataFacade/WolverineFx-6.44.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 1, 2026

Copy link
Copy Markdown
Contributor

Updated WolverineFx from 5.24.0 to 6.44.0.

Release notes

Sourced from WolverineFx's releases.

6.44.0

This release came overwhelmingly from outside the core team. Six people outside JasperFx reported or fixed
the work in it, including three who sent the pull request themselves, and one report that reconstructed a
production cluster failure in enough detail to find two defects nobody had suspected.

Thank you

@​alesdvorakcz (Aleš Dvořák) Found and fixed the EF Core mark-as-handled identity bug (#​4703 / GH-4701) — a fanned-out message under MessageIdentity.IdAndDestination had every destination's inbox row retired by the first handler to commit, losing the copies whose handlers had not run, with no dead letter and no log. Also reported GH-4704.
@​ayuksekkaya (Ali Yuksekkaya) Two OpenAPI accuracy PRs (#​4722, #​4724) and the ten-variant reproduction behind GH-4712 — declaratively loaded entities that were never saved.
@​thomhurst (Tom Longhurst) Found and fixed GH-4729 (#​4730): Azure Service Bus dead-letter moves passed a null propertiesToModify, so poison messages silently never reached $DeadLetterQueue. Bisected across 27 releases and identified why the existing tests were blind to it.
@​erdtsieck GH-4718 — see below. One of the best bug reports this project has received.
@​net0well GH-4699: TypeLoadMode.Static ignored DisableConventionalDiscovery().
@​alexandrefresnais GH-4700: a scheduled retry ran on a node that does not own the partition slot.

The agent control queue stall (GH-4718)

@​erdtsieck reported 5.4–6.7 MB agent commands being rejected on a sharded multi-tenant cluster — 512
databases, ~2,200 tenants, 37,000–58,000 agents — alongside agent assignment stalling for 30–60 minutes
after every node change, and explicitly marked the causal link between the two as a hypothesis rather than
a finding. It held, and the report's detail is what made both halves findable:

  • Stops were the one agent command never chunked. Starts and reassignments chunk by
    AgentStartBatchSize; stops took a destination's whole set in one envelope. A node shedding 7,592 agents
    emitted a single ~6 MB command that the destination refuses to read at all. This is also why lowering
    AgentStartBatchSize changed nothing — that knob had never touched the stop path.
  • One unreadable control queue row killed a node's entire control queue. The poller and the
    expired-row cleanup share one transaction, so the failed read rolled back the very expiry that would have
    cleared the offending row. The node then re-read it every poll and received no control message at all,
    which is why the leader reported "confirmed 0 of N" for chunk after correctly-sized chunk.
  • Both halves now name themselves in the logs — the row, its message type, its size and the node it was
    addressed to.

Fixes

  • GH-4701 / GH-4705 — mark-as-handled inside the caller's transaction now matches the whole inbox
    identity rather than the id alone, on EF Core (#​4703) and on Marten, Polecat and Fisher (#​4725). Both
    also pick up the partition-aware statement, so a redelivered row can be retired under
    EnableInboxPartitioning.
  • GH-4712 / GH-4717 — an entity loaded through [Entity], [All], [FirstOrDefault], [Queryable]
    or a query plan now enrolls the transaction. Mutating one and relying on the transactional middleware
    previously saved nothing at all, on EF Core with no diagnostic. GH-4717 closes the same gap for every
    other provider.
  • GH-4718 — see above.
  • GH-4729 — Azure Service Bus dead-letter moves no longer pass a null propertiesToModify.
  • GH-4700 — a promoted scheduled retry is settled by partition slot ownership rather than by whether
    the promoting node happens to be able to build a local circuit.
  • GH-4699 — the static handler registry honors DisableConventionalDiscovery().
  • GH-4716 — [Transactional] no longer reports a chain as transactional when no provider claimed it
    and no transaction was applied. HTTP endpoints affected by this stop advertising a 409 they cannot raise.
    ... (truncated)

6.43.0

A dependency release. The store pins had been queued behind two releases because every store's current version takes Weasel 9.36.0 as a floor, and 9.36 carried an Oracle regression. That is fixed upstream now, so all three move together.

from to
Marten (+ AspNetCore, Newtonsoft) 9.35.0 9.41.0
Polecat 5.29.0 5.33.1
Fisher 1.10.0 1.14.0
Weasel (all seven packages) 9.35.1 9.38.0

JasperFx stays at 2.76.1.

⚠️ Read this before upgrading: your next migration may emit DDL it never emitted before

This is why 6.43.0 is a minor rather than a patch. Nothing in Wolverine's own public API changed, but crossing six Weasel releases changes what a migration does:

  • A computed column whose definition changed now drops and recreates the indexes and foreign keys that depend on it (Weasel 9.36). That migration previously failed outright on SQL Server and silently dropped the index on PostgreSQL, so the new behaviour is the correct one — but it is new DDL.
  • Identity is now compared as a column difference (Weasel 9.38). A model that declares an identity column against one that lacks it will finally report drift and emit a MODIFY.
  • SQL Server 2025 users: JSON indexes were read as having no columns, so every apply emitted a DROP INDEX for them. Fixed.
  • Oracle users: descending indexes and co-migrated views never settled, and a failed migration used to strand the global advisory lock. Both fixed.

Oracle: the node tables stop drifting

Two fixes in Wolverine itself, both needed to work with Weasel 9.38.0.

Column defaults are no longer hand-escaped. Wolverine wrote every Oracle timestamp default as SYSTIMESTAMP AT TIME ZONE ''UTC'', with the quotes already doubled, because Weasel used to paste DDL verbatim into an EXECUTE IMMEDIATE '...' literal. Weasel 9.36 escapes literals itself, so both sides escaped and the executed default became AT TIME ZONE ''UTC'' — not a time zone region, and every table create failed with ORA-01882.

Identity columns are declared through AutoIncrement() rather than in the type string. wolverine_nodes.node_number and the node-record table's id were declared as NUMBER(10) GENERATED BY DEFAULT AS IDENTITY — with the clause written into the type. Oracle does not store it there: the catalog reports the type as plain NUMBER(10) and carries identity separately. The model could never equal what came back, so every migration emitted an ALTER … MODIFY that Oracle refuses with ORA-01031.

That type string was a workaround rather than an oversight — AutoIncrement() emitted the identity clause after the null constraint, which is ORA-03076, so it could not create a table at all. Fixed upstream in weasel#​667 along with two related defects: identity was never read back from ALL_TAB_COLUMNS.IDENTITY_COLUMN, and a schema containing an identity column could not be dropped at all, because the teardown tried to drop the system-generated sequence behind it.

Existing Oracle deployments should settle rather than migrate: the column is already NUMBER(10) identity in the database, and 9.38.0 now reads that back, so the model matches it.

Verification

Built pinned the way CI builds it (-c Release -f net9.0), 0 errors and 0 warnings, then 17 database suites locally across PostgreSQL, SQL Server, MySQL, Oracle and SQLite — including Marten, Polecat, Fisher, claim check and leader election on four engines. OracleTests went from 181 failing to 203 passing.

6.42.0

A release driven almost entirely by what users reported. Five of the durability fixes below came from one reporter working a multi-tenant deployment hard enough to find them, and the pattern running through them is the same: a failure that belonged to one tenant was being paid for by every tenant, or was not being reported at all.

⚠️ Read this first: a durable publish that cannot be persisted now fails the caller

GH-4662. PublishAsync/ScheduleAsync made outside a handler and outside any transaction, to a local queue made durable by UseDurableLocalQueues(), used to return without an error when the message store was unreachable. Four attempts inside ~400 ms, one line at Information, and the message was gone — not even recovered after the database came back, because no inbox row was ever written.

The cause was the shape of the retry rather than the retry itself. RetryBlock.PostAsync runs one attempt inline and posts the rest to its own worker, so the task the caller awaits completes the moment the first attempt fails. That is the right shape once an envelope is durable somewhere and recovery can find it; it is the wrong shape for the write that makes it durable, because there is no other copy.

DurableWriteRetry keeps the same 50/100/250 ms budget, awaits it inline, and lets the last exception reach the caller.

Who is affected is narrower than it sounds. If you enlist a real outbox (EF Core, Marten, DatabaseEnvelopeTransaction) nothing changes — that path never reached this code. Inside a handler nothing new escapes either; the publish is buffered and a failure is still caught, logged and recorded as a discard, just at Error with a real tracking event instead of a buried Information line. Only the un-transacted publish from outside a handler can now throw.

One tenant's outage stops costing you every tenant

  • A tenant-only inbox failure no longer pauses the listener for everyone (GH-4658), including the single-store batch path that the first fix missed (GH-4659).
  • A deferred tenant-scoped failure no longer spins. It was being redelivered with no delay, so one tenant's outage turned into a hot loop. There is now a per-tenant write brake.
  • An envelope whose completion could not be written no longer stays owned by the live node until that node stops (GH-4664) — the completion retries until the store returns.
  • PostgreSQL: the envelope tables' timestamp default is now a true UTC instant (GH-4663). It depended on the session's time zone, so InboxStaleTime handed back envelopes that were still running. ⚠️ Changing a column default does not migrate already-deployed tables — see the upgrade notes.
  • Documented what actually happens to a durable listener when one tenant's database is down (GH-4660).

Partitioning

A scheduled message no longer runs on a node that does not own the slot (GH-4673). Under global partitioning a scheduled envelope took the local shortcut regardless of slot ownership, so two nodes could process the same group id concurrently — the exact thing partitioning exists to prevent. A promoted scheduled envelope also no longer still reports its status as Scheduled.

HTTP

Stream large multipart uploads through a MultipartReader endpoint parameter, contributed by @​erdtsieck (#​4674) — no buffering the whole upload to handle it. F# code generation emits the new frame too, and the F# codegen coverage is now gated against a committed baseline so a new unsupported frame cannot slip in unnoticed.

Diagnostics, metrics and operations

  • Dead letters from local queues are counted again. Every local:// destination was being classified as System traffic, so a user's own local queue never reached the per-type/per-tenant DeadLetters metric (GH-4665).
  • Scheduled-message listings carry the envelope's tenant (GH-4666).
  • BrokerResource.Check no longer reports a thrown check as a missing resource (GH-4693), which was hiding the real cause behind "Missing known broker resources".
  • A projection agent that can never start is no longer retried forever (GH-4676), using ShardStartException.Reason.
  • SQLite: SqliteNodePersistence honors the table prefix (GH-4668); node shutdown failed outright on a prefixed store.
  • SQL Server tenant stores start against a named-instance connection string (GH-4685).

Dependencies

  • JasperFx 2.76.1 — carries the named-instance fix above.
  • Weasel stays at 9.35.1. The upgrade to 9.37.0 was prepared and deliberately held back: it regresses Oracle, where a node_number column that is already GENERATED BY DEFAULT AS IDENTITY is read back as drift, so every migration emits an ALTER ... MODIFY that Oracle refuses. It will ship once that is fixed upstream.

Thank you

@​framos-varajo Reported five of the durability issues above — GH-4658, 4659, 4662, 4663 and 4664 — with repros. Most of this release is theirs.
@​erdtsieck (Anne Erdtsieck-Wijnen) Contributed the MultipartReader streaming upload support in #​4674
@​alexandrefresnais Reported the global-partitioning scheduled-message ownership bug, GH-4673
@​frankvdb7 Reported the SQL Server named-instance startup failure, GH-4685
... (truncated)

6.41.0

28 commits since V6.40.0. Minor rather than patch because of the new EF Core and topology features below — and because one change is breaking.

⚠️ Breaking change

AutoApplyTransactions() now fails the chain build when two persistence providers can own it (#​4631). A handler taking both a DbContext and a Marten/Polecat/Fisher IDocumentSession matched two providers, and AutoApplyTransactions applied neither — no transaction support, no warning, no log line, and whatever the un-owned store buffered was discarded when the scope closed. [Transactional] on the same handler took the first ordered provider instead, so the document session was never saved. Both outcomes were silent and the handler reported success.

This stops an application that starts today. One that writes through two stores from a single handler has been losing one of those writes all along; it will now refuse to build until the handler says which store owns the transaction. Add [Transactional(typeof(X))] or [Storage(typeof(X))], or [NonTransactional] to opt out. Two-provider atomicity remains unsupported — the designated store commits, the other does not. Saga chains are exempt.

Entity Framework Core

  • Lightweight mode has a real outbox (#​4630). Cascaded messages from a Lightweight message handler now enlist in the same SaveChangesAsync() as the business write, and PublishDomainEventsFromEntityFrameworkCore() — inert in Lightweight before — runs on every path.
  • Declarative EfCoreOps set-based operations (#​4629), which force the eager transaction they need rather than failing quietly.
  • Scraped domain-event envelopes are flushed before the Eager commit (#​4628).
  • Storage actions and conjoined tenancy mean what they say (#​4611, #​4612, #​4613). Declaring a return type as Update<T> or Store<T> rather than IStorageAction<T> used to silently save nothing.
  • The conjoined tenant filter composes with your own query filter (#​4632) instead of replacing it — EF 9 discarded the second filter, EF 10 throws.
  • Four coverage gaps closed (#​4635), including the first tests for a non-flat EF model.

Multi-tenancy

  • Envelope.TenantId is normalised through TenantIdStyle (#​4640). MessageContext.TenantId always applied the style and Envelope.TenantId never did, so under ForceLowerCase a handler saw "RED" on the envelope beside "red" on the context. Polecat and Fisher read the envelope, so they wrote the raw casing — invisible on SQL Server's case-insensitive collation, and on Fisher's BINARY collation the row was unreachable.
  • A conjoined tenancy battery that runs against Marten, Polecat and Fisher (#​4634) — which is what exposed the above.
  • Fisher's OutboxedSessionFactory resolves the Main store lazily (#​4633).

Transports and topology

  • A partitioned topology can declare its own grouping rules, additively rather than authoritatively (#​4616) — thanks @​erdtsieck.
  • Google Pub/Sub shares one subscription across nodes by default (#​4615), rather than a subscription per node.
  • Kafka replay imitates the endpoint that really listens (#​4620), taking the replay group from a ListenToKafkaTopics group and stamping the topic as Destination — thanks @​lahma.

Durability and correctness

  • EnqueueDirectlyAsync retires the inbox row it forwards (#​4645). A delayed message sent from a handler to a database-backed queue that another node owns was silently destroyed: the forwarding node left an Incoming row behind, and the owning node's anti-duplicate probe then deleted the queue row it had just written. Nothing logged, nothing dead-lettered. Reported with a full four-step reconstruction by @​smoqmilus.
  • EndpointCollection._listeners is no longer a plain Dictionary (#​4646). A failover handing one node several exclusive listener agents at once had up to ten threads writing it while every send read it; a Dictionary corrupted mid-resize is poisoned permanently, and the node never routed again until the pod was replaced. Also reported by @​smoqmilus.
  • Messages no longer inherit a stale ambient Activity. Recurring occurrences (#​4647), inline receivers (#​4649) and the agent plumbing loops (#​4650) were parenting every message under whatever Activity.Current happened to be live when the agent or listener started.
  • A handler chain resolves its pre-generated type by full name (#​4625) — thanks @​skoging.
  • Batching honors a batch handler registered with AddMessageHandler.

Also

  • Upgraded to Weasel 9.35.1, finishing the batched-query registration (#​4624).
  • Fixed a CritterWatch link in the RabbitMQ dead letter queues guide — thanks @​jakobt.

Contributors

Thanks to @​erdtsieck, @​jakobt, @​lahma, @​skoging and @​smoqmilus.

6.40.0

51 commits since V6.39.1. A minor release rather than a patch, because this wave carries new capability and not only fixes.

New capability

  • Capacity-aware agent assignment (#​3959) — agent distribution now accounts for node capacity rather than treating every node as interchangeable. Landed as @​mlh758's #​4297, then hardened in #​4596 and #​4598 to require a real load monitor, stop a shed draining the cluster, and hold pins across every distribution path. Note that the node-load advertisement is PostgreSQL-only until #​4593 lands for the other stores.
  • External table transport for Oracle (#​4482) — implemented by @​Trasvi in #​4558, with follow-ups in #​4574, #​4577 and #​4578. The shared ExternalTableTransportCompliance suite now holds Oracle, PostgreSQL, SQL Server, MySQL and Sqlite to the same bar.
  • A deduplication claim now rides the business transaction on Marten (#​4505), Polecat (#​4570) and Fisher (#​4571). The claim is enlisted in the same unit of work as the handler's own writes instead of being committed on a connection of its own and compensated with a release. A refusal that never commits therefore never claims the id, and there is no release left to mis-order.

Behaviour changes

  • An unknown tenant id answers 404 ProblemDetails instead of 500 (#​4516).
  • Concurrency failures can be mapped to 409 with a one-line opt-in (#​4512).
  • MissingNamedConnectionStringsException stays an InvalidOperationException (#​4527), and every named connection string is now validated in one pass at startup.
  • Wolverine's Kafka error handler composes with the user's instead of replacing it (#​4522).

Exception message wave

#​4511–#​4532 — 18 PRs making the framework's exceptions name a remedy rather than only a symptom: saga failures, handler discovery, SNS/Rabbit MQ/Azure Service Bus/Redis/SignalR configuration problems, oversized messages, unreadable HTTP bodies, and the transports' previously message-less exceptions.

Fixes

  • Stop reading steady-state throughput as a stuck recovery batch, and detect a genuinely stuck outbox from the head of the queue (#​4499).
  • Sweep expired handled envelopes on Cosmos DB, and make DeleteAllHandledAsync work (#​4509).
  • Bound SQLite's two reaps (#​4567).
  • Only ever write ASP.NET Core's endpoint data sources from the composition thread (#​4500).
  • Grandfather a group-affinity candidate per member rather than per partition (#​4562) — @​erdtsieck's #​4563, with blue/green Polecat coverage in #​4576.
  • Declare a module's ancillary store once per namespace (#​4507) — @​uniquelau's #​4508.
  • Apply an ISendMyself published from a projection side effect, on all three stores (#​4556) — built on @​erdtsieck's #​4557.
  • Reject conflicting sending and listening modes on shared listeners (#​4059) — built on @​tmorejon's #​4506.
  • Say "disabled tenant" when that is what happened, and actually refuse one (#​4586).
  • Stop the Redis protocol version header surviving a round trip (#​4595).
  • Do not treat a sticky-bound listen-only endpoint as a local send target (#​4510).
  • Scope Polecat's and Fisher's duplicate-message Discard() to the inbox table (#​4565).

Dependencies

Unchanged from 6.39.1 — Marten 9.35.0, Polecat 5.29.0, Fisher 1.10.0, Weasel 9.32.0, JasperFx 2.74.0. This release ships exactly what its CI has been green against; the pin bump follows separately.

Contributors

Thank you to everyone who contributed code to this release:

6.39.1

A bug fix release. If you use codegen test as a CI gate, run dead letter queues on any broker, or persist with Oracle, there's something in here for you.

codegen test works again

Since 6.37.0, dotnet run -- codegen test has failed on a clean checkout for any application that has message handlers, with one CS0234 per handler:

CS0234: The type or namespace name 'CreateInvoiceHandler2048194527' does not
exist in the namespace 'Internal.Generated.WolverineHandlers'

A lot of you use codegen test as the PR gate on pre-generated code, so this has been quietly breaking builds for two releases. codegen write followed by dotnet build was unaffected, and so was running in TypeLoadMode.Static, which is why it took a while to surface.

Two changes collided. codegen test compiles each generated file into its own assembly so it can enforce service-location rules per file, and 6.37.0 taught the handler registry to root every generated handler by name for native AOT. Compiled in isolation, those names point at types in other in-memory assemblies. HTTP-only applications never saw it, because their rooting only names the registry itself.

The fix is in JasperFx 2.73.2, which this release picks up. Wolverine's CI now runs codegen test against a project with message handlers, which nothing here did before -- the drift gate runs codegen write and the AOT smoke tests run publish, so this whole path had no coverage at all. (#​4486)

Thanks to @​andrevlins, who bisected it across four versions, found the root cause, wrote the upstream fix and validated it against four of his own services.

Dead lettering settles the message exactly once

MoveToErrorQueue always calls CompleteAsync() right after moving a message to the dead letter queue, and it has to: on SQS and Google Pub/Sub the dead letter move only sends a copy, so that trailing call is the only thing that ever settles the original. On Azure Service Bus and RabbitMQ the move is itself a settle, and the second one is redundant.

Azure Service Bus never said which it was doing. On a normal queue the redundant settle came back as "the lock supplied is invalid" and was swallowed -- harmless and invisible. On a session-enabled queue it comes back as SessionLockLost, which forces the AMQP management link closed and reopened before the next session can be accepted. The message always reached the dead letter queue; you paid for it in latency on whatever picked up that queue next. (#​4481)

Two more in the same area:

  • A message dead lettered from a session-specific listener now carries the failure diagnostics that the other three Azure Service Bus listeners attach. That listener was the only one not stamping them. (#​4481)
  • SQS was deleting a message twice when a requeue was retried. The guard meant to prevent it read a flag that nothing ever set, so it had never once fired. Deleting twice is harmless on SQS, but the flag is also how SQS reports whether it settled anything, which the next fix needs. (#​4489)
  • If you set MaximumBrokerRedeliveries, an over-delivered duplicate on SQS or Google Pub/Sub was dead lettered and then left unsettled, so the broker redelivered it and it was dead lettered again -- one copy per redelivery, in the very branch that exists to break that loop. (#​4488)

Oracle can recover incoming messages again

The durability agent threw on every cycle:

System.InvalidCastException: Unable to cast object of type 'System.Decimal' to type 'System.Int32'

Oracle returns count(*) as a NUMBER, which ODP.NET surfaces as decimal or Int64, and GetFieldValueAsync<int>() is a cast rather than a conversion -- it throws on anything but Int32. Recovery of incoming messages was dead for Oracle users. (#​4480)

This is the second time the same provider mapping has broken a durability operation, so the conversion now lives in one place rather than being fixed at each call site as it turns up. Thanks to @​Trasvi for the report, the bisect and the fix.

EF Core DbContext abstractions compile

If you registered an abstraction with WithDbContextAbstraction<IBillingDbContext, BillingDbContext>(), the generated handler came out as:

if (billingDbContext is not BillingDbContext billingDbContext) throw ...

... (truncated)

6.39.0

Two themes in this release: multi-tenancy correctness and modular monolith ergonomics, plus a health-signal fix that will quiet a lot of false alerts.

Declare a module's ancillary store once

Modular monoliths on ancillary stores had to repeat [Storage(typeof(IOrdersStore))] on every handler, endpoint and service in a module -- restating on each type a fact that belongs to the module, where missing one meant quietly committing to the wrong database.

opts.Policies.UseAncillaryStorageFromAssemblyContaining<OrdersModule>(typeof(IOrdersStore));

That covers message handlers, HTTP endpoints and gRPC services in that assembly, for Marten, Polecat and Fisher alike. An explicit [Storage] on a type still wins, so one handler can opt out of its module's default.

For gRPC this is not an ergonomic win but the only thing that works: the gRPC chains never apply chain-modifying attributes, so [Storage] on a gRPC service compiles, looks right, and does nothing. (#​4477)

The stuck-poller health check was mostly crying wolf

This was for CritterWatch

The scheduled-job "poller is stuck" signal counted every scheduled envelope, whether or not it was due yet. A queue holding messages that are not due is a queue doing its job -- so any deliberate delay longer than the check window reported the poller as stuck, and stayed that way. Ordinary retry scheduling has the same shape, which means the signal grew with correct usage.

Measured on a production fleet of 512 sharded message databases: 254 of 271 active alerts -- 94% -- were this one check, across 265 databases. 114 of those were "degraded" over a single envelope scheduled 15 minutes out by an application deliberately waiting for a quiet period.

PersistedCounts.ScheduledDue now counts only envelopes already past their execution time, and the health signal reads that instead. It is an int?, and null means not measured rather than zero: a store that does not report it makes the signal stand down rather than falling back to the undifferentiated count, because falling back is the defect. PostgreSQL implements it as a FILTER on the existing scan, so the due count costs no extra query; the other providers report null and are simply silent here for now. (#​4476)

Tenant message stores were sharing an identity

IMessageStore.Name is a tenant routing cache key, so two tenant stores answering to the same name send one tenant's messages to the other tenant's database.

  • PostgreSQL (#​4468): the sticky queue listener agents resolve their database by tenant id instead of falling through to the default store.
  • SQL Server (#​4471): tenant message stores are named after their database.
  • MySQL (#​4472): tenant stores registered by data source are named, and the queue keeps its sender.

If you run multi-tenanted durable messaging on any of these three, this release is worth taking.

RabbitMQ virtual-host tenants never got publisher confirms

ConfigureChannelCreation(...) reached only the parent transport's channels. Every virtual-host tenant created its channels with PublisherConfirmationsEnabled and PublisherConfirmationTrackingEnabled false regardless, with no public way to set them per tenant.

That matters more than a missing option: without confirmation tracking, BasicPublishAsync returns before the broker can refuse the publish, so the sending agent counts it successful and deletes the envelope from the durable outbox. A refused publish -- an ACCESS_REFUSED after a vhost user loses write permission, say -- silently drops a message whose enrolling transaction has already committed.

Behaviour change worth knowing about: if you call ConfigureChannelCreation and have tenants configured, your tenant channels now get confirms and ConsumerDispatchConcurrency where they previously got neither. Publishing to tenant vhosts gets slower, correctly so.

Thanks to @​outofrange-consulting for a report that arrived with a measurement table and the fix already located. (#​4473)

Ancillary-only hosts picked the wrong persistence strategy

A host registering only an ancillary store through IntegrateWithWolverine<T> registered no codegen extension, so [Entity] and storage-action code silently read an empty in-memory dictionary. Fixed for Marten (#​4464), Polecat (#​4465) and Fisher (#​4466).

Scheduled messages promoted from RavenDb and CosmosDb lost their store

... (truncated)

6.38.0

Eight issues, no breaking changes.

This is a correctness and operability release. Most of it is one shape of bug — something resolved against the wrong scope, which looked right only because two defaults usually coincide — plus the two remaining halves of recurring-schedule operability.

Multi-store and multi-tenancy

  • The multi-tenanted message store no longer swallows batch failures (#​4435). A durable batch spanning several tenants was split across stores, but RetryBlock never rethrows — so a store that refused its share failed silently while the receiver acknowledged the whole batch. Messages no store had accepted were acked and lost. The batch is now split by the resolved store and a failure propagates.

  • Natural keys resolve through the store the chain is routed to (#​4439).

  • Identity types resolve through the store the chain is routed to (#​4441).

    These two are a matched pair: a saga's natural key and its identity type are facts about the store, not about the application. A modular monolith with an ancillary store per module resolved both against the main store, so a saga in module B was looked up with module A's rules.

  • A Wolverine service name reaches JasperFx, so the Event Model canvas stays whole (#​4448). WolverineOptions.ServiceName and JasperFxOptions.ServiceName both name the one running service, but the value only ever travelled one way — so the documented way to name a Wolverine service left the JasperFx side on its default, the entry assembly name. The visible damage was an Event Model canvas splitting in two: Wolverine's source named its model one thing, a store's source named it another, and neither canvas held both halves. It only ever looked correct when a host's assembly name and service name happened to coincide, which is exactly why no test caught it.

Recurring schedules

The remaining core operability gaps from #​4437, which is closed by these two. (Durable last-run state, #​4447, stays closed as not-planned: run state lives in OpenTelemetry, and the operability view belongs in CritterWatch.)

  • Non-UTC recurring schedules now record their tracking row (#​4436). Cronos returns each occurrence carrying the schedule's offset, and Npgsql's timestamptz binder refuses any non-zero offset — so every tick of every zoned schedule threw on the bookkeeping write. Delivery was never affected; only the tracking row was missing. Normalizing in RecurringMessageRecord's init accessors fixes it in one place for all four relational providers.

  • Occurrences carry their schedule and their firing instant (#​4445). The schedule name already reached the handler span. The occurrence instant did not: ScheduledTime is cleared by the scheduled machinery at fire time, so a handler could only learn which firing it was serving by string-parsing the deduplication id. Occurrences now carry a recurring-occurrence header, surfaced as the wolverine.schedule.occurrence trace tag.

    Metrics also gained a schedule.name tag, so the success, failure and effective-time counters can finally be sliced per cron job. It is read off the envelope header rather than set locally, because the metric tag list is never serialized — an occurrence published on one node and handled on another would otherwise reach the counters with no attribution at all. The occurrence instant is deliberately trace-only: one distinct value per firing would make those series unbounded in cardinality.

  • IRecurringScheduleControl.TriggerAsync runs a schedule once, on demand (#​4446). Previously an operator's only option was hand-publishing the message type out of band, which bypasses the occurrence and deduplication machinery entirely. The request is recorded on the schedule's durable tracking row and the agent publishes one occurrence for it on its next pass, so it works from any node — the same reason pause already goes through the store.

    A manual run carries its own deduplication id, so a "run now" issued in the same instant as a scheduled firing is never silently collapsed into it. Triggering a paused schedule is refused: pausing says the schedule must not fire. A trigger is extra rather than a replacement — it leaves the cron cadence and the pending occurrence untouched, and it fires even for a fixed-date schedule whose occurrences have run out.

gRPC

  • A code-first gRPC contract can be registered without [WolverineGrpcService] on the interface (#​4396). A contract you do not own — or one carrying only [ServiceContract] — could not be registered at all. AddWolverineGrpc(grpc => grpc.IncludeCodeFirstContract<IMyService>()) now registers it explicitly. Thanks to @​erikshafer for the PR.

6.37.0

Eight issues, one of them breaking.

⚠️ Breaking change

ServiceCapabilities.EventModel is now an EventModelSetDescriptor rather than a single EventModelDescriptor (#​4424). A host can legitimately assemble several Event Models — each store names its own through StoreOptions.EventModelName, and a modular monolith registers an ancillary store per module — and the export used to fold them all into one named for the service, losing a model's name outright and reporting nothing.

This is a compile break for anything reading that property, and the capabilities wire shape changes with it. A consumer that can only render one model asks .Sole, or folds explicitly with .Collapse() and gets a ModelCollapse hotspot recording what it lost. CritterWatch consumes this shape and has the equivalent fold still to follow.

Everything else in the public surface is additive.

Event Modeling

  • The Automation rule is derived from the model's own links (#​4419). FinishModel carried a private copy of the cross-slice join; it is re-based on EventModelDescriptor.Links, so the pattern Wolverine derives and the arrow a viewer draws cannot disagree. A slice triggered by another slice's event through TriggerType — not only CommandType — is now classified too.
  • ReadsFrom is split out of ReadModelTypes (#​4419). [ReadModel] and [Entity] parameters are things a slice reads; IStorageAction<T> returns are what it produces. They shared one list, which meant the Automation input edge — Event → Read Model → ⚙ Command — could not be drawn at all.
  • Each derived source stamps its own Origin (#​4425). Wolverine registers two sources on the Derived rung, so a disagreement between them used to render as Derived claims X; Derived claims Y, naming neither file. It now reads event-model://wolverine against event-model://wolverine-http.

Native AOT

  • codegen write emits its own [DynamicDependency] rooting (#​4426). Every Native AOT application had to hand-write a rooting block covering the generated registry, every generated handler, every handler class, every message type, and MessageRouter<T>/EmptyMessageRouter<T> closed over each one. Codegen now emits an AotRoots companion anchored by [ModuleInitializer] — an unconditional ILC root — so there is no app-side code at all. Verified by a real PublishAot binary booting and dispatching with the hand-written roots deleted.

Bug fixes

  • An outgoing envelope recovered from an ancillary store is acknowledged there (#​4417). Envelope.Store does not survive persistence, so the acknowledgement fell back to the main store — the ancillary row survived, and the message was recovered, sent and handled again on every restart. Thanks to @​raypet-visma for the diagnosis and the fix sketch.
  • Kafka consumer teardown is bounded (#​4422). _consumer.Close() is a synchronous P/Invoke that can block forever against a degraded broker, so IHost.StopAsync never completed — observed wedged 20+ minutes, past both DrainTimeout and ShutdownTimeout. It now runs under the drain budget on a dedicated thread, and an abandoned teardown suppresses the consumer Dispose rather than destroying a handle another thread still owns.
  • A handler-class OnException returning OutgoingMessages compiles again (#​4416). It failed code generation with "Frame chain is being re-arranged" while the same method on a middleware class worked. Thanks to @​uniquelau for the report and for locating the exact divergence. The error-handling docs gained an example of using the hook to publish messages when the original message fails.

Build & dependencies

  • JasperFx 2.69.3, with Marten 9.35.0, Polecat 5.29.0, Fisher 1.10.0 and Weasel 9.32.0 on that line (#​4421). The committed codegen write output is regenerated and a CICodegenDrift gate now guards it — meaningful only now that the emitted statement order is deterministic. Regenerating surfaced real staleness rather than the expected reordering: six orphaned handler files and four missing registry files.

6.36.0

Heads up when upgrading

  • CircuitBreaker() on a buffered local queue now stops the host from starting with an InvalidListenerConfigurationException (#​4410, #​4412). A buffered local queue can't pause, so the circuit breaker used to be accepted and then silently ignored. Add UseDurableInbox() to the queue, or opts.Policies.UseDurableLocalQueues(), or remove the circuit breaker. Durable local queues and external listeners are unaffected. If you use WolverineFx.AI with DurableQueue = false plus a circuit breaker on the callout queue, this applies to you too.
  • In the Event Model, a message handler forwarded from a gRPC RPC now always derives SlicePattern.Command (#​4413).

New

  • [SlicePattern] declares the Event Model slice pattern of a message handler whose message has no producer in the model, such as a message from another service or a hosted service (#​4395, #​4413). It only fills the gap: an HTTP route, gRPC RPC, schedule or inbound external system still decides the pattern.
  • A Wolverine gRPC client can be injected into a handler (#​4403, #​4409).
  • Each retry attempt's trace span links to the failed attempt before it with an ActivityLink (#​4398, #​4405).

Fixes

  • Agent assignment: each node now checks its assigned agents against the ones actually running and fixes any mismatch (#​3987, #​4404). The follow-up makes assignment deletes owner-scoped, adds claim-if-absent, and hardens the sweep (#​4407, #​4408).
  • Batching: the per-pipeline pending count now counts every batched message, not just listener arrivals (#​4397, #​4406).
  • Security: System.Security.Cryptography.Xml is now 10.0.12, clearing a high-severity advisory (#​4401).

Docs

  • Local queues and buffered endpoints use System.Threading.Channels, not TPL Dataflow (#​4411).
  • Removed the nonexistent EnableNodeAgentSupport() from the exclusive node processing page (#​4414).

6.35.0

Store operation side effects, everywhere

MartenOps covered store / insert / update / delete plus StartStream; anything else meant taking an
IDocumentSession and giving up on the handler being a pure function. All three stores now cover
what their own session API supports.

Op Marten Polecat Fisher
HardDelete, HardDeleteWhere, UndoDeleteWhere ✅ ✅ ✅
UpdateExpectedVersion ✅ ✅ —
UpdateRevision ✅ ✅ ✅
TryUpdateRevision ✅ — ✅
Patch, PatchWhere ✅ — ✅
QueueSqlCommand ✅ ✅ ✅
InsertObjects, DeleteObjects ✅ — —
Append, ArchiveStream ✅ ✅ ✅
UnArchiveStream, TombstoneStream — ✅ —

The gaps are deliberate: each set was checked against that store's own session API rather than copied
across, and an op whose Execute could only throw is worse than the absence of one. Polecat's last
row is the reverse case — two operations Marten has no counterpart for.

Every op also implements ITenantedMartenOp / ITenantedPolecatOp / ITenantedFisherOp, so one
extension scopes any of them while preserving the concrete return type:

MartenOps.ArchiveStream(command.OrderId).ForTenant(command.TenantId);
PolecatOps.StoreMany(items).ForTenant(tenantId).With(oneMore);

Thanks to @​erdtsieck for the Marten half, which is where this started.

Event Modeling: a declared model and the code now meet

Three findings from one comparison of a curated Event Model against the application built from it
(#​4385, #​4386, #​4387):

  • Slices join on handler type. Slice names are the merge key, and a board names a slice for the
    behaviour (ConfirmAppointment) while a derived source names it for the message type or the route.
    An eleven-slice application assembled as twenty-two with no disagreements — not because the sources
    agreed, but because they never met.
  • [Emits(typeof(...))] lets a handler name the events its signature cannot carry. EventsToAppend
    and StartStream erase the element types, so the more idiomatically event-modelled an application
    was, the emptier its derived model got.
  • Pattern is left unclaimed for a message handler. A handler cannot tell a Command from an
    Automation, so a declaration wins the role instead of losing to a guess.

⚠️ Behaviour change: a plain message-handler slice no longer reports pattern: "Command" in
event-model output or the ServiceCapabilities snapshot. Pattern is still derived wherever the
code answers the question — HTTP routes, gRPC RPCs, schedules, external systems, and any slice whose
... (truncated)

6.34.0

Seventy commits since 6.33.0. The bulk of it is a sustained performance wave on the durability
and message-execution paths, alongside a new recurring-schedule feature, Native AOT support that
now boots end to end, and a long run of clustering and transport fixes.

New

  • Recurring cron-scheduled messages. opts.Schedules registers messages to be published on a
    cron expression, coordinated across the cluster so exactly one node fires each occurrence. (#​4307)
  • Azure Service Bus takes a configurable prefix for the system queues it names for itself, plus
    a transport-wide default dead letter queue name. Note the migration hazard called out in the docs
    if you adopt the prefix on an existing deployment. (#​4263, #​4281)
  • Amazon SQS takes the same kind of configurable prefix for the queues it names for itself. (#​4292)
  • The HTTP transport now answers to plain http:// destinations, not just https://.
    ITransport.AdditionalProtocols is the general mechanism, so any transport with legitimately
    multi-scheme addresses can opt in. (#​4200 / #​4379)
  • Broker resource setup honors ResourceMigrationFailureMode. FailFast stays the default and
    keeps resources setup strict; ContinueOnFailures lets a host whose broker topology is
    externally owned log the failures and start. (#​4119 / #​4380)

Performance

The GH-4316 wave, measured on the multi-transport perf rig rather than by inspection. Highlights:

  • Recovery poll and handled-cleanup get indexes they can actually use — three partial indexes on
    PostgreSQL and SQL Server, 37ms down to 0.04ms on the measured query. (#​4336)
  • Batched outbox stores for Oracle, RavenDB and Cosmos DB — 13.1x on Oracle, 61.4x on
    RavenDB. (#​4369 / #​4370)
  • Insert-side coalescing of the last un-batched per-message durability round trips: +24.5%
    throughput and 26% lower publish latency where the application publishes concurrently. (#​4319 / #​4368)
  • Fixed-arity batched inserts on PostgreSQL (1.35x) and pooled Envelope bodies above the LOH
    threshold
    (13.5x at 100KB, with Gen1/Gen2 collections gone). (#​4320, #​4333)
  • Batched durability commands are chunked under each provider's parameter ceiling, so a large
    transaction no longer trips SQL Server's 2100-parameter limit. (#​4375 / #​4376)
  • Per-message allocation and lookup trims across routing, the send path, the execution pipeline,
    header handling, and metric accumulation. DateTimeOffset.Now is gone from the per-message paths.
    (#​4322, #​4323, #​4324, #​4325, #​4326, #​4328, #​4335)
  • Idle polling stops hammering (#​4321); the database queue transports drop their per-poll temp
    tables (#​4334); SQL Server metrics counts come from index metadata instead of three full scans
    (#​4318); Redis Streams gains batched durable arrival, measured at +159% (#​4329).

Two changes measured negative on the rig and were reverted rather than shipped — the Azure Service
Bus prefetch default and the coalescer's Queue shape. Both are recorded so they are not revisited.

Native AOT

A Wolverine application now completes a Native AOT publish and boots through its own bootstrap.
(#​4287, #​4298, #​4301, #​4305). A Native AOT app with any external transport used to die building its
first route — fixed in #​4232 / #​4378. The AOT publish smoke test hard-asserts a full boot.

Clustering, agents and durability

... (truncated)

6.33.0

The headline is a new package. WolverineFx.AI makes a one shot LLM call an ordinary Wolverine message: durable, outbox enrolled, retried by the same rules as everything else, and testable without a model anywhere in sight.

WolverineFx.AI (new package)

An LlmCallout is a message. Return one from a handler next to your storage action and it is enrolled in that handler's outbox, so a callout cannot fire for a transaction that did not commit and cannot be lost to a restart in between. The model's answer comes back as an ordinary cascading message, with an ordinary handler, an ordinary retry policy, and its own place in the correlation chain. (closes #​4227)

  • Spend guardrails as middleware on the callout queue. LlmBudget.MaximumPromptCharacters refuses a runaway prompt before your provider is ever called; MaximumTokensPerWindow refuses callouts once the node has burned its allowance. Both dead letter rather than retry, and so does an answer that cannot be parsed into the response type you asked for -- retrying either is the runaway spend the budget exists to stop.
  • A scripted IChatClient for testing. StubChatClient exercises a callout's whole round trip with no key, no network and no model.
  • Trim and AOT clean, guarded by a Wolverine.AI.AotSmoke project under TrimMode=full that CI runs. (closes #​4230)
  • Documentation for tuning it, new in this release: how to control parallelism against your provider, why the answer has its own queue with its own settings, and how to bring your own error handling on both sides.

The package references only the Microsoft.Extensions.AI abstractions, never a vendor SDK. The provider -- Anthropic, OpenAI, Azure, Ollama -- and any middleware over it stay your choice.

Fixes

  • A node no longer sweeps up its own in-flight stop as a wedged shard. An event-subscription agent could end up running on two nodes at once while wolverine_nodes credited only one, so nothing in the system could ever stop the extra copy. (closes #​4240)
  • Node record descriptions no longer overflow the column and fail the insert. An AssignmentChanged description carries an agent URI, a schema name and a destination node, which on a real cluster overran the description column and failed the whole AgentCommand batch behind it. MySQL was worst hit at VARCHAR(255). (closes #​4246)
  • DataAnnotations validation works with ServiceLocationPolicy.NotAllowed -- on HTTP endpoints and, now, on message handlers. Under the Wolverine 6 default this made the validation middleware unusable and threw at bootstrap. (closes #​4238)
  • A failed EF Core rollback no longer displaces the exception that caused it. (closes #​4239)
  • Wolverine parameter attributes work on gRPC before/after hooks -- [Entity], [All], [Queryable], [WriteAggregate] and the rest. (closes #​3935)
  • An application-wide default duplicate status code via opts.DefaultDuplicateStatusCode, and deduplication refusals now advertise their problem document in OpenAPI.
  • Concurrent IHost.StopAsync no longer tears the agents down twice.

Upgrade note

6.33.0 requires Weasel 9.30.0, and that raises the GH-4246 fix from "new databases only" to "existing ones too": the schema differ now compares character lengths, so a widened varchar is no longer invisible to it and an existing table is corrected in place by an ALTER TABLE ... MODIFY that keeps its rows.

Worth knowing before you upgrade: that comparison runs in both directions. Width drift that was previously invisible now generates ALTERs, and a model narrower than an existing column will emit a narrowing ALTER that can fail on real data. Sizes that are not character lengths -- a MySQL int(11) display width, a decimal precision, a datetime fsp -- are still ignored.

Dependencies

JasperFx 2.60.0, Marten 9.30.0, Polecat 5.21.1, Fisher 1.0.6, Weasel 9.30.0.

6.32.0

See CHANGELOG.md for the full entries.

New packages

WolverineFx.AmazonS3 and WolverineFx.AzureBlobStorage carry document and saga persistence plus the claim check store that used to ship separately. Registration is explicit per type — Store<T>() and Saga<T>() are separate calls and each refuses the other's type — and saga writes are guarded by conditional requests, surfacing as SagaConcurrencyException so one OnException<ConcurrencyException> policy still covers every store. (#​4160, originally #​4165 by Anne Erdtsieck.)

WolverineFx.ClaimCheck.AmazonS3 is deprecated. The namespace is unchanged, so migration is a package reference swap — but keeping both referenced produces ambiguous-type errors.

Redis document and saga persistence folds into the existing WolverineFx.Redis rather than a new package, with saga concurrency implemented as a Lua compare-and-swap.

Fixes

  • A shutting-down node no longer dead-letters work whose handler never ran (#​4213). Core, so every transport.
  • Scheduled promotion matches the whole message identity on SQLite, SQL Server, MySQL and Oracle rather than PostgreSQL alone (#​4216) — including a not-yet-due scheduled message being promoted and executed early.
  • A redelivered inbox row can be retired when its identity is already handled under EnableInboxPartitioning (#​4216); previously it could not be retired at all.
  • A listener whose broker entity was deleted underneath it now heals instead of retrying once a second forever (#​4215).
  • Terminal settle failures are classified on the retry block, closing a gap where two of four Azure Service Bus listeners had no classification at all (#​4012).
  • Scope priming no longer manufactures a Marten session for every handler that service-locates anything (#​4198). Requires JasperFx 2.58.0 or later.
  • A duplicated scheduled identity no longer wedges promotion on a partitioned PostgreSQL inbox (#​4202).
  • AddStopConditionIfNull accepts the null identity its signature declares (#​4161).

Diagnostics

  • NativeAck and partitioned listeners report ceilings, per-lane depth and duplicate-suppression counts (#​4199). BufferLimit is now null on the modes that never enforced it, with the broker's prefetch window reported as InFlightLimit.
  • MaximumBrokerRedeliveries is documented as the delivery count it actually is (#​4216). Behaviour unchanged.

Event model

  • A stream-appending handler's return value is reported as a reply rather than an emitted event (#​4204).
  • A generic message type's slice reads the way source spells it, which also stops two relays with different payloads colliding on one slice (#​4205).

Also

  • Explicit per-provider entity attributes, starting with [FromMarten] and [FromEfCore] (#​4214).
  • RabbitMQ documentation for AddResourceSetupOnStartup and AutoProvision (#​4223).

6.31.0

Logical message deduplication

Envelope.Id identifies one delivery. That is the right identity for "the broker handed me this twice" and the wrong one for "the operator clicked Rebuild twice" — those are different deliveries of the same intent, so each carries a different Envelope.Id and every one gets through.

6.31.0 promotes Envelope.DeduplicationId into a first-class logical id, with storage, enforcement, and a retention policy behind it.

opts.Durability.EnableMessageDeduplication = true;   // provisions wolverine_deduplication
opts.Durability.DeduplicationWindow = 24.Hours();    // this IS the guarantee

[Deduplicated]
public static void Handle(RebuildProjection command) { }

It is opt-in throughout — leaving it off means no schema change at all on upgrade. Storage is a separate wolverine_deduplication table rather than a column on the inbox, because under EnableInboxPartitioning the inbox is PARTITION BY LIST (status) and marking an envelope handled moves the row between partitions, which would let one logical id exist as both Incoming and Handled — silently, and only for users who enabled partitioning. Claiming is an INSERT that either succeeds or trips the primary key, never a SELECT-then-INSERT.

Refusals differ per chain type: a message handler discards and acks, HTTP returns 409 with ProblemDetails (configurable to 2xx where a replay is benign), gRPC returns AlreadyExists / InvalidArgument per AIP-193. Storage on PostgreSQL, SQL Server, MySQL and SQLite.

Deriving the id from the message

The publishing side does not have to remember DeliveryOptions.DeduplicationId at every call site. A message type declares its own logical identity once, the way it already declares a topic name with [Topic] or a saga id with [SagaIdentity]:

public record ArchiveInvoice([property: DeduplicationIdentity] string InvoiceNumber, DateOnly AsOf);

[DeduplicationIdentity(nameof(ReceiveShipment.ShipmentId))]   // a contract whose members you cannot decorate
public record ReceiveShipment(Guid ShipmentId, string Warehouse);

// or configured, for composed ids and generated message types
opts.MessageDeduplication.ByMessage<RebuildProjection>(x => $"{x.ProjectionName}|{x.OccurrenceUtc:O}");
opts.MessageDeduplication.ByMemberNamed("IdempotencyKey", "DeduplicationId");
opts.Policies.ForMessagesOfType<CreateOrder>().DeduplicateBy(x => $"{x.Sku}|{x.Quantity}");

These are IEnvelopeRule at the message type level, resolved once when the route is built rather than per message. An explicit DeliveryOptions.DeduplicationId always wins, then configured rules, then the attribute.

Fixes

  • Broker startup is bounded by a clock. A host starting against a dead broker took 21m38s to fail — long enough to look like a hang and to blow past any orchestrator's startup probe. (#​4116)
  • ListeningAgent sees past its receiver wrappers. ReceiverWithRules — installed by a bare endpoint-level MessageType or TenantId — is unconditionally an ILocalQueue, so a wrapped NativeAck or Inline receiver took the wrong branch and threw on the durability agent's re-entry path. The same blindness meant a terminally faulted receiver reported healthy forever on exactly the endpoints most likely to be non-trivially configured. (#​4188, #​4191)
  • A locally-owned shard that stopped with nothing to report is restarted. It was invisible to both recovery paths because each deferred to the other; the status was correct, the assignment was cor...

Description has been truncated

---
updated-dependencies:
- dependency-name: WolverineFx
  dependency-version: 6.44.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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 Oct 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment