Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Fix missing BindMappings for ScenarioContext #84

Merged
merged 2 commits into from
May 7, 2022
Merged
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
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions SpecFlow.DependencyInjection.Tests/ScenarioContextDisposalSteps.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
57 changes: 29 additions & 28 deletions SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ namespace SolidToken.SpecFlow.DependencyInjection
{
public class DependencyInjectionPlugin : IRuntimePlugin
{
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMapping =
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMappings =
new ConcurrentDictionary<IServiceProvider, IContextManager>();

private static readonly ConcurrentDictionary<ISpecFlowContext, IServiceScope> ActiveServiceScopes =
new ConcurrentDictionary<ISpecFlowContext, IServiceScope>();

private readonly object _registrationLock = new object();

public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration)
{
runtimePluginEvents.CustomizeGlobalDependencies += CustomizeGlobalDependencies;
Expand All @@ -46,7 +46,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc
args.ObjectContainer.RegisterTypeAs<ServiceCollectionFinder, IServiceCollectionFinder>();
}

// 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<RootServiceProviderContainer>(() =>
{
Expand All @@ -70,7 +70,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc
args.ObjectContainer.Resolve<IServiceCollectionFinder>();
}
}

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.
Expand All @@ -84,13 +84,22 @@ private static void CustomizeFeatureDependenciesEventHandler(object sender, Cust
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
{
var scope = serviceProvider.CreateScope();
BindMapping.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve<FeatureContext>(), scope);
return scope.ServiceProvider;
});
}
}

private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs)
{
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<FeatureContext>(), 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.
Expand All @@ -103,26 +112,18 @@ private static void CustomizeScenarioDependenciesEventHandler(object sender, Cus
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
{
var scope = serviceProvider.CreateScope();
BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve<ScenarioContext>(), scope);
return scope.ServiceProvider;
});
}
}

private static void AfterScenarioPluginLifecycleEventHandler(object sender, RuntimePluginAfterScenarioEventArgs eventArgs)
{
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<ScenarioContext>(), out var serviceScope))
{
BindMapping.TryRemove(serviceScope.ServiceProvider, out _);
serviceScope.Dispose();
}
}

private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs)
{
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<FeatureContext>(), out var serviceScope))
{
BindMapping.TryRemove(serviceScope.ServiceProvider, out _);
BindMappings.TryRemove(serviceScope.ServiceProvider, out _);
serviceScope.Dispose();
}
}
Expand All @@ -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...

Expand All @@ -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 ??
Expand All @@ -166,15 +167,15 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer

return container.Resolve<ISpecFlowOutputHelper>();
});
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<ITestRunner>());
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<ITestExecutionEngine>());
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<IStepArgumentTypeConverter>());
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<IStepDefinitionMatchService>());

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<ITestRunner>());
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<ITestExecutionEngine>());
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<IStepArgumentTypeConverter>());
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<IStepDefinitionMatchService>());
}

private class RootServiceProviderContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IServiceProvider>().GetRequiredService(bindingType);
Expand All @@ -39,11 +39,16 @@ public object ResolveBindingInstance(Type bindingType, IObjectContainer containe
public bool IsRegistered<T>(IObjectContainer container)
{
if (container.IsRegistered<T>())
{
return true;

}

// IsRegistered is not recursive, it will only check the current container
if (container is ObjectContainer c && c.BaseContainer != null)
{
return IsRegistered<T>(c.BaseContainer);
}

return false;
}
}
Expand Down
7 changes: 4 additions & 3 deletions SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ namespace SolidToken.SpecFlow.DependencyInjection
public enum ScopeLevelType
{
/// <summary>
/// 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.
/// </summary>
Scenario,
/// <summary>
/// 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.
/// </summary>
Feature
}

[AttributeUsage(AttributeTargets.Method)]
public class ScenarioDependenciesAttribute : Attribute
{
/// <summary>
/// Automatically register all SpecFlow bindings.
/// </summary>
public bool AutoRegisterBindings { get; set; } = true;

/// <summary>
/// Define when to create and destroy scope.
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions SpecFlow.DependencyInjection/ServiceCollectionFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class ServiceCollectionFinder : IServiceCollectionFinder
{
private readonly IBindingRegistry bindingRegistry;
private (IServiceCollection, ScopeLevelType) _cache;

public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
{
this.bindingRegistry = bindingRegistry;
Expand All @@ -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)
{
Expand All @@ -47,15 +49,14 @@ public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
throw new MissingScenarioDependenciesException();
}


private static IServiceCollection GetServiceCollection(MethodBase methodInfo)
{
return (IServiceCollection)methodInfo.Invoke(null, null);
}

private static void AddBindingAttributes(IEnumerable<Assembly> 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))))
{
Expand Down