diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 5685489..2a4bc08 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -53,13 +53,15 @@ jobs: /d:sonar.exclusions="**/obj/**,**/bin/**,samples/**,benchmarks/**,docs/**,tests/**" /d:sonar.coverage.exclusions="src/**/DSoftStudio.Mediator.Generators/**" /d:sonar.cpd.exclusions="src/**/DSoftStudio.Mediator.Generators/**" - /d:sonar.issue.ignore.multicriteria="e1,e2,e3" + /d:sonar.issue.ignore.multicriteria="e1,e2,e3,e4" /d:sonar.issue.ignore.multicriteria.e1.ruleKey="csharpsquid:S2326" /d:sonar.issue.ignore.multicriteria.e1.resourceKey="src/**/I*Request.cs" /d:sonar.issue.ignore.multicriteria.e2.ruleKey="csharpsquid:S3267" /d:sonar.issue.ignore.multicriteria.e2.resourceKey="src/**/DSoftStudio.Mediator.Generators/**" /d:sonar.issue.ignore.multicriteria.e3.ruleKey="csharpsquid:S2743" /d:sonar.issue.ignore.multicriteria.e3.resourceKey="src/**/DSoftStudio.Mediator/*Cache.cs" + /d:sonar.issue.ignore.multicriteria.e4.ruleKey="*" + /d:sonar.issue.ignore.multicriteria.e4.resourceKey="tests/**/*" - name: Build run: dotnet build DSoftStudio.Mediator.slnx -c Release --no-incremental diff --git a/CHANGELOG.md b/CHANGELOG.md index c36df67..4bd3a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.3.0] — Unreleased (pre-release `1.3.0-rc.1`) +## [1.3.0] — 2026-07-08 -> Companions: `OpenTelemetry` 1.1.0-rc.1 · `HybridCache` 1.0.9-rc.1 · `FluentValidation` 1.0.9-rc.1. -> Soaking as a release candidate before promotion to the stable `1.3.0`. +> Companions: `OpenTelemetry` 1.1.0 · `HybridCache` 1.0.9 · `FluentValidation` 1.0.9. ### Added diff --git a/docs/mediator/changelog.md b/docs/mediator/changelog.md index 8359cb1..e36af0e 100644 --- a/docs/mediator/changelog.md +++ b/docs/mediator/changelog.md @@ -15,10 +15,9 @@ description: "All notable changes to DSoftStudio.Mediator." # Changelog -## [1.3.0] — Unreleased (pre-release `1.3.0-rc.1`) +## [1.3.0] — 2026-07-08 -> Companions: `OpenTelemetry` 1.1.0-rc.1 · `HybridCache` 1.0.9-rc.1 · `FluentValidation` 1.0.9-rc.1. -> Soaking as a release candidate before promotion to the stable `1.3.0`. +> Companions: `OpenTelemetry` 1.1.0 · `HybridCache` 1.0.9 · `FluentValidation` 1.0.9. ### Added diff --git a/docs/mediator/integrations/hybridcache.md b/docs/mediator/integrations/hybridcache.md index 48708d9..6f6fdfe 100644 --- a/docs/mediator/integrations/hybridcache.md +++ b/docs/mediator/integrations/hybridcache.md @@ -48,7 +48,7 @@ public record GetProduct(Guid Id) : IQuery, ICachedRequest } ``` -That's it — the `CachingBehavior` intercepts the pipeline, checks for `ICachedRequest`, and uses `HybridCache.GetOrCreateAsync()` to cache the result. Requests that don't implement `ICachedRequest` pass through with zero overhead. +That's it — the `CachingBehavior` intercepts the pipeline, checks for `ICachedRequest`, and uses `HybridCache.GetOrCreateAsync()` to cache the result. Requests that don't implement `ICachedRequest` pass through with negligible overhead — a single `is not ICachedRequest` type-check before the behavior awaits the rest of the pipeline. ## Adding Redis as L2 diff --git a/docs/mediator/integrations/opentelemetry.md b/docs/mediator/integrations/opentelemetry.md index 8c77905..e449d8d 100644 --- a/docs/mediator/integrations/opentelemetry.md +++ b/docs/mediator/integrations/opentelemetry.md @@ -27,7 +27,7 @@ dotnet add package DSoftStudio.Mediator.OpenTelemetry services .AddMediator() .RegisterMediatorHandlers() - .AddMediatorInstrumentation() // ← registers tracing + metrics behaviors + .AddMediatorInstrumentation() // ← registers tracing + metrics instrumentation .PrecompilePipelines() .PrecompileNotifications() .PrecompileStreams(); diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/behavior-detail.png b/docs/mediator/pipeline-explorer/assets/screenshots/behavior-detail.png index 081244f..49ff96b 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/behavior-detail.png and b/docs/mediator/pipeline-explorer/assets/screenshots/behavior-detail.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/behaviors.png b/docs/mediator/pipeline-explorer/assets/screenshots/behaviors.png index f60679a..2c0094d 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/behaviors.png and b/docs/mediator/pipeline-explorer/assets/screenshots/behaviors.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/bridge-flame-dependencies.png b/docs/mediator/pipeline-explorer/assets/screenshots/bridge-flame-dependencies.png new file mode 100644 index 0000000..040be08 Binary files /dev/null and b/docs/mediator/pipeline-explorer/assets/screenshots/bridge-flame-dependencies.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/bridge-profiler.png b/docs/mediator/pipeline-explorer/assets/screenshots/bridge-profiler.png new file mode 100644 index 0000000..2328038 Binary files /dev/null and b/docs/mediator/pipeline-explorer/assets/screenshots/bridge-profiler.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/dashboard.png b/docs/mediator/pipeline-explorer/assets/screenshots/dashboard.png index a6df864..fa28ddb 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/dashboard.png and b/docs/mediator/pipeline-explorer/assets/screenshots/dashboard.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/empty-pipeline.png b/docs/mediator/pipeline-explorer/assets/screenshots/empty-pipeline.png index 493246d..c4ecf2a 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/empty-pipeline.png and b/docs/mediator/pipeline-explorer/assets/screenshots/empty-pipeline.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/graph-view-detached.png b/docs/mediator/pipeline-explorer/assets/screenshots/graph-view-detached.png index d5e85ec..150c93b 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/graph-view-detached.png and b/docs/mediator/pipeline-explorer/assets/screenshots/graph-view-detached.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/graph-view.png b/docs/mediator/pipeline-explorer/assets/screenshots/graph-view.png index e555ed4..19187c7 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/graph-view.png and b/docs/mediator/pipeline-explorer/assets/screenshots/graph-view.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/handler-detail.png b/docs/mediator/pipeline-explorer/assets/screenshots/handler-detail.png index 79caf21..7773272 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/handler-detail.png and b/docs/mediator/pipeline-explorer/assets/screenshots/handler-detail.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/pipeline-tree.png b/docs/mediator/pipeline-explorer/assets/screenshots/pipeline-tree.png index ccd53d0..62558f3 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/pipeline-tree.png and b/docs/mediator/pipeline-explorer/assets/screenshots/pipeline-tree.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-active.png b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-active.png index 364a0c6..062d81c 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-active.png and b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-active.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-empty.png b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-empty.png index b6c1679..8aab27b 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-empty.png and b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-empty.png differ diff --git a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-hot-path.png b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-hot-path.png index ee40192..02c62b3 100644 Binary files a/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-hot-path.png and b/docs/mediator/pipeline-explorer/assets/screenshots/runtime-profiler-hot-path.png differ diff --git a/docs/mediator/pipeline-explorer/getting-started/installation.md b/docs/mediator/pipeline-explorer/getting-started/installation.md index aece047..763a9b5 100644 --- a/docs/mediator/pipeline-explorer/getting-started/installation.md +++ b/docs/mediator/pipeline-explorer/getting-started/installation.md @@ -40,7 +40,7 @@ A single license activates both editions, so installing one does not prevent ins Or install from the command line: ```shell -code --install-extension DSoftStudio.dsoftstudio-mediator +code --install-extension DSoftStudio.mediator-pipeline-explorer ``` ### Install from a `.vsix` file @@ -48,7 +48,7 @@ code --install-extension DSoftStudio.dsoftstudio-mediator If your environment can't reach the Marketplace, download the latest release from the [GitHub releases page](https://github.com/DSoftStudio/Mediator.Enterprise/releases) and install: ```shell -code --install-extension dsoftstudio-mediator-.vsix +code --install-extension mediator-pipeline-explorer--.vsix ``` ### First launch @@ -135,7 +135,7 @@ Your license persists across IDE restarts. See [Troubleshooting: activation fail ### Visual Studio Code ```shell -code --uninstall-extension DSoftStudio.dsoftstudio-mediator +code --uninstall-extension DSoftStudio.mediator-pipeline-explorer ``` Or remove it from the Extensions view. diff --git a/docs/mediator/pipeline-explorer/getting-started/quick-start.md b/docs/mediator/pipeline-explorer/getting-started/quick-start.md index d78a05c..ab3b02f 100644 --- a/docs/mediator/pipeline-explorer/getting-started/quick-start.md +++ b/docs/mediator/pipeline-explorer/getting-started/quick-start.md @@ -17,7 +17,7 @@ description: "Your first solution scan, profiling session, and source navigation By the end of this guide you will have scanned your first solution, navigated to a handler in one click, and watched live profiling data flow in. -This walkthrough assumes you have already [installed the extension](installation.md) and opened a solution that references `DSoftStudio.Mediator`. The steps are identical in Visual Studio Code and Visual Studio; the screenshots below are drawn from both — the layout and workflow match in either IDE. +This walkthrough assumes you have already [installed the extension](installation.md) and opened a solution that references `DSoftStudio.Mediator`. The core workflow is the same in Visual Studio Code and Visual Studio, and the screenshots below are drawn from both. A few features are Visual Studio-only, however — detaching/docking the graph, the section-filter dropdown, and some context-menu commands — and those differences are called out inline as you reach them. --- @@ -48,7 +48,7 @@ The tree populates with three sections — **Request Pipelines**, **Notification The badge after each request pipeline tells you two things at a glance: - **CQRS kind** — `Command`, `Query`, or `Request` (when neither marker interface is implemented). -- **Pipeline mode** — `PassThrough` (handler only), `BehaviorsOnly`, or `Full` (pre/post-processors + behaviors). +- **Pipeline mode** — **Direct** (handler only), **Behaviors**, or **Full Pipeline** (pre/post-processors + behaviors). If the tree is empty, click **Refresh** in the toolbar. If it is still empty after refresh, see [Troubleshooting: empty tree](../troubleshooting/empty-tree.md). @@ -72,27 +72,27 @@ Click any item — handler, behavior, or call site — and the IDE jumps to the The execution-order table numbers every step in the exact order it runs — pre-processors, behaviors, the handler, then post-processors. Behavior and processor times are **inclusive**: each wraps the steps below it, so its average equals the combined time of everything it surrounds plus its own overhead. -> **Tip:** right-click any node for **Go to Definition**, **Find All References**, **Copy Type Name**, and **Pin to Quick Access**. +> **Tip:** right-click any node for a context menu. In **Visual Studio** it offers **Go to Definition**, **Find All References**, **Copy Type Name**, and **Pin to Quick Access**. In **VS Code** it offers **Go to Source**, **Show Pipeline Graph**, and **Copy Type Name** (no Find All References / Pin to Quick Access). --- ## 3. Visualize the pipeline graph -The detail panel exposes a **graph** toggle (the branching icon in the toolbar) that opens the interactive graph below the detail. +The detail panel exposes a **graph** toggle (the branching icon in the toolbar) that opens the interactive graph. - **Pan** — drag empty space. - **Zoom** — mouse wheel. - **Click a node** — navigates to source in the editor. -- **Detach** — pop the graph out into its own resizable window, then **Dock back** when you're done. +- **Detach / Dock** (Visual Studio only) — pop the graph out into its own resizable window, then **Dock back** when you're done. -The graph lays out the full request flow left to right: **Send → request → pre-processors → behaviors → handler → post-processors**. It sits docked beneath the detail panel, so you can read the structure and the step list together. +The graph lays out the full request flow left to right: **Send → request → pre-processors → behaviors → handler → post-processors**. In **Visual Studio** it sits docked beneath the detail panel, so you can read the structure and the step list together; in **VS Code** it opens as a side-by-side editor tab next to the detail.
Detail panel above a docked pipeline graph for PlaceOrderCommand: Send, the command, two pre-processors, the handler, and a post-processor laid out left to right
The graph docked beneath the detail panel for a Full pipeline — Send → PlaceOrderCommand → LoggingPreProcessor → ValidationPreProcessor → PlaceOrderCommandHandler → AuditPostProcessor — every node click-to-source.
-Need more room? **Detach** the graph into its own window — handy for wide pipelines or walking your team through the flow — then **Dock back** when you're done. +Need more room? In **Visual Studio** you can **Detach** the graph into its own window — handy for wide pipelines or walking your team through the flow — then **Dock back** when you're done. (In **VS Code** the graph already opens as a full side-by-side editor tab, so there is no detach/dock step.)
The pipeline graph popped out into its own full-window detached view, showing the end-to-end flow from Send through the processors to the post-processor @@ -117,7 +117,7 @@ A handler rarely works alone — it often publishes a notification or dispatches ## 4. Start runtime profiling -Profiling captures live timings as your code runs. The profiling hooks are wired into your application **automatically** by the analyzer — there is nothing to add to `Program.cs`. As long as the project that calls `services.AddMediator(...)` has the Pipeline Explorer analyzer loaded (which the extension auto-injects via `Directory.Build.props`), the hooks are emitted at compile time with zero allocation overhead until a profiling session is attached. +Profiling captures live timings as your code runs. The profiling hooks are wired into your application **automatically** by the analyzer — there is nothing to add to `Program.cs`, and nothing is written into your repository. The extension injects the analyzer machine-locally through an MSBuild user-extensions file (`$(MSBuildUserExtensionsPath)\DSoftStudio.Mediator\enabled.props`), so every build on your machine picks it up without touching your solution's `Directory.Build.props`. (Older versions used to write a marked block into the repo's `Directory.Build.props`; those legacy markers are now actively stripped.) As long as the project that calls `services.AddMediator(...)` has the analyzer loaded, the hooks are emitted at compile time with zero allocation overhead until a profiling session is attached. ### Attach the profiler @@ -131,18 +131,18 @@ Profiling captures live timings as your code runs. The profiling hooks are wired Then issue requests against your application — run an integration test, exercise an endpoint, replay traffic. Within a second or two, the **Runtime Profiler** fills with live timings. -Each behavior is timed independently. Select a pipeline's **Behaviors** node to see every behavior's own runtime profile side by side — calls, average, self time, p50/p95/p99, and max — so you can tell which cross-cutting concern (validation, logging, transactions) is costing you. Tail-heavy handlers — those with a p99 far above the median — are flagged so you can spot bottlenecks instantly. The default threshold (`tailHeavyMinP99Ms`) prevents false positives on sub-millisecond noise. +Each component is timed independently. Select a pipeline's **Components** node to see every component's own runtime profile side by side — calls, average, self time, p50/p95/p99, and max — so you can tell which cross-cutting concern (validation, logging, transactions) is costing you. Tail-heavy handlers — those with a p99 far above the median — are flagged so you can spot bottlenecks instantly. The default threshold (`tailHeavyMinP99Ms`) prevents false positives on sub-millisecond noise.
- Behaviors panel listing each pipeline behavior with its calls, average, self time, p50/p95/p99, max, and kind (Pre or Post) -
The Behaviors panel — each behavior's runtime profile side by side (calls, avg, self, p50/p95/p99, max) with its kind (Pre / Post), so the costly cross-cutting concern stands out.
+ Components panel listing each pipeline component with its calls, average, self time, p50/p95/p99, max, and kind (Pre or Post) +
The Components panel — each component's runtime profile side by side (calls, avg, self, p50/p95/p99, max) with its kind (Pre / Post), so the costly cross-cutting concern stands out.
-Select a single behavior to see its type, lifetime, and source file — with one click to jump to where it's defined. +Select a single component to see its type, lifetime, and source file — with one click to jump to where it's defined.
- Detail for a single behavior: its runtime-stats row plus a properties panel with type, kind, lifetime, file path, and Open in Editor / Copy buttons -
Drilling into one behavior — its runtime stats plus type, kind, lifetime, and source file, with Open in Editor to jump straight to its definition.
+ Detail for a single component: its runtime-stats row plus a properties panel with type, kind, lifetime, file path, and Open in Editor / Copy buttons +
Drilling into one component — its runtime stats plus type, kind, lifetime, and source file, with Open in Editor to jump straight to its definition.
@@ -161,7 +161,7 @@ Sort the table by **p99** or **Avg** to identify the worst offender. The **Hot P Click the bottleneck row to jump to the source. Fix it, re-run, and watch the new numbers replace the old. -When you're done, click **Stop** to detach the profiler. Click **Clear** to reset the in-memory buffer, or **Snapshot** to capture the current state for later comparison. +When you're done, click **Stop** to detach the profiler. Click **Clear** to reset the in-memory buffer. --- @@ -169,7 +169,7 @@ When you're done, click **Stop** to detach the profiler. Click **Clear** to rese As your codebase grows, the tree grows with it. Two tools keep it manageable: -- **Section filter** — the dropdown at the top of the toolbar narrows the tree to one of **All / Commands / Queries / Notifications / Streams**. Picking *Commands* leaves only `ICommand<>`-derived pipelines visible; *Queries* leaves only `IQuery<>`-derived ones. +- **Section filter** (Visual Studio only) — the dropdown at the top of the toolbar narrows the tree to one of **All / Commands / Queries / Notifications / Streams**. Picking *Commands* leaves only `ICommand<>`-derived pipelines visible; *Queries* leaves only `IQuery<>`-derived ones. **VS Code** has no section-filter dropdown — narrow the tree with the search box below instead. - **Search box** — type any fragment of a type name, handler name, behavior name, or file path. Matches highlight and the tree collapses to the matching pipelines. `Ctrl+F` (VS Code) or click the search box (VS) focuses the input. `Esc` clears it. diff --git a/docs/mediator/pipeline-explorer/index.md b/docs/mediator/pipeline-explorer/index.md index a910483..bb4d9c6 100644 --- a/docs/mediator/pipeline-explorer/index.md +++ b/docs/mediator/pipeline-explorer/index.md @@ -23,7 +23,7 @@ Pipeline Explorer is a commercial IDE extension that scans your solution, discov
Pipeline Explorer with the discovered pipeline tree on the left and the Dashboard on the right showing handler counts, pipeline-mode and lifetime distributions, and recent activity -
Pipeline Explorer — the discovered pipeline tree on the left, the Dashboard on the right: live counts of handlers, behaviors, notifications, and streams, the pipeline-mode and DI-lifetime distributions, and a recent-activity feed.
+
Pipeline Explorer — the discovered pipeline tree on the left, the Dashboard on the right: live counts of handlers, components, notifications, and streams, the pipeline-mode and DI-lifetime distributions, and a recent-activity feed.
--- @@ -46,7 +46,7 @@ Pipeline Explorer answers all of these in seconds, with one click to source. | Capability | Outcome | |---|---| -| **Pipeline tree explorer** | Browse Commands, Queries, Notifications, and Streams grouped by kind, with at-a-glance badges and live counts. | +| **Pipeline tree explorer** | Browse request pipelines (commands & queries), notifications, and streams, with at-a-glance badges and live counts. | | **Click-to-source navigation** | Click any node — handler, behavior, call site — and the IDE opens the file at the right line. | | **Interactive pipeline graph** | See pre-processors → behaviors → handler → post-processors visualized per pipeline, with notification fan-out and nested calls inline. Detach it into its own window, and overlay per-node timing + health badges once profiling is on. | | **Runtime profiler** | Live handler / behavior timings with p50 / p95 / p99 percentiles, error rates, and tail-heavy detection — plus per-pipeline statistics, request telemetry, and a Hot Path / Flame breakdown of where each request spends its time. | @@ -97,7 +97,8 @@ See the [full troubleshooting hub](troubleshooting/index.md) for the diagnostic Pipeline Explorer ships as two editions that share the same backend, the same data model, and the same workflow: -- **Visual Studio Code extension** — available on the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=DSoftStudio.dsoftstudio-mediator). Bundled server auto-spawns on Windows, macOS, and Linux. + +- **Visual Studio Code extension** — available on the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=DSoftStudio.mediator-pipeline-explorer). Bundled server auto-spawns on Windows, macOS, and Linux. - **Visual Studio extension** — available on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=DSoftStudio.MediatorPipelineExplorer). Visual Studio 2022 17.0+ and 2026. A single license activates both editions. diff --git a/docs/mediator/pipeline-explorer/opentelemetry-bridge.md b/docs/mediator/pipeline-explorer/opentelemetry-bridge.md index 64636ca..d499c6a 100644 --- a/docs/mediator/pipeline-explorer/opentelemetry-bridge.md +++ b/docs/mediator/pipeline-explorer/opentelemetry-bridge.md @@ -19,7 +19,7 @@ There are two ways the bridge's traces reach Pipeline Explorer: | Mode | Status | When | What you do | |---|---|---|---| | **Import a production trace** | Available | Analyse a trace captured in prod / staging | Export an OTLP/JSON (or Jaeger/JSON) trace, then run **Mediator: Import Production Trace…** and pick the file | -| **Live capture** | In development | Profiling a local run whose solution references the bridge | Build your solution — on a successful build Pipeline Explorer refreshes and detects the bridge automatically | +| **Live capture** | Available | Profiling a local run whose solution references the bridge | Build your solution — on a successful build Pipeline Explorer refreshes and detects the bridge automatically | > **Automatic vs. configured — read this first.** Discovery and profiling activate automatically on a successful > build. The **enriched** spans — the greyed dependency nodes and `db.operation.name` — are **not** automatic and do @@ -38,7 +38,7 @@ GetOrdersQuery query mediator span (ActivitySource "DSoftStudio.Me └─ SELECT orders db dependency (Npgsql; enriched → db.operation.name=SELECT, db.sql.table=orders) ``` -- **Mediator spans** carry `mediator.request.type`, `mediator.request.kind` (command / query / stream / notification) +- **Mediator spans** carry `mediator.request.type`, `mediator.request.kind` (command / query / request / stream / notification) and `mediator.handler.type`. That is how Pipeline Explorer maps each frame to its handler and rebuilds the dispatch tree (a command and the notification it publishes are sibling dispatches, exactly as the source expresses them). @@ -47,6 +47,11 @@ GetOrdersQuery query mediator span (ActivitySource "DSoftStudio.Me `db.operation.name` / `db.sql.table` / `db.stored_procedure.name`, so each query is a distinct, readable dependency — **and the raw statement never leaves the app** (see [Security & redaction](#security--redaction)). +
+ Runtime Profiler Hot Path / Flame for CreateOrderCommand with the bridge active: the pre-processors, behaviors and handler, then greyed DEPENDENCY rows — 'postgresql SELECT inventory', 'postgresql INSERT orders' and the connection — plus a 'SELF wait' row, each with its share of the total and bottleneck markers on the slowest +
Live capture with the bridge active — the Hot Path · Flame nests the enriched dependency spans (postgresql SELECT inventory, INSERT orders, the connection) and a SELF wait row under the handler that made them, each with its share of the total and a bottleneck marker on the slowest.
+
+ --- ## Required configuration @@ -91,19 +96,21 @@ into the IDE for analysis, without attaching a profiler to anything. --- -## Mode B — Live capture (in development) - -> **Status:** in development — not yet in a released build. Use **Mode A** (import) to analyse the bridge's enriched -> traces today. +## Mode B — Live capture Pipeline Explorer **refreshes automatically after a successful build** (with `mediator.autoRefresh` on — the default): it re-discovers the solution, and that re-discovery is where it sees whether your projects reference the bridge. -The planned live mode builds on that: when a discovered solution **references** `DSoftStudio.Mediator.OpenTelemetry`, -the live profiler will capture the bridge's enriched spans directly from the running process — over the same EventPipe -channel it already uses, with no OTLP collector and no file export — giving the dependency spans and -`db.operation.name` live, exactly as in an imported trace. +When a discovered solution **references** `DSoftStudio.Mediator.OpenTelemetry`, the live profiler captures the +bridge's enriched spans directly from the running process — over the same EventPipe channel it already uses, with no +OTLP collector and no file export — giving the dependency spans and `db.operation.name` live, exactly as in an +imported trace. + +
+ Runtime Profiler with the bridge active on the OpenTelemetry sample: per-pipeline statistics for CreateOrderCommand and the queries, a Notification Fanout table for OrderCreatedNotification with its two handlers, and the request-telemetry summary — all captured live over EventPipe +
Live capture on a bridge-enabled solution — commands, queries, the notification fan-out (OrderCreatedNotification → 2 handlers) and streams, all captured over EventPipe with no OTLP collector running.
+
When the solution does **not** reference the bridge, the profiler stays on its built-in instrumentation events (handlers + behaviors, without external dependency spans), and **Mode A** remains the way to get the full picture. diff --git a/docs/mediator/pipeline-explorer/troubleshooting/activation-failed.md b/docs/mediator/pipeline-explorer/troubleshooting/activation-failed.md index 0bf0526..87904ad 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/activation-failed.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/activation-failed.md @@ -64,7 +64,7 @@ The message indicates the seat limit for your subscription has been reached. **Fix** - Free up a seat: open the [customer portal](https://portal.dsoftstudio.com/login), find a machine you no longer use, and release it — then activate again. -- Moving this license from another machine? Use **Move to another machine** in the portal (). +- Moving this license from another machine? Open the [customer portal](https://portal.dsoftstudio.com/login), open the license, and **Deactivate** the old machine — then activate again on the new one. - Or upgrade to a plan with more seats on the [Pricing page](https://mediator.dsoftstudio.com/pricing). --- @@ -79,7 +79,7 @@ The message mentions a machine or hardware mismatch. **Fix** -Release or transfer the previous binding in the [customer portal](https://portal.dsoftstudio.com/login) using **Move to another machine**, then activate again on the new machine. +Open the [customer portal](https://portal.dsoftstudio.com/login), open the license, and **Deactivate** the old machine — then activate again on the new machine. --- diff --git a/docs/mediator/pipeline-explorer/troubleshooting/analyzer-errors.md b/docs/mediator/pipeline-explorer/troubleshooting/analyzer-errors.md index 13dc6a0..297cd6b 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/analyzer-errors.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/analyzer-errors.md @@ -31,13 +31,13 @@ error CS0234: The type or namespace name 'IServiceCollection' does not exist in 'Microsoft.Extensions.DependencyInjection' (are you missing an assembly reference?) ``` -Pipeline Explorer installs a Roslyn analyzer at the solution level via `Directory.Build.props`. The analyzer ships a source generator that emits profiling and visualization code into every project that picks the analyzer up — which can collide with projects that don't reference the types the generated code needs. +Pipeline Explorer installs a Roslyn analyzer **machine-locally**. The MSBuild block that enables it is injected under `$(MSBuildUserExtensionsPath)\DSoftStudio.Mediator\enabled.props` (on Windows, `%LOCALAPPDATA%\Microsoft\MSBuild\DSoftStudio.Mediator\enabled.props`), which MSBuild auto-imports into every project you build on this machine — **nothing is written to your repository**. (Older versions injected this block into the solution's `Directory.Build.props`; that repo-level injection is no longer used, and any leftover markers are stripped on solution open.) The analyzer ships a source generator that emits profiling and visualization code into every project that picks it up — which can collide with projects that don't reference the types the generated code needs. --- ## 1. Pure projects without `DSoftStudio.Mediator.Abstractions` -The classic case: a Clean Architecture solution where the `Domain` project intentionally has zero dependencies. The solution-level `Directory.Build.props` injects the Pipeline Explorer analyzer into the Domain project too, and the generator emits code referencing `DSoftStudio.Mediator.Abstractions` and `Microsoft.Extensions.DependencyInjection` — neither of which the Domain project references. +The classic case: a Clean Architecture solution where the `Domain` project intentionally has zero dependencies. The machine-local analyzer import applies to the Domain project too, and the generator emits code referencing `DSoftStudio.Mediator.Abstractions` and `Microsoft.Extensions.DependencyInjection` — neither of which the Domain project references. This produces the `CS0234 Abstractions does not exist` errors listed above. @@ -85,27 +85,32 @@ dotnet build --- -## 3. `Directory.Build.props` collision +## 3. Machine-local injection and leftover repo blocks -If your solution already has a custom `Directory.Build.props` at the root, Pipeline Explorer's install routine appends to it. A pre-existing `` or `` with conflicting settings can shadow the extension's additions. +Pipeline Explorer no longer writes anything to your repository. When you enable it, the extension writes the analyzer import **machine-locally**, under MSBuild's per-user extension point: + +- the enablement registry → `$(MSBuildUserExtensionsPath)\DSoftStudio.Mediator\enabled.props` +- an `ImportBefore` targets file that MSBuild auto-imports into every SDK build on the machine + +On Windows both resolve under `%LOCALAPPDATA%\Microsoft\MSBuild\`. Because nothing lands in the repo, **your solution's `Directory.Build.props` is not touched and will not contain any Pipeline Explorer markers** — that is the normal, expected state, not a sign that injection failed. Toggling **Diagnostics enabled** off/on rewrites the machine-local files; it will **not** make marker comments appear in your repo. + +> **Older versions** injected the block directly into the solution's `Directory.Build.props`, wrapped in marker comments — `` (VS Code) or `` (Visual Studio). Current versions strip any such block on solution open. If you find one, an outdated tool wrote it. **Check** -Open `Directory.Build.props` at the solution root. Look for the marker comments: +Confirm the machine-local registry exists (Windows PowerShell): -```xml - -... - +```powershell +Get-Content "$env:LOCALAPPDATA\Microsoft\MSBuild\DSoftStudio.Mediator\enabled.props" ``` -If those markers are missing, the extension never injected its block. -If those markers are present but a duplicate analyzer reference exists elsewhere, you have a conflict. +If the file is missing, the extension has not enabled diagnostics on this machine yet. +If it exists **and** you also have a leftover marker block in your repo's `Directory.Build.props`, the stale repo copy can reference an old analyzer path and conflict with the machine-local one. **Fix** -- **Missing block**: open the Pipeline Explorer settings panel and toggle **Diagnostics enabled** off, then on. The injection runs again. -- **Duplicate**: remove the non-extension copy and keep only the block between the marker comments. +- **Registry missing**: open the Pipeline Explorer settings panel and toggle **Diagnostics enabled** off, then on. The machine-local files are rewritten. +- **Leftover repo block**: delete the marker-wrapped block from your solution's `Directory.Build.props` (current versions do this automatically on solution open). The machine-local import supersedes it. --- @@ -129,6 +134,8 @@ If you intentionally have both libraries in the same solution, scope each analyz …in your `Directory.Build.props`. This disables only Pipeline Explorer's analyzer; it does not affect the runtime mediator library. +> **Note**: `DSoftMediatorDiagnosticsEnabled` only takes effect when your project references the `DSoftStudio.Mediator.Diagnostics` NuGet package. If Pipeline Explorer was installed via the IDE extension (the usual case), this property is **not** `CompilerVisible` on the machine-local injected path, so setting it silently does nothing. In that case, disable the analyzer from the Pipeline Explorer **Settings panel → Diagnostics** toggle instead. + > Disabling the analyzer hides Pipeline Explorer from the IDE. To get the tree back, re-enable diagnostics and reload the IDE. --- @@ -151,13 +158,13 @@ For most users this is not a real issue — strong-name conflicts only matter in ## Quick disable as a workaround -If you need to ship a build immediately and the errors are blocking you, disable the Pipeline Explorer analyzer for the broken build: +If you need to ship a build immediately and the errors are blocking you, disable the Pipeline Explorer profiling generator for the broken build. The `CS0234` errors come from the profiling source generator, which is gated on `DSoftMediatorProfilingEnabled` — pass it as `false`: ```shell -dotnet build -p:DSoftMediatorDiagnosticsEnabled=false +dotnet build -p:DSoftMediatorProfilingEnabled=false ``` -This produces a clean build but disables the analyzer and the source generator for that invocation. The IDE tree will not populate until you re-enable diagnostics. +This produces a clean build but disables the profiling code generation for that invocation. The IDE tree will not populate profiling data until you build again without the override. --- diff --git a/docs/mediator/pipeline-explorer/troubleshooting/empty-tree.md b/docs/mediator/pipeline-explorer/troubleshooting/empty-tree.md index f1f1eb8..288d539 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/empty-tree.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/empty-tree.md @@ -20,8 +20,8 @@ description: "The Pipeline Explorer tree is empty after opening a solution. Caus You opened a solution but the Pipeline Explorer shows **No pipeline data found** — empty Request Pipelines, Notifications, and Streams — even though your project contains mediator handlers.
- Pipeline Explorer empty state: 'No pipeline data found' with Refresh, Build Solution and Open .sln buttons and a 'Things to check' checklist -
The empty state — when discovery finds nothing, the panel offers Refresh / Build Solution / Open .sln plus a built-in checklist that mirrors the causes below.
+ Pipeline Explorer empty state in Visual Studio: 'No pipeline data found — Build your solution to generate the pipeline map' with Refresh, Build Solution, Open solution… and 'Analyze anyway (slow)' buttons and a 'Things to check' checklist +
The empty state (Visual Studio) — when discovery finds nothing, the panel offers Refresh / Build Solution / Open solution…, an Analyze anyway (slow) escape hatch, plus a built-in checklist that mirrors the causes below. (In VS Code the empty view shows the checklist without buttons.)
This page walks through the five most common causes in order of likelihood. @@ -45,7 +45,7 @@ Open the folder that contains the `.sln`, or open the `.sln` directly. Click **R ## 2. No project references `DSoftStudio.Mediator` -The discovery step only scans projects that reference the `DSoftStudio.Mediator` NuGet package. +Discovery iterates over **every** project in the solution — projects that don't reference the `DSoftStudio.Mediator` NuGet package simply yield no handlers. If no project references the package, the tree stays empty. **Check** @@ -69,7 +69,7 @@ After the package is added, build the solution once, then click **Refresh** in t ## 3. The solution has never been built -Pipeline Explorer relies on assembly metadata produced by the source generator. A solution that has never been compiled has no metadata to read. +Pipeline Explorer discovers handlers from a generated encrypted source map (`PipelineMap.g.json.cs`) or by analyzing the Roslyn compilation — not from compiled assembly metadata. When the `requireBuild` setting is enabled, a solution that has never been compiled has nothing for discovery to read. **Check** @@ -115,8 +115,8 @@ Pipeline Explorer talks to a bundled background server that performs the actual **Check** -- VS Code: open the **Output** panel (`Ctrl+Shift+U`) and select **Mediator** from the dropdown. Look for a line confirming the server started. If you see startup errors, see [server startup failed](server-startup-failed.md). -- Visual Studio: open **View → Output** and select **Mediator** in the **Show output from** dropdown. +- VS Code: open the **Output** panel (`Ctrl+Shift+U`) and select **Mediator Server** from the dropdown. Look for a line confirming the server started. If you see startup errors, see [server startup failed](server-startup-failed.md). +- Visual Studio: open **View → Output** and select **DSoftStudio Mediator** in the **Show output from** dropdown. **Fix** diff --git a/docs/mediator/pipeline-explorer/troubleshooting/index.md b/docs/mediator/pipeline-explorer/troubleshooting/index.md index c1952bb..f7f6d93 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/index.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/index.md @@ -33,6 +33,18 @@ Quick links to the most common issues. Each page walks through likely causes in --- +## Known issues + +### Handler / component "Open in Editor" is disabled until a full rebuild (layered solutions) + +**Symptom** — In a multi-project solution where a handler or a pipeline component (a pre/post-processor or behavior) is defined in a **referenced** project — while the `Send` / `Publish` call site lives in the host project — the detail panel's **FILE** field is blank and **Open in Editor** / **Copy File Path** are disabled for that handler or component. Call sites still resolve normally, and the pipeline, counts, and profiling are all correct. + +**Why** — To open a solution quickly, the first discovery reads the pipeline maps produced at build time (one per project). In the *host* project's map, a handler/component that lives in a referenced project is recorded as an external symbol with **no source path** — the source path is present only in the *owner* project's own map. An incremental build may not have regenerated the owner project's map, so the source path isn't available yet. + +**Workaround** — **Rebuild the solution** (Visual Studio: `Build → Rebuild Solution`; VS Code / CLI: `dotnet build --no-incremental`, or clean the `obj` folders and build). A full rebuild regenerates every project's map and re-runs the complete analysis, so the handler/component source paths resolve — **FILE**, **Open in Editor**, and **Copy File Path** then work. + +--- + ## Diagnostic checklist before reporting If you plan to open an issue at the [GitHub repository](https://github.com/DSoftStudio/Mediator.Enterprise/issues), collecting these four items up front cuts the round-trip in half: @@ -41,8 +53,8 @@ If you plan to open an issue at the [GitHub repository](https://github.com/DSoft - VS Code: `Help → About` and Extensions view → click the extension → version on the right. - Visual Studio: `Help → About Microsoft Visual Studio`, then `Extensions → Manage Extensions → Installed → DSoftStudio Mediator Pipeline Explorer` for the extension version. 2. **Mediator output panel** (last 50 lines) - - VS Code: `Ctrl+Shift+U` → select **Mediator** in the dropdown. - - Visual Studio: `View → Output` → select **Mediator** in the dropdown. + - VS Code: `Ctrl+Shift+U` → select **Mediator Server** in the dropdown. + - Visual Studio: `View → Output` → select **DSoftStudio Mediator** in the dropdown. 3. **Package list** ```shell diff --git a/docs/mediator/pipeline-explorer/troubleshooting/profiler-not-attaching.md b/docs/mediator/pipeline-explorer/troubleshooting/profiler-not-attaching.md index fe865c3..34da790 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/profiler-not-attaching.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/profiler-not-attaching.md @@ -31,7 +31,7 @@ When everything is wired correctly the profiler fills with live data — total e
A working profiling session — executions and per-pipeline statistics fill in as traffic flows.
-> **Important**: Pipeline Explorer wires `AddMediatorProfiling()` into your application **automatically** via C# 12 interceptors. You do NOT need to add any line to `Program.cs` — referencing the analyzer (which the extension auto-injects into your solution) is enough. If profiling isn't working, the wiring failed for one of the reasons below. +> **Important**: Pipeline Explorer wires `AddMediatorProfiling()` into your application **automatically** via C# 12 interceptors. You do NOT need to add any line to `Program.cs` — referencing the analyzer (which the extension auto-injects for you) is enough. If profiling isn't working, the wiring failed for one of the reasons below. --- @@ -56,7 +56,9 @@ If you are running the application from inside the same IDE (F5 in Visual Studio The auto-wiring is gated on the MSBuild property `DSoftMediatorProfilingEnabled`. When it is `false`, the source generator emits nothing and the auto-registration disappears entirely — no profiling code exists in the produced IL. -The default value is `true`, but the extension's Settings panel exposes a toggle that flips this property in your solution's `Directory.Build.props`. If you (or someone on your team) ever turned profiling off, the toggle persists. +The default value is `false` — profiling is off until something turns it on. When the Pipeline Explorer extension is active, the IDE tooling enables it for you, but only in your **profiling configurations** (Debug by default), and it is **force-disabled** for AOT, trimmed, CI, and publish builds. So a Release / CI / AOT / publish build produces zero events even though `DSoftMediatorProfilingEnabled` is never literally set to `false` — searching your project files for it set to `false` comes up empty. Use the **Profiling Configurations…** command to control which build configurations opt in. + +The extension sets this property machine-local (under `$(MSBuildUserExtensionsPath)`); older versions used to write it into your solution's `Directory.Build.props`. **Check** @@ -68,7 +70,7 @@ Search your solution for `DSoftMediatorProfilingEnabled`. If you find it set to **Fix** -Set it to `true` (or remove the property — the default is `true`): +Set it explicitly to `true`: ```xml @@ -76,13 +78,13 @@ Set it to `true` (or remove the property — the default is `true`): ``` -Then rebuild the affected project so the source generator runs again. The extension also exposes this toggle in the Settings panel (gear icon in the toolbar). +Then rebuild the affected project so the source generator runs again. To turn profiling on or off per build configuration, use the **Profiling Configurations…** command rather than editing this property by hand. --- ## 3. The analyzer isn't loaded in the project that calls `AddMediator` -The auto-registration only fires in projects that pick up the Pipeline Explorer analyzer **and** reference `DSoftStudio.Mediator.Abstractions`. If your composition root lives in a project that doesn't satisfy both conditions, the interceptors are never generated and profiling never wires itself in. +The auto-registration only fires in projects that pick up the Pipeline Explorer analyzer **and** reference the main `DSoftStudio.Mediator` package (which provides `MediatorBuilder`). If your composition root lives in a project that doesn't satisfy both conditions, the interceptors are never generated and profiling never wires itself in. **Check** @@ -94,23 +96,12 @@ In the project that contains your `services.AddMediator(...)` call: dotnet list .csproj package | findstr Mediator ``` -- Confirm the analyzer is loaded. The simplest way is to look for a `Directory.Build.props` at the solution root with this block: - - ```xml - - - - ... - - - ``` - - If that block is missing, the analyzer never got injected (or was removed). +- Confirm the analyzer is loaded. The extension injects it **machine-local** — under `$(MSBuildUserExtensionsPath)\DSoftStudio.Mediator\enabled.props`, not into your repo. (Older versions wrote a `` block into the solution-root `Directory.Build.props`; those legacy repo markers are now actively stripped, so a missing block there is expected and not the problem.) **Fix** - If the package reference is missing: `dotnet add package DSoftStudio.Mediator`. -- If the `Directory.Build.props` block is missing: open the extension's Settings panel, toggle **Diagnostics enabled** off, then on. The auto-injection runs again. +- If the analyzer injection is missing: open the extension's Settings panel, toggle **Diagnostics enabled** off, then on. The machine-local injection runs again. - Rebuild. --- @@ -156,8 +147,6 @@ Raise the cap (up to 50,000): …or open the settings panel in Visual Studio and bump the value there. -For sustained high-volume traffic, use **Snapshot** to capture batches instead of relying on the live buffer. - --- ## 6. Direct handler resolution bypasses the mediator @@ -196,14 +185,11 @@ Is your application a console app that exits right after `mediator.Send(...)`? **Fix** -Either: +Add a small delay at the end of your test / console run so the profiler has time to flush the last events before the process exits: -- Use **Snapshot** instead of live streaming for short-lived processes — it forces a synchronous flush before the snapshot completes. -- Add a small delay at the end of your test / console run: - - ```csharp - await Task.Delay(500); // give the profiler time to flush - ``` +```csharp +await Task.Delay(500); // give the profiler time to flush +``` For long-running services (APIs, workers, daemons), this is not an issue. diff --git a/docs/mediator/pipeline-explorer/troubleshooting/server-startup-failed.md b/docs/mediator/pipeline-explorer/troubleshooting/server-startup-failed.md index 07034bb..20fb47b 100644 --- a/docs/mediator/pipeline-explorer/troubleshooting/server-startup-failed.md +++ b/docs/mediator/pipeline-explorer/troubleshooting/server-startup-failed.md @@ -23,7 +23,20 @@ Work through the causes below in order. --- -## 1. Antivirus or endpoint protection quarantined the server +## 1. The .NET 8 runtime is missing + +The bundled server is published framework-dependent (`--no-self-contained`), so it needs the **.NET 8 runtime** installed on your machine. On startup the extension runs `dotnet --list-runtimes`; if no .NET 8 runtime is found it stops and reports: + +> Mediator: .NET 8 SDK/Runtime is required… + +**Fix** + +- Install the **.NET 8 runtime** (or SDK) from Microsoft's [.NET downloads](https://dotnet.microsoft.com/download/dotnet/8.0), then reload the IDE. +- After installing, confirm it's visible by running `dotnet --list-runtimes` in a terminal — you should see a `Microsoft.NETCore.App 8.x` entry. + +--- + +## 2. Antivirus or endpoint protection quarantined the server Security products sometimes quarantine freshly-installed binaries before they are widely trusted. If the bundled server file is removed or blocked from running, the extension can't launch it. @@ -44,13 +57,20 @@ If the server files are missing right after a successful install, quarantine is --- -## 2. The server can't run on your platform +## 3. A `.vsix` installed for the wrong platform + +The bundled server is matched to your operating system and CPU architecture. The most common cause of a platform mismatch is a **manually installed `.vsix` for the wrong target** — for example a `win-x64` build dropped onto an `osx-arm64` machine. When the server binary doesn't match the host, it can't launch. + +**Fix** + +- Install the platform-specific package for your machine — `mediator-pipeline-explorer--.vsix`, where `` matches your OS/architecture (e.g. `win-x64`, `linux-x64`, `osx-arm64`). +- Easiest: install from the **Marketplace** instead of a downloaded file — it automatically selects the correct target for your machine. -The bundled server is a native binary matched to your operating system and CPU architecture. If it won't start and you're on an uncommon platform, capture the output (see [Capturing logs](#capturing-logs) below) so we can help. +If you're on an uncommon platform and it still won't start, capture the output (see [Capturing logs](#capturing-logs) below) so we can help. --- -## 3. A leftover server process or stale lock +## 4. A leftover server process or stale lock Pipeline Explorer keeps a single background server per machine and tracks it with a lock. If a previous session left a server running, or the lock points at a process that is no longer alive, the new session can fail to connect. @@ -62,7 +82,7 @@ Pipeline Explorer keeps a single background server per machine and tracks it wit --- -## 4. Very old Linux system libraries +## 5. Very old Linux system libraries On Linux, the server needs reasonably current system libraries. Very old distributions may not have them, and the server exits immediately on launch. @@ -79,10 +99,10 @@ On Windows and macOS this is rarely an issue. If you want to look closer or report the problem, capture the full output: -- **VS Code** — open the **Output** panel (`Ctrl+Shift+U`) and select **Mediator** from the dropdown. -- **Visual Studio** — **View → Output**, then select **Mediator** in the dropdown. +- **VS Code** — open the **Output** panel (`Ctrl+Shift+U`) and select **Mediator Server** from the dropdown. +- **Visual Studio** — **View → Output**, then select **DSoftStudio Mediator** in the dropdown. -Reload the IDE so the log captures startup from the beginning, reproduce the failure, then copy the **Mediator** output. You can [open an issue](https://github.com/DSoftStudio/Mediator.Enterprise/issues) with that output plus your IDE and extension version — no project source is needed. +Reload the IDE so the log captures startup from the beginning, reproduce the failure, then copy the output (from the **Mediator Server** channel in VS Code, or **DSoftStudio Mediator** in Visual Studio). You can [open an issue](https://github.com/DSoftStudio/Mediator.Enterprise/issues) with that output plus your IDE and extension version — no project source is needed. --- diff --git a/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj b/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj index 6115d03..319267c 100644 --- a/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj +++ b/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj @@ -8,7 +8,7 @@ DSoftStudio.Mediator.Abstractions - 1.3.0-rc.2 + 1.3.0 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj b/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj index 2c02f5d..c0d3630 100644 --- a/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj +++ b/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj @@ -9,7 +9,7 @@ DSoftStudio.Mediator.FluentValidation - 1.0.9-rc.2 + 1.0.9 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.Generators/DependencyInjectionGenerator.cs b/src/DSoftStudio.Mediator.Generators/DependencyInjectionGenerator.cs index 1c11c8b..9d63ce5 100644 --- a/src/DSoftStudio.Mediator.Generators/DependencyInjectionGenerator.cs +++ b/src/DSoftStudio.Mediator.Generators/DependencyInjectionGenerator.cs @@ -258,91 +258,96 @@ private static void ReportMissingHandlers( // Capture the dependency types of the constructor DI will use (the greediest public ctor) so the // runtime optimizer can raise this handler's lifetime from its dependency lifetimes (AOT-safe: the // types are emitted as typeof, never reflected). Empty for stateless handlers. - string depTypes = ""; - if (!isStateless) + string depTypes = isStateless ? "" : GetDependencyTypes(symbol); + + // An explicit [HandlerLifetime(...)] pins the lifetime: it is emitted directly and skips the optimizer. + string? explicitLifetime = GetExplicitLifetime(symbol); + + return MatchHandlerInterface(symbol, isStateless, depTypes, explicitLifetime); + } + + /// + /// The '|'-joined fully-qualified constructor dependency types of the greediest public constructor DI + /// would pick, or "" when there is no public constructor with parameters. Caller guarantees the handler + /// is not stateless. + /// + private static string GetDependencyTypes(INamedTypeSymbol symbol) + { + var ctor = symbol.InstanceConstructors + .Where(static c => c.DeclaredAccessibility == Accessibility.Public) + .OrderByDescending(static c => c.Parameters.Length) + .FirstOrDefault(); + + if (ctor is null || ctor.Parameters.IsEmpty) + return ""; + + var depsBuilder = new System.Text.StringBuilder(); + for (int p = 0; p < ctor.Parameters.Length; p++) { - var ctor = symbol.InstanceConstructors - .Where(static c => c.DeclaredAccessibility == Accessibility.Public) - .OrderByDescending(static c => c.Parameters.Length) - .FirstOrDefault(); - if (ctor is not null && !ctor.Parameters.IsEmpty) - { - var depsBuilder = new System.Text.StringBuilder(); - for (int p = 0; p < ctor.Parameters.Length; p++) - { - if (p > 0) depsBuilder.Append('|'); // '|' never appears in a type name (generics use < , >) - depsBuilder.Append(ctor.Parameters[p].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - depTypes = depsBuilder.ToString(); - } + if (p > 0) depsBuilder.Append('|'); // '|' never appears in a type name (generics use < , >) + depsBuilder.Append(ctor.Parameters[p].Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } - // An explicit [HandlerLifetime(...)] pins the lifetime: it is emitted directly and skips the optimizer. - string? explicitLifetime = null; + return depsBuilder.ToString(); + } + + /// + /// The lifetime pinned by an explicit [HandlerLifetime(...)] attribute ("Scoped"/"Singleton"/ + /// "Transient"), or null when the attribute is absent - in which case the runtime optimizer decides. + /// + private static string? GetExplicitLifetime(INamedTypeSymbol symbol) + { foreach (var attr in symbol.GetAttributes()) { if (attr.AttributeClass?.ToDisplayString() == "DSoftStudio.Mediator.Abstractions.HandlerLifetimeAttribute" && attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value is int lifetimeValue) { - explicitLifetime = lifetimeValue switch { 1 => "Scoped", 2 => "Singleton", _ => "Transient" }; - break; + return lifetimeValue switch { 1 => "Scoped", 2 => "Singleton", _ => "Transient" }; } } + return null; + } + + /// + /// Maps the first recognized mediator handler interface the symbol implements to its , + /// or null when the type implements none. + /// + private static HandlerInfo? MatchHandlerInterface( + INamedTypeSymbol symbol, + bool isStateless, + string depTypes, + string? explicitLifetime) + { + var implName = symbol.ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); + foreach (var iface in symbol.AllInterfaces) { - var ns = iface.ContainingNamespace.ToDisplayString(); - - if (ns != "DSoftStudio.Mediator.Abstractions") + if (iface.ContainingNamespace.ToDisplayString() != "DSoftStudio.Mediator.Abstractions") continue; - switch (iface.MetadataName) + string? serviceType = iface.MetadataName switch { - case "IRequestHandler`2": - { - var request = iface.TypeArguments[0] - .ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); - - var response = iface.TypeArguments[1] - .ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); - - return new HandlerInfo( - $"global::DSoftStudio.Mediator.Abstractions.IRequestHandler<{request},{response}>", - symbol.ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat), - isStateless, depTypes, explicitLifetime); - } - - case "INotificationHandler`1": - { - var notification = iface.TypeArguments[0] - .ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); - - return new HandlerInfo( - $"global::DSoftStudio.Mediator.Abstractions.INotificationHandler<{notification}>", - symbol.ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat), - isStateless, depTypes, explicitLifetime); - } - - case "IStreamRequestHandler`2": - { - var request = iface.TypeArguments[0] - .ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); - - var response = iface.TypeArguments[1] - .ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); - - return new HandlerInfo( - $"global::DSoftStudio.Mediator.Abstractions.IStreamRequestHandler<{request},{response}>", - symbol.ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat), - isStateless, depTypes, explicitLifetime); - } - } + "IRequestHandler`2" => + $"global::DSoftStudio.Mediator.Abstractions.IRequestHandler<{TypeArg(iface, 0)},{TypeArg(iface, 1)}>", + "INotificationHandler`1" => + $"global::DSoftStudio.Mediator.Abstractions.INotificationHandler<{TypeArg(iface, 0)}>", + "IStreamRequestHandler`2" => + $"global::DSoftStudio.Mediator.Abstractions.IStreamRequestHandler<{TypeArg(iface, 0)},{TypeArg(iface, 1)}>", + _ => null, + }; + + if (serviceType is not null) + return new HandlerInfo(serviceType, implName, isStateless, depTypes, explicitLifetime); } return null; } + private static string TypeArg(INamedTypeSymbol iface, int index) => + iface.TypeArguments[index].ToDisplayString(HandlerDiscovery.NullableFullyQualifiedFormat); + private static RequestTypeEntry? GetRequestTypeInfo( GeneratorSyntaxContext ctx, CancellationToken ct) diff --git a/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj b/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj index 53a75a8..781c614 100644 --- a/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj +++ b/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj @@ -9,7 +9,7 @@ DSoftStudio.Mediator.HybridCache - 1.0.9-rc.2 + 1.0.9 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj b/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj index b655ad1..fc7cd4b 100644 --- a/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj +++ b/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj @@ -9,12 +9,7 @@ DSoftStudio.Mediator.OpenTelemetry - - 1.1.0-rc.4 + 1.1.0 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj b/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj index 84af492..39aa4fe 100644 --- a/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj +++ b/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj @@ -14,7 +14,7 @@ DSoftStudio.Mediator - 1.3.0-rc.2 + 1.3.0 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator/HandlerLifetimeOptimizer.cs b/src/DSoftStudio.Mediator/HandlerLifetimeOptimizer.cs index ff31981..bff84bf 100644 --- a/src/DSoftStudio.Mediator/HandlerLifetimeOptimizer.cs +++ b/src/DSoftStudio.Mediator/HandlerLifetimeOptimizer.cs @@ -69,7 +69,7 @@ public static void Stage( IServiceCollection services, (ServiceDescriptor Descriptor, Type[] Dependencies)[] handlers) { - if (services is null) throw new ArgumentNullException(nameof(services)); + ArgumentNullException.ThrowIfNull(services); if (handlers is null || handlers.Length == 0) return; var list = Staged.GetOrCreateValue(services); @@ -88,7 +88,7 @@ public static void Stage( /// The fully-populated service collection, immediately before building. public static void Apply(IServiceCollection services) { - if (services is null) throw new ArgumentNullException(nameof(services)); + ArgumentNullException.ThrowIfNull(services); if (!Staged.TryGetValue(services, out var staged) || staged.Count == 0) return; Staged.Remove(services); @@ -109,15 +109,7 @@ public static void Apply(IServiceCollection services) // The last descriptor for the service type is the one DI resolves. Upgrade ours only when it // is STILL that winner (reference identity) - any user re-registration appends a newer // descriptor and is therefore respected, including an identical re-Add that forces Transient. - int last = -1; - for (int i = services.Count - 1; i >= 0; i--) - { - if (services[i].ServiceType == descriptor.ServiceType) - { - last = i; - break; - } - } + int last = FindWinningDescriptorIndex(services, descriptor.ServiceType); if (last < 0 || !ReferenceEquals(services[last], descriptor)) continue; @@ -129,6 +121,21 @@ public static void Apply(IServiceCollection services) } } + /// + /// Index of the last descriptor registered for - the one DI resolves + /// (last registration wins) - or -1 if none. A separate pass keeps flat. + /// + private static int FindWinningDescriptorIndex(IServiceCollection services, Type serviceType) + { + for (int i = services.Count - 1; i >= 0; i--) + { + if (services[i].ServiceType == serviceType) + return i; + } + + return -1; + } + /// /// The safe lifetime for a handler given its dependency lifetimes. Only called for handlers that /// HAVE dependencies (stateless handlers are already Singleton): Singleton if all are singletons, diff --git a/src/DSoftStudio.Mediator/PipelineChainHandler.cs b/src/DSoftStudio.Mediator/PipelineChainHandler.cs index be13ff7..bf1480a 100644 --- a/src/DSoftStudio.Mediator/PipelineChainHandler.cs +++ b/src/DSoftStudio.Mediator/PipelineChainHandler.cs @@ -50,11 +50,12 @@ public PipelineChainHandler( // mediator carries no tracing dependency and _observer stays null. IEnumerable observers) { - // First registered observer wins (one tracing adapter in practice). foreach+break avoids a LINQ - // allocation; constructed once per scope, not on the hot path. - IMediatorDispatchObserver? firstObserver = null; - foreach (var obs in observers) { firstObserver = obs; break; } - _observer = firstObserver; + // First registered observer wins (one tracing adapter in practice). Materialize like the component + // arrays below — DI hands us an array, so the `is` check avoids any copy — then take element 0. No + // LINQ, and no boxed enumerator (which a foreach over IEnumerable would allocate anyway). + // Constructed once per scope, never on the hot path. + var observerArray = observers is IMediatorDispatchObserver[] obsArray ? obsArray : [.. observers]; + _observer = observerArray.Length > 0 ? observerArray[0] : null; _behaviors = behaviors is IPipelineBehavior[] bArray ? bArray