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

Commit cc72c8d

Browse files
authored
Fix missing BindMappings for ScenarioContext (#84)
Fixes: #68
1 parent 1258fb0 commit cc72c8d

File tree

6 files changed

+76
-39
lines changed

6 files changed

+76
-39
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Feature: ScenarioContextDisposal
2+
Issue #68: After ScenarioContext class is used in steps, disposal fails
3+
https://github.com/solidtoken/SpecFlow.DependencyInjection/issues/68
4+
5+
Scenario: Assert context is disposed correctly
6+
Given I have scenario context with number 7
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using TechTalk.SpecFlow;
2+
3+
namespace SolidToken.SpecFlow.DependencyInjection.Tests
4+
{
5+
[Binding]
6+
public class ScenarioContextDisposalSteps
7+
{
8+
private readonly ScenarioContext _context;
9+
10+
public ScenarioContextDisposalSteps(ScenarioContext context)
11+
{
12+
_context = context;
13+
}
14+
15+
[Given(@"I have scenario context with number (.*)")]
16+
public void GivenIHaveScenarioContextWithNumber(int number)
17+
{
18+
_context["number"] = number;
19+
//or
20+
//_context.Set(number);
21+
}
22+
}
23+
}

SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ namespace SolidToken.SpecFlow.DependencyInjection
1919
{
2020
public class DependencyInjectionPlugin : IRuntimePlugin
2121
{
22-
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMapping =
22+
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMappings =
2323
new ConcurrentDictionary<IServiceProvider, IContextManager>();
24-
24+
2525
private static readonly ConcurrentDictionary<ISpecFlowContext, IServiceScope> ActiveServiceScopes =
2626
new ConcurrentDictionary<ISpecFlowContext, IServiceScope>();
2727

2828
private readonly object _registrationLock = new object();
29-
29+
3030
public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration)
3131
{
3232
runtimePluginEvents.CustomizeGlobalDependencies += CustomizeGlobalDependencies;
@@ -46,7 +46,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc
4646
args.ObjectContainer.RegisterTypeAs<ServiceCollectionFinder, IServiceCollectionFinder>();
4747
}
4848

49-
// We store the service provider in the global container, we create it only once
49+
// We store the (MS) service provider in the global (BoDi) container, we create it only once.
5050
// It must be lazy (hence factory) because at this point we still don't have the bindings mapped.
5151
args.ObjectContainer.RegisterFactoryAs<RootServiceProviderContainer>(() =>
5252
{
@@ -70,7 +70,7 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc
7070
args.ObjectContainer.Resolve<IServiceCollectionFinder>();
7171
}
7272
}
73-
73+
7474
private static void CustomizeFeatureDependenciesEventHandler(object sender, CustomizeFeatureDependenciesEventArgs args)
7575
{
7676
// 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
8484
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
8585
{
8686
var scope = serviceProvider.CreateScope();
87-
BindMapping.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
87+
BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
8888
ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve<FeatureContext>(), scope);
8989
return scope.ServiceProvider;
9090
});
9191
}
9292
}
9393

94+
private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs)
95+
{
96+
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<FeatureContext>(), out var serviceScope))
97+
{
98+
BindMappings.TryRemove(serviceScope.ServiceProvider, out _);
99+
serviceScope.Dispose();
100+
}
101+
}
102+
94103
private static void CustomizeScenarioDependenciesEventHandler(object sender, CustomizeScenarioDependenciesEventArgs args)
95104
{
96105
// 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
103112
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
104113
{
105114
var scope = serviceProvider.CreateScope();
115+
BindMappings.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
106116
ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve<ScenarioContext>(), scope);
107117
return scope.ServiceProvider;
108118
});
109119
}
110120
}
111-
121+
112122
private static void AfterScenarioPluginLifecycleEventHandler(object sender, RuntimePluginAfterScenarioEventArgs eventArgs)
113123
{
114124
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<ScenarioContext>(), out var serviceScope))
115125
{
116-
BindMapping.TryRemove(serviceScope.ServiceProvider, out _);
117-
serviceScope.Dispose();
118-
}
119-
}
120-
121-
private static void AfterFeaturePluginLifecycleEventHandler(object sender, RuntimePluginAfterFeatureEventArgs eventArgs)
122-
{
123-
if (ActiveServiceScopes.TryRemove(eventArgs.ObjectContainer.Resolve<FeatureContext>(), out var serviceScope))
124-
{
125-
BindMapping.TryRemove(serviceScope.ServiceProvider, out _);
126+
BindMappings.TryRemove(serviceScope.ServiceProvider, out _);
126127
serviceScope.Dispose();
127128
}
128129
}
@@ -132,7 +133,7 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer
132133
// Required for DI of binding classes that want container injections
133134
// While they can (and should) use the method params for injection, we can support it.
134135
// Note that in Feature mode, one can't inject "ScenarioContext", this can only be done from method params.
135-
136+
136137
// Bases on this: https://docs.specflow.org/projects/specflow/en/latest/Extend/Available-Containers-%26-Registrations.html
137138
// Might need to add more...
138139

@@ -157,7 +158,7 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer
157158

158159
services.AddTransient(sp =>
159160
{
160-
var container = BindMapping.TryGetValue(sp, out var ctx)
161+
var container = BindMappings.TryGetValue(sp, out var ctx)
161162
? ctx.ScenarioContext?.ScenarioContainer ??
162163
ctx.FeatureContext?.FeatureContainer ??
163164
ctx.TestThreadContext?.TestThreadContainer ??
@@ -166,15 +167,15 @@ private static void RegisterProxyBindings(IObjectContainer objectContainer, ISer
166167

167168
return container.Resolve<ISpecFlowOutputHelper>();
168169
});
169-
170-
services.AddTransient(sp => BindMapping[sp]);
171-
services.AddTransient(sp => BindMapping[sp].TestThreadContext);
172-
services.AddTransient(sp => BindMapping[sp].FeatureContext);
173-
services.AddTransient(sp => BindMapping[sp].ScenarioContext);
174-
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<ITestRunner>());
175-
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<ITestExecutionEngine>());
176-
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<IStepArgumentTypeConverter>());
177-
services.AddTransient(sp => BindMapping[sp].TestThreadContext.TestThreadContainer.Resolve<IStepDefinitionMatchService>());
170+
171+
services.AddTransient(sp => BindMappings[sp]);
172+
services.AddTransient(sp => BindMappings[sp].TestThreadContext);
173+
services.AddTransient(sp => BindMappings[sp].FeatureContext);
174+
services.AddTransient(sp => BindMappings[sp].ScenarioContext);
175+
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<ITestRunner>());
176+
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<ITestExecutionEngine>());
177+
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<IStepArgumentTypeConverter>());
178+
services.AddTransient(sp => BindMappings[sp].TestThreadContext.TestThreadContainer.Resolve<IStepDefinitionMatchService>());
178179
}
179180

180181
private class RootServiceProviderContainer

SpecFlow.DependencyInjection/DependencyInjectionTestObjectResolver.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public class DependencyInjectionTestObjectResolver : ITestObjectResolver
2727
public object ResolveBindingInstance(Type bindingType, IObjectContainer container)
2828
{
2929
// Can remove if IsRegistered(Type type) exists
30-
var mi = IsRegisteredMethodInfoCache.GetOrAdd(bindingType, CreateGenericMethodInfo);
31-
var registered = (bool) mi.Invoke(this, new object[] { container });
30+
var methodInfo = IsRegisteredMethodInfoCache.GetOrAdd(bindingType, CreateGenericMethodInfo);
31+
var registered = (bool)methodInfo.Invoke(this, new object[] { container });
3232
// var registered = container.IsRegistered(bindingType);
33-
33+
3434
return registered
3535
? container.Resolve(bindingType)
3636
: container.Resolve<IServiceProvider>().GetRequiredService(bindingType);
@@ -39,11 +39,16 @@ public object ResolveBindingInstance(Type bindingType, IObjectContainer containe
3939
public bool IsRegistered<T>(IObjectContainer container)
4040
{
4141
if (container.IsRegistered<T>())
42+
{
4243
return true;
43-
44+
}
45+
4446
// IsRegistered is not recursive, it will only check the current container
4547
if (container is ObjectContainer c && c.BaseContainer != null)
48+
{
4649
return IsRegistered<T>(c.BaseContainer);
50+
}
51+
4752
return false;
4853
}
4954
}

SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@ namespace SolidToken.SpecFlow.DependencyInjection
55
public enum ScopeLevelType
66
{
77
/// <summary>
8-
/// Scoping is created for every scenario and it is destroyed once the scenario ends.
8+
/// Scoping is created for every Scenario and it is destroyed once the Scenario ends.
99
/// </summary>
1010
Scenario,
1111
/// <summary>
12-
/// Scoping is created for Feature scenario and it is destroyed once the Feature ends.
12+
/// Scoping is created for every Feature and it is destroyed once the Feature ends.
1313
/// </summary>
1414
Feature
1515
}
16-
16+
1717
[AttributeUsage(AttributeTargets.Method)]
1818
public class ScenarioDependenciesAttribute : Attribute
1919
{
2020
/// <summary>
2121
/// Automatically register all SpecFlow bindings.
2222
/// </summary>
2323
public bool AutoRegisterBindings { get; set; } = true;
24+
2425
/// <summary>
2526
/// Define when to create and destroy scope.
2627
/// </summary>

SpecFlow.DependencyInjection/ServiceCollectionFinder.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class ServiceCollectionFinder : IServiceCollectionFinder
1212
{
1313
private readonly IBindingRegistry bindingRegistry;
1414
private (IServiceCollection, ScopeLevelType) _cache;
15-
15+
1616
public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
1717
{
1818
this.bindingRegistry = bindingRegistry;
@@ -21,8 +21,10 @@ public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
2121
public (IServiceCollection, ScopeLevelType) GetServiceCollection()
2222
{
2323
if (_cache != default)
24+
{
2425
return _cache;
25-
26+
}
27+
2628
var assemblies = bindingRegistry.GetBindingAssemblies();
2729
foreach (var assembly in assemblies)
2830
{
@@ -47,15 +49,14 @@ public ServiceCollectionFinder(IBindingRegistry bindingRegistry)
4749
throw new MissingScenarioDependenciesException();
4850
}
4951

50-
5152
private static IServiceCollection GetServiceCollection(MethodBase methodInfo)
5253
{
5354
return (IServiceCollection)methodInfo.Invoke(null, null);
5455
}
5556

5657
private static void AddBindingAttributes(IEnumerable<Assembly> bindingAssemblies, IServiceCollection serviceCollection)
5758
{
58-
foreach(var assembly in bindingAssemblies)
59+
foreach (var assembly in bindingAssemblies)
5960
{
6061
foreach (var type in assembly.GetTypes().Where(t => Attribute.IsDefined(t, typeof(BindingAttribute))))
6162
{

0 commit comments

Comments
 (0)