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

Commit 1258fb0

Browse files
authored
Enhance dependency injection scoping (#74)
Contributor: @shlomiassaf
1 parent 8128ae8 commit 1258fb0

File tree

5 files changed

+211
-102
lines changed

5 files changed

+211
-102
lines changed
Lines changed: 142 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using BoDi;
34
using Microsoft.Extensions.DependencyInjection;
45
using 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;
511
using TechTalk.SpecFlow.Infrastructure;
612
using TechTalk.SpecFlow.Plugins;
13+
using TechTalk.SpecFlow.Tracing;
714
using 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
}
Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,50 @@
11
using System;
2+
using System.Collections.Concurrent;
3+
using System.Reflection;
24
using BoDi;
5+
using Microsoft.Extensions.DependencyInjection;
36
using TechTalk.SpecFlow.Infrastructure;
47

58
namespace SolidToken.SpecFlow.DependencyInjection
69
{
10+
/* TODO
11+
If SpecFlow will add an "IObjectContainer.IsRegistered(Type type)" method next to the existing "IsRegistered<T>()"
12+
We can remove most of the code here!
13+
*/
714
public class DependencyInjectionTestObjectResolver : ITestObjectResolver
815
{
9-
public object ResolveBindingInstance(Type bindingType, IObjectContainer scenarioContainer)
16+
// Can remove if IsRegistered(Type type) exists
17+
private static readonly ConcurrentDictionary<Type, MethodInfo> IsRegisteredMethodInfoCache =
18+
new ConcurrentDictionary<Type, MethodInfo>();
19+
20+
// Can remove if IsRegistered(Type type) exists
21+
private static readonly MethodInfo IsRegisteredMethodInfo = typeof(DependencyInjectionTestObjectResolver)
22+
.GetMethod(nameof(IsRegistered), BindingFlags.Instance | BindingFlags.Public);
23+
24+
// Can remove if IsRegistered(Type type) exists
25+
private static MethodInfo CreateGenericMethodInfo(Type t) => IsRegisteredMethodInfo.MakeGenericMethod(t);
26+
27+
public object ResolveBindingInstance(Type bindingType, IObjectContainer container)
28+
{
29+
// Can remove if IsRegistered(Type type) exists
30+
var mi = IsRegisteredMethodInfoCache.GetOrAdd(bindingType, CreateGenericMethodInfo);
31+
var registered = (bool) mi.Invoke(this, new object[] { container });
32+
// var registered = container.IsRegistered(bindingType);
33+
34+
return registered
35+
? container.Resolve(bindingType)
36+
: container.Resolve<IServiceProvider>().GetRequiredService(bindingType);
37+
}
38+
39+
public bool IsRegistered<T>(IObjectContainer container)
1040
{
11-
var provider = scenarioContainer.Resolve<IServiceProvider>();
12-
return provider.GetService(bindingType);
41+
if (container.IsRegistered<T>())
42+
return true;
43+
44+
// IsRegistered is not recursive, it will only check the current container
45+
if (container is ObjectContainer c && c.BaseContainer != null)
46+
return IsRegistered<T>(c.BaseContainer);
47+
return false;
1348
}
1449
}
1550
}

SpecFlow.DependencyInjection/IServiceCollectionFinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ namespace SolidToken.SpecFlow.DependencyInjection
55
{
66
public interface IServiceCollectionFinder
77
{
8-
Func<IServiceCollection> GetCreateScenarioServiceCollection();
8+
(IServiceCollection, ScopeLevelType) GetServiceCollection();
99
}
1010
}

SpecFlow.DependencyInjection/ScenarioDependenciesAttribute.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22

33
namespace SolidToken.SpecFlow.DependencyInjection
44
{
5+
public enum ScopeLevelType
6+
{
7+
/// <summary>
8+
/// Scoping is created for every scenario and it is destroyed once the scenario ends.
9+
/// </summary>
10+
Scenario,
11+
/// <summary>
12+
/// Scoping is created for Feature scenario and it is destroyed once the Feature ends.
13+
/// </summary>
14+
Feature
15+
}
16+
517
[AttributeUsage(AttributeTargets.Method)]
618
public class ScenarioDependenciesAttribute : Attribute
719
{
820
/// <summary>
921
/// Automatically register all SpecFlow bindings.
1022
/// </summary>
1123
public bool AutoRegisterBindings { get; set; } = true;
24+
/// <summary>
25+
/// Define when to create and destroy scope.
26+
/// </summary>
27+
public ScopeLevelType ScopeLevel { get; set; } = ScopeLevelType.Scenario;
1228
}
1329
}

0 commit comments

Comments
 (0)