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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ jobs:
uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.*.props', 'global.json') }}
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.*.props', '**/packages.lock.json', 'global.json') }}
restore-keys: nuget-${{ runner.os }}-

- name: Restore
run: dotnet restore
run: dotnet restore -p:RestoreLockedMode=true

- name: Build
run: dotnet build --no-restore -c Release
Expand Down
14 changes: 4 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# .gitignore — RequestFlow
# Modern SDK-style .NET library solution (.slnx), multi-target
# netstandard2.0;net8.0;net10.0. NuGet-published. No project.json /
# packages.config legacy cruft on purpose.
# RequestFlow: SDK-style .NET library solution (.slnx), NuGet-published.
# No project.json / packages.config legacy cruft on purpose.

# ── Build output ─────────────────────────────────────────────
[Bb]in/
Expand All @@ -24,17 +22,13 @@ artifacts/
!.vscode/launch.json
!.vscode/extensions.json

# ── Local build settings (per-machine) ───────────────────────
# Optional override file. Directory.Build.props imports it when it exists.
Directory.Local.props

# ── NuGet package output ─────────────────────────────────────
# Packing RequestFlow / RequestFlow.Cqrs. Symbols too (.snupkg).
*.nupkg
*.snupkg

# ── Test results & coverage ──────────────────────────────────
# Unit test project + coverage (v1 checklist #7).
# Unit test project + coverage.
[Tt]est[Rr]esults/
*.trx
*.coverage
Expand All @@ -44,7 +38,7 @@ coverage*.xml
coverage*.info

# ── BenchmarkDotNet ──────────────────────────────────────────
# Dispatch-cost benchmarks vs MediatR (plan §"Targets and quality bar").
# Dispatch-cost benchmarks vs MediatR.
BenchmarkDotNet.Artifacts/

# ── MSBuild / binary logs ────────────────────────────────────
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ Pre-1.0: the public API can still change between previews.

The `release` workflow uses the section matching the pushed tag as the GitHub Release body. A missing section fails the build before publishing. Before tagging, rename `[Unreleased]` to the release version and add its date.

## [Unreleased]

### Added

- [Benchmark suites](benchmarks/README.md) on .NET 10. [`RequestFlow.Benchmarks.MediatR`](benchmarks/RequestFlow.Benchmarks.MediatR/README.md) compares requests, void requests, streams, events, catalog size, mixed traffic, and short-circuit pipelines against MediatR 12.5.0, the last Apache-2.0 release. `RequestFlow.Benchmarks` compares RequestFlow's Task and ValueTask paths and stage chains.

### Changed

- The MediatR notes in [events.md](docs/events.md#notes-for-mediatr-migrations) describe MediatR 12.5.0 probes only.

### Performance

- Dispatch allocates less for a stream sent without a cancelable token and for a `ParallelPublishStrategy` event with one applicable handler. Exception types, parameter names, and synchronous throws are unchanged.

## [1.0.0-preview.9] - 2026-09-08

### Added
Expand Down
3 changes: 0 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,4 @@
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>

<!-- Per-machine build settings, when the file exists. Not in the repo. -->
<Import Project="$(RepoRoot)Directory.Local.props" Condition="Exists('$(RepoRoot)Directory.Local.props')" />

</Project>
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ Void handlers return plain `Task`; `Unit` does not appear in user code. Existing

The [MediatR migration guide](https://github.com/illia1f/RequestFlow/blob/main/docs/migrating-from-mediatr.md) covers the file-by-file sequence, event differences, conditional registration, and unsupported extension points.

## Benchmarks

The [MediatR comparison](https://github.com/illia1f/RequestFlow/blob/main/benchmarks/RequestFlow.Benchmarks.MediatR/README.md) measures requests, stages, streams, events, and short-circuit paths against MediatR 12.5.0 on .NET 10, with correctness probes and package lock files. Its results describe MediatR 12.5.0 only, not later releases. The [benchmark guide](https://github.com/illia1f/RequestFlow/blob/main/benchmarks/README.md) explains how to run the suites.

## Packages

- **[`RequestFlow.Abstractions`](https://www.nuget.org/packages/RequestFlow.Abstractions)** holds Task and ValueTask requests, handlers, dispatchers, stages, streaming, events, publish strategies, validation models, and exceptions. It has no package dependency on `net8.0` or `net10.0`.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Release: [1.0.0-preview.9](CHANGELOG.md#100-preview9---2026-09-08).
- [x] Streaming requests via `IAsyncEnumerable<T>`
- [x] Opt-in ValueTask request family: `IValueRequest`, ValueTask stages, and CQRS command/query twins
- [x] MediatR migration and target compatibility guides
- [ ] BenchmarkDotNet suite against pinned MediatR, martinothamar/Mediator, LiteBus, and DispatchR packages, with raw results
- [ ] BenchmarkDotNet suite against pinned MediatR, martinothamar/Mediator, and DispatchR packages, with raw results

Targets: `netstandard2.0;net462;net8.0;net10.0`. The `net462` target has shipped since `1.0.0-preview.2`.

Expand Down
8 changes: 8 additions & 0 deletions RequestFlow.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<Solution>
<Folder Name="/benchmarks/">
<File Path="benchmarks/README.md" />
<Project Path="benchmarks/RequestFlow.Benchmarks/RequestFlow.Benchmarks.csproj" />
<Project Path="benchmarks/RequestFlow.Benchmarks.Core/RequestFlow.Benchmarks.Core.csproj" />
<Project Path="benchmarks/RequestFlow.Benchmarks.Harness/RequestFlow.Benchmarks.Harness.csproj" />
<Project Path="benchmarks/RequestFlow.Benchmarks.MediatR/RequestFlow.Benchmarks.MediatR.csproj" />
<Project Path="benchmarks/RequestFlow.Benchmarks.RequestFlowFixtures/RequestFlow.Benchmarks.RequestFlowFixtures.csproj" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples/Orders.Api.Violations/Orders.Api.Violations.csproj" />
<Project Path="samples/Orders.Api/Orders.Api.csproj" />
Expand Down
11 changes: 11 additions & 0 deletions benchmarks/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<AssemblyMetadata Include="RequestFlow.Benchmarks.ProjectDirectory" Value="$(MSBuildProjectDirectory)" />
</ItemGroup>
</Project>
8 changes: 8 additions & 0 deletions benchmarks/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<Target Name="EmbedBenchmarkBuildDependencies" BeforeTargets="AssignTargetPaths"
DependsOnTargets="ResolvePackageAssets" Condition="'$(OutputType)' == 'Exe'">
<ItemGroup>
<EmbeddedResource Include="$(ProjectAssetsFile)" LogicalName="RequestFlow.Benchmarks.BuildDependencies.json" />
</ItemGroup>
</Target>
</Project>
139 changes: 139 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Benchmarks

Two BenchmarkDotNet suites, both on .NET 10:

| Suite | Cases | Measures |
| --- | --- | --- |
| `RequestFlow.Benchmarks.MediatR` | 224 | RequestFlow against MediatR 12.5.0 in one executable, with matching workloads and service lifetimes. See the [MediatR comparison](RequestFlow.Benchmarks.MediatR/README.md). |
| `RequestFlow.Benchmarks` | 34 | RequestFlow alone: Task vs ValueTask paths and stage chains. |

- A case is one benchmark method with one parameter set.
- No timing results are published yet.
- RequestFlow is built from the checked-out source, so record the commit when sharing results.
- BenchmarkDotNet 0.15.8 and Microsoft DI 10.0.9. Package lock files pin the resolved dependencies.
- CI builds both suites but does not run them.

## Quick start

Requires the .NET 10 SDK. Run from the repository root:

```powershell
dotnet restore benchmarks/RequestFlow.Benchmarks.MediatR -p:RestoreLockedMode=true
dotnet build benchmarks/RequestFlow.Benchmarks.MediatR -c Release --no-restore

# Check results, execution order, lifetimes, tokens, and disposal without timing.
dotnet run --project benchmarks/RequestFlow.Benchmarks.MediatR -c Release -f net10.0 --no-build --no-restore -- probe

# Rough timings for one group. Not publishable.
dotnet run --project benchmarks/RequestFlow.Benchmarks.MediatR -c Release -f net10.0 --no-build --no-restore -- --quick --filter '*TaskRetainedScopeBenchmarks*'

# Full run of every case into an unused directory.
dotnet run --project benchmarks/RequestFlow.Benchmarks.MediatR -c Release -f net10.0 --no-build --no-restore -- --filter '*' --artifacts benchmarks/artifacts/YYYY-MM-DD-mediatr-full
```

- These commands build only the suite and the `net10.0` build of RequestFlow. To build the whole solution, run `dotnet restore -p:RestoreLockedMode=true` and `dotnet build -c Release --no-restore`.
- Single-quoted filters work in PowerShell and bash. In cmd.exe, use double quotes.

## RequestFlow suite

```powershell
dotnet restore benchmarks/RequestFlow.Benchmarks -p:RestoreLockedMode=true
dotnet build benchmarks/RequestFlow.Benchmarks -c Release --no-restore

# Verify the workloads without timing.
dotnet run --project benchmarks/RequestFlow.Benchmarks -c Release -f net10.0 --no-build --no-restore -- probe

# Rough timings for Task and ValueTask void dispatch.
dotnet run --project benchmarks/RequestFlow.Benchmarks -c Release -f net10.0 --no-build --no-restore -- --quick --filter '*VoidCompletionBenchmarks*'
```

- For a full run, use `--filter '*'` without `--quick`.
- Each class uses the Task path as the baseline for the ValueTask path.
- `ChainLevelBenchmarks` bypasses the dispatcher. It enters the Task, ValueTask, and void plans that `AddRequestFlow` froze with 0, 1, or 5 stages.
- Void completion and short-circuit benchmarks dispatch through RequestFlow.
- Rows use the shared fixture handlers and stages, so the handler work matches the comparison suite.

## Run modes

| Argument | What it does |
| --- | --- |
| `probe` | Checks every workload without timing. A pass ends with `Probe passed:` and the number of classes, parameter sets, and cases checked. |
| `--smoke` | Runs each case once to check it executes. |
| `--quick` | Rough timings. Not publishable. |
| `--filter` without `--smoke` or `--quick` | Runs `probe`, then the full profile: two launches, 15 to 50 warm-up and 60 to 100 measured iterations, 5% relative error target. |

- Without arguments, `dotnet run` applies the MediatR suite's first launch profile, **Verify comparison**, so it only runs `probe`.
- The RequestFlow suite has no launch profile. Without arguments it runs `probe`, then shows BenchmarkDotNet's selection prompt.

## Filters

- `--filter '*'` selects every case.
- When narrowing, keep both libraries selected within each scenario.
- Pass several patterns to one `--filter` to combine scenarios: `--filter '*MediatRStream*' '*MediatRVoidRequest*'`.

| MediatR scenario | Filter | Cases |
| --- | --- | --- |
| Task requests | `*MediatRTask*Scope*` | 36 |
| Void requests | `*MediatRVoidRequest*` | 24 |
| Streams | `*MediatRStream*` | 72 |
| Sequential events | `*MediatREventSequential*` | 16 |
| Parallel events | `*MediatREventParallel*` | 16 |
| Catalog size | `*MediatRCatalogSize*` | 18 |
| Mixed traffic | `*MediatRMixedTraffic*` | 18 |
| Short circuit | `*MediatRShortCircuit*` | 18 |
| Direct-handler reference | `*MediatRTaskDirectHandler*` | 6 |

## Reports

- `--artifacts <dir>` sets the output directory. Without it, reports go to `BenchmarkDotNet.Artifacts/<suite>/<tier>/<run>/` under the current directory, where `<suite>` is `mediatr` or `requestflow`.
- Readable tables are the `results/*-report-github.md` files. `run-manifest.json` records the run status and host details.
- Git ignores `BenchmarkDotNet.Artifacts/` and `benchmarks/artifacts/`.

## Publishing a run

Publish only a run whose `run-manifest.json` shows `status: completed` and `publicationCandidate: true`. That requires:

- the full profile, with no measurement overrides
- a passing probe
- Git can read HEAD and the status of every build input
- a clean Git status for every build input, with HEAD and that status unchanged from start to end of the run
- no `DOTNET_*`, `COMPlus_*`, or `CORECLR_*` environment variables, except the SDK and host settings the harness ignores

Before the run:

- Build the suite after your last commit. A full run fails before timing anything when the build's commit differs from HEAD.
- Delete stray files under `src/`, the suite folder, and each benchmark project the suite references, such as old reports and `.csproj.user` files. The source check includes ignored files and skips only `bin` and `obj`.
- Start without a processor affinity mask. The jobs set none, but child processes inherit any mask set with `start /affinity`, `taskset`, or a similar tool.
- Leave the machine otherwise idle.

After the run:

- Check that `hostEnvironment.processorCount` in the manifest equals the logical core count in the report header. `publicationCandidate` does not check affinity, and parallel event and suspending rows depend on how many cores the process can use.
- A run stopped with Ctrl+C stays at `status: running` and is not publishable.

## Visual Studio

Use Visual Studio for exploration only. Publish from the command line.

1. Open `RequestFlow.slnx` in a Visual Studio version that supports the .NET 10 SDK and `.slnx` solutions.
2. In the `benchmarks` solution folder, set `RequestFlow.Benchmarks.MediatR` as the startup project.
3. Select the **Release** configuration.
4. Pick a launch profile: **Verify comparison** (`probe`), **Smoke test** (`--smoke` on the Task retained-scope class only), or **Full comparison** (`--filter *`).
5. Start without debugging (**Ctrl+F5**).

- Reports go under `BenchmarkDotNet.Artifacts/mediatr/` at the repository root, because the profiles set `workingDirectory` to `../..`. `dotnet run` ignores that setting.
- A Visual Studio run is never a publication candidate. Visual Studio saves the selected profile in `RequestFlow.Benchmarks.MediatR.csproj.user`, which the source check flags.

## Projects

All five are in the root solution.

| Project | Contents |
| --- | --- |
| `RequestFlow.Benchmarks` | Stage-chain measurements, Task vs ValueTask dispatch, and probes |
| `RequestFlow.Benchmarks.MediatR` | MediatR fixtures, paired benchmarks, and probes. Does not depend on `RequestFlow.Benchmarks`. |
| `RequestFlow.Benchmarks.Harness` | BenchmarkDotNet configuration, runner, run manifest, and provenance records. Knows nothing about workloads. |
| `RequestFlow.Benchmarks.Core` | Probe engine, trace names, provider hosts, shared workloads, and the short-circuit dependency graph |
| `RequestFlow.Benchmarks.RequestFlowFixtures` | RequestFlow handlers, stages, registration, and message catalogs |

- Benchmark classes are public and inheritable so BenchmarkDotNet can generate its harness.
Loading