Conversation
…e captive fix Dispatch observer (Ports & Adapters): - Abstractions: IMediatorDispatchObserver port — one observation scope per dispatch wrapping the WHOLE pipeline (pre/post-processors included), with no Activity dependency in the core. - OpenTelemetry: MediatorDispatchTracingObserver adapter replaces MediatorTracingBehavior (deleted) — opens the span once per dispatch. Zero overhead for non-OTel apps: the observer is null on the hot path (a single field-null check in Handle). - MediatorBuilder.AddDispatchObserver; the generator forces a chain when an observer is registered so handler-only requests are still observed. Chain-lifetime captive fix: - PrecompilePipelines now folds the HANDLER's lifetime into the chain lifetime, not only the pipeline components. A Singleton chain wrapping a Transient/Scoped handler captured it (and its scoped deps, e.g. an injected IMediator) for the application lifetime -> "Cannot consume scoped service from singleton" at BuildServiceProvider. The chain is now Scoped when the handler is non-singleton, and still Singleton (zero-alloc, pre-linked once) when everything it wraps is singleton. - Observer-forced chains no longer pin allSingleton=false; the folded lifetimes decide, so a singleton handler keeps a cached Singleton chain. Docs: ADR-0001 + dispatch-pipeline.md (chain lifetime = lowest of handler + components).
A handler with constructor dependencies was always registered Transient,
so under scope-per-request (one DI scope per HTTP request) it was
re-resolved and re-allocated every request - ~24 B + ~6.5 ns each,
against the zero-allocation premise.
RegisterMediatorHandlers now stages each dependency-carrying request
handler (recording the descriptor it created plus the dependency types).
The finalization step - PrecompilePipelines or the single-call AddMediator
- runs HandlerLifetimeOptimizer.Apply once every registration is present,
raising each handler to the longest SAFE lifetime its dependencies allow:
- Singleton when every dependency is a singleton (cached, zero-alloc per
request, and via the chain-lifetime fold a cached Singleton chain too)
- Scoped when any dependency is scoped
- Transient (unchanged) when any dependency is transient or unregistered
Running at finalization makes it order-independent: a dependency
registered AFTER RegisterMediatorHandlers (the common composition-root
order) is still seen. A reference-identity guard upgrades a handler only
while the generator's own descriptor is still the winning registration, so
any user re-registration - including an identical AddTransient that pins
Transient - is respected. [HandlerLifetime(...)] pins a lifetime
explicitly. No reflection: only registered ServiceDescriptor.Lifetime
values are read, so it is AOT/trim-safe and never touches the dispatch hot
path (generated dispatch IL is byte-identical).
Tests: HandlerLifetimeOptimizerTests (logic, order-independence, reference
guard, idempotency) and HandlerLifetimeAutoDetectionTests (end-to-end
through the generated path). Full OSS suite green.
…erver seam Handle() delegated the 3-way dispatch switch to HandleCore(); despite the AggressiveInlining hint the JIT did not inline it, adding a real call to every chain dispatch. Peel the cold observer path off first and run the switch inline in Handle() (as it was pre-seam); HandleCore() now serves only the cold observer paths. The no-observer hot path is back to a single field-null check + the inline switch. No behavior change: 555 OSS tests green (incl. 101 OpenTelemetry adapter tests).
…erver seam Handle() delegated the 3-way dispatch switch to HandleCore(); despite the AggressiveInlining hint the JIT did not inline it, adding a real call to every chain dispatch. Peel the cold observer path off first and run the switch inline in Handle() (as it was pre-seam); HandleCore() now serves only the cold observer paths. The no-observer hot path is back to a single field-null check + the inline switch. No behavior change: 555 OSS tests green (incl. 101 OpenTelemetry adapter tests).
…dler-lifetime release core + Abstractions 1.3.0-rc.1 -> 1.3.0-rc.2; OpenTelemetry 1.1.0-rc.2 -> 1.1.0-rc.3; FluentValidation + HybridCache 1.0.9-rc.1 -> 1.0.9-rc.2. Version-string only; carries the already-committed dispatch-observer seam + order-independent handler DI-lifetime auto-detection. Build green (0 errors).
…mmary (Ports & Adapters)
Contributor
SummarySummary
CoverageDSoftStudio.Mediator - 98.4%
DSoftStudio.Mediator.Abstractions - 91.3%
DSoftStudio.Mediator.FluentValidation - 100%
DSoftStudio.Mediator.Generators - 91.9%
DSoftStudio.Mediator.HybridCache - 100%
DSoftStudio.Mediator.OpenTelemetry - 96.8%
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Highlights
feat(core): dispatch-observer seam (Ports & Adapters)
Exposes the request-dispatch lifecycle as an optional observation port (
IMediatorDispatchObserver) so an external adapter (e.g. the OpenTelemetry bridge) can wrap the WHOLE pipeline — pre-processors, behaviors, handler, post-processors. A pipeline behavior cannot do this because pre-/post-processors run OUTSIDE the behavior chain. The mediator itself never observes or traces; it only exposes the boundary. Includes the chain-lifetime captive-dependency fix.feat(di): order-independent handler DI-lifetime auto-detection
A request handler is raised from the Transient default to the longest SAFE lifetime its constructor dependencies allow — Singleton when every dependency is a singleton, Scoped when any dependency is scoped; a stateless handler (no constructor parameters) is Singleton. Staged at registration and applied at finalization (
PrecompilePipelines) so dependencies registered BEFORE or AFTER the handler are both seen, with a reference-identity guard that respects any user re-registration.[HandlerLifetime]opts out.perf(dispatch): keep the PipelineChainHandler hot path flat under the observer seam
The observation port adds no overhead on the dispatch hot path when no observer is attached.
Release / versions
DSoftStudio.Mediator(core) +DSoftStudio.Mediator.Abstractions: 1.3.0-rc.2DSoftStudio.Mediator.OpenTelemetry: 1.1.0-rc.3DSoftStudio.Mediator.FluentValidation+DSoftStudio.Mediator.HybridCache: 1.0.9-rc.2Validation
Full OSS solution builds green (0 errors); test suite green.