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
72 changes: 72 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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/*
71 changes: 71 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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<T>`, 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.
36 changes: 16 additions & 20 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
<Project>

<PropertyGroup>
<Version>1.0.0-preview.1</Version>
<Authors>Illia Filippov</Authors>
<Company>Illia Filippov</Company>
<Copyright>Copyright Illia Filippov</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/illia1f/RequestFlow</PackageProjectUrl>
<RepositoryUrl>https://github.com/illia1f/RequestFlow</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>mediator;cqrs;pipeline;dispatch;request;dotnet</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)RequestFlow.snk</AssemblyOriginatorKeyFile>
<RequestFlowPublicKey>0024000004800000140100000602000000240000525341310008000001000100f14a878288aef3a3fb12a8f09ad6a1c518fe7b2b7ca5ff7e03cdecfb0ab9536980cd3286a6b32f7f023f360142ec05809a2196b89b2d7ae6a9a333c2a24d36273a48ba1d4618f7f279fa9f29343e422e812e1a40acdbf881a29a227fe13ce8fcd00ca8623bd7a861205e0372dead23d3e0a43edab1050362f02d50cbc6e4cae05d1feb3a99dc1b6262a14b124893d72a71c83fecde50cea1991d9fcffab314e119e61f6455cccea86e98a2b2391a232c4ca05aac048f01d5a1df77247a3b6586fa6fc71678851663c88bd99bbddd2ef1bf0b652cb2af497d867defe233f8f9c3a804806c7586479f5b227a5aa69a892c996efa77010768fe43413e092f6f3bbc</RequestFlowPublicKey>
<RepoRoot>$(MSBuildThisFileDirectory)</RepoRoot>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- src opts back in. Keeps tests, fixtures, and samples out of dotnet pack. -->
<IsPackable>false</IsPackable>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" />
<!-- Repo-wide rather than src-only: an InternalsVisibleTo from a signed assembly only resolves
if the consuming test assembly carries the same strong name. -->
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(RepoRoot)RequestFlow.snk</AssemblyOriginatorKeyFile>
<RequestFlowPublicKey>0024000004800000140100000602000000240000525341310008000001000100f14a878288aef3a3fb12a8f09ad6a1c518fe7b2b7ca5ff7e03cdecfb0ab9536980cd3286a6b32f7f023f360142ec05809a2196b89b2d7ae6a9a333c2a24d36273a48ba1d4618f7f279fa9f29343e422e812e1a40acdbf881a29a227fe13ce8fcd00ca8623bd7a861205e0372dead23d3e0a43edab1050362f02d50cbc6e4cae05d1feb3a99dc1b6262a14b124893d72a71c83fecde50cea1991d9fcffab314e119e61f6455cccea86e98a2b2391a232c4ca05aac048f01d5a1df77247a3b6586fa6fc71678851663c88bd99bbddd2ef1bf0b652cb2af497d867defe233f8f9c3a804806c7586479f5b227a5aa69a892c996efa77010768fe43413e092f6f3bbc</RequestFlowPublicKey>
</PropertyGroup>

<!-- Lets net462 targets build on machines without a .NET Framework developer pack, such as Linux CI. -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
>
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
40 changes: 40 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<!-- All four packages target this exact set. net462 is not redundant with netstandard2.0:
it puts explicit .NET Framework support on the NuGet page for brownfield migrators. -->
<TargetFrameworks>netstandard2.0;net462;net8.0;net10.0</TargetFrameworks>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>

<PropertyGroup Label="Package identity">
<!-- Split so CI can stamp a different preview with -p:VersionSuffix=preview.2 -->
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>preview.1</VersionSuffix>
<Authors>Illia Filippov</Authors>
<Company>Illia Filippov</Company>
<Copyright>Copyright Illia Filippov</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/illia1f/RequestFlow</PackageProjectUrl>
<PackageTags>mediator;cqrs;pipeline;dispatch;request;dotnet</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Label="Package output">
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<RepositoryUrl>https://github.com/illia1f/RequestFlow</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<ItemGroup>
<None Include="$(RepoRoot)README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
5 changes: 1 addition & 4 deletions src/RequestFlow.Abstractions/RequestFlow.Abstractions.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Description>Zero-dependency contracts for RequestFlow: requests, handlers, and dispatch abstractions. Reference this from application layers; the RequestFlow package provides the runtime.</Description>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Description>CQRS contracts for RequestFlow: ICommand, IQuery, handler and dispatcher interfaces. No runtime dependency, safe to reference from domain and application layers.</Description>
<Description>CQRS contracts for RequestFlow: ICommand, IQuery, handler and dispatcher interfaces. No runtime dependency, safe to reference from domain and application layers.</Description>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 0 additions & 3 deletions src/RequestFlow.Cqrs/RequestFlow.Cqrs.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Description>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.</Description>
</PropertyGroup>

Expand Down
5 changes: 1 addition & 4 deletions src/RequestFlow/RequestFlow.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Description>Small, fast request/handler mediator for .NET: assembly scanning registration, startup validation, and reflection-free frozen-map dispatch. Free MIT alternative to MediatR.</Description>
</PropertyGroup>

Expand Down
5 changes: 5 additions & 0 deletions tests/Compat/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -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
{ }
35 changes: 30 additions & 5 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
<Project>

<Import Project="..\Directory.Build.props" Condition="Exists('..\Directory.Build.props')" />
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<!-- net462 matches the library's oldest target, so tests exercise that exact build.
.NET Framework only runs on Windows. Requires the xunit runner pinned at 2.8.2. -->
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(TargetFrameworks);net462</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<!-- ValidationFixtures lives here too, but it is a scanned library rather than a test host,
so it must not pull in the runner. -->
<IsUnitTestProject Condition="'$(IsUnitTestProject)' == '' And $(MSBuildProjectName.EndsWith('.Tests.Unit'))">true</IsUnitTestProject>
</PropertyGroup>

<ItemGroup Condition="'$(IsUnitTestProject)' == 'true'">
<PackageReference Include="coverlet.collector" Version="10.0.1" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="10.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<!-- Pinned: 2.8.2 is the last runner that targets net462; 3.x needs net472+. -->
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" PrivateAssets="all" />
</ItemGroup>

<ItemGroup Condition="'$(IsUnitTestProject)' == 'true'">
<Using Include="NSubstitute" />
<Using Include="Shouldly" />
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<Compile Include="$(MSBuildThisFileDirectory)Compat\*.cs" LinkBase="Compat" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,34 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\RequestFlow\RequestFlow.csproj" />
<ProjectReference Include="..\..\src\RequestFlow.Cqrs\RequestFlow.Cqrs.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="NSubstitute" />
<Using Include="Shouldly" />
<Using Include="Xunit" />
</ItemGroup>

</Project>
Loading
Loading