From 76b3c7618f68fd1bcd50a5b63f90d2d3b29b16ed Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Wed, 29 Jul 2026 00:41:35 +0200 Subject: [PATCH 1/3] build: target net462, centralize MSBuild props net462 puts explicit .NET Framework support on the NuGet page for brownfield migrators. netstandard2.0 already covers the runtime, so this is about what the package listing shows. Props now layer. The root file holds what every project needs and sets IsPackable=false; src/Directory.Build.props opts back in, so tests, fixtures, and samples can no longer leak into dotnet pack. Signing stays at the root: InternalsVisibleTo from a signed assembly only resolves when the consuming test assembly carries the same strong name. Version splits into VersionPrefix and VersionSuffix so CI can stamp a different preview. Tests run on net462 too, on Windows only, so the oldest library build gets exercised. That target needs IsExternalInit for record init accessors, a real Assembly subclass in HandlerScannerTests because Castle cannot proxy Assembly on .NET Framework, and xunit.runner.visualstudio pinned at 2.8.2, the last version that targets net462. global.json pins the SDK major so local and CI builds agree. --- Directory.Build.props | 36 ++++++++--------- README.md | 2 +- ROADMAP.md | 2 +- global.json | 6 +++ src/Directory.Build.props | 40 +++++++++++++++++++ .../RequestFlow.Abstractions.csproj | 5 +-- .../RequestFlow.Cqrs.Abstractions.csproj | 7 +--- src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj | 3 -- src/RequestFlow/RequestFlow.csproj | 5 +-- tests/Compat/IsExternalInit.cs | 5 +++ tests/Directory.Build.props | 35 +++++++++++++--- .../RequestFlow.Cqrs.Tests.Unit.csproj | 26 ------------ .../HandlerScannerTests.cs | 18 +++++---- .../RegisterGenericHandlerTests.cs | 3 +- .../RequestFlow.Tests.Unit.csproj | 28 +------------ 15 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 global.json create mode 100644 src/Directory.Build.props create mode 100644 tests/Compat/IsExternalInit.cs diff --git a/Directory.Build.props b/Directory.Build.props index e82b5fd..637e008 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,30 +1,26 @@ - 1.0.0-preview.1 - Illia Filippov - Illia Filippov - Copyright Illia Filippov - MIT - https://github.com/illia1f/RequestFlow - https://github.com/illia1f/RequestFlow - git - true - README.md - mediator;cqrs;pipeline;dispatch;request;dotnet - true - true - $(MSBuildThisFileDirectory)RequestFlow.snk - 0024000004800000140100000602000000240000525341310008000001000100f14a878288aef3a3fb12a8f09ad6a1c518fe7b2b7ca5ff7e03cdecfb0ab9536980cd3286a6b32f7f023f360142ec05809a2196b89b2d7ae6a9a333c2a24d36273a48ba1d4618f7f279fa9f29343e422e812e1a40acdbf881a29a227fe13ce8fcd00ca8623bd7a861205e0372dead23d3e0a43edab1050362f02d50cbc6e4cae05d1feb3a99dc1b6262a14b124893d72a71c83fecde50cea1991d9fcffab314e119e61f6455cccea86e98a2b2391a232c4ca05aac048f01d5a1df77247a3b6586fa6fc71678851663c88bd99bbddd2ef1bf0b652cb2af497d867defe233f8f9c3a804806c7586479f5b227a5aa69a892c996efa77010768fe43413e092f6f3bbc + $(MSBuildThisFileDirectory) + latest + enable true - true - snupkg - true + + false true - - + + + true + $(RepoRoot)RequestFlow.snk + 0024000004800000140100000602000000240000525341310008000001000100f14a878288aef3a3fb12a8f09ad6a1c518fe7b2b7ca5ff7e03cdecfb0ab9536980cd3286a6b32f7f023f360142ec05809a2196b89b2d7ae6a9a333c2a24d36273a48ba1d4618f7f279fa9f29343e422e812e1a40acdbf881a29a227fe13ce8fcd00ca8623bd7a861205e0372dead23d3e0a43edab1050362f02d50cbc6e4cae05d1feb3a99dc1b6262a14b124893d72a71c83fecde50cea1991d9fcffab314e119e61f6455cccea86e98a2b2391a232c4ca05aac048f01d5a1df77247a3b6586fa6fc71678851663c88bd99bbddd2ef1bf0b652cb2af497d867defe233f8f9c3a804806c7586479f5b227a5aa69a892c996efa77010768fe43413e092f6f3bbc + + + + + diff --git a/README.md b/README.md index 81bdde6..fe149b7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The core library stays unopinionated about how you name your requests. If you wa [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/illia1f/RequestFlow/blob/main/LICENSE) ![Status](https://img.shields.io/badge/status-preview-orange) -![Targets](https://img.shields.io/badge/targets-netstandard2.0%20%7C%20net8.0%20%7C%20net10.0-512BD4) +![Targets](https://img.shields.io/badge/targets-netstandard2.0%20%7C%20net462%20%7C%20net8.0%20%7C%20net10.0-512BD4) > **Status:** preview on NuGet. The request/handler core, startup validation, and the CQRS package are live; stages are still in development. Install with the `--prerelease` flag: > diff --git a/ROADMAP.md b/ROADMAP.md index e39a254..58028bc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,7 +14,7 @@ Deliberately minimal: request/response dispatch, the stage pipeline, and the CQR - [x] Exceptions and startup validation (`ValidateRequestFlow`) - [ ] NuGet publish and package ID prefix reservation -Targets: `netstandard2.0;net8.0;net10.0`. +Targets: `netstandard2.0;net462;net8.0;net10.0`. ## v1.x diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..023bc7d --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,40 @@ + + + + + + + netstandard2.0;net462;net8.0;net10.0 + $(NoWarn);CS1591 + + + + + 1.0.0 + preview.1 + Illia Filippov + Illia Filippov + Copyright Illia Filippov + MIT + https://github.com/illia1f/RequestFlow + mediator;cqrs;pipeline;dispatch;request;dotnet + README.md + + + + true + true + true + snupkg + https://github.com/illia1f/RequestFlow + git + true + true + + + + + + + diff --git a/src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj b/src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj index a67df88..92bf445 100644 --- a/src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj +++ b/src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj @@ -1,9 +1,6 @@ - + - netstandard2.0;net8.0;net10.0 - latest - enable Zero-dependency contracts for RequestFlow: requests, handlers, and dispatch abstractions. Reference this from application layers; the RequestFlow package provides the runtime. diff --git a/src/RequestFlow.Cqrs.Abstractions/RequestFlow.Cqrs.Abstractions.csproj b/src/RequestFlow.Cqrs.Abstractions/RequestFlow.Cqrs.Abstractions.csproj index dafcfd8..76f2873 100644 --- a/src/RequestFlow.Cqrs.Abstractions/RequestFlow.Cqrs.Abstractions.csproj +++ b/src/RequestFlow.Cqrs.Abstractions/RequestFlow.Cqrs.Abstractions.csproj @@ -1,10 +1,7 @@ - + - netstandard2.0;net8.0;net10.0 - latest - enable - CQRS contracts for RequestFlow: ICommand, IQuery, handler and dispatcher interfaces. No runtime dependency, safe to reference from domain and application layers. + CQRS contracts for RequestFlow: ICommand, IQuery, handler and dispatcher interfaces. No runtime dependency, safe to reference from domain and application layers. diff --git a/src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj b/src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj index 57c7d7f..069d9e4 100644 --- a/src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj +++ b/src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj @@ -1,9 +1,6 @@ - netstandard2.0;net8.0;net10.0 - latest - enable Opt-in, type-enforced command/query split for RequestFlow: typed dispatcher implementations and AddCqrs registration on top of the RequestFlow runtime. Contracts live in RequestFlow.Cqrs.Abstractions. diff --git a/src/RequestFlow/RequestFlow.csproj b/src/RequestFlow/RequestFlow.csproj index 4c6c2ef..7fa1af0 100644 --- a/src/RequestFlow/RequestFlow.csproj +++ b/src/RequestFlow/RequestFlow.csproj @@ -1,9 +1,6 @@ - + - netstandard2.0;net8.0;net10.0 - latest - enable Small, fast request/handler mediator for .NET: assembly scanning registration, startup validation, and reflection-free frozen-map dispatch. Free MIT alternative to MediatR. diff --git a/tests/Compat/IsExternalInit.cs b/tests/Compat/IsExternalInit.cs new file mode 100644 index 0000000..df3e4e8 --- /dev/null +++ b/tests/Compat/IsExternalInit.cs @@ -0,0 +1,5 @@ +// Records compile to init accessors, and the compiler wants this marker type; .NET Framework does not ship it. +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 7260934..525f361 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,14 +1,39 @@ - + - net10.0 + net8.0;net10.0 + + $(TargetFrameworks);net462 enable - enable - false - false false + + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/RequestFlow.Cqrs.Tests.Unit/RequestFlow.Cqrs.Tests.Unit.csproj b/tests/RequestFlow.Cqrs.Tests.Unit/RequestFlow.Cqrs.Tests.Unit.csproj index 4790120..3894aa4 100644 --- a/tests/RequestFlow.Cqrs.Tests.Unit/RequestFlow.Cqrs.Tests.Unit.csproj +++ b/tests/RequestFlow.Cqrs.Tests.Unit/RequestFlow.Cqrs.Tests.Unit.csproj @@ -1,34 +1,8 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - diff --git a/tests/RequestFlow.Tests.Unit/HandlerScannerTests.cs b/tests/RequestFlow.Tests.Unit/HandlerScannerTests.cs index 112d782..06dadb0 100644 --- a/tests/RequestFlow.Tests.Unit/HandlerScannerTests.cs +++ b/tests/RequestFlow.Tests.Unit/HandlerScannerTests.cs @@ -1,5 +1,4 @@ using System.Reflection; -using NSubstitute.ExceptionExtensions; using RequestFlow; namespace RequestFlow.Tests.Unit; @@ -68,12 +67,7 @@ public void Given_Assembly_With_Open_Generic_Request_When_Scanning_Then_Open_Gen [Fact] public void Given_Assembly_With_Unloadable_Types_When_Scanning_Then_Loadable_Types_Are_Still_Scanned() { - var assembly = Substitute.For(); - assembly.GetTypes().Throws(new ReflectionTypeLoadException( - [typeof(ScanPingHandler), typeof(ScanPing), null], - [new TypeLoadException("Could not load type 'Broken'.")])); - - ScanResult result = HandlerScanner.Scan([assembly]); + ScanResult result = HandlerScanner.Scan([new PartiallyLoadableAssembly()]); result.Handlers.ShouldContain(h => h.ImplementationType == typeof(ScanPingHandler)); result.RequestTypes.ShouldContain(typeof(ScanPing)); @@ -105,5 +99,15 @@ public abstract class AbstractHandler : IRequestHandler public abstract Task HandleAsync(ScanPing request, CancellationToken cancellationToken); } + // Castle cannot proxy Assembly on .NET Framework (ISerializable without a deserialization constructor), + // so this is a real subclass instead of a substitute. + private sealed class PartiallyLoadableAssembly : Assembly + { + public override Type[] GetTypes() + => throw new ReflectionTypeLoadException( + [typeof(ScanPingHandler), typeof(ScanPing), null], + [new TypeLoadException("Could not load type 'Broken'.")]); + } + #endregion } diff --git a/tests/RequestFlow.Tests.Unit/RegisterGenericHandlerTests.cs b/tests/RequestFlow.Tests.Unit/RegisterGenericHandlerTests.cs index 66b2dbc..4d198ea 100644 --- a/tests/RequestFlow.Tests.Unit/RegisterGenericHandlerTests.cs +++ b/tests/RequestFlow.Tests.Unit/RegisterGenericHandlerTests.cs @@ -128,7 +128,8 @@ public void Given_Invalid_Handler_Type_When_Resolving_Dispatcher_Then_Validation var exception = Should.Throw(() => services.BuildServiceProvider().GetRequiredService()); - exception.Problems.ShouldContain(p => p.Contains(handlerType.Name.Split('`')[0])); + string handlerName = handlerType.Name.Split('`')[0]; + exception.Problems.ShouldContain(p => p.Contains(handlerName)); } [Fact] diff --git a/tests/RequestFlow.Tests.Unit/RequestFlow.Tests.Unit.csproj b/tests/RequestFlow.Tests.Unit/RequestFlow.Tests.Unit.csproj index 8c8d84b..6066b1c 100644 --- a/tests/RequestFlow.Tests.Unit/RequestFlow.Tests.Unit.csproj +++ b/tests/RequestFlow.Tests.Unit/RequestFlow.Tests.Unit.csproj @@ -1,34 +1,8 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - - - - From 2a8b8a1dccfd3dc41083ffd2376660e65f2aa72e Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Wed, 29 Jul 2026 00:41:35 +0200 Subject: [PATCH 2/3] ci: add build, test, and pack workflow Build and test on Windows because the net462 test target only runs there. Pack on Ubuntu, one project at a time rather than the whole solution, which would restore and build every test project again. --- .github/workflows/ci.yml | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ade0dc1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CI: true + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + build: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore -c Release + + - name: Test + run: dotnet test --no-build -c Release --logger trx --collect:"XPlat Code Coverage" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: "**/TestResults/**" + + pack: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + # Packing the solution would also restore and build every test project. + - name: Pack + run: | + for project in src/*/*.csproj; do + dotnet pack "$project" -c Release -o artifacts/packages + done + + - name: Upload packages + uses: actions/upload-artifact@v4 + with: + name: packages + path: artifacts/packages/* From 196137052f8e2329417490b73d80dcdb20581148 Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Wed, 29 Jul 2026 00:41:35 +0200 Subject: [PATCH 3/3] docs: add CONTRIBUTING Setup, the rules that cannot bend (no MediatR code, nothing reflective on the dispatch path, fixed targets and dependencies), and the test conventions. --- CONTRIBUTING.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6238c87 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Contributing to RequestFlow + +Pre-1.0: the public API can still change between previews. + +## Setup + +- .NET 10 SDK (`global.json` pins the major version) +- .NET 8 runtime (tests also run on `net8.0`) +- On Windows, tests also run on .NET Framework (the `net462` target), so the library's oldest build gets tested too. Nothing to install: the 4.8 runtime that ships with Windows runs them. + +``` +dotnet build +dotnet test +dotnet test --filter "FullyQualifiedName~RequestDispatcherTests" +``` + +## Before you push + +Builds treat warnings as errors in `src/`. Test projects turn warnings-as-errors off. + +CI builds and tests on Windows, then packs on Ubuntu. Tests target net462 only on Windows, which is why the test job needs a Windows runner. + +CI sets `CI=true`, which turns on `ContinuousIntegrationBuild`: debug symbols then record repository paths instead of local machine paths. Locally you only need it to reproduce a packaging problem: + +``` +dotnet build -c Release -p:CI=true +``` + +## Rules + +- No code from MediatR (commercially licensed since 2025). Shared vocabulary is fine, shared code is not. Use MIT or Apache licensed references. +- No reflection, LINQ, or locking on the dispatch path. Reflection is fine at registration and startup validation. +- Public API returns `Task`/`Task`, never `ValueTask`. Methods are `SendAsync`/`HandleAsync`. Handler interfaces take a `CancellationToken`. `NoResult` never appears in a user-written handler signature. +- Target frameworks and dependencies are fixed. All four `src/` projects target `netstandard2.0;net462;net8.0;net10.0`. + +| Project | Depends on | +| ------------------------------- | ----------------------------------------------------------------------------------- | +| `RequestFlow.Abstractions` | nothing | +| `RequestFlow.Cqrs.Abstractions` | `RequestFlow.Abstractions` | +| `RequestFlow` | `RequestFlow.Abstractions`, `Microsoft.Extensions.DependencyInjection.Abstractions` | +| `RequestFlow.Cqrs` | `RequestFlow.Cqrs.Abstractions`, `RequestFlow` | + +## Tests + +xUnit, NSubstitute, Shouldly (not FluentAssertions, whose license changed). Names follow `Given_When_Then`: + +```csharp +[Fact] +public async Task Given_Null_Request_When_Sending_Request_Then_Throws_Argument_Null_Exception() +``` + +Fixtures go in a region at the bottom of the file. When a test checks DI registration itself, use a real `ServiceCollection` instead of mocks. + +## Commits + +Conventional Commits: + +``` +feat(cqrs): add CQRS contracts, dispatchers, AddCqrs +test: expand request dispatcher coverage +``` + +## Where to read next + +- [docs/getting-started.md](docs/getting-started.md) +- [docs/registration.md](docs/registration.md) +- [docs/lifetimes.md](docs/lifetimes.md) +- [docs/exceptions.md](docs/exceptions.md) +- [ROADMAP.md](ROADMAP.md) for what is planned and what is out of scope + +Open an issue before starting anything large.