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

Commit

Permalink
support complex injections
Browse files Browse the repository at this point in the history
  • Loading branch information
shlomiassaf committed Dec 20, 2021
1 parent c43653b commit abb484b
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 7 deletions.
81 changes: 76 additions & 5 deletions SpecFlow.DependencyInjection/DependencyInjectionPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
using BoDi;
using Microsoft.Extensions.DependencyInjection;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Bindings;
using TechTalk.SpecFlow.Bindings.Discovery;
using TechTalk.SpecFlow.BindingSkeletons;
using TechTalk.SpecFlow.Configuration;
using TechTalk.SpecFlow.ErrorHandling;
using TechTalk.SpecFlow.Infrastructure;
using TechTalk.SpecFlow.Plugins;
using TechTalk.SpecFlow.Tracing;
using TechTalk.SpecFlow.UnitTestProvider;

[assembly: RuntimePlugin(typeof(SolidToken.SpecFlow.DependencyInjection.DependencyInjectionPlugin))]
Expand All @@ -13,8 +19,11 @@ namespace SolidToken.SpecFlow.DependencyInjection
{
public class DependencyInjectionPlugin : IRuntimePlugin
{
private static readonly ConcurrentDictionary<ISpecFlowContext, IDisposable> ActiveServiceScopes =
new ConcurrentDictionary<ISpecFlowContext, IDisposable>();
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMapping =
new ConcurrentDictionary<IServiceProvider, IContextManager>();

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

private readonly object _registrationLock = new object();

Expand Down Expand Up @@ -43,12 +52,16 @@ private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenc
{
var serviceCollectionFinder = args.ObjectContainer.Resolve<IServiceCollectionFinder>();
var (services, scoping) = serviceCollectionFinder.GetServiceCollection();
services.AddSingleton<IObjectContainer>(ctx => args.ObjectContainer);
RegisterProxyBindings(args.ObjectContainer, services);
return new RootServiceProviderContainer(services.BuildServiceProvider(), scoping);
});

args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
{
return args.ObjectContainer.Resolve<RootServiceProviderContainer>().ServiceProvider;
});

// Will make sure DI scope is disposed.
var lcEvents = args.ObjectContainer.Resolve<RuntimePluginTestExecutionLifecycleEvents>();
lcEvents.AfterScenario += AfterScenarioPluginLifecycleEventHandler;
Expand All @@ -66,10 +79,12 @@ private static void CustomizeFeatureDependenciesEventHandler(object sender, Cust
if (spContainer.Scoping == ScopeLevelType.Feature)
{
var serviceProvider = spContainer.ServiceProvider;

// Now we can register a new scoped service provider
args.ObjectContainer.RegisterFactoryAs<IServiceProvider>(() =>
{
var scope = serviceProvider.CreateScope();
BindMapping.TryAdd(scope.ServiceProvider, args.ObjectContainer.Resolve<IContextManager>());
ActiveServiceScopes.TryAdd(args.ObjectContainer.Resolve<FeatureContext>(), scope);
return scope.ServiceProvider;
});
Expand Down Expand Up @@ -97,15 +112,71 @@ private static void CustomizeScenarioDependenciesEventHandler(object sender, Cus
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 _);
serviceScope.Dispose();
}
}


private static void RegisterProxyBindings(IObjectContainer objectContainer, IServiceCollection services)
{
// 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...

services.AddSingleton<IObjectContainer>(objectContainer);
services.AddSingleton(sp => objectContainer.Resolve<IRuntimeConfigurationProvider>());
services.AddSingleton(sp => objectContainer.Resolve<ITestRunnerManager>());
services.AddSingleton(sp => objectContainer.Resolve<IStepFormatter>());
services.AddSingleton(sp => objectContainer.Resolve<ITestTracer>());
services.AddSingleton(sp => objectContainer.Resolve<ITraceListener>());
services.AddSingleton(sp => objectContainer.Resolve<ITraceListenerQueue>());
services.AddSingleton(sp => objectContainer.Resolve<IErrorProvider>());
services.AddSingleton(sp => objectContainer.Resolve<IRuntimeBindingSourceProcessor>());
services.AddSingleton(sp => objectContainer.Resolve<IBindingRegistry>());
services.AddSingleton(sp => objectContainer.Resolve<IBindingFactory>());
services.AddSingleton(sp => objectContainer.Resolve<IStepDefinitionRegexCalculator>());
services.AddSingleton(sp => objectContainer.Resolve<IBindingInvoker>());
services.AddSingleton(sp => objectContainer.Resolve<IStepDefinitionSkeletonProvider>());
services.AddSingleton(sp => objectContainer.Resolve<ISkeletonTemplateProvider>());
services.AddSingleton(sp => objectContainer.Resolve<IStepTextAnalyzer>());
services.AddSingleton(sp => objectContainer.Resolve<IRuntimePluginLoader>());
services.AddSingleton(sp => objectContainer.Resolve<IBindingAssemblyLoader>());

services.AddTransient(sp =>
{
var container = BindMapping.TryGetValue(sp, out var ctx)
? ctx.ScenarioContext?.ScenarioContainer ??
ctx.FeatureContext?.FeatureContainer ??
ctx.TestThreadContext?.TestThreadContainer ??
objectContainer
: objectContainer;
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>());
}

private class RootServiceProviderContainer
{
public IServiceProvider ServiceProvider { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ public object ResolveBindingInstance(Type bindingType, IObjectContainer containe
? container.Resolve(bindingType)
: container.Resolve<IServiceProvider>().GetRequiredService(bindingType);
}

public bool IsRegistered<T>(IObjectContainer container) => container.IsRegistered<T>();

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;
}
}
}

0 comments on commit abb484b

Please sign in to comment.