In the first post of this series, we removed the mediator pattern's runtime tax: source-generated dispatch measured 0.99× of a direct call in a realistic three-behavior benchmark, and the mediator's own per-Send overhead stays a constant 72 B no matter how deep the pipeline gets.
But there's a second tax, and no benchmark can refund it: the visibility tax. In any grown CQRS codebase:
- Hidden behaviors silently wrap every dispatch (validation, logging, retry, transactions) and nobody remembers the chain.
- Nested mediator calls inside handlers turn a single endpoint into a dozen invocations the IDE won't show you.
- Notification fan-out scatters business effects across projects; tracing them takes grep and luck.
-
Cross-project handlers live in
*.Application, get dispatched from*.Api, and decorated in*.Infrastructure; no single source file tells the story.
Here's the thing though: source generation doesn't just make the pipeline fast. It makes the pipeline knowable at compile time. If a generator can emit the whole chain, tooling can show you the whole chain: exactly, not heuristically.
That's what DSoftStudio Mediator Pipeline Explorer is built on. It's an extension for VS Code and for Visual Studio 2022/2026, and it closes one loop: understand → navigate → measure → fix, without leaving the editor.
See the pipeline
Open your solution, build once, and the extension discovers every command, query, notification, and stream: zero registration, zero attributes, zero config files. The pipeline tree groups them CQRS-style, and under each request you see the full chain: pre-processors, behaviors, handler, post-processors, exception handlers. One click (or Ctrl+F12 on a request type; Cmd+F12 on macOS) jumps to the source, including handlers that live three projects away.
The same data renders as an interactive graph, and this is where hidden structure becomes obvious:
Notification fan-out and nested mediator calls are drawn inline, so "one endpoint" that's actually eleven invocations across four projects stops being tribal knowledge.
Debugging a slow pipeline
The static picture is half the value. The other half: the profiler speaks mediator, not spans.
Here's the loop as it plays out on a real solution. Your dashboard flags a pipeline as tail-heavy: p99 way out of proportion to p50. In a generic APM tool, you'd now be digging through trace waterfalls trying to map spans back to behaviors.
Instead, you start a profiling session and hit the endpoint. The graph nodes light up with live timings and heat coloring; the hot node is visually obvious. Then the Hot Path view attributes every millisecond in execution order:
Three things in that view do the actual debugging for you:
- External dependency attribution. EF Core query compilation, connection acquisition, individual SQL statements, HTTP calls, all attached to the exact pipeline component that triggered them. "The handler is slow" becomes "this one SQL statement inside this handler is slow." (This level of detail comes from the OpenTelemetry companion package; see the setup notes below.)
- Self-wait vs work. Time lost to I/O and async scheduling is separated from CPU time, so you stop optimizing code that was never busy in the first place.
- The bottleneck badge. The dominant row is flagged automatically. In a dozen-deep chain, finding the one slow behavior takes seconds.
You fix the offender, profile again, and watch the heat drain out of the graph. That's the whole loop: no add-a-log-line-and-redeploy cycles, no span-to-code guesswork.
Beyond single requests, the statistics dashboard tracks per-pipeline Calls / Errors / p50 / p95 / p99 / Max, flags tail-heavy pipelines against configurable SLO thresholds, and breaks down notification fan-out with per-handler overhead, so you can see what that one innocent Publish actually fans out to. (The repo's domain-events sample is a ready-made playground for this: one POST publishes an event that fans out to three independent handlers.) It profiles apps you launch and processes already running outside the debugger.
Setup: what's automatic, and what isn't
The core profiling (handler and behavior timings, percentiles, fan-out) is wired without touching your code, and under one engineering constraint I care a lot about: the tooling keeps its build wiring out of your repository.
- The profiling hooks wire themselves in at compile time when the analyzer loads; nothing to add to
Program.cs. - The build wiring is machine-local (per-user), never committed to your repo.
- Profiling is scoped to Debug builds by default, so Release and AOT builds ship untouched.
- Teammates without the extension and your CI runners build plain, unmodified code.
One part is not automatic, and it's worth being precise about: the external dependency attribution (the EF Core / SQL / HTTP frames in the Hot Path) comes from the DSoftStudio.Mediator.OpenTelemetry companion package, which you install and configure in the app like any OpenTelemetry integration. Without it you still get the full mediator-level picture (component timings, percentiles, fan-out), just not the dependency breakdown inside your handlers.
The integration is a few lines in Program.cs. Here's the wiring from a demo API that inserts users into PostgreSQL and publishes a domain event (the repo's opentelemetry sample shows the same pattern, with metrics and a console exporter):
// Instrumentations ENRICH spans with semantic tags: URL/host, SQL statement,
// pipeline component. Note there's no exporter: nothing leaves the machine.
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddAspNetCoreInstrumentation() // tags the incoming HTTP request
.AddNpgsql() // PostgreSQL spans: statement, host
.AddMediatorInstrumentation()); // subscribes to the "DSoftStudio.Mediator" source
builder.Services
.AddMediator()
.RegisterMediatorHandlers()
.AddMediatorInstrumentation(o =>
o.Filter = type => !type.Name.StartsWith("HealthCheck")) // keep the flame focused
.PrecompilePipelines()
.PrecompileNotifications();
Both calls are named AddMediatorInstrumentation(): one registers the instrumentation inside the mediator pipeline (with an options callback; here it filters out health checks), the other subscribes the OpenTelemetry SDK to the mediator's ActivitySource. It's the same wiring you'd write for any OTel instrumentation library.
And note what's missing: an exporter. For the IDE workflow you don't need one. The extension reads the already-enriched spans from the running process, and nothing is exported anywhere. When you also want production observability, add your exporter (OTLP, Application Insights, or any other) and the exact same wiring feeds both.
Either way, adopting the tooling is a personal choice, not a team-wide commit.
Licensing
The library and all companion packages from part 1 are MIT and stay MIT. Pipeline Explorer is commercial software with a free trial; the same activation works in both the VS Code and Visual Studio editions. If a license ever lapses, your code keeps compiling and running on the MIT core: the extension adds visibility on top of your build and never becomes a dependency of it.
If your day job involves a mediator pipeline deeper than three behaviors, point the trial at your gnarliest real solution; that's the fastest honest test of whether it earns a place in your toolbox: mediator.dsoftstudio.com · docs · pricing
That closes the series: source generation removes the runtime cost, and the tooling uses that same compile-time knowledge to remove the blind spots.


Top comments (0)