Skip to content

Bump WolverineFx and WolverineFx.RuntimeCompilation - #2436

Open
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/nuget/multi-712ccbe0f9
Open

dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/nuget/multi-712ccbe0f9

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Sep 21, 2026

Copy link
Copy Markdown
Contributor

Updated WolverineFx from 6.36.0 to 6.39.1.

Release notes

Sourced from WolverineFx's releases.

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.

Commits viewable in compare view.

Updated WolverineFx.RuntimeCompilation from 6.36.0 to 6.39.1.

Release notes

Sourced from WolverineFx.RuntimeCompilation's releases.

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.

Commits viewable in compare view.

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps WolverineFx from 6.36.0 to 6.39.1
Bumps WolverineFx.RuntimeCompilation from 6.36.0 to 6.39.1

---
updated-dependencies:
- dependency-name: WolverineFx
  dependency-version: 6.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.39.1
  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 Sep 21, 2026
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: NosCoreIO/NosCore/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e448174b-dd53-4e92-aaf0-789326afc56c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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