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