From c46347b15e66ac7981ebaea98a4cc6868b2d99a6 Mon Sep 17 00:00:00 2001 From: Mark Hoek Date: Fri, 3 Sep 2021 11:30:04 +0200 Subject: [PATCH 1/2] Add test to reproduce issue #68 --- .../ScenarioContextDisposal.feature | 6 +++++ .../ScenarioContextDisposalSteps.cs | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 SpecFlow.DependencyInjection.Tests/ScenarioContextDisposal.feature create mode 100644 SpecFlow.DependencyInjection.Tests/ScenarioContextDisposalSteps.cs diff --git a/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposal.feature b/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposal.feature new file mode 100644 index 0000000..d7138de --- /dev/null +++ b/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposal.feature @@ -0,0 +1,6 @@ +Feature: ScenarioContextDisposal + Issue #68: After ScenarioContext class is used in steps, disposal fails + https://github.com/solidtoken/SpecFlow.DependencyInjection/issues/68 + +Scenario: Assert context is disposed correctly + Given I have scenario context with number 7 diff --git a/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposalSteps.cs b/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposalSteps.cs new file mode 100644 index 0000000..2511e34 --- /dev/null +++ b/SpecFlow.DependencyInjection.Tests/ScenarioContextDisposalSteps.cs @@ -0,0 +1,23 @@ +using TechTalk.SpecFlow; + +namespace SolidToken.SpecFlow.DependencyInjection.Tests +{ + [Binding] + public class ScenarioContextDisposalSteps + { + private readonly ScenarioContext _context; + + public ScenarioContextDisposalSteps(ScenarioContext context) + { + _context = context; + } + + [Given(@"I have scenario context with number (.*)")] + public void GivenIHaveScenarioContextWithNumber(int number) + { + _context["number"] = number; + //or + //_context.Set(number); + } + } +} From 8bdf084de5c8fd9b25df397bf19764d85173c0f6 Mon Sep 17 00:00:00 2001 From: Mark Hoek Date: Sat, 7 May 2022 14:13:37 +0200 Subject: [PATCH 2/2] Fix missing BindMappings for ScenarioContext Fixes: #68 --- .../DependencyInjectionPlugin.cs | 57 ++++++++++--------- .../DependencyInjectionTestObjectResolver.cs | 13 +++-- .../ScenarioDependenciesAttribute.cs | 7 ++- .../ServiceCollectionFinder.cs | 9 +-- 4 files changed, 47 insertions(+), 39 deletions(-) diff --git a/SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs b/SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs index 3b8fb9d..a921fb5 100644 --- a/SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs +++ b/SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs @@ -19,14 +19,14 @@ namespace SolidToken.SpecFlow.DependencyInjection { public class DependencyInjectionPlugin : IRuntimePlugin { - private static readonly ConcurrentDictionary BindMapping = + 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; @@ -46,7 +46,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc args.ObjectContainer.RegisterTypeAs(); } - // We store the service provider in the global container, we create it only once + // 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(() => { @@ -70,7 +70,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc 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. @@ -84,13 +84,22 @@ private static void CustomizeFeatureDependenciesEventHandler(object sender, Cust args.ObjectContainer.RegisterFactoryAs(() => { var scope = serviceProvider.CreateScope(); - BindMapping.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve()); + 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. @@ -103,26 +112,18 @@ private static void CustomizeScenarioDependenciesEventHandler(object sender, Cus 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)) { - BindMapping.TryRemove(serviceScope.ServiceProvider, out _); - serviceScope.Dispose(); - } - } - - private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs) - { - if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve(), out var serviceScope)) - { - BindMapping.TryRemove(serviceScope.ServiceProvider, out _); + BindMappings.TryRemove(serviceScope.ServiceProvider, out _); serviceScope.Dispose(); } } @@ -132,7 +133,7 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer // 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... @@ -157,7 +158,7 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer services.AddTransient(sp => { - var container = BindMapping.TryGetValue(sp, out var ctx) + var container = BindMappings.TryGetValue(sp, out var ctx) ? ctx.ScenarioContext?.ScenarioContainer ?? ctx.FeatureContext?.FeatureContainer ?? ctx.TestThreadContext?.TestThreadContainer ?? @@ -166,15 +167,15 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer return container.Resolve(); }); - - services.AddTransient(sp => BindMapping[sp]); - services.AddTransient(sp => BindMapping[sp].TestThreadContext); - services.AddTransient(sp => BindMapping[sp].FeatureContext); - services.AddTransient(sp => BindMapping[sp].ScenarioContext); - services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve()); - services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve()); - services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve()); - services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.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 diff --git a/SpecFlow.DependencyInjection/DependencyInjectionTestObjectResolver.cs b/SpecFlow.DependencyInjection/DependencyInjectionTestObjectResolver.cs index 1261870..7dbef35 100644 --- a/SpecFlow.DependencyInjection/DependencyInjectionTestObjectResolver.cs +++ b/SpecFlow.DependencyInjection/DependencyInjectionTestObjectResolver.cs @@ -27,10 +27,10 @@ public class DependencyInjectionTestObjectResolver : ITestObjectResolver public object ResolveBindingInstance(Type bindingType, IObjectContainer container) { // Can remove if IsRegistered(Type type) exists - var mi = IsRegisteredMethodInfoCache.GetOrAdd(bindingType, CreateGenericMethodInfo); - var registered = (bool) mi.Invoke(this, new object[] { container }); + var methodInfo = IsRegisteredMethodInfoCache.GetOrAdd(bindingType, CreateGenericMethodInfo); + var registered = (bool)methodInfo.Invoke(this, new object[] { container }); // var registered = container.IsRegistered(bindingType); - + return registered ? container.Resolve(bindingType) : container.Resolve().GetRequiredService(bindingType); @@ -39,11 +39,16 @@ public object ResolveBindingInstance(Type bindingType, IObjectContainer containe public bool IsRegistered(IObjectContainer container) { if (container.IsRegistered()) + { return true; - + } + // IsRegistered is not recursive, it will only check the current container if (container is ObjectContainer c && c.BaseContainer != null) + { return IsRegistered(c.BaseContainer); + } + return false; } } diff --git a/SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs b/SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs index 0ffa03a..a7cc032 100644 --- a/SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs +++ b/SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs @@ -5,15 +5,15 @@ namespace SolidToken.SpecFlow.DependencyInjection public enum ScopeLevelType { /// - /// Scoping is created for every scenario and it is destroyed once the scenario ends. + /// Scoping is created for every Scenario and it is destroyed once the Scenario ends. /// Scenario, /// - /// Scoping is created for Feature scenario and it is destroyed once the Feature ends. + /// Scoping is created for every Feature and it is destroyed once the Feature ends. /// Feature } - + [AttributeUsage(AttributeTargets.Method)] public class ScenarioDependenciesAttribute : Attribute { @@ -21,6 +21,7 @@ public class ScenarioDependenciesAttribute : Attribute /// Automatically register all SpecFlow bindings. /// public bool AutoRegisterBindings { get; set; } = true; + /// /// Define when to create and destroy scope. /// diff --git a/SpecFlow.DependencyInjection/ServiceCollectionFinder.cs b/SpecFlow.DependencyInjection/ServiceCollectionFinder.cs index 84e1086..1467b25 100644 --- a/SpecFlow.DependencyInjection/ServiceCollectionFinder.cs +++ b/SpecFlow.DependencyInjection/ServiceCollectionFinder.cs @@ -12,7 +12,7 @@ public class ServiceCollectionFinder : IServiceCollectionFinder { private readonly IBindingRegistry bindingRegistry; private (IServiceCollection, ScopeLevelType) _cache; - + public ServiceCollectionFinder(IBindingRegistry bindingRegistry) { this.bindingRegistry = bindingRegistry; @@ -21,8 +21,10 @@ public ServiceCollectionFinder(IBindingRegistry bindingRegistry) public (IServiceCollection, ScopeLevelType) GetServiceCollection() { if (_cache != default) + { return _cache; - + } + var assemblies = bindingRegistry.GetBindingAssemblies(); foreach (var assembly in assemblies) { @@ -47,7 +49,6 @@ public ServiceCollectionFinder(IBindingRegistry bindingRegistry) throw new MissingScenarioDependenciesException(); } - private static IServiceCollection GetServiceCollection(MethodBase methodInfo) { return (IServiceCollection)methodInfo.Invoke(null, null); @@ -55,7 +56,7 @@ private static IServiceCollection GetServiceCollection(MethodBase methodInfo) private static void AddBindingAttributes(IEnumerable bindingAssemblies, IServiceCollection serviceCollection) { - foreach(var assembly in bindingAssemblies) + foreach (var assembly in bindingAssemblies) { foreach (var type in assembly.GetTypes().Where(t => Attribute.IsDefined(t, typeof(BindingAttribute)))) {