Skip to content

Fix invalid generated class name for batched (array) message types (GH-3399)#3405

Closed
jeremydmiller wants to merge 1 commit into
mainfrom
fix-3399-array-message-codegen-name
Closed

Fix invalid generated class name for batched (array) message types (GH-3399)#3405
jeremydmiller wants to merge 1 commit into
mainfrom
fix-3399-array-message-codegen-name

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3399. Reported by @outofrange-consulting — thank you for the repro sample, it made this quick to pin down.

The bug: an illegal C# identifier, at startup

HandlerGraph's duplicate-TypeName disambiguation composed the generated handler class name straight off the message type:

chain.TypeName =
    $"{chain.MessageType.ToSuffixedTypeName("")}_{chain.HandlerCalls().First().HandlerType.ToSuffixedTypeName("Handler")}";

ToSuffixedTypeName only strips the generic arity (the backtick suffix) — it does nothing about arrays. BatchMessagesOf<T>() makes T[] the handled message type, so this produced:

ItemDeleted[]1177234954_TelemetryHandlerHandler550305999

which is not a valid C# identifier. Codegen failed with CS1514: { expected / CS1513: } expected / CS1001: Identifier expectedCompilation failures!the app dies at startup. This is startup-fatal, and any batched message type can reach it.

Not a 6.17.3 regression — the disambiguation path is old.

The trigger, and why existing batch tests missed it

Three things have to line up, and it was the third that our coverage lacked:

  1. MultipleHandlerBehavior.Separated, and
  2. one handler class handling two message types, one of them batched, where
  3. each of those message types also has a second handler.

(3) is what pushes each message type's grouping past 1 (HandlerChain.cs:114), which is the gate for creating sticky per-endpoint chains. Sticky chains are named off the handler type (HandlerChain.cs:85), so the shared handler class ends up with two sticky chains carrying the same TypeName — and only then does the duplicate-disambiguation path run and rebuild the name off the message type.

We already had Separated + batching coverage (separated_batch_routing.cs, batching_with_separated_handlers.cs), but every handler class there handles exactly one message type, so the sticky TypeNames never collided and the disambiguation branch was never exercised. Batching alone was never the missing ingredient — the Separated split with a shared handler class was.

The fix

Centralized the codegen-safe name composition that HandlerChain was already doing ad hoc (.Replace("[]", "Array"), in three places) into one helper, and used it on the disambiguation path that was missing it:

internal static string GeneratedTypeNameFor(Type type, string suffix)
{
    return type.ToSuffixedTypeName(suffix).Replace("[]", "Array").Sanitize();
}

Sanitize() is the existing JasperFx.Core.Reflection helper, added as a belt-and-braces pass for any other punctuation rather than hand-rolling a new sanitizer.

Collisions remain impossible. ToSuffixedTypeName appends a stable hash of the type's full name, and T and T[] have different full names — so ItemDeleted and ItemDeleted[] carry different hashes and cannot collide after sanitizing. The new test asserts distinctness explicitly, not just legality.

Applied the helper to SagaChain too, which carried the same idiom.

Red → green

New test src/Testing/CoreTests/Bugs/Bug_3399_batched_message_separated_handler_codegen.cs reproduces the reporter's shape and asserts every generated class name is a legal C# identifier and that names stay distinct.

Before the fix (the reporter's exact errors):

Bug_3399...can_start_up_with_separated_batched_handler [FAIL]
Bug_3399...batched_handler_actually_executes [FAIL]
  System.InvalidOperationException : Compilation failures!
  CS1514: { expected
  CS1513: } expected
  CS1001: Identifier expected
Failed! - Failed: 2, Passed: 0

After:

Passed! - Failed: 0, Passed: 2
  • Full CoreTests (net9.0): 1923 passed, 0 failed (2 pre-existing skips)
  • Batching + saga + separated filter: 183 passed, 0 failed
  • dotnet build wolverine.slnx -c Release: 0 Warning(s), 0 Error(s)

🤖 Generated with Claude Code

A header-identified saga cannot work over a gRPC hop: the saga-id envelope
header is not propagated across the call, so no id can be resolved. That is
unchanged and is not what this fixes. What it fixes is the diagnostic.

The caller used to get StatusCode.Internal with "Could not determine a valid
saga state id for Envelope ..." -- true, but it names no cause and no remedy,
and the remedy is not guessable, because the reason the id is missing is a
property of the transport rather than of the message.

It now maps to StatusCode.InvalidArgument with a detail that says the identity
must be on the message body, why (the header does not cross the hop), how to
supply it ([SagaIdentity] or a matching property), and where to read more.
InvalidArgument rather than Internal is deliberate per AIP-193: the request
cannot succeed as sent and retrying will not change that -- it is a caller-side
contract problem, not a transient server fault.

The mapping lives in WolverineGrpcExceptionMapper, which only runs at the gRPC
boundary, so the core exception is untouched and non-gRPC users see no change.

Message-identified sagas over gRPC already work and are unaffected (covered by
#3386). Per @erikshafer's analysis on the issue, full three-point saga-id
propagation is deliberately NOT taken: HTTP does not exercise the header path
either, so "works like HTTP" is already satisfied, and the complexity is not
warranted until someone shows a concrete need.

Flips the #3386 characterization test, and updates the gRPC + Sagas docs page.
Full Wolverine.Grpc.Tests 273/273. Full wolverine.slnx Release build clean.

Closes #3385.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller

Copy link
Copy Markdown
Member Author

Superseded — this branch got the wrong commit pushed to it from a shared working tree (my error, not the fix's). The actual GH-3399 commit is unchanged; reopening it on a clean branch off main.

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.

Codegen emits an invalid class name for array (batched) message types when duplicate handler TypeNames are disambiguated

1 participant