Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Microsoft.Extensions.DependencyInjection integration plugin #94

Merged
merged 7 commits into from
May 22, 2024
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
3 changes: 3 additions & 0 deletions .ncrunch/Reqnroll.PluginTests.v3.ncrunchproject
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<NamedTestSelector>
<TestName>Reqnroll.PluginTests.Infrastructure.WindsorPluginTests.Can_load_Windsor_plugin</TestName>
</NamedTestSelector>
<NamedTestSelector>
<TestName>Reqnroll.PluginTests.Infrastructure.MicrosoftExtensionsDependencyInjectionTests.LoadPlugin_MicrosoftExtensionsDependencyInjection_ShouldNotBeNull</TestName>
</NamedTestSelector>
</IgnoredTests>
</Settings>
</ProjectConfiguration>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Fix: #71 StackOverflowException when using [StepArgumentTransformation] with same input and output type (for example string)
* Fix: Autofac without hook does not run GlobalDependencies (#127)
* Fix: Reqnroll.Autofac shows wrongly ambiguous step definition (#56)
* Port SolidToken.SpecFlow.DependencyInjection to Reqnroll. Thanks to @SolidToken for the contribution! (#94)

# v1.0.1 - 2024-02-16

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Reqnroll.Bindings;
using Reqnroll.Bindings.Reflection;

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
public static class BindingRegistryExtensions
{
public static IEnumerable<IBindingType> GetBindingTypes(this IBindingRegistry bindingRegistry)
{
return bindingRegistry.GetStepDefinitions().Cast<IBinding>()
.Concat(bindingRegistry.GetHooks().Cast<IBinding>())
.Concat(bindingRegistry.GetStepTransformations())
.Select(b => b.Method.Type)
.Distinct();
}

public static IEnumerable<Assembly> GetBindingAssemblies(this IBindingRegistry bindingRegistry)
{
return bindingRegistry.GetBindingTypes().OfType<RuntimeBindingType>()
.Select(t => t.Type.Assembly)
.Distinct();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System;
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Reqnroll.Bindings;
using Reqnroll.Bindings.Discovery;
using Reqnroll.BindingSkeletons;
using Reqnroll.BoDi;
using Reqnroll.Configuration;
using Reqnroll.ErrorHandling;
using Reqnroll.Infrastructure;
using Reqnroll.Plugins;
using Reqnroll.Tracing;
using Reqnroll.UnitTestProvider;

[assembly: RuntimePlugin(typeof(Reqnroll.Microsoft.Extensions.DependencyInjection.DependencyInjectionPlugin))]

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
public class DependencyInjectionPlugin : IRuntimePlugin
{
private static readonly ConcurrentDictionary<IServiceProvider, IContextManager> BindMappings =
new ConcurrentDictionary<IServiceProvider, IContextManager>();

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

private readonly object _registrationLock = new object();

public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration)
{
runtimePluginEvents.CustomizeGlobalDependencies += CustomizeGlobalDependencies;
runtimePluginEvents.CustomizeFeatureDependencies += CustomizeFeatureDependenciesEventHandler;
runtimePluginEvents.CustomizeScenarioDependencies += CustomizeScenarioDependenciesEventHandler;
}

private void CustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEventArgs args)
{
if (!args.ObjectContainer.IsRegistered<IServiceCollectionFinder>())
{
lock (_registrationLock)
{
if (!args.ObjectContainer.IsRegistered<IServiceCollectionFinder>())
{
args.ObjectContainer.RegisterTypeAs<DependencyInjectionTestObjectResolver, ITestObjectResolver>();
args.ObjectContainer.RegisterTypeAs<ServiceCollectionFinder, IServiceCollectionFinder>();
}

// 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>(() =>
{
var serviceCollectionFinder = args.ObjectContainer.Resolve<IServiceCollectionFinder>();
var (services, scoping) = serviceCollectionFinder.GetServiceCollection();

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;
lcEvents.AfterFeature += AfterFeaturePluginLifecycleEventHandler;
}
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.
var spContainer = args.ObjectContainer.Resolve<RootServiceProviderContainer>();

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();
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.
var spContainer = args.ObjectContainer.Resolve<RootServiceProviderContainer>();

if (spContainer.Scoping == ScopeLevelType.Scenario)
{
var serviceProvider = spContainer.ServiceProvider;
// Now we can register a new scoped service provider
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))
{
BindMappings.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>());
// TODO: Use async version of the interface (IAsyncBindingInvoker)
//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.AddSingleton(sp => objectContainer.Resolve<IUnitTestRuntimeProvider>());

services.AddTransient(sp =>
{
var container = BindMappings.TryGetValue(sp, out var ctx)
? ctx.ScenarioContext?.ScenarioContainer ??
ctx.FeatureContext?.FeatureContainer ??
ctx.TestThreadContext?.TestThreadContainer ??
objectContainer
: objectContainer;

return container.Resolve<IReqnrollOutputHelper>();
});

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
{
public IServiceProvider ServiceProvider { get; }
public ScopeLevelType Scoping { get; }

public RootServiceProviderContainer(IServiceProvider sp, ScopeLevelType scoping)
{
ServiceProvider = sp;
Scoping = scoping;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Reqnroll.BoDi;
using Reqnroll.Infrastructure;

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
public class DependencyInjectionTestObjectResolver : ITestObjectResolver
{
public object ResolveBindingInstance(Type bindingType, IObjectContainer container)
{
var registered = IsRegistered(container, bindingType);

return registered
? container.Resolve(bindingType)
: container.Resolve<IServiceProvider>().GetRequiredService(bindingType);
}

private bool IsRegistered(IObjectContainer container, Type type)
{
if (container.IsRegistered(type))
{
return true;
}

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

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Extensions.DependencyInjection;

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
public interface IServiceCollectionFinder
{
(IServiceCollection, ScopeLevelType) GetServiceCollection();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
[Serializable]
public class MissingScenarioDependenciesException : ReqnrollException
{
public MissingScenarioDependenciesException()
: base("No method marked with [ScenarioDependencies] attribute found.")
{
HelpLink = "https://go.reqnroll.net/doc-msdi";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyOriginatorKeyFile>$(Reqnroll_KeyFile)</AssemblyOriginatorKeyFile>
<SignAssembly>$(Reqnroll_EnableStrongNameSigning)</SignAssembly>
<PublicSign>$(Reqnroll_PublicSign)</PublicSign>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuspecFile>$(MSBuildThisFileDirectory)Reqnroll.Microsoft.Extensions.DependencyInjection.nuspec</NuspecFile>

<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

<RootNamespace>Reqnroll.Microsoft.Extensions.DependencyInjection</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Reqnroll\Reqnroll.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Reqnroll.Microsoft.Extensions.DependencyInjection</id>
<version>$version$</version>
<title>Reqnroll Microsoft.Extensions.DependencyInjection integration plugin</title>
<authors>Mark Hoek, Solid Token, $author$</authors>
<owners>Mark Hoek, Solid Token, $owner$</owners>
<description>Reqnroll plugin that enables to use Microsoft.Extensions.DependencyInjection for resolving test dependencies.</description>
<summary>Reqnroll plugin that enables to use Microsoft.Extensions.DependencyInjection for resolving test dependencies.</summary>
<language>en-US</language>
<projectUrl>https://www.reqnroll.net</projectUrl>
<icon>images\reqnroll-icon.png</icon>
<copyright>Copyright © Mark Hoek, Solid Token, $author$</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="file">LICENSE</license>
<tags>reqnroll microsoft extensions di dependency injection</tags>
<dependencies>
<group targetFramework=".NETStandard2.0">
<dependency id="Reqnroll" version="[$version$]" />
<dependency id="Microsoft.Extensions.DependencyInjection" version="6.0.0" />
</group>
</dependencies>
</metadata>

<files>
<file src="build\**\*" target="build" />
<file src="bin\$config$\netstandard2.0\Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.dll" target="lib\netstandard2.0" />
<file src="bin\$config$\netstandard2.0\Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin.pdb" target="lib\netstandard2.0" />

<file src="$SolutionDir$LICENSE" target="LICENSE" />
<file src="$SolutionDir$reqnroll-icon.png" target="images\" />
</files>
</package>
Loading