dotnet-stryker-netx
3.3.13
dotnet tool install --global dotnet-stryker-netx --version 3.3.13
dotnet new tool-manifest
dotnet tool install --local dotnet-stryker-netx --version 3.3.13
#tool dotnet:?package=dotnet-stryker-netx&version=3.3.13
nuke :add-package dotnet-stryker-netx --version 3.3.13
Stryker.NET-X
Stryker.NET-X is a port of Stryker.NET 4.14.1 to C# 14 / .NET 10 — fully
.slnx-aware — with an extended mutation-operator and feature catalogue that rivals PIT, cargo-mutants, and mutmut.
Stryker.NET-X is a fork of Stryker.NET that targets .NET 10, eliminates the Buildalyzer dependency in favour of Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace, supports the modern .slnx (XML-based) solution format, and ships a substantially expanded mutation-operator catalogue plus a MutationProfile opt-in surface for tunable noise/aggression. All public CLI flags and the stryker-config.json schema remain 1:1 backwards-compatible with upstream Stryker.NET 4.14.1.
What's new in Stryker.NET-X
Fully backwards-compatible: existing users see zero behavioral change unless they opt into a stronger mutation profile.
--mutation-profileflag (Defaults|Stronger|All) — orthogonal to--mutation-level; controls which mutators run (not just which mutations). Since v3.1.0 a stronger profile auto-bumps a too-low mutation level (ADR-025).- 24 net-new mutators (50 total) across 6 batches (typed-driven, PIT-1, PIT-2 + cargo-mutants, .NET-greenfield, spec-gap closure, filter pipeline + operator completion).
- 5 equivalent-mutant filters in the pipeline (
IdentityArithmetic,IdempotentBoolean,ConservativeDefaultsEquality,RoslynDiagnostics— the mutmut-style pre-filter — andRoslynSemanticDiagnosticswith O(1) speculative binding). - Operator hierarchy +
[MutationProfileMembership]attribute on every mutator. - SemanticModel-driven type-aware mutators — used by
TypeDrivenReturn,ArgumentPropagation,MemberVariable,MethodBodyReplacement. - Equivalent-Mutant Filter pipeline as a first-class stage.
- Production-hardened CLI surface (v3.2.x, reporter-driven):
--break-after <phase>diagnostic runs,--test-case-filter/--test-filter,--all-projectsmulti-project mutation,--tool-version/-T, solution-mode heartbeat diagnostics, multi-line// Stryker disablescoping. - Type-aware literal emission (v3.3.x): both constant-emitting mutators (
InlineConstants,ConstantReplacement) emit typedSyntaxFactory.Literal(T)for {int, uint, long, ulong, float, double, decimal} instead of lossydoubleround-trips — eliminates whole classes of CS0029/CS0266 Safe-Mode warnings (ADR-047/049).
Why this fork exists
Upstream Stryker.NET 4.14.1 (released 2026-04-10) does not run on .NET 9 / .NET 10 projects nor on .slnx solutions because:
- Buildalyzer 8.0 transitive dependency cannot parse .NET 10 MSBuild structures. Buildalyzer 9.0 — the fix — landed eight days after Stryker.NET 4.14.1.
MsBuildHelperfalls back tovswhere/MsBuild.exepaths that don't exist on .NET-10-SDK-only machines..slnxsupport is missing from upstream'sSolutionFileparser.
stryker-netx replaces the Buildalyzer pipeline entirely with Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace, gives the CLI a parallel Microsoft.VisualStudio.SolutionPersistence-based .slnx reader, and renames the tool to avoid colliding with the upstream package.
Disclaimer
stryker-netx is an independent community fork. It is NOT affiliated with, endorsed by, or sponsored by the official Stryker.NET project, the Stryker Mutator team, or Info Support BV. The "Stryker" name is used here descriptively (Stryker-Mutator-compatible tooling) and not as a trademark assertion.
For the official Stryker.NET project see https://github.com/stryker-mutator/stryker-net.
Compatibility
| Component | Supported |
|---|---|
| .NET Runtime (the tool itself) | .NET 10 |
| Test-project Target Frameworks | net8.0, net9.0, net10.0 |
| C# Language Version (in user code) | C# 12, 13, 14 |
| Solution formats | .sln, .slnx |
| Test runners | VsTest |
| Test frameworks | xUnit, MSTest 2/3, NUnit |
| OS | Windows ✓, Linux ✓ (CI), macOS (best-effort) |
Microsoft Testing Platform (MTP) and TUnit (which is MTP-exclusive) are not yet supported — Stryker drives tests through VsTest only, and MTP projects are rejected at analysis time. An MTP runner is roadmapped (see stryker-net#3094 and ADR-044). Note: on the .NET 10 SDK the VsTest target for MTP was removed entirely, so MTP-based projects — including TUnit, which has no VsTest adapter — cannot currently be mutated.
Installation
dotnet tool install -g dotnet-stryker-netx
Pin a specific version:
dotnet tool install -g dotnet-stryker-netx --version 3.3.12
Quickstart
In your test project directory:
cd /path/to/your/Tests
dotnet stryker-netx
Or against a solution from the solution directory:
cd /path/to/your/solution
dotnet stryker-netx --solution YourApp.slnx
⚠️ Solution-mode requires the working directory to match the solution directory. Passing
--solution path/to/elsewhere.slnxwhilecd'd into a different directory (e.g.tests/MyApp.Tests/) does not activate whole-solution mutation mode — stryker-netx falls back to single-project mode. To enable solution-wide discovery,cdto the solution directory first. (Heartbeat logs emitted every 30s during long analysis + initial-test-run phases will tell you whether the tool is making progress; see ADR-043.)
Or with a stryker-config.json:
dotnet stryker-netx --config-file stryker-config.json
CLI flags, configuration schema, and reporter outputs are mostly compatible with upstream Stryker.NET configuration docs.
Mutation Profiles (v2.0.0)
--mutation-profile controls which mutators are active during a run. It is independent of --mutation-level (which controls which mutations a given mutator emits).
| Profile | Active mutators | Use case |
|---|---|---|
Defaults (default) |
26 v1.x mutators only | Drop-in v1.x parity. Same behavior as upstream Stryker.NET. |
Stronger |
Defaults + 17 type-aware / catalogue-closing mutators (= 43 total) | Catch more bugs while keeping noise manageable. |
All |
Stronger + 7 most-aggressive operators (UoiMutator, NakedReceiver, ExceptionSwap, ArgumentPropagation, AsSpanAsMemory, MethodBodyReplacement, SpanReadOnlySpanDeclaration) (= 50 total) | Maximum coverage; expect mutation volume to grow ~3-5× and runtime accordingly. |
Set via CLI:
dotnet stryker-netx --mutation-profile Stronger
dotnet stryker-netx --mutation-profile All
Or in stryker-config.json:
{
"stryker-config": {
"mutation-profile": "Stronger"
}
}
Excluding tests from the run (--test-filter)
Mixed unit + integration test suites can pass a dotnet test --filter-style expression to exclude container-gated or otherwise slow tests from both the initial test run and per-mutation runs:
# Both names accepted — the canonical flag matches the JSON config key,
# the alias matches `dotnet test --filter` and the wider ecosystem.
dotnet stryker-netx --test-filter "Category!=Integration"
dotnet stryker-netx --test-case-filter "Category!=Integration"
Or in stryker-config.json:
{
"stryker-config": {
"test-case-filter": "Category!=Integration"
}
}
The expression is forwarded verbatim to the underlying VsTest test host (via TestRunCriteria.TestCaseFilter and the <TestCaseFilter> runsettings element). Syntax matches Microsoft's selective-unit-tests guide — Category=Unit, Priority!=High, FullyQualifiedName~Namespace.Sub, &/| boolean combinators. Currently supported on the VsTest runner only; MTP-runner forwarding remains roadmapped (see ADR-044).
Diagnostic-only runs (--break-after, v3.2.18+)
Verify project / build / test-discovery configuration without paying for the full per-mutant test loop. Useful when iterating on --project, --solution, --mutate, or --test-filter arguments against a large suite.
dotnet stryker-netx --break-after analysis # ~30 s — verify project discovery only
dotnet stryker-netx --break-after build # build the projects, then exit
dotnet stryker-netx --break-after initial-test-run # baseline test run, then exit (no mutations)
dotnet stryker-netx --break-after mutation-generation # generate mutants + flush partial HTML report, then exit (no per-mutant test loop)
Or via stryker-config.json:
{ "stryker-config": { "break-after": "mutation-generation" } }
The Aisess team's pain point (Anomalies Report § 10 wishlist #9): a 3 600-test suite paid ≈ 9 min per diagnostic run before any actionable output appeared. With --break-after, the same diagnostic finishes in ≈ 30 s. See ADR-046 §C.
Operator catalogue (v2.4.0)
| Family | v1.x mutators (Defaults) | Stronger additions | All-only additions |
|---|---|---|---|
| Arithmetic | BinaryExpression, Math | Aod (operator deletion), InlineConstants, ConstantReplacement (PIT CRCR, v2.1) | — |
| Relational | BinaryExpression, RelationalPattern | RorMatrix (full 5-replacement matrix) | — |
| Logical / Boolean | Boolean, NegateCondition, ConditionalExpression, BinaryPattern | — | — |
| Unary | PrefixUnary, PostfixUnary | — | UoiMutator |
| Strings | String, StringEmpty, StringMethod, StringMethodToConstant, InterpolatedString | — | — |
| Collections / LINQ | Linq, Collection, Initializer, ArrayCreation | TypeDrivenReturn (cargo-mutants C2) | — |
| Object construction | ObjectCreation | ConstructorNull (PIT CONSTRUCTOR_CALLS) | — |
| Method calls | (covered indirectly) | — | NakedReceiver (PIT EXP_NAKED_RECEIVER), ArgumentPropagation (PIT EXP_ARGUMENT_PROPAGATION, v2.0.1) |
| Pattern matching | IsPatternExpression, RelationalPattern, BinaryPattern | MatchGuard (cargo-mutants C4), SwitchArmDeletion (cargo-mutants C3, v2.0.1) | — |
| Records | (covered by ObjectCreation/Initializer) | WithExpression (cargo-mutants C5) | — |
| Member variables | (covered by AssignmentExpression) | MemberVariable (PIT EXP_MEMBER_VARIABLE, v2.0.1) | — |
| Method bodies | (covered by Block/Statement) | — | MethodBodyReplacement (cargo-mutants C1, v2.0.1) |
| Async / await | (none in v1.x) | AsyncAwait, AsyncAwaitResult (v2.3 — spec-faithful .Result variant), ConfigureAwait (v2.0.1), TaskWhenAllToWhenAny (v2.0.1) |
— |
| DateTime | (none in v1.x) | DateTime, DateTimeAddSign (v2.0.1) | — |
| Span / Memory | (none in v1.x) | SpanMemory | AsSpanAsMemory (v2.0.1), SpanReadOnlySpanDeclaration (v2.1) |
| Exceptions | (none in v1.x) | — | ExceptionSwap |
| Other | Block, Statement, Assignment, Checked, Regex, NullCoalescing | — | — |
Total: 26 (Defaults) + 17 (Stronger) + 7 (All-only) = 50 mutators (v3.3.13; the two generic-constraint mutators were removed in Sprint 188 — generic constraints are compile-time-only and can never yield a killable mutant, see ADR-062).
The equivalent-mutant filter pipeline ships 5 filters (v2.4.0): IdentityArithmeticFilter, IdempotentBooleanFilter, ConservativeDefaultsEqualityFilter, RoslynDiagnosticsEquivalenceFilter (v2.1 — parser-error pre-filter), and RoslynSemanticDiagnosticsEquivalenceFilter (v2.4 — uses Roslyn speculative-binding for O(1) per-mutation semantic-error pre-filtering, catches type-checking errors that the parser-only v2.1 filter misses).
After v2.1.0, the catalogue closes essentially all operator-shaped recommendations from the comparison spec. Operator-level gaps from PIT/cargo-mutants/mutmut are exhausted.
Mutation execution model (updated v2.2.0)
Stryker.NET (and stryker-netx) compile all mutations into a single assembly with runtime ActiveMutationId switching. There is no per-mutant compile to optimize away. The actual cost driver is test-host process spawn per batch — already mitigated by the existing --coverage-analysis flag (default perTest), which skips uncovered mutants and reuses the test host across covered mutants.
The --engine Recompile|HotSwap flag introduced in v2.0.0 was based on a wrong mental model of this cost structure. v2.2.0 walks it back per ADR-021:
- The flag is still accepted for backwards compatibility (no breaking change for existing scripts/configs).
- Both values are treated identically — they have no functional effect.
- A deprecation warning is logged when the flag is supplied explicitly.
- The
HotSwapEngineandRecompileEngineimplementation classes have been deleted. - The
MutationEngineenum,IMutationEngineinterface,IStrykerOptions.MutationEngineproperty, andMutationEngineInputconfig class are marked[Obsolete]as v2.x source-compat shims; v3.0 may hard-remove them.
For real performance tuning, use --coverage-analysis perTest (default) or --coverage-analysis all — see OptimizationModes documentation. A future incremental-mutation-testing direction (file-watcher + change-driven re-run) is tracked in ADR-022 (Proposed), without commitment.
Migration from upstream Stryker.NET
If you already use dotnet-stryker, switching is a two-step rename:
- Uninstall the upstream tool, install
stryker-netx:dotnet tool uninstall -g dotnet-stryker dotnet tool install -g dotnet-stryker-netx - In your scripts / CI workflows, replace
dotnet strykerwithdotnet stryker-netx.
That's it. stryker-config.json, CLI flags, and reporter output formats are unchanged. No config file edits required.
Migration from stryker-netx v1.x to v2.0.0
See MIGRATION-v1-to-v2.md. Short version: no breaking changes for the default profile. To opt into the expanded catalogue, add --mutation-profile Stronger or --mutation-profile All.
Known limitations (v3.3.x)
- NetFramework projects (legacy
packages.configstyle —<TargetFramework>net48</TargetFramework>) requirenuget.exe restoreof the .sln before invocation, becausedotnet msbuild -restoreonly handles<PackageReference>style. CI'swindows-latestrunner shipsnuget.exe; local-only blocked unlessnuget.exeis on PATH. (Carried forward from v1.0.) JsonReportreporter uses hybrid source-gen + custom-converter serialization as of v2.3.0. Source-genJsonReportSerializerContextprovides JsonTypeInfo for the entry typesJsonReport/IJsonReport; custom polymorphic converters (SourceFileConverter,JsonMutantConverter, etc.) handle interface-typed properties at runtime. Net effect: AOT-trim-progress, not AOT-trim-complete. Full AOT-trim deferred to v3.0 per ADR-024 — requires flatteningIJsonReport/ISourceFile/IJsonMutant(7 interfaces, 34 referencing files) to concrete types, which is a v3.0-cadence breaking change.- Validation framework count-based assertions in
integrationtest/Validation/ValidationProject/ValidateStrykerResults.csare principled-skip as of v2.3.0 per ADR-023. Counts were hardcoded for upstream Stryker.NET 4.14.1's exact mutator output; our v2.x catalogue (50 mutators vs upstream's 26) legitimately produces different counts; manual reconciliation would be a Sisyphean treadmill driftung with every Operator-Addition. The 11 affected[Fact]tests carry[Fact(Skip = "...")]with link to ADR-023. Fixture-level reconciliation is intentionally not roadmapped. - HotSwap engine — removed in v2.2.0 per ADR-021. The
--engineflag is accepted as a deprecated no-op shim; bothRecompileandHotSwapare treated identically with a deprecation warning. - AsyncAwait family — both variants ship:
AsyncAwaitMutator(v2.0.0) emitsawait x → x.GetAwaiter().GetResult()(unwraps exceptions);AsyncAwaitResultMutator(v2.3.0) emitsawait x → x.Result(wraps inAggregateException). Tests asserting on specific exception types may pass under one and fail under the other — having both maximises kill-detection sensitivity. - Generic-constraint mutators removed (v3.3.13, Sprint 188) —
GenericConstraintMutator(v2.0.0, drop-all) andGenericConstraintLoosenMutator(v2.1.0, per-clause) were removed per ADR-062. Generic constraints are compile-time-only and produce no IL, so a constraint mutation can only compile-error or be equivalent — never a killable mutant — and the constraint-relevant body logic is already covered by the operator mutators. - SpanMemoryMutator semantics — emits
span.Slice(start, length) → span.Slice(0, length), a stryker-netx-specific variant. AsSpanAsMemoryMutator (v2.0.1) handles invocation-siteAsSpan() ↔ AsMemory(); SpanReadOnlySpanDeclarationMutator (v2.1) handles declaration-siteSpan<T> ↔ ReadOnlySpan<T>. All three coexist. - Coverage-driven mutation skip (mutmut-style) — already shipped via
OptimizationModes.SkipUncoveredMutantsandCoverageBasedTest(v1.x). Use--coverage-analysis perTest(the default) or--coverage-analysis allto enable; mutants for lines without test coverage are flaggedNoCoverageand never run. testhost.exekeeps source-project DLLs file-locked during a run (v3.3.0 doc-note). VsTest'stesthost.exeloads the test-output DLLs with the default file-share mode and only releases the handles when the Stryker process tree exits. Any concurrentdotnet buildof the same solution will fail withMSB3027retry-exhausted. Workaround: use--output <path-outside-source-tree>so MSBuild's target-copy doesn't collide with the locked test-output DLLs, or simply don't run a concurrentdotnet buildwhile Stryker is mid-run. Not a stryker-netx code path — assembly-loading is internal to vstest/testhost.- Cold-run wall-clock variance (v3.3.0 doc-note). The very first mutation run on a fresh machine pays JIT-compilation + NuGet symbol-load amortization in the initial test phase — typically 4-5× slower than warm runs. Subsequent runs stabilize at the warm timing. CI runners starting cold every time will see this on every run; consider pre-warming the tool with a no-op
--versioncall earlier in the same job. - Per-mutant coverage attribution is class-fixture-bound (v3.3.0 doc-note, see ADR-048). When a test constructs its own SUT instance inside the test body (e.g., to inject a
Mock<ILogger<T>>per-test for assertion of log emissions),--coverage-analysis perTestmay fail to attribute mutations on that SUT's methods to the test. The recommended pattern is to wire theMock<ILogger>at the class-fixture level so every test reuses the same SUT instance. SUT-instance-aware coverage capture is roadmapped for v3.4+. dotnet-tools.jsonrollForwardpinning (general advice for bug filers). If yourdotnet-tools.jsonpins a specific Stryker version with"rollForward": false, the in-tool upgrade banner does not roll your local install forward — only the announcement. Before filing a bug, rundotnet tool update --local dotnet-stryker-netx --version <latest>(or editdotnet-tools.jsonanddotnet tool restore) and re-verify against the latest released version.
Project status
Current: v3.3.13 (Sprint 188, 2026-06) — production, published on NuGet.org. 188 sprints, 62 ADRs, ~2,200 tests green, 174 tagged releases. Full release history: GitHub Releases.
| Era | Sprints | Versions | Outcome |
|---|---|---|---|
| Architecture & design | 0 | — | 12 founding ADRs, FRs/NFRs, test stack chosen |
| Port | 1–4 | v1.0.0-preview.1 → v1.0.0 |
Buildalyzer fully removed (MSBuildWorkspace), all 11+6 projects on .NET 10, C# 14 modernization, integration suite vendored, CI pipeline |
| Operator expansion | 5–14 | v2.0.0 → v2.1.0 |
Mutation profiles (Defaults/Stronger/All), 52-mutator catalogue (PIT + cargo-mutants + mutmut parity), SemanticModel-driven mutators, equivalence-filter pipeline |
| Consolidation | 15–17 | v2.2.0 → v2.4.0 |
HotSwap walk-back (ADR-021), RoslynSemanticDiagnostics filter (speculative binding), catalogue closure — operator-shaped gaps vs PIT/cargo-mutants/mutmut exhausted |
| Hardening | 18–24 | v2.5.0 → v2.11.0 |
Full mutator/filter unit-test coverage, FsCheck properties, NetFramework CI, nightly dogfood workflow (Stryker on Stryker) |
| Upstream-test-suite port | 25–138 | v2.12.0 → v3.0.24 |
MSTest→xUnit/FluentAssertions migration of the upstream suites (~1,200 dogfood tests), architectural-deferral elimination, release pipeline repair + first public NuGet push (Sprint 138) |
| Production bug-report era | 139–169 | v3.0.25 → v3.3.1 |
Three external reporter teams (Calculator, Aisess, filesystem-mcp-server) drove ADR-025…049: profile auto-bump, syntax-slot validation layers, .slnx filter defense, disable-comment scoping, --test-case-filter, --break-after, type-aware literal emission |
| CI reanimation | 170 | v3.3.2 |
NuGet-audit advisory bump (Nerdbank.MessagePack), nightly dogfood schedule repair (ADR-050), workspace + documentation refresh |
| Self-audit & hardening | 171–184 | v3.3.3 → v3.3.9 |
Dogfood-config netx layout + MatchesFilter fix (#270); internal 360° source audit (~430 files, 139 findings) driving ADR-053…058 — CE-noise reduction, score-integrity filters, crash/hang robustness, config reach, RegisterCoverage perf (11×) |
| External 360° black/white-box fixes | 185–188 | v3.3.10 → v3.3.13 |
Independent test team's 7 bugs (ADR-059…062): equivalence-filter correctness (method groups, unsigned-zero, arithmetic identities), MathMutator semantic-model path, --solution cwd-independence, README/MTP compatibility, MethodBodyReplacement injection; the two generic-constraint mutators removed as a compile-time-only model limit (ADR-062, INJ-001 closed) |
See _docs/ for per-sprint lessons and the architecture specification for all ADRs.
Building from source
Requires .NET SDK 10.0.100+ (global.json pins the 10.0.1xx band with rollForward: latestFeature).
dotnet build stryker-netx.slnx
dotnet test stryker-netx.slnx
dotnet pack src/Stryker.CLI/Stryker.CLI.csproj -c Release -p:PackageVersion=$YOUR_VERSION
Contributing
See CONTRIBUTING.md. All contributions require DCO sign-off (git commit -s).
License
Licensed under the Apache License 2.0. See NOTICE for attribution to the original Stryker.NET project and its authors.
References
- Original Stryker.NET project: https://github.com/stryker-mutator/stryker-net
- Stryker Mutator docs: https://stryker-mutator.io/
- Architecture decisions (incl. v2.0.0 ADRs 013–018): _docs/architecture spec/architecture_specification.md
- v1→v2 migration guide: MIGRATION-v1-to-v2.md
- Per-sprint lessons: _docs/
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.