Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Changelog

All notable changes to Switchboard. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).

For step-by-step upgrade instructions, see [Upgrading to 1.2](README.md#upgrading-to-12) in the README.

## [1.2.0] — 2026-09-13

No breaking API changes: every 1.1 call site compiles and behaves the same, apart from the fix and the small behavior notes below.

### Added

- **Startup validation** — `services.ValidateSwitchboard()` reads the registrations (without building the container or constructing handlers) and throws a `SwitchboardValidationException` listing every problem:
- requests in scanned assemblies with no handler,
- requests with more than one handler (only the last one registered would ever run),
- open behaviors that never run for void requests because they are constrained to `IRequest<TResponse>`.
Each check can be switched off through `SwitchboardValidationOptions`.
- **OpenTelemetry tracing and metrics** through `ActivitySource` and `Meter` from the base class library, with no new dependency. The spans are `Send {Request}`, `Publish {Notification}` and `Handle {Notification}` (one per handler). The histograms are `switchboard.request.duration` and `switchboard.notification.duration`. Nothing is recorded until you subscribe to `SwitchboardTelemetry.ActivitySourceName` / `SwitchboardTelemetry.MeterName`.
- **Scope per dispatch** — `cfg.UseScopePerDispatch()` runs every top-level `Send` and `Publish` in its own DI scope, reusing it for nested dispatches. It has overloads that take a callback (`DispatchScope` exposes `Parent`, `ServiceProvider` and `Message`) for carrying ambient state such as the current user into the new scope. Built for Blazor Server, where the caller's scope is the whole circuit.
- Tests and documentation for **constrained open behaviors** (`where TRequest : ISomeMarker`).

### Fixed

- Calling `AddSwitchboard` more than once with the same assembly or behavior no longer registers handlers and behaviors twice. Before, a notification handler or behavior registered twice ran twice.

### Behavior notes

- **Duplicates are skipped, not appended.** If the same open behavior is registered directly (`services.AddTransient(typeof(IPipelineBehavior<,>), typeof(X<,>))`) *and* through `AddOpenBehavior(typeof(X<,>))`, it now runs once, at the position of the first registration. Previously it ran twice.
- **Exceptions surface through the returned `Task`** when tracing/metrics are being listened to or scope-per-dispatch is on. Before, an exception such as a missing handler could be thrown synchronously from `Send`. Code that `await`s the call is unaffected.

## [1.1.0] — 2026-08-09

### Added

- `net8.0` and `net9.0` targets alongside `net10.0`, each floored at the lowest `Microsoft.Extensions.DependencyInjection.Abstractions` patch of its major. No API changes.

## [1.0.1] — 2026-07-26

### Added

- `AddBehavior<TBehavior>()` / `AddBehavior(Type)` for closed pipeline behaviors, sharing one ordering with `AddOpenBehavior`.

## [1.0.0] — 2026-07-26

- Initial release: `IRequest`/`IRequest<T>`, `INotification`, `IPipelineBehavior<,>`, `ISender`/`IPublisher`/`IMediator`, untyped `Send(object)`/`Publish(object)`, and assembly scanning, all MediatR-compatible.

[1.2.0]: https://github.com/Software-First-Gr/Switchboard/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/Software-First-Gr/Switchboard/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/Software-First-Gr/Switchboard/releases/tag/v1.0.1
[1.0.0]: https://github.com/Software-First-Gr/Switchboard/commits/v1.0.1
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

📖 Overview, migration guide and FAQ: **[softwarefirst.gr/switchboard](https://softwarefirst.gr/switchboard)**

> **New in 1.2:** startup validation, built-in OpenTelemetry, scope per dispatch for Blazor Server, and a fix for handlers registered twice. It's a drop-in upgrade from 1.1 — see **[Upgrading to 1.2](#upgrading-to-12)** and the [changelog](CHANGELOG.md).

Switchboard implements the request/response, notification, and pipeline-behavior surface of MediatR on top of `Microsoft.Extensions.DependencyInjection`, in under 500 lines of code with a single dependency (`Microsoft.Extensions.DependencyInjection.Abstractions`). It was extracted from a production system that moved off MediatR when it became commercially licensed: swap your `using` directives, change one registration call, and your handlers, behaviors, and call sites compile unchanged.

## Install
Expand Down Expand Up @@ -262,6 +264,82 @@ services.AddSwitchboard(cfg => cfg

> Work that outlives the dispatch — `Task.Run` fire-and-forget started inside a handler — must not send through the mediator it inherited: the scope it would reuse is disposed when the outer dispatch completes. Create a scope of your own for background work.

## Upgrading to 1.2

1.2 is a drop-in upgrade from 1.1: no API was removed or changed, so bumping the package is enough to compile and run. The new features are opt-in. The steps below take about fifteen minutes and are worth doing, because the validation tends to find something real.

### 1. Bump the package

```bash
dotnet add package SoftwareFirst.Switchboard --version 1.2.0
```

With central package management, change the version in `Directory.Packages.props` instead.

### 2. Check the two behavior notes

Most applications are unaffected by either note, but they are the only differences you can observe:

| What changed | Who notices | What to do |
| --- | --- | --- |
| Registering the same handler or behavior twice (two `AddSwitchboard` calls, or `AddOpenBehavior` plus a direct `AddTransient` of the same type) now registers it **once**. | Apps where a notification handler or behavior was running twice without anyone meaning it to. That was a bug, and it's fixed. | Nothing, unless you relied on the double run. If a behavior was registered both ways, it now sits at the position of its **first** registration, so check the pipeline order. |
| Exceptions can surface through the returned `Task` instead of being thrown synchronously from `Send` (when telemetry is listened to or scope-per-dispatch is on). | Only code that calls `Send` without `await` inside a `try`. | `await` the call. |

### 3. Turn on startup validation

Add it after every registration, just before the provider is built:

```csharp
builder.Services.ValidateSwitchboard();
var app = builder.Build();
```

Also add a one-line test over your real registrations, so a problem fails the build before it fails a deploy:

```csharp
[Fact]
public void Switchboard_registrations_are_valid()
=> new ServiceCollection().AddApplication().ValidateSwitchboard();
```

If it throws, each message says what to fix:

| Message | Fix |
| --- | --- |
| `X has no handler` | Add the handler. If the request is handled in another host, turn the check off for this one: `ValidateSwitchboard(o => o.RequireHandlerForEveryRequest = false)`. |
| `X has 2 handlers (A, B)` | Delete the handler that shouldn't exist. Until now only the last one registered was running. |
| `SomeBehaviour<TRequest, TResponse> never runs for N void request(s)` | Change the constraint from `where TRequest : IRequest<TResponse>` to `where TRequest : IBaseRequest`. |

> **Before widening a constraint, read the behavior.** After the change it starts running for your void commands too. For logging, timing, exception handling and validation that is the point. For a transaction or caching behavior, make sure that's what you want. Any metrics the behavior records also gain a series per void command, so dashboards and alerts that filter by request name may start matching commands they never saw before.

### 4. Opt into telemetry (optional)

```csharp
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource(SwitchboardTelemetry.ActivitySourceName))
.WithMetrics(metrics => metrics.AddMeter(SwitchboardTelemetry.MeterName));
```

If you already have a `PerformanceBehaviour` that starts its own span, you'll see Switchboard's `Send X` span as its parent. Either is fine to keep. The `Publish` and `Handle` spans for notification handlers are usually the new information. See [OpenTelemetry](#opentelemetry) for the span and metric names.

### 5. Blazor Server: replace a hand-written scoping sender (optional)

If you wrote an `ISender` decorator that creates a scope per call to avoid sharing a `DbContext` across a circuit, 1.2 does the same thing for `Send` **and** `Publish`, and handles nested dispatches:

```csharp
// Before
services.AddScoped<ISender, ScopingSender>();

// After
services.AddSwitchboard(cfg => cfg.UseScopePerDispatch(async (scope, cancellationToken) =>
{
var auth = await scope.Parent.GetRequiredService<AuthenticationStateProvider>().GetAuthenticationStateAsync();
scope.ServiceProvider.GetRequiredService<CurrentUser>().Set(auth.User);
}));
```

`AddSwitchboard` is safe to call again for this, so you can put it in the host or infrastructure layer without touching your application layer's registration. Port the decorator's tests too, especially for identity: the callback is where the user carries over. See [Scope per dispatch](#scope-per-dispatch-blazor-server).

## Migrating from MediatR

1. Replace the `MediatR` package reference with `SoftwareFirst.Switchboard`.
Expand Down
Loading