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/* 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. 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 - - + - - - - - -