Skip to content
Open
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# [vNext]

## Improvements:
* Make it so that the [Binding] attribute is no longer required on types declaring binding methods (#1044)

## Bug fixes:
* Fix: Formatters incorrectly handle Unicode text file content of attachments.

*Contributors of this release (in alphabetical order):* @clrudolphi
*Contributors of this release (in alphabetical order):* @clrudolphi, @Code-Grump

# v3.3.3 - 2026-01-27

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ using Reqnroll;

namespace {namespace}
{
[Binding]
public class {className}
{
private readonly IReqnrollOutputHelper _outputHelper;
Expand Down
9 changes: 2 additions & 7 deletions Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,9 @@ public bool CanProcessMethodAttribute(string attributeTypeName)
return true;
}

private static bool IsPotentialBindingClass(IEnumerable<string> attributeTypeNames)
{
return attributeTypeNames.Any(attr => attr.EndsWith($".{nameof(BindingAttribute)}", StringComparison.InvariantCulture));
}

public bool PreFilterType(IEnumerable<string> attributeTypeNames)
{
return IsPotentialBindingClass(attributeTypeNames);
return true;
}

public bool ProcessType(BindingSourceType bindingSourceType)
Expand Down Expand Up @@ -80,7 +75,7 @@ private IEnumerable<BindingScope> GetScopes(IEnumerable<BindingSourceAttribute>

private bool IsBindingType(BindingSourceType bindingSourceType)
{
return bindingSourceType.Attributes.Any(attr => typeof(BindingAttribute).IsAssignableFrom(attr.AttributeType));
return bindingSourceType.MethodAttributes != null && bindingSourceType.MethodAttributes.Any();
}

private bool IsStepDefinitionAttribute(BindingSourceAttribute attribute)
Expand Down
2 changes: 2 additions & 0 deletions Reqnroll/Bindings/Discovery/BindingSourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class BindingSourceType

public BindingSourceAttribute[] Attributes { get; set; }

public BindingSourceAttribute[] MethodAttributes { get; set; }

public override string ToString() => BindingType?.ToString() ?? "<null>";
}
}
34 changes: 30 additions & 4 deletions Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,34 @@ internal bool BuildBindingsFromType(Type type)
if (!_bindingSourceProcessor.PreFilterType(filteredAttributes.Select(attr => attr.GetType().FullName)))
return false;

var bindingSourceType = CreateBindingSourceType(type, filteredAttributes);
var candiateMethods = type.GetMethods(
Copy link
Copy Markdown
Member

@304NotModified 304NotModified Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m curious about the performance impact — there could be many methods involved. In time and memory.

Creating a list for every type at least adds memory pressure, and reflection is relatively slow.

P.S. There’s a typo in “candidate”.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely adds a cost for us - the system is written to produce a lightweight model layer between the actual reflection and the processing of binding methods, and without restructuring it more significantly, there's going to be allocations we could avoid.

However, I'd simply make the case that it's happening on a scale that's hard to perceive for most use-cases. This is the model that NUnit has used for a long time and that xunit has always used. If we'd like to make it as efficient as theirs, we'd need to do something more breaking in our APIs to refactor the component to yield types more efficiently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one about xUnit and NUnit. Im don't know if they use some kind of cache.

BindingFlags.Static |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic);

List<Attribute> methodAttributes;
try
{
methodAttributes = [..
candiateMethods.SelectMany(method => method.GetCustomAttributes(true).Cast<Attribute>())];
}
catch (TypeLoadException ex)
{
_bindingSourceProcessor.RegisterTypeLoadError($"Could not load attributes for type '{type.FullName}': {ex}");
// When the type attributes cannot be loaded, the type cannot be processed anyway so we can return with false here to avoid reporting further errors.
return false;
}

var filteredMethodAttributes = methodAttributes
.Where(attr => _bindingSourceProcessor.CanProcessMethodAttribute(attr.GetType().FullName));

var bindingSourceType = CreateBindingSourceType(type, filteredAttributes, filteredMethodAttributes);

if (!_bindingSourceProcessor.ProcessType(bindingSourceType))
return false;

foreach (var methodInfo in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
foreach (var methodInfo in candiateMethods)
{
_bindingSourceProcessor.ProcessMethod(CreateBindingSourceMethod(methodInfo));
}
Expand Down Expand Up @@ -131,7 +153,10 @@ private IBindingType CreateBindingType(Type type)
return new RuntimeBindingType(type);
}

private BindingSourceType CreateBindingSourceType(Type type, IEnumerable<Attribute> filteredAttributes)
private BindingSourceType CreateBindingSourceType(
Type type,
IEnumerable<Attribute> filteredAttributes,
IEnumerable<Attribute> filteredMethodAttributes)
{
return new BindingSourceType
{
Expand All @@ -141,7 +166,8 @@ private BindingSourceType CreateBindingSourceType(Type type, IEnumerable<Attribu
IsPublic = type.IsPublic,
IsNested = TypeHelper.IsNested(type),
IsGenericTypeDefinition = type.IsGenericTypeDefinition,
Attributes = GetAttributes(filteredAttributes)
Attributes = GetAttributes(filteredAttributes),
MethodAttributes = GetAttributes(filteredMethodAttributes)
};
}

Expand Down
4 changes: 2 additions & 2 deletions Reqnroll/ScenarioContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public static void StepIsPending()
/// <param name="bindingType">The type of the binding class.</param>
/// <returns>The binding class instance</returns>
/// <remarks>
/// The binding classes are the classes with the [Binding] attribute, that might
/// contain step definitions, hooks or step argument transformations. The method
/// The binding classes are the classes with which contain one or more methods decorated with binding attributes.
/// This includes step definitions, hooks or step argument transformations. The method
/// is called when any binding method needs to be called.
/// </remarks>
public object GetBindingInstance(Type bindingType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ When I eat 5 cukes

namespace CucumberMessages.CompatibilityTests.Smoke
{
[Binding]
internal class Hooks
{
public Hooks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,33 +168,25 @@ private async Task<SampleBindings> PerformStepExecution(string methodName, strin
transformations?.ToList().ForEach(binding => bindingRegistry.RegisterStepArgumentTransformationBinding(binding));
onBindingRegistryPreparation?.Invoke(bindingRegistry);

var givenAttribute = new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(GivenAttribute)),
AttributeValues = [ new BindingSourceAttributeValueProvider(expression) ]
};

var bindingSourceMethod = new BindingSourceMethod
{
BindingMethod = new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(methodName)),
IsPublic = true,
Attributes = new[]
{
new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(GivenAttribute)),
AttributeValues = new IBindingSourceAttributeValueProvider[]
{
new BindingSourceAttributeValueProvider(expression)
}
}
}
Attributes = [ givenAttribute ]
};

bindingSourceProcessor.ProcessType(
new BindingSourceType
{
BindingType = new RuntimeBindingType(typeof(SampleBindings)),
Attributes = new[]
{
new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(BindingAttribute))
}
},
Attributes = [],
MethodAttributes = [ givenAttribute ],
IsPublic = true,
IsClass = true
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,33 +123,26 @@ private static void SetupBoundMethod(string expression, IBindingRegistry binding
{
bindingRegistry.RegisterStepArgumentTransformationBinding(transformation);
}

var givenAttribute = new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(GivenAttribute)),
AttributeValues = [new BindingSourceAttributeValueProvider(expression)]
};

var bindingSourceMethod = new BindingSourceMethod
{
BindingMethod = new RuntimeBindingMethod(testType.GetMethod(methodName)),
IsPublic = true,
Attributes = new[]
{
new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(GivenAttribute)),
AttributeValues = new IBindingSourceAttributeValueProvider[]
{
new BindingSourceAttributeValueProvider(expression)
}
}
}
Attributes = [givenAttribute]
};

bindingSourceProcessor.ProcessType(
new BindingSourceType
{
BindingType = new RuntimeBindingType(typeof(CucumberExpressionIntegrationTests.SampleBindings)),
Attributes = new[]
{
new BindingSourceAttribute
{
AttributeType = new RuntimeBindingType(typeof(BindingAttribute))
}
},
Attributes = [],
MethodAttributes = [givenAttribute],
IsPublic = true,
IsClass = true
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding()
//ARRANGE
var sut = CreateBindingSourceProcessor();

var bindingSourceType = CreateSyntheticBindingSourceType();
var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod();
var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod);

//ACT
sut.ProcessType(bindingSourceType).Should().BeTrue();
Expand Down Expand Up @@ -62,15 +62,13 @@ private BindingSourceMethod CreateSyntheticHookBindingSourceMethod(HookType hook
};
}

private BindingSourceType CreateSyntheticBindingSourceType()
private BindingSourceType CreateSyntheticBindingSourceType(BindingSourceMethod bindingSourceMethod)
{
var bindingSourceType = new BindingSourceType
{
IsClass = true,
Attributes = new[]
{
CreateBindingSourceAttribute("BindingAttribute", "Reqnroll.BindingAttribute")
},
Attributes = [],
MethodAttributes = bindingSourceMethod?.Attributes ?? []
};
return bindingSourceType;
}
Expand Down Expand Up @@ -112,7 +110,7 @@ public async Task Async_void_binding_methods_are_not_supported()
var bindingSourceMethod = CreateBindingSourceMethod(typeof(StepDefClassWithAsyncVoid), nameof(StepDefClassWithAsyncVoid.AsyncVoidStepDef),
CreateBindingSourceAttribute("GivenAttribute", "Reqnroll.GivenAttribute").WithValue("an authenticated user"));

sut.ProcessType(CreateSyntheticBindingSourceType()).Should().BeTrue();
sut.ProcessType(CreateSyntheticBindingSourceType(bindingSourceMethod)).Should().BeTrue();
sut.ProcessMethod(bindingSourceMethod);
sut.BuildingCompleted();

Expand All @@ -126,7 +124,9 @@ public void Binding_type_errors_should_be_captured(bool isClass, bool isGenericT
{
var sut = CreateBindingSourceProcessor();

var bindingSourceType = CreateSyntheticBindingSourceType();
var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod();
var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod);

// make it invalid
bindingSourceType.IsClass = isClass;
bindingSourceType.IsGenericTypeDefinition = isGenericTypeDefinition;
Expand All @@ -143,11 +143,12 @@ public void Binding_method_errors_should_be_captured(bool isClassAbstract, bool
{
var sut = CreateBindingSourceProcessor();

var bindingSourceType = CreateSyntheticBindingSourceType();
bindingSourceType.IsAbstract = isClassAbstract;
var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod();
bindingSourceMethod.IsStatic = isMethodStatic;

var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod);
bindingSourceType.IsAbstract = isClassAbstract;

sut.ProcessType(bindingSourceType).Should().BeTrue();
sut.ProcessMethod(bindingSourceMethod);
sut.BuildingCompleted();
Expand All @@ -164,10 +165,11 @@ public void Non_static_feature_and_test_run_hook_errors_should_be_captured(HookT
{
var sut = CreateBindingSourceProcessor();

var bindingSourceType = CreateSyntheticBindingSourceType();
var bindingSourceMethod = CreateSyntheticHookBindingSourceMethod(hookType);
bindingSourceMethod.IsStatic = false;

var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod);

sut.ProcessType(bindingSourceType).Should().BeTrue();
sut.ProcessMethod(bindingSourceMethod);
sut.BuildingCompleted();
Expand Down
Loading
Loading