11using System ;
2+ using System . Collections . Concurrent ;
23using BoDi ;
34using Microsoft . Extensions . DependencyInjection ;
45using TechTalk . SpecFlow ;
6+ using TechTalk . SpecFlow . Bindings ;
7+ using TechTalk . SpecFlow . Bindings . Discovery ;
8+ using TechTalk . SpecFlow . BindingSkeletons ;
9+ using TechTalk . SpecFlow . Configuration ;
10+ using TechTalk . SpecFlow . ErrorHandling ;
511using TechTalk . SpecFlow . Infrastructure ;
612using TechTalk . SpecFlow . Plugins ;
13+ using TechTalk . SpecFlow . Tracing ;
714using TechTalk . SpecFlow . UnitTestProvider ;
815
916[ assembly: RuntimePlugin ( typeof ( SolidToken . SpecFlow . DependencyInjection . DependencyInjectionPlugin ) ) ]
@@ -12,113 +19,174 @@ namespace SolidToken.SpecFlow.DependencyInjection
1219{
1320 public class DependencyInjectionPlugin : IRuntimePlugin
1421 {
15- private readonly object registrationLock = new object ( ) ;
16-
22+ private static readonly ConcurrentDictionary < IServiceProvider , IContextManager > BindMapping =
23+ new ConcurrentDictionary < IServiceProvider , IContextManager > ( ) ;
24+
25+ private static readonly ConcurrentDictionary < ISpecFlowContext , IServiceScope > ActiveServiceScopes =
26+ new ConcurrentDictionary < ISpecFlowContext , IServiceScope > ( ) ;
27+
28+ private readonly object _registrationLock = new object ( ) ;
29+
1730 public void Initialize ( RuntimePluginEvents runtimePluginEvents , RuntimePluginParameters runtimePluginParameters , UnitTestProviderConfiguration unitTestProviderConfiguration )
1831 {
19- runtimePluginEvents . CustomizeGlobalDependencies += ( sender , args ) =>
32+ runtimePluginEvents . CustomizeGlobalDependencies += CustomizeGlobalDependencies ;
33+ runtimePluginEvents . CustomizeFeatureDependencies += CustomizeFeatureDependenciesEventHandler ;
34+ runtimePluginEvents . CustomizeScenarioDependencies += CustomizeScenarioDependenciesEventHandler ;
35+ }
36+
37+ private void CustomizeGlobalDependencies ( object sender , CustomizeGlobalDependenciesEventArgs args )
38+ {
39+ if ( ! args . ObjectContainer . IsRegistered < IServiceCollectionFinder > ( ) )
2040 {
21- if ( ! args . ObjectContainer . IsRegistered < IServiceCollectionFinder > ( ) )
41+ lock ( _registrationLock )
2242 {
23- lock ( registrationLock )
43+ if ( ! args . ObjectContainer . IsRegistered < IServiceCollectionFinder > ( ) )
2444 {
25- if ( ! args . ObjectContainer . IsRegistered < IServiceCollectionFinder > ( ) )
26- {
27- args . ObjectContainer . RegisterTypeAs < DependencyInjectionTestObjectResolver , ITestObjectResolver > ( ) ;
28- args . ObjectContainer . RegisterTypeAs < ServiceCollectionFinder , IServiceCollectionFinder > ( ) ;
29- }
45+ args . ObjectContainer . RegisterTypeAs < DependencyInjectionTestObjectResolver , ITestObjectResolver > ( ) ;
46+ args . ObjectContainer . RegisterTypeAs < ServiceCollectionFinder , IServiceCollectionFinder > ( ) ;
3047 }
31- args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
32- }
33- } ;
3448
35- runtimePluginEvents . CustomizeScenarioDependencies += ( sender , args ) =>
36- {
37- args . ObjectContainer . RegisterFactoryAs < IServiceProvider > ( ( ) =>
38- {
39- var serviceCollectionFinder = args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
40- var createScenarioServiceCollection = serviceCollectionFinder . GetCreateScenarioServiceCollection ( ) ;
41- var services = createScenarioServiceCollection ( ) ;
49+ // We store the service provider in the global container, we create it only once
50+ // It must be lazy (hence factory) because at this point we still don't have the bindings mapped.
51+ args . ObjectContainer . RegisterFactoryAs < RootServiceProviderContainer > ( ( ) =>
52+ {
53+ var serviceCollectionFinder = args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
54+ var ( services , scoping ) = serviceCollectionFinder . GetServiceCollection ( ) ;
4255
43- RegisterObjectContainer ( args . ObjectContainer , services ) ;
44- RegisterScenarioSpecFlowDependencies ( services ) ;
45- RegisterFeatureSpecFlowDependencies ( services ) ;
46- RegisterTestThreadSpecFlowDependencies ( services ) ;
56+ RegisterProxyBindings ( args . ObjectContainer , services ) ;
57+ return new RootServiceProviderContainer ( services . BuildServiceProvider ( ) , scoping ) ;
58+ } ) ;
4759
48- return services . BuildServiceProvider ( ) ;
49- } ) ;
50- } ;
60+ args . ObjectContainer . RegisterFactoryAs < IServiceProvider > ( ( ) =>
61+ {
62+ return args . ObjectContainer . Resolve < RootServiceProviderContainer > ( ) . ServiceProvider ;
63+ } ) ;
64+
65+ // Will make sure DI scope is disposed.
66+ var lcEvents = args . ObjectContainer . Resolve < RuntimePluginTestExecutionLifecycleEvents > ( ) ;
67+ lcEvents . AfterScenario += AfterScenarioPluginLifecycleEventHandler ;
68+ lcEvents . AfterFeature += AfterFeaturePluginLifecycleEventHandler ;
69+ }
70+ args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
71+ }
72+ }
73+
74+ private static void CustomizeFeatureDependenciesEventHandler ( object sender , CustomizeFeatureDependenciesEventArgs args )
75+ {
76+ // At this point we have the bindings, we can resolve the service provider, which will build it if it's the first time.
77+ var spContainer = args . ObjectContainer . Resolve < RootServiceProviderContainer > ( ) ;
5178
52- runtimePluginEvents . CustomizeFeatureDependencies += ( sender , args ) =>
79+ if ( spContainer . Scoping == ScopeLevelType . Feature )
5380 {
81+ var serviceProvider = spContainer . ServiceProvider ;
82+
83+ // Now we can register a new scoped service provider
5484 args . ObjectContainer . RegisterFactoryAs < IServiceProvider > ( ( ) =>
5585 {
56- var serviceCollectionFinder = args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
57- var createScenarioServiceCollection = serviceCollectionFinder . GetCreateScenarioServiceCollection ( ) ;
58- var services = createScenarioServiceCollection ( ) ;
59-
60- RegisterObjectContainer ( args . ObjectContainer , services ) ;
61- RegisterFeatureSpecFlowDependencies ( services ) ;
62- RegisterTestThreadSpecFlowDependencies ( services ) ;
63-
64- return services . BuildServiceProvider ( ) ;
86+ var scope = serviceProvider . CreateScope ( ) ;
87+ BindMapping . TryAdd ( scope . ServiceProvider , args . ObjectContainer . Resolve < IContextManager > ( ) ) ;
88+ ActiveServiceScopes . TryAdd ( args . ObjectContainer . Resolve < FeatureContext > ( ) , scope ) ;
89+ return scope . ServiceProvider ;
6590 } ) ;
66- } ;
91+ }
92+ }
6793
68- runtimePluginEvents . CustomizeTestThreadDependencies += ( sender , args ) =>
94+ private static void CustomizeScenarioDependenciesEventHandler ( object sender , CustomizeScenarioDependenciesEventArgs args )
95+ {
96+ // At this point we have the bindings, we can resolve the service provider, which will build it if it's the first time.
97+ var spContainer = args . ObjectContainer . Resolve < RootServiceProviderContainer > ( ) ;
98+
99+ if ( spContainer . Scoping == ScopeLevelType . Scenario )
69100 {
101+ var serviceProvider = spContainer . ServiceProvider ;
102+ // Now we can register a new scoped service provider
70103 args . ObjectContainer . RegisterFactoryAs < IServiceProvider > ( ( ) =>
71104 {
72- var serviceCollectionFinder = args . ObjectContainer . Resolve < IServiceCollectionFinder > ( ) ;
73- var createScenarioServiceCollection = serviceCollectionFinder . GetCreateScenarioServiceCollection ( ) ;
74- var services = createScenarioServiceCollection ( ) ;
75-
76- RegisterObjectContainer ( args . ObjectContainer , services ) ;
77- RegisterTestThreadSpecFlowDependencies ( services ) ;
78-
79- return services . BuildServiceProvider ( ) ;
105+ var scope = serviceProvider . CreateScope ( ) ;
106+ ActiveServiceScopes . TryAdd ( args . ObjectContainer . Resolve < ScenarioContext > ( ) , scope ) ;
107+ return scope . ServiceProvider ;
80108 } ) ;
81- } ;
109+ }
82110 }
83-
84- private static void RegisterObjectContainer (
85- IObjectContainer objectContainer ,
86- IServiceCollection services )
111+
112+ private static void AfterScenarioPluginLifecycleEventHandler ( object sender , RuntimePluginAfterScenarioEventArgs eventArgs )
87113 {
88- services . AddTransient < IObjectContainer > ( ctx => objectContainer ) ;
114+ if ( ActiveServiceScopes . TryRemove ( eventArgs . ObjectContainer . Resolve < ScenarioContext > ( ) , out var serviceScope ) )
115+ {
116+ BindMapping . TryRemove ( serviceScope . ServiceProvider , out _ ) ;
117+ serviceScope . Dispose ( ) ;
118+ }
89119 }
90-
91- private static void RegisterScenarioSpecFlowDependencies (
92- IServiceCollection services )
120+
121+ private static void AfterFeaturePluginLifecycleEventHandler ( object sender , RuntimePluginAfterFeatureEventArgs eventArgs )
93122 {
94- services . AddTransient < ScenarioContext > ( ctx =>
123+ if ( ActiveServiceScopes . TryRemove ( eventArgs . ObjectContainer . Resolve < FeatureContext > ( ) , out var serviceScope ) )
95124 {
96- var specflowContainer = ctx . GetService < IObjectContainer > ( ) ;
97- var scenarioContext = specflowContainer . Resolve < ScenarioContext > ( ) ;
98- return scenarioContext ;
99- } ) ;
125+ BindMapping . TryRemove ( serviceScope . ServiceProvider , out _ ) ;
126+ serviceScope . Dispose ( ) ;
127+ }
100128 }
101129
102- private static void RegisterFeatureSpecFlowDependencies (
103- IServiceCollection services )
130+ private static void RegisterProxyBindings ( IObjectContainer objectContainer , IServiceCollection services )
104131 {
105- services . AddTransient < FeatureContext > ( ctx =>
132+ // Required for DI of binding classes that want container injections
133+ // While they can (and should) use the method params for injection, we can support it.
134+ // Note that in Feature mode, one can't inject "ScenarioContext", this can only be done from method params.
135+
136+ // Bases on this: https://docs.specflow.org/projects/specflow/en/latest/Extend/Available-Containers-%26-Registrations.html
137+ // Might need to add more...
138+
139+ services . AddSingleton < IObjectContainer > ( objectContainer ) ;
140+ services . AddSingleton ( sp => objectContainer . Resolve < IRuntimeConfigurationProvider > ( ) ) ;
141+ services . AddSingleton ( sp => objectContainer . Resolve < ITestRunnerManager > ( ) ) ;
142+ services . AddSingleton ( sp => objectContainer . Resolve < IStepFormatter > ( ) ) ;
143+ services . AddSingleton ( sp => objectContainer . Resolve < ITestTracer > ( ) ) ;
144+ services . AddSingleton ( sp => objectContainer . Resolve < ITraceListener > ( ) ) ;
145+ services . AddSingleton ( sp => objectContainer . Resolve < ITraceListenerQueue > ( ) ) ;
146+ services . AddSingleton ( sp => objectContainer . Resolve < IErrorProvider > ( ) ) ;
147+ services . AddSingleton ( sp => objectContainer . Resolve < IRuntimeBindingSourceProcessor > ( ) ) ;
148+ services . AddSingleton ( sp => objectContainer . Resolve < IBindingRegistry > ( ) ) ;
149+ services . AddSingleton ( sp => objectContainer . Resolve < IBindingFactory > ( ) ) ;
150+ services . AddSingleton ( sp => objectContainer . Resolve < IStepDefinitionRegexCalculator > ( ) ) ;
151+ services . AddSingleton ( sp => objectContainer . Resolve < IBindingInvoker > ( ) ) ;
152+ services . AddSingleton ( sp => objectContainer . Resolve < IStepDefinitionSkeletonProvider > ( ) ) ;
153+ services . AddSingleton ( sp => objectContainer . Resolve < ISkeletonTemplateProvider > ( ) ) ;
154+ services . AddSingleton ( sp => objectContainer . Resolve < IStepTextAnalyzer > ( ) ) ;
155+ services . AddSingleton ( sp => objectContainer . Resolve < IRuntimePluginLoader > ( ) ) ;
156+ services . AddSingleton ( sp => objectContainer . Resolve < IBindingAssemblyLoader > ( ) ) ;
157+
158+ services . AddTransient ( sp =>
106159 {
107- var specflowContainer = ctx . GetService < IObjectContainer > ( ) ;
108- var featureContext = specflowContainer . Resolve < FeatureContext > ( ) ;
109- return featureContext ;
160+ var container = BindMapping . TryGetValue ( sp , out var ctx )
161+ ? ctx . ScenarioContext ? . ScenarioContainer ??
162+ ctx . FeatureContext ? . FeatureContainer ??
163+ ctx . TestThreadContext ? . TestThreadContainer ??
164+ objectContainer
165+ : objectContainer ;
166+
167+ return container . Resolve < ISpecFlowOutputHelper > ( ) ;
110168 } ) ;
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 > ( ) ) ;
111178 }
112179
113- private static void RegisterTestThreadSpecFlowDependencies (
114- IServiceCollection services )
180+ private class RootServiceProviderContainer
115181 {
116- services . AddTransient < TestThreadContext > ( ctx =>
182+ public IServiceProvider ServiceProvider { get ; }
183+ public ScopeLevelType Scoping { get ; }
184+
185+ public RootServiceProviderContainer ( IServiceProvider sp , ScopeLevelType scoping )
117186 {
118- var specflowContainer = ctx . GetService < IObjectContainer > ( ) ;
119- var testThreadContext = specflowContainer . Resolve < TestThreadContext > ( ) ;
120- return testThreadContext ;
121- } ) ;
187+ ServiceProvider = sp ;
188+ Scoping = scoping ;
189+ }
122190 }
123191 }
124192}
0 commit comments