Skip to content

Release 1.3.0: dispatch-observer seam + order-independent handler DI-lifetime - #9

Merged
yandersr merged 7 commits into
mainfrom
1.3.0
Jun 28, 2026
Merged

Release 1.3.0: dispatch-observer seam + order-independent handler DI-lifetime#9
yandersr merged 7 commits into
mainfrom
1.3.0

Conversation

@yandersr

Copy link
Copy Markdown
Contributor

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.2
  • DSoftStudio.Mediator.OpenTelemetry: 1.1.0-rc.3
  • DSoftStudio.Mediator.FluentValidation + DSoftStudio.Mediator.HybridCache: 1.0.9-rc.2

Validation

Full OSS solution builds green (0 errors); test suite green.

yandersr added 7 commits June 25, 2026 02:43
…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).
@github-actions

Copy link
Copy Markdown
Contributor

Summary

Summary
Generated on: 06/28/2026 - 15:42:12
Coverage date: 06/28/2026 - 15:41:50 - 06/28/2026 - 15:42:07
Parser: MultiReport (8x Cobertura)
Assemblies: 6
Classes: 69
Files: 63
Line coverage: 93.2% (3524 of 3779)
Covered lines: 3524
Uncovered lines: 255
Coverable lines: 3779
Total lines: 8956
Branch coverage: 81.4% (1412 of 1734)
Covered branches: 1412
Total branches: 1734
Method coverage: Feature is only available for sponsors
Tag: 141_28327350924

Coverage

DSoftStudio.Mediator - 98.4%
Name Line Branch
DSoftStudio.Mediator 98.4% 92.8%
DSoftStudio.Mediator.BehaviorHandlerAdapter`2 100% 100%
DSoftStudio.Mediator.HandlerCache`2 100% 100%
DSoftStudio.Mediator.HandlerLifetimeOptimizer 100% 90.4%
DSoftStudio.Mediator.Mediator 100% 91.6%
DSoftStudio.Mediator.MediatorBuilder 100% 100%
DSoftStudio.Mediator.NotificationCachedDispatcher 100% 100%
DSoftStudio.Mediator.NotificationDispatch`1 100%
DSoftStudio.Mediator.NotificationHandlerCache`1 100% 100%
DSoftStudio.Mediator.NotificationObjectDispatch 90.4% 75%
DSoftStudio.Mediator.NotificationPublisherFlag 100% 100%
DSoftStudio.Mediator.ParallelNotificationPublisher 100% 100%
DSoftStudio.Mediator.PipelineChainCache`2 100% 100%
DSoftStudio.Mediator.PipelineChainHandler`2 98.4% 91.8%
DSoftStudio.Mediator.RequestDispatch`2 100%
DSoftStudio.Mediator.RequestObjectDispatch 90.9% 50%
DSoftStudio.Mediator.SequentialNotificationPublisher 100% 100%
DSoftStudio.Mediator.ServiceCollectionExtensions 100%
DSoftStudio.Mediator.StreamBehaviorHandlerAdapter`2 100% 100%
DSoftStudio.Mediator.StreamDispatch`2 100%
DSoftStudio.Mediator.StreamHandlerCache`2 100% 100%
DSoftStudio.Mediator.StreamPipelineChainCache`2 100% 100%
DSoftStudio.Mediator.StreamPipelineChainHandler`2 92.8% 75%
DSoftStudio.Mediator.StreamPipelineInvoker 100% 100%
DSoftStudio.Mediator.Abstractions - 91.3%
Name Line Branch
DSoftStudio.Mediator.Abstractions 91.3% ****
DSoftStudio.Mediator.Abstractions.HandlerLifetimeAttribute 0%
DSoftStudio.Mediator.Abstractions.MediatorHandlerRegistrationAttribute 100%
DSoftStudio.Mediator.Abstractions.RequestExceptionHandlerState`1 100%
DSoftStudio.Mediator.Abstractions.Unit 100%
DSoftStudio.Mediator.FluentValidation - 100%
Name Line Branch
DSoftStudio.Mediator.FluentValidation 100% 87.5%
DSoftStudio.Mediator.FluentValidation.FluentValidationServiceCollectionExte
nsions
100%
DSoftStudio.Mediator.FluentValidation.MediatorValidationException 100% 83.3%
DSoftStudio.Mediator.FluentValidation.ValidationBehavior`2 100% 90%
DSoftStudio.Mediator.Generators - 91.9%
Name Line Branch
DSoftStudio.Mediator.Generators 91.9% 76.9%
DSoftStudio.Mediator.Generators.BehaviorTypeInfo 100% 100%
DSoftStudio.Mediator.Generators.CqrsSemanticAnalyzer 86.3% 61.5%
DSoftStudio.Mediator.Generators.DependencyInjectionGenerator 89.8% 78.1%
DSoftStudio.Mediator.Generators.DiagnosticDescriptors 100%
DSoftStudio.Mediator.Generators.EquatableArray`1 100% 92.8%
DSoftStudio.Mediator.Generators.HandlerDiscovery 97.1% 84%
DSoftStudio.Mediator.Generators.InterceptorHelpers 84.2% 55.8%
DSoftStudio.Mediator.Generators.MediatorExtensionsGenerator 93.4% 69.3%
DSoftStudio.Mediator.Generators.MediatorPipelineGenerator 93.4% 78.2%
DSoftStudio.Mediator.Generators.MixedRegistrationApiAnalyzer 91.1% 74.5%
DSoftStudio.Mediator.Generators.MockDetectionAnalyzer 100% 100%
DSoftStudio.Mediator.Generators.NotificationGenerator 92.5% 77.7%
DSoftStudio.Mediator.Generators.PublishInterceptorGenerator 90.4% 67.2%
DSoftStudio.Mediator.Generators.ReferencedAssemblyScanner 85.8% 81.6%
DSoftStudio.Mediator.Generators.SelfHandlerDetail 100% 83.3%
DSoftStudio.Mediator.Generators.SelfHandlerParam 100% 83.3%
DSoftStudio.Mediator.Generators.SendInterceptorGenerator 91.6% 75%
DSoftStudio.Mediator.Generators.StreamGenerator 94.9% 81%
DSoftStudio.Mediator.Generators.StreamInterceptorGenerator 90.9% 73.3%
DSoftStudio.Mediator.HybridCache - 100%
Name Line Branch
DSoftStudio.Mediator.HybridCache 100% 100%
DSoftStudio.Mediator.HybridCache.CachingBehavior`2 100% 100%
DSoftStudio.Mediator.HybridCache.HybridCacheServiceCollectionExtensions 100%
DSoftStudio.Mediator.HybridCache.ICachedRequest 100%
DSoftStudio.Mediator.OpenTelemetry - 96.8%
Name Line Branch
DSoftStudio.Mediator.OpenTelemetry 96.8% 90.2%
DSoftStudio.Mediator.OpenTelemetry.ActivityHelper 100% 100%
DSoftStudio.Mediator.OpenTelemetry.DatabaseSpanEnrichmentProcessor 95% 92.3%
DSoftStudio.Mediator.OpenTelemetry.InstrumentedNotificationPublisher 97.3% 89.1%
DSoftStudio.Mediator.OpenTelemetry.MediatorDispatchTracingObserver 100% 93.7%
DSoftStudio.Mediator.OpenTelemetry.MediatorInstrumentation 100% 50%
DSoftStudio.Mediator.OpenTelemetry.MediatorInstrumentationOptions 100%
DSoftStudio.Mediator.OpenTelemetry.MediatorMetrics 100%
DSoftStudio.Mediator.OpenTelemetry.MediatorMetricsBehavior`2 100% 100%
DSoftStudio.Mediator.OpenTelemetry.MediatorNotificationMetadata`1 100%
DSoftStudio.Mediator.OpenTelemetry.MediatorStreamMetadata`2 100%
DSoftStudio.Mediator.OpenTelemetry.MediatorStreamMetricsBehavior`2 100% 85.7%
DSoftStudio.Mediator.OpenTelemetry.MediatorStreamTracingBehavior`2 100% 83.3%
DSoftStudio.Mediator.OpenTelemetry.MediatorTelemetryMetadata`2 100% 100%
DSoftStudio.Mediator.OpenTelemetry.OpenTelemetryServiceCollectionExtensions 86.8% 77.2%
DSoftStudio.Mediator.OpenTelemetry.SqlStatementParser 95.2% 94.2%
OpenTelemetry.Metrics.MediatorMeterProviderBuilderExtensions 100%
OpenTelemetry.Trace.MediatorTracerProviderBuilderExtensions 100%

@yandersr
yandersr merged commit d4f16e8 into main Jun 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant