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
176 changes: 79 additions & 97 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,104 +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.4.0-rc.2] — 2026-09-16
## [1.4.0] — 2026-10-13

> Companions: `OpenTelemetry` 1.1.1-rc.2 · `HybridCache` 1.0.10-rc.2 · `FluentValidation` 1.0.10-rc.2.

No behavior changes beyond rc.1. Ten defects found by the Pipeline Explorer team, who measured each
one against packages built from rc.1 and sent a reproduction with every report.

All five packages move together even though three of them changed only their README. The companions
reference the core by project, so a published companion pins the core version it was packed against:
leaving `HybridCache` at rc.1 would keep pulling the rc.1 core — with the generator defect below —
into any application that installs the companion without naming the core itself.

### Added

- **`MediatorCapabilities`** — a public declaration of what a build of the core guarantees, for source
generators written against it. `ChainLifetimeFold` is the first: the lifetime of a handler constrains
the lifetime of the pipeline chain that consumes it.

Tooling that registers its own pipeline components has to know this to pick their lifetime, and until
now it could only infer it — one generator was reading the presence of `IMediatorDispatchObserver` as
evidence of the fold, because the type and the fix shipped in the same commit. That inference would
have survived a rename by luck and failed silently otherwise, and the failure lands in the
consumer's `BuildServiceProvider` rather than at build time.

It lives in the core rather than in `Abstractions` on purpose. The core declares a dependency on a
MINIMUM version of `Abstractions`, not a pinned one, so NuGet can resolve an old core beside a new
`Abstractions` — and a capability declared there would then describe a core that does not have it.
Constants are `int` rather than `bool` so a capability can advance without needing a new name, and
`const` rather than `static readonly` so a generator can read the value through
`IFieldSymbol.ConstantValue`.


### Fixed
- **Interceptors could rewrite a call inside an expression tree when the type name was ambiguous.**
The guard that keeps `Send`, `Publish` and `CreateStream` out of expression trees — the reason a Moq
`Setup` or `Verify` still works — resolved `Expression<T>` with `GetTypeByMetadataName`. That returns
null both when the type is absent and when it is declared in more than one referenced assembly, and
the two are not the same: with the type absent a consumer cannot write an expression tree at all,
but with it ambiguous they can, and the guard opened. Reachable by a project pulling the legacy
`System.Linq.Expressions` package alongside the framework one. Now resolved with the plural
`GetTypesByMetadataName`, matching against any candidate.


- **A stream behavior that narrows its own constraint broke the consumer's build.** `StreamGenerator`
closed it over every pair, producing `CS0311` in `StreamRegistry.g.cs`, a file nobody can edit. The
narrowing data had been computed for stream behaviors all along — the scanner does not branch on the
interface kind — and this generator never read it. The equivalent guard on the request side shipped
in rc.1; this is the half that was missing.
- **A `file`-local stream behavior reached the generated registry**, through a hand-rolled
accessibility test: a `file` type reports `Internal`, so "Public or Internal" waved it through and
the generated file named something the compiler cannot resolve — `CS0400`. It now uses the shared
check, which also covers private nested types and types nested in a generic one.
- **DSOFT011 described a consequence the developer could not reach.** For that same stream case it
reported the behavior and said it "works on the ordinary runtime but throws under Native AOT", while
the same generator run emitted code that does not compile. Fixing the generator makes the message
true again.
- **A stream whose consumer stopped reading was reported as failed.** `break` after the first page
disposed the iterator and the span went red — a paged query reading its first N rows looked like an
outage.
- **No failing stream carried `error.type` or an exception event**, and cancelling, breaking and
crashing produced spans identical character for character. Streams now report
`mediator.stream.termination` as `completed`, `cancelled`, `early` or `faulted`, and only a fault
sets Error status.
- **An empty stream reported `first_item_ms` and `throughput_per_sec` as zero**, indistinguishable
from measurements and averaged as measurements. Both tags are now omitted when nothing was produced.
- **`mediator.request.errors` was never incremented for a stream.** Error rate stayed flat while the
traces beside it showed failures. C# forbids a catch clause in an iterator containing `yield return`,
so both stream behaviors now drive the enumerator by hand — awaiting `MoveNextAsync` inside its own
try and yielding outside every try, which is the shape that makes a catch legal.
- **Spans opened inside a stream handler escaped the stream span from the second item onward.**
`Activity.Current` is an `AsyncLocal`, and after the first `yield return` the iterator resumes under
the consumer's context. A stream created under one span and enumerated after it ended came out as a
root span in a fresh trace — the ordinary shape of any API handing an `IAsyncEnumerable` to a
framework that enumerates it later. The span is now made ambient around every `MoveNextAsync`.
- **The notification error metric lost `mediator.request.type`.** It read the value back off the
envelope span, which returns null whenever that span carries no tags — not only when there is no
span, since tags are set under `IsAllDataRequested` and any head sampler returning `PropagationData`
leaves the envelope alive and untagged.
- **The README's manual registration armed one dispatch table of three.** `AddMediator()
.RegisterMediatorHandlers().PrecompilePipelines()`. The two you can forget fail very differently:
can forget fail very differently. `CreateStream` throws and names the missing call. `Publish` says
nothing at all — it reaches no handlers and returns a completed task, because an empty table is
indistinguishable from an application that has no notifications. The generated
`AddMediator(configure)` overload calls all three.

### Documentation

- The four packaged READMEs, which are the nuget.org pages. The OpenTelemetry one described
instrumentation as attaching "via standard pipeline behaviors", which rc.1 replaced: `Send` and
`Publish` are observed through the core's observation ports and only stream tracing and metrics are
behaviors. It also gained the registration-order section the other companions have. The HybridCache
one gained the Native AOT section it never had — it is the one companion that fails there without a
step from the application, and DSOFT012 shipped in rc.1 unmentioned.


## [1.4.0-rc.1] — 2026-09-02

> Companions: `OpenTelemetry` 1.1.1-rc.1 · `HybridCache` 1.0.10-rc.1 · `FluentValidation` 1.0.10-rc.1.
> Companions: `OpenTelemetry` 1.1.1 · `HybridCache` 1.0.10 · `FluentValidation` 1.0.10.

**Four** changes in this release alter observable behavior. Each is marked *This is a behavior
change* under **Changed**, and all four are worth reading before you upgrade:
Expand Down Expand Up @@ -184,6 +89,23 @@ change* under **Changed**, and all four are worth reading before you upgrade:
scaling suite, `run-all-benchmarks.cmd`, and `compare-runs.ps1`, which compares two runs by overhead
above each suite's own baseline so machine drift cancels out.

- **`MediatorCapabilities`** — a public declaration of what a build of the core guarantees, for source
generators written against it. `ChainLifetimeFold` is the first: the lifetime of a handler constrains
the lifetime of the pipeline chain that consumes it.

Tooling that registers its own pipeline components has to know this to pick their lifetime, and until
now it could only infer it — one generator was reading the presence of `IMediatorDispatchObserver` as
evidence of the fold, because the type and the fix shipped in the same commit. That inference would
have survived a rename by luck and failed silently otherwise, and the failure lands in the
consumer's `BuildServiceProvider` rather than at build time.

It lives in the core rather than in `Abstractions` on purpose. The core declares a dependency on a
MINIMUM version of `Abstractions`, not a pinned one, so NuGet can resolve an old core beside a new
`Abstractions` — and a capability declared there would then describe a core that does not have it.
Constants are `int` rather than `bool` so a capability can advance without needing a new name, and
`const` rather than `static readonly` so a generator can read the value through
`IFieldSymbol.ConstantValue`.

### Changed

- **Pipeline components registered through `MediatorBuilder` now default to `Scoped`, not
Expand Down Expand Up @@ -360,12 +282,72 @@ change* under **Changed**, and all four are worth reading before you upgrade:
default (no publisher is registered by default), and `PrecompilePipelines()` was described as
capturing the component list (it records only whether a chain is needed, and freezes its lifetime).

- **Interceptors could rewrite a call inside an expression tree when the type name was ambiguous.**
The guard that keeps `Send`, `Publish` and `CreateStream` out of expression trees — the reason a Moq
`Setup` or `Verify` still works — resolved `Expression<T>` with `GetTypeByMetadataName`. That returns
null both when the type is absent and when it is declared in more than one referenced assembly, and
the two are not the same: with the type absent a consumer cannot write an expression tree at all,
but with it ambiguous they can, and the guard opened. Reachable by a project pulling the legacy
`System.Linq.Expressions` package alongside the framework one. Now resolved with the plural
`GetTypesByMetadataName`, matching against any candidate.


- **A stream behavior that narrows its own constraint broke the consumer's build.** `StreamGenerator`
closed it over every pair, producing `CS0311` in `StreamRegistry.g.cs`, a file nobody can edit. The
narrowing data had been computed for stream behaviors all along — the scanner does not branch on the
interface kind — and this generator never read it. The request side gained the same guard in this
release; both halves land together.
- **A `file`-local stream behavior reached the generated registry**, through a hand-rolled
accessibility test: a `file` type reports `Internal`, so "Public or Internal" waved it through and
the generated file named something the compiler cannot resolve — `CS0400`. It now uses the shared
check, which also covers private nested types and types nested in a generic one.
- **A stream whose consumer stopped reading was reported as failed.** `break` after the first page
disposed the iterator and the span went red — a paged query reading its first N rows looked like an
outage.
- **No failing stream carried `error.type` or an exception event**, and cancelling, breaking and
crashing produced spans identical character for character. Streams now report
`mediator.stream.termination` as `completed`, `cancelled`, `early` or `faulted`, and only a fault
sets Error status.
- **An empty stream reported `first_item_ms` and `throughput_per_sec` as zero**, indistinguishable
from measurements and averaged as measurements. Both tags are now omitted when nothing was produced.
- **`mediator.request.errors` was never incremented for a stream.** Error rate stayed flat while the
traces beside it showed failures. C# forbids a catch clause in an iterator containing `yield return`,
so both stream behaviors now drive the enumerator by hand — awaiting `MoveNextAsync` inside its own
try and yielding outside every try, which is the shape that makes a catch legal.
- **Spans opened inside a stream handler escaped the stream span from the second item onward.**
`Activity.Current` is an `AsyncLocal`, and after the first `yield return` the iterator resumes under
the consumer's context. A stream created under one span and enumerated after it ended came out as a
root span in a fresh trace — the ordinary shape of any API handing an `IAsyncEnumerable` to a
framework that enumerates it later. The span is now made ambient around every `MoveNextAsync`.
- **The README's manual registration armed one dispatch table of three.** `AddMediator()
.RegisterMediatorHandlers().PrecompilePipelines()`. The two you can forget fail very differently:
can forget fail very differently. `CreateStream` throws and names the missing call. `Publish` says
nothing at all — it reaches no handlers and returns a completed task, because an empty table is
indistinguishable from an application that has no notifications. The generated
`AddMediator(configure)` overload calls all three.

### Removed

- **`BehaviorLinkRegistry`** — a middle chain-construction tier no generator ever populated. Its
dictionary was null in every process and `Link()` fell through to the interface-typed adapter on
every call. Per-pair generic static types: 6 → 5.

### Documentation

- **The four packaged READMEs**, which are the pages a reader sees on nuget.org, were checked against
the code rather than against the docs site.

The OpenTelemetry one described instrumentation as attaching "via standard pipeline behaviors". That
is no longer how it attaches: `Send` and `Publish` are observed through the core's observation ports
and only stream tracing and metrics are behaviors. The difference is not cosmetic — the request span
is opened at the dispatch boundary, so it wraps pre- and post-processors, which a behavior cannot
reach. It also gained the registration-order section the other companions already had.

The HybridCache one gained the Native AOT section it never had. It is the one companion that fails
there without a step from the application, and DSOFT012 exists to say so at build time.

`Abstractions` was the only one of the four without the logo header.

## [1.3.0] — 2026-07-08

> Companions: `OpenTelemetry` 1.1.0 · `HybridCache` 1.0.9 · `FluentValidation` 1.0.9.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<!-- NuGet -->
<PackageId>DSoftStudio.Mediator.Abstractions</PackageId>
<Version>1.4.0-rc.2</Version>
<Version>1.4.0</Version>

<Authors>DSoftStudio</Authors>
<Company>DSoftStudio</Company>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<!-- NuGet -->
<PackageId>DSoftStudio.Mediator.FluentValidation</PackageId>
<Version>1.0.10-rc.2</Version>
<Version>1.0.10</Version>

<Authors>DSoftStudio</Authors>
<Company>DSoftStudio</Company>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<!-- NuGet -->
<PackageId>DSoftStudio.Mediator.HybridCache</PackageId>
<Version>1.0.10-rc.2</Version>
<Version>1.0.10</Version>

<Authors>DSoftStudio</Authors>
<Company>DSoftStudio</Company>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<!-- NuGet -->
<PackageId>DSoftStudio.Mediator.OpenTelemetry</PackageId>
<Version>1.1.1-rc.2</Version>
<Version>1.1.1</Version>

<Authors>DSoftStudio</Authors>
<Company>DSoftStudio</Company>
Expand Down
2 changes: 1 addition & 1 deletion src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<!-- NuGet -->
<PackageId>DSoftStudio.Mediator</PackageId>
<Version>1.4.0-rc.2</Version>
<Version>1.4.0</Version>

<Authors>DSoftStudio</Authors>
<Company>DSoftStudio</Company>
Expand Down
Loading