Core concept · Middleware
Wrap message handling with ordered filters.
Filters add validation, logging, authorization, transactions, telemetry, retry, and other cross-cutting behavior around consumers and mediator handlers. C# and Java share the same ordering, lifetime, cancellation, and failure semantics.
Mediator dispatch and broker-backed delivery enter the same consume pipeline. A filter attached to a handler therefore behaves the same way before and after that handler moves across a process boundary.
Think in contexts and continuations
Every filter receives the current context and the next pipe segment. Code before next runs in registration order; code after it completes runs in reverse order.
The first registered filter is the outermost filter. It can inspect or enrich the context, await the remaining pipeline, observe failures, or deliberately stop execution by not calling next. The same context instance and cancellation signal flow through the whole operation.
Define a consume filter
This validation filter rejects an invalid command before the handler runs. The two clients expose idiomatic asynchronous APIs while preserving the same pipeline contract.
Keep application-supplied filter instances thread-safe because a built pipe may execute concurrently. When a filter depends on scoped services, register the filter in the container and use the scoped registration API shown below.
Replace a MediatR pipeline behavior
A MediatR IPipelineBehavior<TRequest, TResponse> and a MyServiceBus consume filter both wrap handler execution. Port the responsibility rather than copying the type: validation becomes a validation filter, request logging becomes a logging filter, and transaction handling becomes a scoped filter that owns the transaction around next.
| MediatR concept | MyServiceBus equivalent | Migration note |
|---|---|---|
IPipelineBehavior<TRequest, TResponse> | IFilter<ConsumeContext<TMessage>> / Filter<ConsumeContext<T>> | Port the before/after logic and failure policy. |
RequestHandlerDelegate<TResponse> | next.Send(context) / next.send(context) | Await or compose the returned asynchronous operation. |
| Open generic behavior registration | Per-message consumer or handler pipe configuration | Register the filter for each applicable message boundary. |
| Scoped behavior dependency | UseScopedFilter<TFilter> / useScopedFilter(FilterClass.class) | The scope remains alive until downstream asynchronous work completes. |
Attach the filter while registering the handler. In Java, the configurable addConsumer overload is used because every handler is also a consumer; it still participates in mediator send and publish dispatch.
Completing a filter without calling next also prevents the handler from producing its response. A result-bearing mediator request must still receive an intentional response or fault; otherwise the caller can wait until its request timeout.
Choose the right lifetime
Application supplied
UseFilter(instance) reuses that instance. It must be safe for concurrent calls.
Type based
UseFilter<T> or useFilter(Class) creates or resolves one instance when the pipe is built.
Per operation
UseScopedFilter creates a scope for one execution and disposes it after downstream work settles.
Execute callback
UseExecute / useExecute runs a callback and then always continues to the next segment.
Place retry deliberately
Retry re-invokes only the filters and handler after it. Filters registered before retry enter once and observe the final outcome; filters registered after retry enter for every attempt. Put operation-level logging outside retry and attempt-level measurements inside it.
Retry is opt-in. When all attempts fail, the last handler failure remains the terminal failure and flows back through the outer filters. Cancellation during a fixed-delay retry stops the wait and prevents another attempt.
For local command, query, and notification semantics, continue with the mediator guide.