Handlers and the behavior pipeline
The request-to-response mental model that Immediate.Validations and Immediate.Cache both build on.
Almost everything in ImmediatePlatform sits somewhere on one path: a request goes in, a chain of behaviors wraps the call, your handler method runs, and a response comes back. Understanding where each package attaches to that chain explains most of the platform’s behavior.
The chain
caller
└─ GetUsersQuery.Handler ← generated; what you inject
└─ LoggingBehavior ← first in [Behaviors(...)] = outermost
└─ ValidationBehavior
└─ HandleBehavior ← generated; calls your method
└─ GetUsersQuery.HandleAsync(...) ← your code The generated Handler class is the only piece consuming code touches. Its constructor takes
the generated HandleBehavior plus one parameter for each behavior that applies, wires them
into a chain in the constructor, and delegates HandleAsync to the outermost link.
Three consequences follow directly from that shape:
- The chain is built at compile time, not resolved at runtime. Which behaviors apply to
which handler is decided by the generator from the
[Behaviors]list and each behavior’s generic constraints. There is no service-locator lookup and no ordering surprise at startup. - Order is the order you wrote. The first type listed in
[Behaviors(...)]is the outermost link, so it sees the request first and the response last. - Every link is a real DI service. A behavior that cannot be resolved is a runtime DI failure, not a silently skipped step.
Where each package attaches
Immediate.Handlers owns the chain itself: the [Handler] and [Behaviors] attributes, the Behavior<,> and StreamingBehavior<,> base classes, and the generated Handler / HandleBehavior pair. See Creating behaviors.
Immediate.Validations is a behavior. ValidationBehavior<,> goes in your assembly-wide [Behaviors] list, validates the request before calling Next, and throws ValidationException when the request is invalid. Because it is an ordinary link in the chain,
its position matters: anything listed before it runs on invalid requests too. See Integrating with Immediate.Handlers.
Immediate.Apis sits outside the chain. It generates a minimal-API endpoint that resolves GetUsersQuery.Handler from the request’s DI scope and awaits it. The whole pipeline runs
inside one HTTP request. See How it works.
Immediate.Cache also sits outside the chain, and deliberately so. It wraps the entire Handler — pipeline included — rather than inserting a link into it, so a cache hit skips every
behavior as well as your handler method. It is registered as a singleton and mints a fresh DI
scope per handler execution. See How it works.
Streaming
A handler that returns IAsyncEnumerable<T> is a streaming handler. It gets the same
treatment, with IStreamingHandler<,> in place of IHandler<,> and StreamingBehavior<,> in
place of Behavior<,>. Streaming and non-streaming behaviors coexist in one [Behaviors] list;
each attaches only to the handlers it matches. See Streaming handlers.
When a behavior does not run
Because matching happens at compile time against generic constraints, a behavior that silently
does not appear in a pipeline almost always failed a match. The Immediate.Handlers diagnostics page closes with a
checklist for exactly this; the two that catch people out most often are nullable-annotation
mismatches and a handler-level [Behaviors] attribute replacing the assembly-wide list rather
than adding to it.