skip to content

In SpecFlow/Reqnroll, what does [Binding] mark, and how does context injection supply scenario state?

level: middleimportance: should knowfreq 44%

answer

  1. A class-level attribute the assembly is scanned for
  2. Step attributes only count inside such a class
  3. Constructor parameters, resolved per scenario
  4. Two classes, one shared instance
  5. Run-level hooks must be static

basics

~20 s

[Binding] marks a class whose step definitions and hooks SpecFlow/Reqnroll discover in the test assembly. Context injection builds those classes once per scenario from a scenario-scoped container, so several binding classes share one instance of a state class.

solid answer

~50 s

In SpecFlow and its successor Reqnroll, the framework scans the test assembly for classes carrying `[Binding]`. Only inside such a class do `[Given]`, `[When]`, `[Then]` and `[StepDefinition]` attach a method to a Gherkin step, and only there do hook attributes such as `[BeforeScenario]`, `[AfterScenario]`, `[BeforeFeature]` and `[BeforeTestRun]` take effect — the feature-level and run-level ones must be static. **Context injection** is the state mechanism: the built-in container creates one instance of each binding class per scenario and resolves the constructor parameters from the same scenario-scoped container. Ask for a plain class in two different binding classes and both receive the **same** instance for that scenario, and a fresh one for the next. Built-in `ScenarioContext` and `FeatureContext` are injectable the same way. The idiom is therefore several small typed state classes rather than one large shared object.

code

csharp · 29 lines
csharp
public class TimetableContext          // a plain class, no registration needed
{
    public List<string> Sailings { get; } = new();
    public void Seed(int count) { /* ... */ }
    public void Book(int hour, int minute) { /* ... */ }
}

[Binding]
public class SailingSearchSteps
{
    private readonly TimetableContext _timetable;

    public SailingSearchSteps(TimetableContext timetable) => _timetable = timetable;

    [Given(@"the Harbour Point route has (\d+) sailings")]
    public void GivenTheRouteHasSailings(int count) => _timetable.Seed(count);
}

[Binding]
public class BookingSteps
{
    private readonly TimetableContext _timetable;   // the SAME instance this scenario

    public BookingSteps(TimetableContext timetable) => _timetable = timetable;

    [When(@"I book the (\d+):(\d+) sailing")]
    public void WhenIBookTheSailing(int hour, int minute)
        => _timetable.Book(hour, minute);
}

go deeper

for a junior

Recall that [Binding] on the class is what makes step methods and hooks discoverable, and that [Given], [When] and [Then] carry the pattern. Know that scenario state arrives through the constructor rather than through globals.

for a middle

Explain the lifetimes: one container per scenario, binding classes built from it, and a plain constructor parameter resolved to the same instance for every class that asks. Know which hook attributes require static methods and why.

for a senior

Show the operating judgement — keeping constructors cheap, splitting state into small typed contexts rather than one bag, avoiding statics that only break under parallel execution, and knowing that .feature files are compiled at build time.

for a principal

Own the convention across a .NET estate: how many context types are too many, when the untyped key/value store is acceptable, and how the build-time code generation shapes review, packaging and CI compared with run-time-parsing implementations.

SpecFlow is the .NET member of the Gherkin family and **Reqnroll** is its community continuation; they share this model, differing mainly in namespace and packaging. Where cucumber-js binds a World to `this` and Behave passes a `context` argument, .NET uses the language's own idiom: **constructor injection into classes the framework discovers by attribute**. ## What [Binding] marks `[Binding]` is a class-level attribute. At run time the framework scans the test assembly and treats every `[Binding]` class as a source of glue. Inside such a class: - `[Given(...)]`, `[When(...)]`, `[Then(...)]` and the generic `[StepDefinition(...)]` attach a method to a Gherkin step, with the pattern supplied as the attribute argument. - Hook attributes register lifecycle callbacks: `[BeforeScenario]`/`[AfterScenario]`, `[BeforeFeature]`/`[AfterFeature]`, `[BeforeStep]`/`[AfterStep]` and `[BeforeTestRun]`/`[AfterTestRun]`. - `[StepArgumentTransformation]` registers a converter turning captured text into a domain type. Two rules trip people up. First, a step-definition method in a class **without** `[Binding]` is invisible — the symptom is an unbound step, not a compile error. Second, the feature-level and run-level hooks (`[BeforeFeature]`, `[AfterFeature]`, `[BeforeTestRun]`, `[AfterTestRun]`) must be **static methods**, because there is no per-scenario instance for them to hang off. Scenario-level and step-level hooks are instance methods and can therefore use injected state, which is exactly why the static requirement feels arbitrary until you see the lifetime it follows from. There is also a structural difference from the rest of the family worth naming: SpecFlow and Reqnroll generate a code-behind test class from each `.feature` file at **build time**, which the underlying .NET test runner then executes. Cucumber-JVM, cucumber-js and Behave all parse the feature file at **run time**. That is why a .NET Gherkin suite shows up in the IDE as ordinary discovered tests, and why a `.feature` file edit that is not rebuilt appears to have no effect. ## Context injection Context injection is the framework's built-in dependency-injection mechanism, and the rule is short: **binding classes are constructed per scenario, and their constructor parameters are resolved from a container whose lifetime is that scenario.** 1. A scenario begins; a fresh container is created for it. 2. Each binding class the scenario needs is instantiated from that container. 3. Every constructor parameter is resolved from the same container. A plain class with a public constructor needs no registration at all — the container will create it. 4. The container caches what it created, so a type requested by two binding classes is **one instance** for the whole scenario. 5. The scenario ends and the container goes with it; the next scenario starts from nothing. That gives you sharing without globals and isolation without cleanup code. It also explains the .NET idiom: instead of one big object holding everything, you write several small typed context classes — a `TimetableContext`, a `BookingContext` — and each binding class declares only what it needs. The compiler then tells you when a step reaches for state it has no business touching. | Concern | How it is expressed | | --- | --- | | Marking glue | `[Binding]` on the class | | Binding a step | `[Given]` / `[When]` / `[Then]` / `[StepDefinition]` on the method | | Per-scenario state | a plain class taken as a constructor parameter | | Framework-supplied state | `ScenarioContext`, `FeatureContext` injected the same way | | Scenario-level setup | `[BeforeScenario]` on an instance method | | Run-level setup | `[BeforeTestRun]` on a **static** method | `ScenarioContext` and `FeatureContext` are the framework's own state holders and can be injected as constructor parameters just like your own types. They expose run information — the current scenario's information object, whether an error has occurred — and they also offer untyped key/value storage. Reaching for that dictionary is the usual smell: it throws away the type safety that is the whole reason to prefer injection, so teams treat it as a last resort behind their own typed context classes. ## Where the failure modes are - **Constructor work.** Constructors run per scenario. Opening a browser session or a connection there costs you once per scenario; on a ferry-timetable product with a 63-scenario feature set and an eleven-minute pipeline budget, that is the difference between a suite that fits and one that does not. Expensive shared resources belong behind a `[BeforeTestRun]` hook or a lazily-initialised singleton the container hands out. - **Circular dependencies.** Two context classes that take each other fail at resolution time. The fix is a third, smaller type rather than a property setter. - **Static fields as a shortcut.** A `static` field appears to work and then corrupts results the moment the suite runs scenarios in parallel, because it is the one piece of state the per-scenario container does not own. - **Missing `[Binding]`.** The most common first-hour failure, and it presents as a step with no matching definition rather than as anything pointing at the attribute.

  • Why must a [BeforeTestRun] hook be static when [BeforeScenario] need not be?
    Instance hooks hang off a binding-class instance, and those instances only exist within a scenario's container. A run-level hook fires before any scenario has started, so there is no instance and nothing to inject; making it static is the only lifetime that is coherent. The same reasoning makes `[BeforeFeature]` and `[AfterFeature]` static.
  • When would you inject ScenarioContext instead of your own context class?
    When you need framework information rather than domain state — the current scenario's information object, its tags, or whether the scenario has already failed, typically inside an `[AfterScenario]` hook that captures diagnostics. For your own data, prefer a typed class: `ScenarioContext`'s key/value storage is untyped, so it trades away exactly the compile-time safety that makes injection worth using.
  • A team put a shared browser session in a context class constructor and the suite slowed sharply. Why?
    Context classes are constructed per scenario, so the session was created and discarded once for every scenario rather than once for the run. Expensive shared resources belong behind a run-level hook or a container-provided singleton, with the per-scenario class holding only a reference. That keeps isolation where it matters — the mutable scenario data — without paying setup cost 63 times.

saying these in an interview costs you the question

  • Writes step methods in a class without [Binding] and expects discovery
  • Thinks each binding class gets its own copy of an injected context class
  • Puts run-level setup in a non-static [BeforeTestRun] method
  • Uses static fields to share state, then enables parallel execution
  • Stores all scenario data in ScenarioContext's untyped key-value storage