C# and Java consumer declarations · Preview
Declare consumers with methods or consumer types.
Attributed methods are an alternative declaration style for the same consumer pipeline. Their signatures bind messages and dependencies without requiring one interface implementation or class per message.
The corresponding Java declaration
Java uses @MessageConsumer because Consumer<T> already names its interface-consumer contract. Its binding and endpoint semantics match the C# declaration without requiring a shared attribute or interface.
The Kotlin function projection
Kotlin declares the same idea as a top-level suspending function. Its message is bound directly, other parameters come from the delivery scope, and the returned value becomes the response.
Return a response
A request handler can return its response contract directly. MyServiceBus awaits the operation and sends the value through the active consume context, preserving the normal request correlation metadata. C# supports Task<T> and ValueTask<T>; Java supports CompletableFuture<T> and CompletionStage<T>.
Returning a response does not make the context parameter mandatory. Add ConsumeContext<TMessage> when the method needs headers, correlation identifiers, addresses, or other receive metadata; otherwise the message and application services are enough.
A response-bearing method requires an incoming request with a response address. If the method fails—or no response address is present—consumption fails through the normal retry and fault pipeline. Synchronous response values are not supported.
Choose by size and grouping
Use an IConsumer<T> class when a consumer is substantial enough to deserve its own type. Use consumer methods when the containing class mainly groups related handlers and acts as their de-facto namespace. Neither declaration style changes the message contract or wire behavior.
C# currently requires methods to be contained by a type. That containing type is organizational and is not itself the consumer. Static methods do not cause the container to be instantiated or registered as a service.
Parameter binding
The method name does not determine eligibility or the message contract. Consume, ReceiveOrder, Handle, and other function names are all valid; the signature determines the binding.
Message
The first ordinary parameter identifies the consumed message contract.
Context
ConsumeContext<T> or ConsumeContext comes from the receive pipeline.
Services
Additional ordinary parameters resolve from the dependency-injection scope created for the message.
Cancellation
CancellationToken binds to the active consume context.
Group methods by endpoint
A class-level [Consumer("orders")] discovers all eligible methods on the class and maps them to one endpoint. Grouping is an optional organizational benefit: related handlers can stay together instead of being spread across separate consumer classes. The endpoint binds every declared message type; if two methods on that endpoint consume the same message type, both methods run.
Override or configure the mapping
The attribute string is the receive endpoint name. A method-level attribute can override the class mapping for an individual method, and an explicit fluent value overrides both. Explicit names are never replaced by an endpoint-name formatter.
A bare method-level [Consumer] still declares a consumer. Its endpoint name comes from the method name, so the example below maps to OrderSubmittedConsumer.
Endpoint precedence is: explicit fluent mapping, method attribute, class attribute, then convention.
Method containers do not implement an IConsumer marker or share a consumer interface with another framework.
Mark only the method
The containing class does not need an attribute. A method-level attribute is a complete declaration in both reflection and generated discovery, which also keeps the model suitable for external integrations that cannot annotate a container.
Limit reflection scanning
Reflection discovery is already limited to the supplied assemblies. An optional type predicate can narrow both interface and method-consumer discovery further.
Java offers the similar bounded addConsumers(...) surface for both Consumer<T> implementations and annotated methods. Callers supply the finite candidate classes because MyServiceBus does not perform implicit classpath scanning.
Override an interface consumer endpoint
The attribute is also an alternative to fluent endpoint mapping for a normal IConsumer<T> class. In that case it overrides the consumer's endpoint name; it does not turn Consume into a separately discovered method consumer.
Reflection or generated dispatch
Registration is an application choice. Explicit registration names a consumer implementation or supplies a typed method/function invoker without discovery. Reflection inspects explicitly selected declarations at startup. The C# generator, Java JSR 269 processor, and Kotlin KSP processor scan their compilations and emit the explicit typed registrations for the application. Every path enters the same runtime pipeline.
Explicit
Register consumer implementations or typed invokers directly for maximum control and no discovery.
Reflection
Discover bounded assemblies, classes, or function references with a startup and AOT tradeoff.
Generated
Let build-time tooling emit direct registrations for predictable startup and closed-world deployment.
- Assembly discovery builds method descriptors and invokes methods through reflection.
- The C#, Java, and Kotlin generators bind parameters and call methods or functions directly.
- Both modes use the same receive, retry, fault, telemetry, and dependency scopes.
- Java reflection inspects only explicitly registered classes; it does not scan the classpath.
- Applications may mix styles when different components need different tradeoffs.
Consumer declaration syntax stays local to each language. Wire compatibility is determined by message identity, envelopes, headers, topology, and transport behavior. A future compatibility adapter can translate another framework's conventions without coupling the core runtime to its interfaces.
Raven can project the same method-consumer descriptor as an attributed namespace-level function, avoiding a container type that carries no domain meaning. This remains part of the preview Raven integration; see Raven and MyServiceBus for the current evidence and maturity boundaries.