Skip to content

Convention-based handler support#7641

Open
danielmarbach wants to merge 45 commits intomasterfrom
handlers
Open

Convention-based handler support#7641
danielmarbach wants to merge 45 commits intomasterfrom
handlers

Conversation

@danielmarbach
Copy link
Contributor

@danielmarbach danielmarbach commented Mar 6, 2026

This PR completes support for convention-based handlers across registration, source generation, analyzers, and OpenTelemetry tracing.

The result is that convention-based handlers are now treated as first-class handler definitions in the supported generated/intercepted registration paths, while the runtime reflection-based registration path remains intentionally limited and fails fast when used outside those supported scenarios.

convention-based handlers can be registered through generated/intercepted APIs

AddHandler<T>() now accepts convention-based handlers at compile time.

When source generation/interception is active, convention-based handlers can be registered through:

  • config.AddHandler<THandler>()
  • generated config.Handlers...Add...() methods
  • generated config.Handlers...AddAll() methods

This support includes:

  • static convention-based handlers
  • instance convention-based handlers
  • constructor injection
  • method parameter injection
  • cancellation token uplift from context.CancellationToken
  • inherited public Handle(...) methods on base classes for pure convention-based handlers

The reflection-based MessageHandlerRegistry.AddHandler<T>() path still only supports types implementing IHandleMessages and throws a clear exception for non-intercepted convention-based handlers.

Generated registrations preserve original handler identity

Generated registrations and handler deduplication use the original handler type for convention-based handlers.

This means:

  • duplicate registrations are deduplicated correctly
  • tracing and diagnostics report the original handler type rather than a generated adapter type

convention-based handler method shape is now explicit

A method is treated as a supported convention-based handler only when it matches the supported shape:

  • public Handle(...)
  • first parameter is the message
  • second parameter is IMessageHandlerContext
  • return type is Task
  • explicit interface implementations are excluded

ValueTask is intentionally not treated as a supported convention-based handler return type.

Generated adapters handle DI collisions correctly

Generated convention-based adapters keep constructor injection and method injection distinct even when the source parameter names collide.

A handler like:

  • Ctor(IServiceA service)
  • Handle(..., IServiceB service)

generates valid code and passes the correct dependency to each call site.

Analyzer and fixer behavior

HandlerAttribute analysis now supports both handler styles

HandlerAttributeAnalyzer now correctly recognizes pure convention-based handlers and their hierarchies.

That means:

  • missing [Handler] is reported for valid convention-based handlers
  • misplaced [Handler] is reported for convention-based abstract bases
  • concrete types inheriting an convention-based Handle(...) method are treated as convention-based handlers for attribute analysis/fixing

HandlerAttribute diagnostics are split by handler style

The handler-attribute diagnostics are now separate for interface-based and convention-based handlers.

Interface-based handlers use:

  • NSB0022 missing [Handler]
  • NSB0023 misplaced [Handler]

convention-based handlers use:

  • NSB0034 missing [Handler]
  • NSB0035 misplaced [Handler]

This allows future policy and severity differences between the two styles without changing the analyzer model again.

The fixer works for both styles

HandlerAttributeFixer now supports both interface-based and convention-based handlers.

It can:

  • add [Handler] to concrete leaf handlers
  • move [Handler] from abstract bases to the concrete handler type
  • do so across interface-based, convention-based, and relevant hybrid inheritance shapes

Hybrid inheritance behavior is defined

A derived type that implements IHandleMessages<T> is treated as interface-based even if a base class provides a virtual Handle(...) method.

That prevents accidental reclassification of interface-based handlers as convention-based or mixed-style.

OpenTelemetry

OpenTelemetry handler spans now report the correct handler type for convention-based handlers.

In particular, the nservicebus.handler.handler_type tag uses the original convention-based handler type, not the generated adapter type.

Acceptance coverage now verifies this in:

  • a focused convention-based handler scenario
  • a multiple-handlers scenario containing both an interface-based handler and an convention-based handler

Scope and boundaries

This PR does not broaden the runtime assembly-scanning/reflection model to fully support convention-based handlers.

The intended boundary remains:

  • generated/intercepted registration paths: supported
  • reflection-based registration path without interception/source generation: not supported for convention-based handlers

@danielmarbach danielmarbach changed the title Interfaceless handlers Interfaceless handler support Mar 7, 2026
@danielmarbach danielmarbach added this to the 10.2.0 milestone Mar 7, 2026
@danielmarbach danielmarbach marked this pull request as ready for review March 7, 2026 13:25
@danielmarbach danielmarbach requested a review from Copilot March 7, 2026 15:14
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes first-class support for interface-less handlers in the generated/intercepted registration paths, while explicitly failing fast for interface-less handlers registered via the reflection-based runtime path. It also preserves original handler identity for deduplication and OpenTelemetry tagging, and updates analyzers/fixers accordingly.

Changes:

  • Allow AddHandler<T>() registration APIs to accept interface-less handlers at compile time, with a runtime guard for the reflection-based path.
  • Extend source generation/interception to emit adapter handler types and register them while deduplicating and reporting the original handler type for diagnostics/tracing.
  • Update analyzers/fixers and expand unit/approval/acceptance coverage for interface-less handler shapes and CancellationToken uplift behavior.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs Removes generic constraint from AddHandler<T>(), adds reflection-path guard, and preserves original handler identity for dedup + tracing.
src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs Updates EndpointConfiguration.AddHandler<T>() signature to allow interface-less handlers when interception/generation is active.
src/NServiceBus.Core/Pipeline/Incoming/MessageHandler.cs Minor doc comment punctuation tweak.
src/NServiceBus.Core.Tests/Handlers/MessageHandlerRegistryTests.cs Adds tests for rejecting interface-less handlers on reflection path and adapter dedup by original type.
src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt Updates approved public API surface for signature/overload changes.
src/NServiceBus.Core.Analyzer/SupressionIds.cs Adds suppression id for interface-less handler CancellationToken binding scenario.
src/NServiceBus.Core.Analyzer/Handlers/InterfaceLessHandlerCancellationTokenSuppressor.cs New diagnostic suppressor for CancellationToken parameters bound from handler context.
src/NServiceBus.Core.Analyzer/Handlers/InterfaceLessHandlerCancellationTokenBinding.cs New helper to identify interface-less handlers with bound CancellationToken parameters.
src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs Parses interface-less handler method shapes, injected params, and generates deterministic adapter names.
src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs Emits registrations for interface-less adapters and generates adapter types implementing IHandleMessages<T>.
src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs Extends HandlerAttribute analysis to support interface-less handlers and detects mixed-style handlers.
src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Emitter.cs Filters mixed-style handlers and emits adapter types for intercepted AddHandler<T>() calls.
src/NServiceBus.Core.Analyzer/Handlers/AddHandlerGenerator.Emitter.cs Filters mixed-style handlers and emits adapter types alongside generated handler registries.
src/NServiceBus.Core.Analyzer/ForwardCancellationTokenAnalyzer.cs Skips CancellationToken forwarding diagnostics for interface-less handlers with bound CancellationToken params.
src/NServiceBus.Core.Analyzer/DiagnosticIds.cs Introduces diagnostic IDs for interface-less handler attribute scenarios and mixed-style errors.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeFixerTests.cs Adds fixer coverage for adding/moving [Handler] in interface-less and hybrid inheritance cases.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs Adds analyzer coverage for interface-less handlers, ValueTask exclusion, and mixed-style detection.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs Adds approval-based coverage for interface-less interception, ctor/method injection collisions, inheritance, and mixed-style filtering.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs Adds approval-based coverage for interface-less generated registrations and mixed-style filtering.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ForwardCancellationToken/ForwardCancellationTokenTests.cs Adds tests ensuring no diagnostic when interface-less handlers expose a bound CancellationToken.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.MixedStyleHandlerProducesNoInterception.approved.txt Updates/introduces approved output for mixed-style handler interception behavior.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandlersCtorAndParameterInjection.approved.txt Approved output for adapter emission with ctor+method injection.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandlers.approved.txt Approved output for multiple interface-less handlers (static + instance).
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandlerReturningValueTaskProducesNoInterception.approved.txt Approved output verifying ValueTask handlers don’t intercept.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandlerInheritedFromBaseClass.approved.txt Approved output for inherited interface-less Handle(...) methods.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandlerCtorAndMethodInjectionWithSameParameterName.approved.txt Approved output for DI name-collision handling in adapters.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.InterfaceLessHandler.approved.txt Approved output for single interface-less handler interception.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.MixedStyleHandlerProducesNoRegistration.approved.txt Approved output confirming mixed-style handlers generate no registrations.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.InterfaceLessHandlersCtorAndParameterInjection.approved.txt Approved output for generated registry methods + adapter types.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.InterfaceLessHandlers.approved.txt Approved output for multiple interface-less handlers in generated registrations.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.InterfaceLessHandlerReturningValueTaskProducesNoRegistration.approved.txt Approved output verifying ValueTask handlers generate no registration.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.InterfaceLessHandlerInheritedFromBaseClass.approved.txt Approved output for inherited interface-less handler generation.
src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.InterfaceLessHandlerCtorAndMethodInjectionWithSameParameterName.approved.txt Approved output for generated adapter handling ctor/method param name collisions.
src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs Extends fixer logic to move/add [Handler] for interface-less handlers and relevant hybrids.
src/NServiceBus.AcceptanceTests/Registrations/Handlers/When_registering_interface_less_handlers.cs New acceptance test covering interface-less registration, ctor/method DI, and CancellationToken uplift.
src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_multiple_handlers.cs Updates tracing acceptance test to use NonScanning + explicit registrations, and makes one handler interface-less.
src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_interface_less_handler.cs New acceptance test verifying OpenTelemetry tag reports original interface-less handler type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Member

@WilliamBZA WilliamBZA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will mean we can change many of our docs samples and snippets way shorter and more concise. Can we track that in a follow-up issue?

[Handler]
public class ParameterDiHandler
{
public static Task Handle(MyMessage message, IMessageHandlerContext context, Context testContext)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wonderful 😍

@danielmarbach danielmarbach changed the title Interfaceless handler support Convention-based handler support Mar 12, 2026
Add `AddConventionBasedHandleMethodFixer` and `AddIHandleMessagesInterfaceFixer` to scaffold handlers for convention-based patterns. Updated relevant tests and adjusted `HandlerAttributeFixer` to delegate empty shell cases.
…pe` using `GenericName` for improved clarity and consistency.
…ixer` and update tests to validate method scaffolding
dvdstelt

This comment was marked as resolved.

…duplication logic; add EditorBrowsable attribute to `AddMessageHandlerForMessage`.
…nsolidate logic into `ConstructorAnalysis` struct; update diagnostics accordingly
…ved type discovery and propagation across handlers and sagas
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants