ServiceComposer executes all composition handlers registered for a given request concurrently usingDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ServiceComposer/ServiceComposer.AspNetCore/llms.txt
Use this file to discover all available pages before exploring further.
Task.WhenAll. Understanding this execution model is essential for writing handlers that are both correct and efficient, particularly when those handlers share state or depend on services that are not safe for concurrent use.
Handler execution model
Every incoming HTTP request causes ServiceComposer to:- Resolve one fresh instance of each registered handler (transient lifetime).
- Invoke all handlers concurrently via
Task.WhenAll. - Collect the results and compose the final response.
Handler lifetime
All composition components — handlers, event subscribers, and event handlers — are registered in the DI container as transient. A new instance is created for each request, and two concurrent HTTP requests never share handler instances.Two concurrent HTTP requests get independent sets of handler instances. Concurrency concerns apply within a single request — among the multiple handlers executing in parallel for that request.
The shared view model
Within a single request, every handler writes to the same view model object (dynamic ExpandoObject by default, or a strongly typed instance if a view model factory is configured). Because handlers run in parallel, any write to the view model is subject to concurrent access.
ExpandoObject is not thread-safe. In practice, writes targeting completely independent properties are usually safe, but the following patterns create data races:
Unsafe — same property
Two handlers writing the same property simultaneously produce undefined behaviour. The last write wins — non-deterministically.
Safe — independent properties
Each handler owns a distinct, non-overlapping set of properties. No two handlers write the same key, so no synchronisation is needed.
Singleton and scoped dependencies
DI-registered services injected into handlers follow their own registered lifetimes:| Lifetime | Shared across… | Thread-safety requirement |
|---|---|---|
| Singleton | All requests and all handlers | Must be fully thread-safe |
| Scoped | All handlers within the same request | Must tolerate concurrent access from parallel handlers |
| Transient | Not shared (new instance per resolution) | No requirement |
DbContext is the canonical example — sharing it across parallel handlers will corrupt internal state.
Using scoped dependencies safely with child scopes
The safest pattern for non-thread-safe scoped services is to create a short-lived child DI scope inside each handler that needs the service.CreateAsyncScope() produces a scope whose lifetime is tied to the await using block, ensuring deterministic disposal:
SalesDbContext resolved from the child scope is private to this handler invocation and is never shared with any other handler running in parallel.
Composition events
Event handlers (ICompositionEventsHandler<T> and route-scoped subscribers) also run in parallel with each other and with composition handlers. The same threading considerations apply:
- Each event handler instance is transient — not shared across requests.
- All event handlers within a single request share the same view model.
- Scoped services resolved from the request scope are shared by all event handlers running concurrently in that request.
Summary checklist
Before shipping a composition handler, verify:
Before shipping a composition handler, verify:
- View model properties are non-overlapping. No two handlers on the same route write to the same property key.
- Singleton services are thread-safe. Any singleton injected into handlers must tolerate concurrent calls from multiple requests.
- Non-thread-safe scoped services use child scopes. Services such as
DbContextare resolved insideawait using var scope = _serviceProvider.CreateAsyncScope()rather than injected directly. - Shared mutable state is avoided. Handlers should not maintain mutable static fields or closure-captured state that can be observed by concurrent requests.