diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c7e77..7e86961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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: @@ -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 @@ -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` 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. diff --git a/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj b/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj index 6df2b78..faa440f 100644 --- a/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj +++ b/src/DSoftStudio.Mediator.Abstractions/DSoftStudio.Mediator.Abstractions.csproj @@ -12,7 +12,7 @@ DSoftStudio.Mediator.Abstractions - 1.4.0-rc.2 + 1.4.0 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj b/src/DSoftStudio.Mediator.FluentValidation/DSoftStudio.Mediator.FluentValidation.csproj index a7c15a3..6205b0e 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.10-rc.2 + 1.0.10 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj b/src/DSoftStudio.Mediator.HybridCache/DSoftStudio.Mediator.HybridCache.csproj index 8b99550..b81eed6 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.10-rc.2 + 1.0.10 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj b/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj index 7a93828..b115b51 100644 --- a/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj +++ b/src/DSoftStudio.Mediator.OpenTelemetry/DSoftStudio.Mediator.OpenTelemetry.csproj @@ -9,7 +9,7 @@ DSoftStudio.Mediator.OpenTelemetry - 1.1.1-rc.2 + 1.1.1 DSoftStudio DSoftStudio diff --git a/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj b/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj index aece4d2..1328e07 100644 --- a/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj +++ b/src/DSoftStudio.Mediator/DSoftStudio.Mediator.csproj @@ -23,7 +23,7 @@ DSoftStudio.Mediator - 1.4.0-rc.2 + 1.4.0 DSoftStudio DSoftStudio