Skip to content

GH-3974: let a consumer ask which message types are handled, and how a batch is shaped - #3977

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3974-handled-types-and-batch-mapping
Aug 17, 2026
Merged

GH-3974: let a consumer ask which message types are handled, and how a batch is shaped#3977
jeremydmiller merged 1 commit into
mainfrom
gh-3974-handled-types-and-batch-mapping

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3974. Two related gaps, both of which forced consumers to re-derive something Wolverine already knows.

1. "Will this message type have a handler?"

Discovery and the static TypeLoadMode registry both materialize after options time, so an extension or app-level convention that installs fallback handlers — a relay, a batch forwarder, a catch-all — had to hand-roll a mirror of Wolverine's own discovery convention to avoid clobbering a real one.

Any reflection-based reimplementation of the framework's convention drifts from it, and the drift is silent. The reported case scanned a single assembly, so moving two handlers into a second one made them invisible to the mirror and the sweep installed a bare relay over a real handler — the exact defect the guard existed to prevent, with every codegen test still green.

opts.OnHandlersDiscovered(handlers =>
{
    if (!handlers.Handles<ServiceUpdates>())
    {
        // safe to install a fallback
    }
});

OnHandlersDiscovered fires from HandlerGraph.Compile immediately after Group() — the earliest point at which the question has a real answer. DiscoveredHandlers exposes Handles<T>(), Handles(Type) and the full resolved set.

This is the "hook that runs once discovery has resolved but before the app needs the answer" that the issue offers as an acceptable answer to this half. A genuine options-time query would mean forcing discovery while WolverineOptions is still being configured, which would freeze the handler set before later configuration could add to it — a worse failure mode than the one being fixed.

2. The batcher's element → batch message mapping

IMessageBatcher.BatchMessageType is a free-form Type. The default batcher produces T[], but nothing requires that, and the auto-swap in WolverineRuntime.HostService deliberately leaves an application-supplied batcher alone precisely so it can produce its own type. So consumers inferred the handled type from array-ness — parameters[0].ParameterType.GetElementType() — which is silently wrong for exactly those batchers.

if (options.TryFindBatchMessageType(typeof(SubTaskCompleted), out var batchMessageType))
{
    // SubTaskCompletedBatch, not SubTaskCompleted[]
}

foreach (var mapping in options.BatchMappings) { /* ElementType -> BatchMessageType */ }

The data already existed on BatchingOptions; it was just internal. This is the half the issue notes is "cheap even if the first is not" and would on its own "remove the special case."

Tests

Four in discovering_handled_message_types:

  • the callback reports the resolved handled types, and gives a definitive no for an unhandled type — which is the whole point for a fallback installer
  • the default batcher maps to T[]
  • a custom batcher reports its own type, asserted to not be an array — the case array-ness inference gets wrong
  • a non-batched element type reports false rather than guessing

Batching regression: 118/118.

Docs

docs/guide/handlers/batching.md now warns that BatchMessageType is free-form and shows both query forms next to the custom-batcher sample.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC

…a batch is shaped

Two related gaps, both of which forced consumers to re-derive something Wolverine already
knows.

1. "Will this message type have a handler?" could not be asked. Discovery and the static
   TypeLoadMode registry both materialize AFTER options time, so an extension or app-level
   convention that installs FALLBACK handlers -- a relay, a batch forwarder, a catch-all --
   had to hand-roll a mirror of Wolverine's own discovery convention to avoid clobbering a
   real handler.

   Any reflection-based reimplementation of the framework's convention drifts from it, and
   the drift is silent. The reported case scanned a single assembly, so moving two handlers
   into a second assembly made them invisible to the mirror and the sweep installed a bare
   relay OVER a real handler -- the exact defect the guard existed to prevent, with every
   codegen test still green.

   Adds WolverineOptions.OnHandlersDiscovered(Action<DiscoveredHandlers>), invoked from
   HandlerGraph.Compile immediately after Group() -- the earliest point at which the question
   has a real answer. DiscoveredHandlers exposes Handles<T>() / Handles(Type) and the full
   resolved set. This is the "hook that runs once discovery has resolved" the issue offers
   as an acceptable answer to the first half.

2. IMessageBatcher.BatchMessageType is a free-form Type. The default batcher produces T[],
   but nothing requires that, and the auto-swap in WolverineRuntime.HostService deliberately
   leaves an application-supplied batcher alone precisely so it can produce its own type. So
   consumers inferred the handled type from array-ness --
   parameters[0].ParameterType.GetElementType() -- which is silently wrong for exactly those
   batchers.

   Adds WolverineOptions.TryFindBatchMessageType(elementType, out batchMessageType) and
   WolverineOptions.BatchMappings. The data already existed on BatchingOptions; it was just
   internal.

The custom-batcher test asserts the mapping reports the batcher's OWN type and that the type
is not an array, which is the case an array-ness inference gets wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC
@jeremydmiller
jeremydmiller merged commit ce814b4 into main Aug 17, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let an options-time extension ask which message types will be handled — and expose the batcher's element/batch-message mapping

1 participant