<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yander Santiesteban Rojas</title>
    <description>The latest articles on DEV Community by Yander Santiesteban Rojas (@yandersr).</description>
    <link>https://dev.to/yandersr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1935909%2Fbbbde581-679c-4167-935c-a5eb86305273.jpg</url>
      <title>DEV Community: Yander Santiesteban Rojas</title>
      <link>https://dev.to/yandersr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yandersr"/>
    <language>en</language>
    <item>
      <title>The Mediator Pattern's Visibility Tax: Profiling and Debugging Your Pipeline Inside the IDE</title>
      <dc:creator>Yander Santiesteban Rojas</dc:creator>
      <pubDate>Thu, 30 Jul 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/yandersr/the-mediator-patterns-visibility-tax-profiling-and-debugging-your-pipeline-inside-the-ide-1352</link>
      <guid>https://dev.to/yandersr/the-mediator-patterns-visibility-tax-profiling-and-debugging-your-pipeline-inside-the-ide-1352</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/yandersr/the-hidden-runtime-cost-of-mediatr-style-pipelines-4j5f"&gt;the first post of this series&lt;/a&gt;, we removed the mediator pattern's &lt;strong&gt;runtime tax&lt;/strong&gt;: source-generated dispatch measured 0.99× of a direct call in a realistic three-behavior benchmark, and the mediator's own per-&lt;code&gt;Send&lt;/code&gt; overhead stays a constant 72 B no matter how deep the pipeline gets.&lt;/p&gt;

&lt;p&gt;But there's a second tax, and no benchmark can refund it: &lt;strong&gt;the visibility tax.&lt;/strong&gt; In any grown CQRS codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hidden behaviors&lt;/strong&gt; silently wrap every dispatch (validation, logging, retry, transactions) and nobody remembers the chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested mediator calls&lt;/strong&gt; inside handlers turn a single endpoint into a dozen invocations the IDE won't show you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification fan-out&lt;/strong&gt; scatters business effects across projects; tracing them takes grep and luck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project handlers&lt;/strong&gt; live in &lt;code&gt;*.Application&lt;/code&gt;, get dispatched from &lt;code&gt;*.Api&lt;/code&gt;, and decorated in &lt;code&gt;*.Infrastructure&lt;/code&gt;; no single source file tells the story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the thing though: source generation doesn't just make the pipeline fast. It makes the pipeline &lt;strong&gt;knowable at compile time&lt;/strong&gt;. If a generator can emit the whole chain, tooling can &lt;em&gt;show&lt;/em&gt; you the whole chain: exactly, not heuristically.&lt;/p&gt;

&lt;p&gt;That's what &lt;strong&gt;DSoftStudio Mediator Pipeline Explorer&lt;/strong&gt; is built on. It's an extension for &lt;strong&gt;VS Code&lt;/strong&gt; and for &lt;strong&gt;Visual Studio 2022/2026&lt;/strong&gt;, and it closes one loop: &lt;strong&gt;understand → navigate → measure → fix, without leaving the editor.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  See the pipeline
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;full&lt;/em&gt; chain: pre-processors, behaviors, handler, post-processors, exception handlers. One click (or &lt;code&gt;Ctrl+F12&lt;/code&gt; on a request type; &lt;code&gt;Cmd+F12&lt;/code&gt; on macOS) jumps to the source, including handlers that live three projects away.&lt;/p&gt;

&lt;p&gt;The same data renders as an &lt;strong&gt;interactive graph&lt;/strong&gt;, and this is where hidden structure becomes obvious:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbdum9ey4s1qis21q6ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frbdum9ey4s1qis21q6ii.png" alt="The full pipeline for a command (pre-processors, behaviors, handler, post-processors, and the exception branch) with per-node average timings and heat coloring" width="799" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging a slow pipeline
&lt;/h2&gt;

&lt;p&gt;The static picture is half the value. The other half: the profiler speaks &lt;em&gt;mediator&lt;/em&gt;, not spans.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;Hot Path&lt;/strong&gt; view attributes every millisecond in execution order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03l6bdi0x4qy4y24xls6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03l6bdi0x4qy4y24xls6.png" alt="Hot Path: per-component timings, external dependency attribution (EF Core / PostgreSQL), and self-wait, with the dominant row flagged automatically" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things in that view do the actual debugging for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;External dependency attribution.&lt;/strong&gt; 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.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-wait vs work.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bottleneck badge.&lt;/strong&gt; The dominant row is flagged automatically. In a dozen-deep chain, finding the one slow behavior takes seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Beyond single requests, the &lt;strong&gt;statistics dashboard&lt;/strong&gt; 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 &lt;code&gt;Publish&lt;/code&gt; actually fans out to. (The repo's &lt;code&gt;domain-events&lt;/code&gt; 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 &lt;em&gt;and&lt;/em&gt; processes already running outside the debugger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: what's automatic, and what isn't
&lt;/h2&gt;

&lt;p&gt;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: &lt;strong&gt;the tooling keeps its build wiring out of your repository.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The profiling hooks wire themselves in at compile time when the analyzer loads; nothing to add to &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The build wiring is machine-local (per-user), never committed to your repo.&lt;/li&gt;
&lt;li&gt;Profiling is scoped to Debug builds by default, so Release and AOT builds ship untouched.&lt;/li&gt;
&lt;li&gt;Teammates without the extension and your CI runners build plain, unmodified code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One part is &lt;em&gt;not&lt;/em&gt; automatic, and it's worth being precise about: the &lt;strong&gt;external dependency attribution&lt;/strong&gt; (the EF Core / SQL / HTTP frames in the Hot Path) comes from the &lt;code&gt;DSoftStudio.Mediator.OpenTelemetry&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The integration is a few lines in &lt;code&gt;Program.cs&lt;/code&gt;. Here's the wiring from a demo API that inserts users into PostgreSQL and publishes a domain event (the repo's &lt;code&gt;opentelemetry&lt;/code&gt; sample shows the same pattern, with metrics and a console exporter):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instrumentations ENRICH spans with semantic tags: URL/host, SQL statement,&lt;/span&gt;
&lt;span class="c1"&gt;// pipeline component. Note there's no exporter: nothing leaves the machine.&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAspNetCoreInstrumentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// tags the incoming HTTP request&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNpgsql&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                      &lt;span class="c1"&gt;// PostgreSQL spans: statement, host&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMediatorInstrumentation&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// subscribes to the "DSoftStudio.Mediator" source&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMediator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMediatorHandlers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMediatorInstrumentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HealthCheck"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// keep the flame focused&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrecompilePipelines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrecompileNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both calls are named &lt;code&gt;AddMediatorInstrumentation()&lt;/code&gt;: 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 &lt;code&gt;ActivitySource&lt;/code&gt;. It's the same wiring you'd write for any OTel instrumentation library.&lt;/p&gt;

&lt;p&gt;And note what's &lt;em&gt;missing&lt;/em&gt;: 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.&lt;/p&gt;

&lt;p&gt;Either way, adopting the tooling is a personal choice, not a team-wide commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing
&lt;/h2&gt;

&lt;p&gt;The library and all companion packages from &lt;a href="https://dev.to/yandersr/the-hidden-runtime-cost-of-mediatr-style-pipelines-4j5f"&gt;part 1&lt;/a&gt; are &lt;strong&gt;MIT and stay MIT&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;&lt;a href="https://mediator.dsoftstudio.com/" rel="noopener noreferrer"&gt;mediator.dsoftstudio.com&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://docs.dsoftstudio.com/mediator/pipeline-explorer/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; · &lt;a href="https://mediator.dsoftstudio.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;That closes the series: source generation removes the runtime cost, and the tooling uses that same compile-time knowledge to remove the blind spots.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Hidden Runtime Cost of MediatR-Style Pipelines</title>
      <dc:creator>Yander Santiesteban Rojas</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:40:24 +0000</pubDate>
      <link>https://dev.to/yandersr/the-hidden-runtime-cost-of-mediatr-style-pipelines-4j5f</link>
      <guid>https://dev.to/yandersr/the-hidden-runtime-cost-of-mediatr-style-pipelines-4j5f</guid>
      <description>&lt;p&gt;If you've shipped a CQRS codebase on a MediatR-style library, you know the trade-off. The mediator pattern buys you decoupling: thin controllers, one handler per use case, cross-cutting concerns as pipeline behaviors. But it charges two taxes in return:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The runtime tax.&lt;/strong&gt; Reflection-based dispatch, service resolution on the hot path, and allocations that &lt;em&gt;grow with pipeline depth&lt;/em&gt;. At scale, that's GC pressure, and GC pressure shows up directly in your p99.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visibility tax.&lt;/strong&gt; The pipeline is assembled at runtime inside the DI container, so nobody can actually &lt;em&gt;see&lt;/em&gt; it: which behaviors wrap this command, where that notification fans out, which component is slow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is about removing the first tax with &lt;strong&gt;&lt;a href="https://github.com/DSoftStudio/Mediator" rel="noopener noreferrer"&gt;DSoftStudio.Mediator&lt;/a&gt;&lt;/strong&gt;, an MIT-licensed, source-generated mediator for .NET. The second tax gets its own post next in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the runtime tax comes from
&lt;/h2&gt;

&lt;p&gt;In a classic runtime-composed mediator, every &lt;code&gt;Send&lt;/code&gt; walks the DI container: resolve the handler wrapper, resolve the behavior chain, and wrap each behavior around the next as a delegate with a closure. Three behaviors = three delegate allocations + closures, &lt;em&gt;per request&lt;/em&gt;. It works, but you pay in allocations that scale with pipeline depth, and in a call path the runtime can't see through.&lt;/p&gt;

&lt;p&gt;The fix isn't micro-optimizing that machinery. It's moving the work to compile time.&lt;/p&gt;

&lt;p&gt;DSoftStudio.Mediator does this with a source generator that emits handler discovery and registration, the precompiled behavior chain per request type, closed notification dispatch tables, and C# 12 interceptors that turn &lt;code&gt;Send&lt;/code&gt;/&lt;code&gt;Publish&lt;/code&gt;/&lt;code&gt;CreateStream&lt;/code&gt; call sites into direct typed invocations. By the time your app starts, there is no pipeline left to compose at runtime; what executes is ordinary generated C# you can read in your IDE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frml07vhntqs6oi820p8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frml07vhntqs6oi820p8d.png" alt="Runtime-composed mediator vs source-generated dispatch: on the left, each behavior wraps a delegate and closure and the handler is resolved per call; on the right, generated code makes direct typed calls through interface dispatch to a cached handler" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package DSoftStudio.Mediator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PingHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ping&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMediator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoggingBehavior&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;gt;));&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddRequestPreProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuditPreProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mediator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// that's it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is deliberately MediatR-compatible (requests, notifications, streams, behaviors, pre/post processors, exception handlers), so migration is mostly mechanical (handlers return &lt;code&gt;ValueTask&lt;/code&gt; instead of &lt;code&gt;Task&lt;/code&gt;; there's a migration table in the README). You also get CQRS aliases: &lt;code&gt;ICommand&amp;lt;T&amp;gt;&lt;/code&gt; / &lt;code&gt;IQuery&amp;lt;T&amp;gt;&lt;/code&gt; with matching handler interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  No magic under the hood
&lt;/h2&gt;

&lt;p&gt;Everything above is ordinary C# emitted into your build: the DI registrations, the typed &lt;code&gt;Send&lt;/code&gt; overloads, the dispatch tables. On the hot path there is no reflection, no &lt;code&gt;MakeGenericType&lt;/code&gt;, no delegate chain: just a static flag check and a direct, typed &lt;code&gt;Handle&lt;/code&gt; call, with a single cached resolve as the only DI touch. That is also why the library is &lt;strong&gt;Native AOT and trimming safe by construction&lt;/strong&gt;: there is no dynamic code path to guard.&lt;/p&gt;

&lt;p&gt;And if you ever want to audit exactly what runs, &lt;code&gt;dotnet build -p:EmitCompilerGeneratedFiles=true&lt;/code&gt; drops the generated sources under &lt;code&gt;obj/…/generated/&lt;/code&gt;. What you register is what runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design detail that makes allocations constant
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPipelineBehavior&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IRequestHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ← not a delegate&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next&lt;/code&gt; is a &lt;strong&gt;typed handler, not a &lt;code&gt;Func&amp;lt;&amp;gt;&lt;/code&gt;&lt;/strong&gt;. Behaviors chain through interface dispatch: no closures, no per-behavior allocations. The result: &lt;strong&gt;72 B per &lt;code&gt;Send&lt;/code&gt;, whether you have 0, 3, or 5 behaviors.&lt;/strong&gt; Delegate-wrapping mediators grow linearly with depth.&lt;/p&gt;

&lt;p&gt;Two more compile-time perks worth knowing (the rest live in the README):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact-type notification dispatch.&lt;/strong&gt; Publishing &lt;code&gt;DerivedEvent&lt;/code&gt; never invokes &lt;code&gt;INotificationHandler&amp;lt;BaseEvent&amp;gt;&lt;/code&gt;. No more "why did this handler fire twice" archaeology. It's a deliberate semantic difference from MediatR; know it before migrating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build-time diagnostics.&lt;/strong&gt; The analyzer flags a request with no handler, duplicate handlers, or a mocking library colliding with interceptors &lt;em&gt;while you compile&lt;/em&gt;, not at 2 a.m. in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers, and how to reproduce them
&lt;/h2&gt;

&lt;p&gt;The question that matters isn't "how fast is &lt;code&gt;Send&lt;/code&gt; in a vacuum". It's &lt;strong&gt;does the mediator add cost to your actual pipeline?&lt;/strong&gt; So the headline benchmark is a realistic one: &lt;strong&gt;Validation → Logging → Metrics → a simulated async database write&lt;/strong&gt; (a real async hop via &lt;code&gt;Task.Yield&lt;/code&gt;), three behaviors, real DI, each library measured against &lt;em&gt;its own&lt;/em&gt; direct-call baseline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Pipeline latency&lt;/th&gt;
&lt;th&gt;Allocated&lt;/th&gt;
&lt;th&gt;vs its own direct call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DSoftStudio.Mediator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;667 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;255 B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.99×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DispatchR 2.1&lt;/td&gt;
&lt;td&gt;667 ns&lt;/td&gt;
&lt;td&gt;255 B&lt;/td&gt;
&lt;td&gt;1.01×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mediator (Source Gen) 3.0&lt;/td&gt;
&lt;td&gt;718 ns&lt;/td&gt;
&lt;td&gt;397 B (1.5×)&lt;/td&gt;
&lt;td&gt;1.06×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MediatR 14.1&lt;/td&gt;
&lt;td&gt;857 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,032 B (3.8×)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.20×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;sup&gt;Allocation multiples are each library's ratio vs its own direct-call baseline (~270 B).&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In this pipeline, dispatching through DSoftStudio.Mediator is statistically indistinguishable from calling the code directly (0.99×). The measured cost is the handler's own work, not the dispatch layer.&lt;/p&gt;

&lt;p&gt;The micro numbers explain why: pure &lt;code&gt;Send&lt;/code&gt; is 7.2 ns / 72 B (direct call: 7.0 ns). With five behaviors: 15.6 ns, &lt;em&gt;still 72 B&lt;/em&gt;, while MediatR reaches 153 ns / 1,088 B. &lt;code&gt;Publish&lt;/code&gt; is 4.5 ns and zero-alloc. Constant allocation is the property that matters in production: fewer Gen0 collections → tighter p99/p999.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Environment: BenchmarkDotNet 0.15.8, .NET 10, Windows 11, i7-12700F, each library in an isolated process.&lt;/em&gt; And here's the only call to action in this post: &lt;strong&gt;&lt;a href="https://github.com/DSoftStudio/Mediator" rel="noopener noreferrer"&gt;clone the repo and run the benchmark suite on your own hardware&lt;/a&gt;&lt;/strong&gt;; per-library run scripts are included. Distrust of vendor benchmarks is healthy; that's why they ship with the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DSoftStudio.Mediator companion packages
&lt;/h2&gt;

&lt;p&gt;The core stays lean; integrations are focused MIT packages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DSoftStudio.Mediator.Abstractions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contracts only (&lt;code&gt;netstandard2.0&lt;/code&gt;), with no dependency on the mediator runtime or generators.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DSoftStudio.Mediator.OpenTelemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tracing + metrics for &lt;code&gt;Send&lt;/code&gt;, &lt;code&gt;Publish&lt;/code&gt;, and &lt;code&gt;Stream&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DSoftStudio.Mediator.FluentValidation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic request validation as a pipeline behavior.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DSoftStudio.Mediator.HybridCache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L1 + L2 response caching via Microsoft's &lt;code&gt;HybridCache&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Abstractions split keeps clean/hexagonal layering honest. The generator runs only in the composition root and discovers handlers across project references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host / API      → DSoftStudio.Mediator              (AddMediator + source generator)
Application     → DSoftStudio.Mediator.Abstractions (handlers, requests, behaviors)
Domain          → (no mediator dependency)
Infrastructure  → (no mediator dependency)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guides for the core and every companion package (installation, CQRS concepts, streaming, Native AOT notes, integrations) live at &lt;strong&gt;&lt;a href="https://docs.dsoftstudio.com/mediator/" rel="noopener noreferrer"&gt;docs.dsoftstudio.com/mediator&lt;/a&gt;&lt;/strong&gt;, and the project homepage is &lt;strong&gt;&lt;a href="https://mediator.dsoftstudio.com/" rel="noopener noreferrer"&gt;mediator.dsoftstudio.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should &lt;em&gt;not&lt;/em&gt; use this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's &lt;strong&gt;not a message bus&lt;/strong&gt;; for cross-process messaging use MassTransit, NServiceBus, or a broker.&lt;/li&gt;
&lt;li&gt;It's &lt;strong&gt;not an event sourcing framework&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If your design depends on runtime flexibility (inheritance-based notification dispatch, dynamic handler discovery), the compile-time model trades that away by design; weigh that before migrating.&lt;/li&gt;
&lt;li&gt;And if you don't need the mediator pattern at all, a direct method call is still the fastest mediator ever written.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That covers the runtime tax. The second tax is one no benchmark can fix: &lt;strong&gt;you still can't see the pipeline.&lt;/strong&gt; Which behaviors wrap each command, where notifications fan out, which component is the slow one, and on which request. Next post in this series: turning the pipeline into something you can navigate, graph, and profile without leaving your IDE.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
