What is new in .NET 11, and should you move from .NET 10?
A fair tour of .NET 11 and C# 15 as released on 10 November 2026, the breaking changes to read first, and a decision guide for production teams on .NET 10.
· 7 min readA working introduction to Reqnroll for .NET teams: Gherkin feature files, step bindings, scenario outlines, CI and moving from SpecFlow, with a short example.

Your unit tests tell you the code does what the developer intended. They don't tell you whether that matches what the business agreed, and they're rarely something a finance or operations lead can read. Behaviour-driven development (BDD) fills that gap with plain-language scenarios that are also executable tests. If the idea is new to you, our plain-English explainer on BDD covers the why. This article covers the how, for .NET.
The tool is Reqnroll. We'll go through the moving parts, build one small example, and then look at running it in a pipeline, migrating from SpecFlow, and the ways teams get this wrong.
All versions and features below are as of October 2026. The code samples are illustrative and have not been compiled or run for this article; treat them as a starting point and check them against the Reqnroll documentation.
Reqnroll is an open-source .NET test automation tool for BDD, and a port of Cucumber. It began in 2024 as a community fork of SpecFlow. Tricentis announced in December 2024 that the open-source SpecFlow project would reach end of life on 31 December 2024, so teams still on SpecFlow are now on software with no upstream support. We come back to migration below.
Reqnroll's compatibility page lists support for .NET Framework 4.6.2, 4.7.2 and 4.8.1, and .NET 8, 9 and 10, running on Windows, Linux and macOS. It works with NUnit, MSTest, TUnit and xUnit (including xUnit v3), and MSTest v4 needs Reqnroll 3.2 or later. The setup guide adds that xUnit does not support test attachments, and suggests NUnit if you have no preference. The latest release on NuGet when we checked was 3.3.4, published in March 2026.
Feature files are plain-text .feature files written in Gherkin. A feature holds scenarios, and each scenario is a list of steps beginning with Given (context), When (action) or Then (outcome), with And and But to continue. Gherkin also has Background for shared setup, Rule to group scenarios around one business rule, and Scenario Outline with an Examples table to run one scenario against several rows of data.
Step definitions, also called bindings, are C# methods that connect each sentence to code. A class marked [Binding] holds methods marked [Given], [When] or [Then] with a matching text pattern. Reqnroll generates a code-behind file for each feature that calls the matching test framework, so the scenarios show up in the normal test runner.
Context injection passes shared state between steps. If a step class takes a plain class in its constructor, Reqnroll creates one instance per scenario and hands it to every binding that asks for it. That avoids static fields and keeps scenarios independent.
Hooks such as [BeforeScenario] and [AfterScenario] run set-up and tear-down code around scenarios.
This is the same invoice approval rule used in the explainer: invoices above one person's limit need a second approver. The wording is tightened for automation: the limit moves into a Background, and amounts are written without commas so the {int} step patterns below can match them. Start with the feature file.
Feature: Invoice approval
Invoices above one person's limit need a second approver.
Background:
Given the approval limit for one person is £5000
Scenario Outline: The approval route depends on the amount
Given an invoice for £<amount> has been entered
When the first approver approves it
Then the invoice is <outcome>
Examples:
| amount | outcome |
| 800 | ready for payment |
| 5000 | ready for payment |
| 12000 | waiting for a second approver |
Notice that nothing here mentions a button, a screen or a database. That is deliberate, and we return to it under pitfalls.
Next, the system under test. This is a simplified stand-in for your real domain code, which in practice lives in a separate project that the specifications reference.
public enum InvoiceStatus { AwaitingFirstApproval, AwaitingSecondApproval, ReadyForPayment }
public sealed class Invoice
{
private readonly decimal _singleApproverLimit;
public Invoice(decimal amount, decimal singleApproverLimit)
{
Amount = amount;
_singleApproverLimit = singleApproverLimit;
}
public decimal Amount { get; }
public InvoiceStatus Status { get; private set; } = InvoiceStatus.AwaitingFirstApproval;
public void Approve()
{
Status = Status switch
{
InvoiceStatus.AwaitingFirstApproval when Amount > _singleApproverLimit
=> InvoiceStatus.AwaitingSecondApproval,
InvoiceStatus.AwaitingFirstApproval or InvoiceStatus.AwaitingSecondApproval
=> InvoiceStatus.ReadyForPayment,
_ => Status
};
}
}
Then the bindings, using NUnit for the assertion and a small class to carry state between steps.
using NUnit.Framework;
using Reqnroll;
public sealed class InvoiceWorld
{
public decimal Limit { get; set; }
public Invoice? Invoice { get; set; }
}
[Binding]
public sealed class InvoiceApprovalSteps
{
private readonly InvoiceWorld _world;
public InvoiceApprovalSteps(InvoiceWorld world) => _world = world;
[Given("the approval limit for one person is £{int}")]
public void GivenTheApprovalLimit(int limit) => _world.Limit = limit;
[Given("an invoice for £{int} has been entered")]
public void GivenAnInvoice(int amount) =>
_world.Invoice = new Invoice(amount, _world.Limit);
[When("the first approver approves it")]
public void WhenTheFirstApproverApproves() => _world.Invoice!.Approve();
[Then(@"^the invoice is (.*)$")]
public void ThenTheInvoiceIs(string outcome)
{
var expected = outcome switch
{
"ready for payment" => InvoiceStatus.ReadyForPayment,
"waiting for a second approver" => InvoiceStatus.AwaitingSecondApproval,
_ => throw new ArgumentException($"Unknown outcome: {outcome}")
};
Assert.That(_world.Invoice!.Status, Is.EqualTo(expected));
}
}
The {int} placeholders are Cucumber Expressions, which Reqnroll supports. The last step uses a regular expression with ^ and $ markers so Reqnroll treats it as a regex and not an expression; the migration guide warns that expression detection can differ from SpecFlow.
To create a project, install the templates with dotnet new install Reqnroll.Templates.DotNet and run dotnet new reqnroll-project, choosing your test framework and target. The docs also describe a Visual Studio project template, and adding a package such as Reqnroll.NUnit to an existing test project. Keep these specifications in their own project, separate from your ordinary unit tests, as the setup guide recommends.
Because the scenarios become ordinary tests, dotnet test runs them. Any pipeline that already builds and tests your .NET code, whether GitHub Actions or Azure Pipelines, needs no special runner. A failing scenario fails the build.
Since version 3.0, Reqnroll has a reporting feature built on "formatters", including an HTML report configured in reqnroll.json. The project presents it as the replacement for the closed-source SpecFlow+ LivingDoc generator. Based on the formatter configuration page:
{
"$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json",
"formatters": {
"html": { "outputFilePath": "reqnroll_report.html" }
}
}
The path is relative to the project's output folder. Publish that file as a pipeline artefact and a non-technical colleague can read what the system does and which scenarios passed on the latest run. Check the current option names in the docs, as this area is newer than the rest.
One housekeeping point: Reqnroll generates *.feature.cs code-behind files, and the docs advise against committing them. From v3.3 you can send them to the intermediate output folder with ReqnrollUseIntermediateOutputPathForCodeBehind set to true.
The two do different jobs, and one doesn't replace the other.
| Unit tests | Behaviour scenarios (BDD) | |
|---|---|---|
| Written for | Developers | Developers, the business and anyone taking over |
| Checks | How a piece of code works | The behaviour the business agreed |
| Speed | Milliseconds | Slower, as they exercise more of the system |
| Quantity | Many | A deliberate, smaller set |
Keep the unit tests for design and fast feedback, and use scenarios for the outcomes someone outside the team would recognise. If you have untested legacy code, characterisation tests give you a safety net first, and scenarios can then be added for the rules that matter most.
If you have SpecFlow tests, the Reqnroll migration guide describes two routes. The first uses a compatibility package, Reqnroll.SpecFlowCompatibility, so that most code changes are limited to packages and namespace usings. The second is a search-and-replace of TechTalk.SpecFlow with Reqnroll, renaming specflow.json to reqnroll.json, and tidying SpecFlow-named types. If you're coming from SpecFlow v3, read the listed breaking changes first, including the removal of calling steps from other steps by string. The guide also notes that SpecFlow+ LivingDoc is not included. Migrate on a branch and run the full suite before and after.
When we take on someone else's .NET application, the most valuable document is often the one that isn't there: a statement of what it is supposed to do. Feature files written for the main business rules, run against the existing system, tell you which behaviours you can rely on and which are already broken. They become the specification for later changes, framework upgrades and dependency updates. This fits our own method of specification first, then automated tests, independent review and human sign-off.
Pick one business rule that your team argues about, write four or five scenarios for it in Gherkin, and bind them to the existing code in a new, separate test project. Run them with dotnet test. If you can get one rule to green in an afternoon, you will know quickly whether the approach suits you.
If you would like a second pair of eyes on how a system is tested today, a software health audit looks at exactly that, and our software and integration work covers building new systems with these habits from the start. Our software care page explains how we look after what's already running.

A fair tour of .NET 11 and C# 15 as released on 10 November 2026, the breaking changes to read first, and a decision guide for production teams on .NET 10.
· 7 min read
As of 10 November 2026, .NET 8 and .NET 9 no longer get security fixes. What has changed, what has not, and a calm 30-day plan for software on them.
· 4 min read
How a non-technical owner can decide what comes first on a product roadmap: plain criteria, simple scoring, outcome-based thinking, and saying no with reasons.
· 6 min read