diff --git a/.ncrunch/Reqnroll.PluginTests.v3.ncrunchproject b/.ncrunch/Reqnroll.PluginTests.v3.ncrunchproject
index 79ff1b6af..321148d13 100644
--- a/.ncrunch/Reqnroll.PluginTests.v3.ncrunchproject
+++ b/.ncrunch/Reqnroll.PluginTests.v3.ncrunchproject
@@ -10,6 +10,9 @@
Reqnroll.PluginTests.Infrastructure.WindsorPluginTests.Can_load_Windsor_plugin
+
+ Reqnroll.PluginTests.Infrastructure.MicrosoftExtensionsDependencyInjectionTests.LoadPlugin_MicrosoftExtensionsDependencyInjection_ShouldNotBeNull
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78d557aa3..66733b501 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
* Fix: #71 StackOverflowException when using [StepArgumentTransformation] with same input and output type (for example string)
* Fix: Autofac without hook does not run GlobalDependencies (#127)
* Fix: Reqnroll.Autofac shows wrongly ambiguous step definition (#56)
+* Port SolidToken.SpecFlow.DependencyInjection to Reqnroll. Thanks to @SolidToken for the contribution! (#94)
# v1.0.1 - 2024-02-16
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/BindingRegistryExtensions.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/BindingRegistryExtensions.cs
new file mode 100644
index 000000000..8d5e7a9b9
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/BindingRegistryExtensions.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Reqnroll.Bindings;
+using Reqnroll.Bindings.Reflection;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public static class BindingRegistryExtensions
+ {
+ public static IEnumerable GetBindingTypes(this IBindingRegistry bindingRegistry)
+ {
+ return bindingRegistry.GetStepDefinitions().Cast()
+ .Concat(bindingRegistry.GetHooks().Cast())
+ .Concat(bindingRegistry.GetStepTransformations())
+ .Select(b => b.Method.Type)
+ .Distinct();
+ }
+
+ public static IEnumerable GetBindingAssemblies(this IBindingRegistry bindingRegistry)
+ {
+ return bindingRegistry.GetBindingTypes().OfType()
+ .Select(t => t.Type.Assembly)
+ .Distinct();
+ }
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionPlugin.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionPlugin.cs
new file mode 100644
index 000000000..f3058dec6
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionPlugin.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Concurrent;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.Bindings;
+using Reqnroll.Bindings.Discovery;
+using Reqnroll.BindingSkeletons;
+using Reqnroll.BoDi;
+using Reqnroll.Configuration;
+using Reqnroll.ErrorHandling;
+using Reqnroll.Infrastructure;
+using Reqnroll.Plugins;
+using Reqnroll.Tracing;
+using Reqnroll.UnitTestProvider;
+
+[assembly: RuntimePlugin(typeof(Reqnroll.Microsoft.Extensions.DependencyInjection.DependencyInjectionPlugin))]
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public class DependencyInjectionPlugin : IRuntimePlugin
+ {
+ private static readonly ConcurrentDictionary BindMappings =
+ new ConcurrentDictionary();
+
+ private static readonly ConcurrentDictionary ActiveServiceScopes =
+ new ConcurrentDictionary();
+
+ private readonly object _registrationLock = new object();
+
+ public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration)
+ {
+ runtimePluginEvents.CustomizeGlobalDependencies += CustomizeGlobalDependencies;
+ runtimePluginEvents.CustomizeFeatureDependencies += CustomizeFeatureDependenciesEventHandler;
+ runtimePluginEvents.CustomizeScenarioDependencies += CustomizeScenarioDependenciesEventHandler;
+ }
+
+ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEventArgs args)
+ {
+ if (!args.ObjectContainer.IsRegistered())
+ {
+ lock (_registrationLock)
+ {
+ if (!args.ObjectContainer.IsRegistered())
+ {
+ args.ObjectContainer.RegisterTypeAs();
+ args.ObjectContainer.RegisterTypeAs();
+ }
+
+ // We store the (MS) service provider in the global (BoDi) container, we create it only once.
+ // It must be lazy (hence factory) because at this point we still don't have the bindings mapped.
+ args.ObjectContainer.RegisterFactoryAs(() =>
+ {
+ var serviceCollectionFinder = args.ObjectContainer.Resolve();
+ var (services, scoping) = serviceCollectionFinder.GetServiceCollection();
+
+ RegisterProxyBindings(args.ObjectContainer, services);
+ return new RootServiceProviderContainer(services.BuildServiceProvider(), scoping);
+ });
+
+ args.ObjectContainer.RegisterFactoryAs(() =>
+ {
+ return args.ObjectContainer.Resolve().ServiceProvider;
+ });
+
+ // Will make sure DI scope is disposed.
+ var lcEvents = args.ObjectContainer.Resolve();
+ lcEvents.AfterScenario += AfterScenarioPluginLifecycleEventHandler;
+ lcEvents.AfterFeature += AfterFeaturePluginLifecycleEventHandler;
+ }
+ args.ObjectContainer.Resolve();
+ }
+ }
+
+ private static void CustomizeFeatureDependenciesEventHandler(object sender, CustomizeFeatureDependenciesEventArgs args)
+ {
+ // At this point we have the bindings, we can resolve the service provider, which will build it if it's the first time.
+ var spContainer = args.ObjectContainer.Resolve();
+
+ if (spContainer.Scoping == ScopeLevelType.Feature)
+ {
+ var serviceProvider = spContainer.ServiceProvider;
+
+ // Now we can register a new scoped service provider
+ args.ObjectContainer.RegisterFactoryAs(() =>
+ {
+ var scope = serviceProvider.CreateScope();
+ BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve());
+ ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve(), scope);
+ return scope.ServiceProvider;
+ });
+ }
+ }
+
+ private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs)
+ {
+ if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve(), out var serviceScope))
+ {
+ BindMappings.TryRemove(serviceScope.ServiceProvider, out _);
+ serviceScope.Dispose();
+ }
+ }
+
+ private static void CustomizeScenarioDependenciesEventHandler(object sender, CustomizeScenarioDependenciesEventArgs args)
+ {
+ // At this point we have the bindings, we can resolve the service provider, which will build it if it's the first time.
+ var spContainer = args.ObjectContainer.Resolve();
+
+ if (spContainer.Scoping == ScopeLevelType.Scenario)
+ {
+ var serviceProvider = spContainer.ServiceProvider;
+ // Now we can register a new scoped service provider
+ args.ObjectContainer.RegisterFactoryAs(() =>
+ {
+ var scope = serviceProvider.CreateScope();
+ BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve());
+ ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve(), scope);
+ return scope.ServiceProvider;
+ });
+ }
+ }
+
+ private static void AfterScenarioPluginLifecycleEventHandler(object sender, RuntimePluginAfterScenarioEventArgs eventArgs)
+ {
+ if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve(), out var serviceScope))
+ {
+ BindMappings.TryRemove(serviceScope.ServiceProvider, out _);
+ serviceScope.Dispose();
+ }
+ }
+
+ private static void RegisterProxyBindings(IObjectContainer objectContainer, IServiceCollection services)
+ {
+ // Required for DI of binding classes that want container injections
+ // While they can (and should) use the method params for injection, we can support it.
+ // Note that in Feature mode, one can't inject "ScenarioContext", this can only be done from method params.
+
+ // Bases on this: https://docs.specflow.org/projects/specflow/en/latest/Extend/Available-Containers-%26-Registrations.html
+ // Might need to add more...
+
+ services.AddSingleton(objectContainer);
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ // TODO: Use async version of the interface (IAsyncBindingInvoker)
+ //services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+ services.AddSingleton(sp => objectContainer.Resolve());
+
+ services.AddTransient(sp =>
+ {
+ var container = BindMappings.TryGetValue(sp, out var ctx)
+ ? ctx.ScenarioContext?.ScenarioContainer ??
+ ctx.FeatureContext?.FeatureContainer ??
+ ctx.TestThreadContext?.TestThreadContainer ??
+ objectContainer
+ : objectContainer;
+
+ return container.Resolve();
+ });
+
+ services.AddTransient(sp => BindMappings[sp]);
+ services.AddTransient(sp => BindMappings[sp].TestThreadContext);
+ services.AddTransient(sp => BindMappings[sp].FeatureContext);
+ services.AddTransient(sp => BindMappings[sp].ScenarioContext);
+ services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve());
+ services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve());
+ services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve());
+ services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve());
+ }
+
+ private class RootServiceProviderContainer
+ {
+ public IServiceProvider ServiceProvider { get; }
+ public ScopeLevelType Scoping { get; }
+
+ public RootServiceProviderContainer(IServiceProvider sp, ScopeLevelType scoping)
+ {
+ ServiceProvider = sp;
+ Scoping = scoping;
+ }
+ }
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionTestObjectResolver.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionTestObjectResolver.cs
new file mode 100644
index 000000000..b8b1e9697
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionTestObjectResolver.cs
@@ -0,0 +1,35 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.BoDi;
+using Reqnroll.Infrastructure;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public class DependencyInjectionTestObjectResolver : ITestObjectResolver
+ {
+ public object ResolveBindingInstance(Type bindingType, IObjectContainer container)
+ {
+ var registered = IsRegistered(container, bindingType);
+
+ return registered
+ ? container.Resolve(bindingType)
+ : container.Resolve().GetRequiredService(bindingType);
+ }
+
+ private bool IsRegistered(IObjectContainer container, Type type)
+ {
+ if (container.IsRegistered(type))
+ {
+ return true;
+ }
+
+ // IObjectContainer.IsRegistered is not recursive, it will only check the current container
+ if (container is ObjectContainer c && c.BaseContainer != null)
+ {
+ return IsRegistered(c.BaseContainer, type);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/IServiceCollectionFinder.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/IServiceCollectionFinder.cs
new file mode 100644
index 000000000..707b2c2f1
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/IServiceCollectionFinder.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public interface IServiceCollectionFinder
+ {
+ (IServiceCollection, ScopeLevelType) GetServiceCollection();
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs
new file mode 100644
index 000000000..581f75b0e
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ [Serializable]
+ public class MissingScenarioDependenciesException : ReqnrollException
+ {
+ public MissingScenarioDependenciesException()
+ : base("No method marked with [ScenarioDependencies] attribute found.")
+ {
+ HelpLink = "https://go.reqnroll.net/doc-msdi";
+ }
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.csproj b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.csproj
new file mode 100644
index 000000000..372e6918a
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.csproj
@@ -0,0 +1,33 @@
+
+
+ netstandard2.0
+ $(Reqnroll_KeyFile)
+ $(Reqnroll_EnableStrongNameSigning)
+ $(Reqnroll_PublicSign)
+
+ true
+ $(MSBuildThisFileDirectory)Reqnroll.Microsoft.Extensions.DependencyInjection.nuspec
+
+ true
+ true
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+ Reqnroll.Microsoft.Extensions.DependencyInjection
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.nuspec b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.nuspec
new file mode 100644
index 000000000..14110f9af
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/Reqnroll.Microsoft.Extensions.DependencyInjection.nuspec
@@ -0,0 +1,34 @@
+
+
+
+ Reqnroll.Microsoft.Extensions.DependencyInjection
+ $version$
+ Reqnroll Microsoft.Extensions.DependencyInjection integration plugin
+ Mark Hoek, Solid Token, $author$
+ Mark Hoek, Solid Token, $owner$
+ Reqnroll plugin that enables to use Microsoft.Extensions.DependencyInjection for resolving test dependencies.
+ Reqnroll plugin that enables to use Microsoft.Extensions.DependencyInjection for resolving test dependencies.
+ en-US
+ https://www.reqnroll.net
+ images\reqnroll-icon.png
+ Copyright © Mark Hoek, Solid Token, $author$
+ false
+ LICENSE
+ reqnroll microsoft extensions di dependency injection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ScenarioDependenciesAttribute.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ScenarioDependenciesAttribute.cs
new file mode 100644
index 000000000..156793148
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ScenarioDependenciesAttribute.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public enum ScopeLevelType
+ {
+ ///
+ /// Scoping is created for every Scenario and it is destroyed once the Scenario ends.
+ ///
+ Scenario,
+ ///
+ /// Scoping is created for every Feature and it is destroyed once the Feature ends.
+ ///
+ Feature
+ }
+
+ [AttributeUsage(AttributeTargets.Method)]
+ public class ScenarioDependenciesAttribute : Attribute
+ {
+ ///
+ /// Automatically register all SpecFlow bindings.
+ ///
+ public bool AutoRegisterBindings { get; set; } = true;
+
+ ///
+ /// Define when to create and destroy scope.
+ ///
+ public ScopeLevelType ScopeLevel { get; set; } = ScopeLevelType.Scenario;
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs
new file mode 100644
index 000000000..8d83080e9
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using Reqnroll.Bindings;
+
+namespace Reqnroll.Microsoft.Extensions.DependencyInjection
+{
+ public class ServiceCollectionFinder : IServiceCollectionFinder
+ {
+ private readonly IBindingRegistry bindingRegistry;
+ private (IServiceCollection, ScopeLevelType) _cache;
+
+ public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
+ {
+ this.bindingRegistry = bindingRegistry;
+ }
+
+ public (IServiceCollection, ScopeLevelType) GetServiceCollection()
+ {
+ if (_cache != default)
+ {
+ return _cache;
+ }
+
+ var assemblies = bindingRegistry.GetBindingAssemblies();
+ foreach (var assembly in assemblies)
+ {
+ foreach (var type in assembly.GetTypes())
+ {
+ foreach (var methodInfo in type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
+ {
+ var scenarioDependenciesAttribute = (ScenarioDependenciesAttribute)Attribute.GetCustomAttribute(methodInfo, typeof(ScenarioDependenciesAttribute));
+
+ if (scenarioDependenciesAttribute != null)
+ {
+ var serviceCollection = GetServiceCollection(methodInfo);
+ if (scenarioDependenciesAttribute.AutoRegisterBindings)
+ {
+ AddBindingAttributes(assemblies, serviceCollection);
+ }
+ return _cache = (serviceCollection, scenarioDependenciesAttribute.ScopeLevel);
+ }
+ }
+ }
+ }
+ throw new MissingScenarioDependenciesException();
+ }
+
+ private static IServiceCollection GetServiceCollection(MethodBase methodInfo)
+ {
+ return (IServiceCollection)methodInfo.Invoke(null, null);
+ }
+
+ private static void AddBindingAttributes(IEnumerable bindingAssemblies, IServiceCollection serviceCollection)
+ {
+ foreach (var assembly in bindingAssemblies)
+ {
+ foreach (var type in assembly.GetTypes().Where(t => Attribute.IsDefined(t, typeof(BindingAttribute))))
+ {
+ serviceCollection.AddScoped(type);
+ }
+ }
+ }
+ }
+}
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.props b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.props
new file mode 100644
index 000000000..f5309f5b2
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.props
@@ -0,0 +1,9 @@
+
+
+
+ %(Filename)%(Extension)
+ PreserveNewest
+ False
+
+
+
diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.targets b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.targets
new file mode 100644
index 000000000..dbe364e2b
--- /dev/null
+++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/build/Reqnroll.Microsoft.Extensions.DependencyInjection.targets
@@ -0,0 +1,6 @@
+
+
+ <_Reqnroll_MicrosoftExtensionsDependencyInjectionPluginFramework>netstandard2.0
+ <_Reqnroll_MicrosoftExtensionsDependencyInjectionPluginPath>$(MSBuildThisFileDirectory)\..\lib\$(_Reqnroll_MicrosoftExtensionsDependencyInjectionPluginFramework)\Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.dll
+
+
diff --git a/Reqnroll.sln b/Reqnroll.sln
index 4828a1c46..6aac1cdec 100644
--- a/Reqnroll.sln
+++ b/Reqnroll.sln
@@ -112,6 +112,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHubWorkflows", "GitHubWo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.SystemTests", "Tests\Reqnroll.SystemTests\Reqnroll.SystemTests.csproj", "{C658B37D-FD36-4868-9070-4EB452FAE526}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin", "Plugins\Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin\Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.csproj", "{6CBEB31D-5F98-460F-B193-578AEEA78CF9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -234,6 +236,10 @@ Global
{C658B37D-FD36-4868-9070-4EB452FAE526}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C658B37D-FD36-4868-9070-4EB452FAE526}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C658B37D-FD36-4868-9070-4EB452FAE526}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -270,6 +276,7 @@ Global
{C073A609-8A6A-4707-86B0-7BB32EF8ACEE} = {6070E0CF-FA21-4E82-8A22-3B638CA84525}
{B6B374C2-ABD8-4F3B-BBF4-505992309168} = {577A0375-1436-446C-802B-3C75C8CEF94F}
{C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437}
+ {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4}
diff --git a/Reqnroll/BoDi/IObjectContainer.cs b/Reqnroll/BoDi/IObjectContainer.cs
index d906e0638..578ea2cf1 100644
--- a/Reqnroll/BoDi/IObjectContainer.cs
+++ b/Reqnroll/BoDi/IObjectContainer.cs
@@ -109,17 +109,18 @@ public interface IObjectContainer : IDisposable
IEnumerable ResolveAll() where T : class;
///
- /// Determines whether the interface or type is registered.
+ /// Determines whether the interface or type is registered optionally with the specified name.
///
/// The interface or type.
+ /// The name or null.
/// true if the interface or type is registered; otherwise false.
- bool IsRegistered();
+ bool IsRegistered(string name = null);
///
/// Determines whether the interface or type is registered with the specified name.
///
- /// The interface or type.
+ /// The interface or type.
/// The name.
/// true if the interface or type is registered; otherwise false.
- bool IsRegistered(string name);
+ bool IsRegistered(Type type, string name = null);
}
diff --git a/Reqnroll/BoDi/ObjectContainer.cs b/Reqnroll/BoDi/ObjectContainer.cs
index 4e7ab8603..9f7df16c6 100644
--- a/Reqnroll/BoDi/ObjectContainer.cs
+++ b/Reqnroll/BoDi/ObjectContainer.cs
@@ -487,16 +487,11 @@ public IStrategyRegistration RegisterFactoryAs(Delegate factoryDelegate, Type in
return factoryRegistration;
}
- public bool IsRegistered()
- {
- return IsRegistered(null);
- }
+ public bool IsRegistered(string name = null) => IsRegistered(typeof(T), name);
- public bool IsRegistered(string name)
+ public bool IsRegistered(Type type, string name = null)
{
- Type typeToResolve = typeof(T);
-
- var keyToResolve = new RegistrationKey(typeToResolve, name);
+ var keyToResolve = new RegistrationKey(type, name);
return _registrations.ContainsKey(keyToResolve);
}
diff --git a/Tests/Reqnroll.PluginTests/Infrastructure/MicrosoftExtensionsDependencyInjectionTests.cs b/Tests/Reqnroll.PluginTests/Infrastructure/MicrosoftExtensionsDependencyInjectionTests.cs
new file mode 100644
index 000000000..5382cc49b
--- /dev/null
+++ b/Tests/Reqnroll.PluginTests/Infrastructure/MicrosoftExtensionsDependencyInjectionTests.cs
@@ -0,0 +1,22 @@
+using FluentAssertions;
+using Moq;
+using Reqnroll.Plugins;
+using Reqnroll.Tracing;
+using Xunit;
+
+namespace Reqnroll.PluginTests.Infrastructure
+{
+ public class MicrosoftExtensionsDependencyInjectionTests
+ {
+ [Fact]
+ public void LoadPlugin_MicrosoftExtensionsDependencyInjection_ShouldNotBeNull()
+ {
+ var loader = new RuntimePluginLoader(new PluginAssemblyLoader());
+ var listener = new Mock();
+
+ var plugin = loader.LoadPlugin("Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.dll", listener.Object, It.IsAny());
+
+ plugin.Should().NotBeNull();
+ }
+ }
+}
diff --git a/Tests/Reqnroll.PluginTests/Infrastructure/WindsorPluginTests.cs b/Tests/Reqnroll.PluginTests/Infrastructure/WindsorPluginTests.cs
index 3910edf0a..a829295b8 100644
--- a/Tests/Reqnroll.PluginTests/Infrastructure/WindsorPluginTests.cs
+++ b/Tests/Reqnroll.PluginTests/Infrastructure/WindsorPluginTests.cs
@@ -36,7 +36,7 @@ public void Resolutions_are_not_forwarded_to_Windsor_before_it_is_registered()
var objectContainer = new Mock();
var container = new Mock();
- objectContainer.Setup(x => x.IsRegistered()).Returns(false);
+ objectContainer.Setup(x => x.IsRegistered(null)).Returns(false);
objectContainer.Setup(x => x.Resolve()).Returns(container.Object);
resolver.ResolveBindingInstance(typeof(ITraceListener), objectContainer.Object);
@@ -52,7 +52,7 @@ public void Resolutions_are_forwarded_to_Windsor()
var objectContainer = new Mock();
var container = new Mock();
- objectContainer.Setup(x => x.IsRegistered()).Returns(true);
+ objectContainer.Setup(x => x.IsRegistered(null)).Returns(true);
objectContainer.Setup(x => x.Resolve()).Returns(container.Object);
resolver.ResolveBindingInstance(typeof(ITraceListener), objectContainer.Object);
diff --git a/Tests/Reqnroll.PluginTests/Reqnroll.PluginTests.csproj b/Tests/Reqnroll.PluginTests/Reqnroll.PluginTests.csproj
index d1b454626..9547d9520 100644
--- a/Tests/Reqnroll.PluginTests/Reqnroll.PluginTests.csproj
+++ b/Tests/Reqnroll.PluginTests/Reqnroll.PluginTests.csproj
@@ -10,6 +10,7 @@
+
diff --git a/docs/integrations/available-plugins.md b/docs/integrations/available-plugins.md
index e150fc0e6..cec3178c2 100644
--- a/docs/integrations/available-plugins.md
+++ b/docs/integrations/available-plugins.md
@@ -4,8 +4,9 @@
| Name | Description | Download |
|---|---|---|
-|[Reqnroll.Autofac](https://github.com/reqnroll/Reqnroll)|Reqnroll plugin for using Autofac as a dependency injection framework for step definitions. [Read more...](https://github.com/reqnroll/Reqnroll)|![](https://img.shields.io/nuget/v/Reqnroll.Autofac.svg)|
-|[Reqnroll.Windsor](https://github.com/reqnroll/Reqnroll)|Reqnroll plugin for using Castle Windsor as a dependency injection framework for step definitions. [Read more...](https://github.com/reqnroll/Reqnroll)|![](https://img.shields.io/nuget/v/Reqnroll.Windsor.svg)|
+|[Reqnroll.Autofac](https://github.com/reqnroll/Reqnroll)|Reqnroll plugin for using Autofac as a dependency injection framework for step definitions. [Read more...](autofac.md)|![](https://img.shields.io/nuget/v/Reqnroll.Autofac.svg)|
+|[Reqnroll.Windsor](https://github.com/reqnroll/Reqnroll)|Reqnroll plugin for using Castle Windsor as a dependency injection framework for step definitions. [Read more...](windsor.md)|![](https://img.shields.io/nuget/v/Reqnroll.Windsor.svg)|
+|[Reqnroll.Microsoft.Extensions.DependencyInjection](https://github.com/reqnroll/Reqnroll)|Reqnroll plugin for using Microsoft.Extensions.DependencyInjection as a dependency injection framework for step definitions. [Read more...](https://github.com/reqnroll/Reqnroll)|![](https://img.shields.io/nuget/v/Reqnroll.Microsoft.Extensions.DependencyInjection.svg)|
## Other Plugins
diff --git a/docs/integrations/dependency-injection.md b/docs/integrations/dependency-injection.md
new file mode 100644
index 000000000..cf69e5630
--- /dev/null
+++ b/docs/integrations/dependency-injection.md
@@ -0,0 +1,38 @@
+# Microsoft.Extensions.DependencyInjection
+
+## Introduction
+Reqnroll plugin for using Microsoft.Extensions.DependencyInjection as a dependency injection framework for step definitions.
+
+```{note}
+Currently supports Microsoft.Extensions.DependencyInjection v6.0.0 or above
+```
+
+## Step by step walkthrough of using Reqnroll.Microsoft.Extensions.DependencyInjection
+
+
+### 1. Install plugin from NuGet into your Reqnroll project.
+
+```csharp
+PM> Install-Package Reqnroll.Microsoft.Extensions.DependencyInjection
+```
+### 2. Create static methods somewhere in the Reqnroll project
+
+Create a static method in your SpecFlow project that returns a Microsoft.Extensions.DependencyInjection.IServiceCollection and tag it with the [ScenarioDependencies] attribute. Configure your test dependencies for the scenario execution within this method. Step definition classes (i.e. classes with the SpecFlow [Binding] attribute) are automatically added to the service collection.
+
+### 3. A typical dependency builder method looks like this:
+
+```csharp
+public class SetupTestDependencies
+{
+ [ScenarioDependencies]
+ public static IServiceCollection CreateServices()
+ {
+ var services = new ServiceCollection();
+
+ // TODO: add your test dependencies here
+ services.AddSingleton();
+
+ return services;
+ }
+}
+```
diff --git a/docs/integrations/index.md b/docs/integrations/index.md
index 5e1644ff3..773c43e82 100644
--- a/docs/integrations/index.md
+++ b/docs/integrations/index.md
@@ -7,8 +7,9 @@ This part contains details of the following topics.
available-plugins
autofac
-fsharp
+dependency-injection
windsor
+fsharp
externaldata
mstest
nunit