Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions .github/workflows/crossBrowserTesting.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Cross-browser testing

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

on:
push:
branches: [ "master" ]
Expand Down
57 changes: 57 additions & 0 deletions CSF.Screenplay.Reqnroll/EnumerableResolutionAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Generic;

#if SPECFLOW
using BoDi;
#else
using Reqnroll.BoDi;
#endif

namespace CSF.Screenplay
{
/// <summary>
/// Adapter class which - when added to DI - permits the BoDi DI container to resolve arbitrary <see cref="IEnumerable{T}"/> of
/// service instances.
/// </summary>
/// <remarks>
/// <para>
/// The BoDi DI container which is included in Reqnroll/SpecFlow does not fully support the functionality of Microsoft's DI standard.
/// Notably, it cannot natively resolve an <see cref="IEnumerable{T}"/>, where type T is a service type which may have multiple implementations
/// added to/registered with the container.
/// BoDi does have conceptually identical functionality, in its <see cref="IObjectContainer.ResolveAll{T}"/> function.
/// </para>
/// <para>
/// The purpose of this type is to provide a mechanism by which BoDi may resolve enumerables of service types.
/// This class wraps an instance of <see cref="IObjectContainer"/> and - in its <see cref="GetEnumerator"/> method - redirects to the
/// <see cref="IObjectContainer.ResolveAll{T}"/> method.
/// </para>
/// <para>
/// The limitation of this type (as a workaround) is that this type must be added to the container manually for each <see cref="IEnumerable{T}"/>
/// type which could be resolved from the container.
/// </para>
/// </remarks>
public class EnumerableResolutionAdapter<T> : IEnumerable<T> where T : class
{
readonly IObjectContainer container;

/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
{
var allOptions = container.ResolveAll<T>();
return allOptions.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

/// <summary>
/// Initializes a new instance of <see cref="EnumerableResolutionAdapter{T}"/>.
/// </summary>
/// <param name="container">The BoDi object container.</param>
/// <exception cref="ArgumentNullException"><paramref name="container"/> is <c>null</c>.</exception>
public EnumerableResolutionAdapter(IObjectContainer container)
{
this.container = container ?? throw new ArgumentNullException(nameof(container));
}
}
}
46 changes: 46 additions & 0 deletions CSF.Screenplay.Reqnroll/ObjectContainerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
#if SPECFLOW
using BoDi;
#else
using Reqnroll.BoDi;
#endif
using Microsoft.Extensions.DependencyInjection;

namespace CSF.Screenplay
{
/// <summary>
/// Extension methods for the Reqnroll/SpecFlow "BoDi" DI container.
/// </summary>
public static class ObjectContainerExtensions
{
/// <summary>
/// Gets an adapter object which permits limited use of the BoDi <see cref="IObjectContainer"/> as if it were an
/// <see cref="IServiceCollection"/>.
/// </summary>
/// <remarks>
/// <para>
/// Note that this is an imperfect solution. The BoDi container shipped with Reqnroll/SpecFlow does not support all the functionality
/// which is expected from <see cref="IServiceCollection"/>. Many methods of the returned object will throw <see cref="NotSupportedException"/>
/// if attempts are made to use them (a known LSP violation). Additionally, not all service collection DI behaviour will operate in the same
/// manner when using this adapter. In short "your mileage may vary".
/// </para>
/// <para>
/// However, for the most simple of usages, this enables the use of "Add to DI" logic which has been crafted for service collection,
/// in such a way that services may be added to the BoDi container without additional logic.
/// </para>
/// </remarks>
/// <param name="bodiContainer">A Reqnroll/SpecFlow BoDi DI container.</param>
/// <returns>An adapter object which implements some of the functionality of <see cref="IServiceCollection"/></returns>
public static IServiceCollection ToServiceCollection(this IObjectContainer bodiContainer)
=> new ServiceCollectionAdapter(bodiContainer);

/// <summary>
/// Gets an adapter object which permits the use of the BoDi <see cref="IObjectContainer"/> as if it were an
/// <see cref="IServiceProvider"/>.
/// </summary>
/// <param name="bodiContainer">A Reqnroll/SpecFlow BoDi DI container.</param>
/// <returns>An adapter object which implements the functionality of <see cref="IServiceProvider"/></returns>
public static IServiceProvider ToServiceProvider(this IObjectContainer bodiContainer)
=> new ServiceProviderAdapter(bodiContainer);
}
}
57 changes: 57 additions & 0 deletions CSF.Screenplay.Reqnroll/PerformanceProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Concurrent;
using CSF.Screenplay.Performances;
using Microsoft.Extensions.DependencyInjection;
#if SPECFLOW
using TechTalk.SpecFlow;
#else
using Reqnroll;
#endif

namespace CSF.Screenplay
{
/// <summary>
/// Factory type for instances of <see cref="PerformanceProvider"/>.
/// </summary>
public class PerformanceProviderFactory
{
readonly ConcurrentDictionary<FeatureInfo, Guid> featureContextIds = new ConcurrentDictionary<FeatureInfo, Guid>();
readonly ConcurrentDictionary<ScenarioAndFeatureInfoKey, Guid> scenarioContextIds = new ConcurrentDictionary<ScenarioAndFeatureInfoKey, Guid>();

/// <summary>
/// Gets an instance of <see cref="PerformanceProvider"/> for the specified service provider.
/// </summary>
/// <param name="services">A service provider</param>
/// <returns>A <see cref="PerformanceProvider"/>.</returns>
public PerformanceProvider GetPerformanceContainer(IServiceProvider services)
{
var performance = new Performance(services, new [] { GetFeatureIdAndName(services), GetScenarioIdAndName(services) });
var performanceContainer = new PerformanceProvider();
performanceContainer.SetCurrentPerformance(performance);
return performanceContainer;
}

IdentifierAndName GetFeatureIdAndName(IServiceProvider services)
{
var featureInfo = services.GetRequiredService<FeatureInfo>();
return new IdentifierAndName(GetFeatureId(featureInfo).ToString(),
featureInfo.Title,
true);
}

Guid GetFeatureId(FeatureInfo featureContext) => featureContextIds.GetOrAdd(featureContext, _ => Guid.NewGuid());

IdentifierAndName GetScenarioIdAndName(IServiceProvider services)
{
var featureInfo = services.GetRequiredService<FeatureInfo>();
var scenarioInfo = services.GetRequiredService<ScenarioInfo>();
return new IdentifierAndName(GetScenarioId(featureInfo, scenarioInfo).ToString(),
scenarioInfo.Title,
true);
}

Guid GetScenarioId(FeatureInfo featureInfo, ScenarioInfo scenarioInfo)
=> scenarioContextIds.GetOrAdd(new ScenarioAndFeatureInfoKey(scenarioInfo, featureInfo), _ => Guid.NewGuid());

}
}
58 changes: 18 additions & 40 deletions CSF.Screenplay.Reqnroll/ScreenplayPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Reflection;
using CSF.Screenplay.Actors;
using CSF.Screenplay.Performances;
using Microsoft.Extensions.DependencyInjection;

#if SPECFLOW
using BoDi;
using TechTalk.SpecFlow;
Expand Down Expand Up @@ -31,7 +33,7 @@ namespace CSF.Screenplay
/// </para>
/// <para>
/// This may be easily worked-around, though. If you are using a third-party DI plugin then do not use this plugin.
/// Instead use the <see cref="ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection, Action{ScreenplayOptions})"/>
/// Instead use the <see cref="ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection)"/>
/// method to add Screenplay to that third-party DI system, when customising the dependency registrations.
/// Adding Screenplay in that way is equivalent to the work done by this plugin.
/// </para>
Expand All @@ -46,8 +48,6 @@ namespace CSF.Screenplay
public class ScreenplayPlugin : IRuntimePlugin
{
readonly object syncRoot = new object();
readonly ConcurrentDictionary<FeatureInfo, Guid> featureContextIds = new ConcurrentDictionary<FeatureInfo, Guid>();
readonly ConcurrentDictionary<ScenarioAndFeatureInfoKey, Guid> scenarioContextIds = new ConcurrentDictionary<ScenarioAndFeatureInfoKey, Guid>();

bool initialised;

Expand Down Expand Up @@ -104,50 +104,28 @@ void OnCustomizeGlobalDependencies(object sender, CustomizeGlobalDependenciesEve
{
if (initialised) return;

var container = args.ObjectContainer;
var serviceCollection = new ServiceCollectionAdapter(container);
serviceCollection.AddScreenplay();
container.RegisterFactoryAs<IServiceProvider>(c => new ServiceProviderAdapter(c));
Screenplay = container.Resolve<Screenplay>();
var boDiContainer = args.ObjectContainer;
var services = new ServiceCollectionAdapter(boDiContainer);
services.AddScreenplayPlugin();
boDiContainer.RegisterFactoryAs(c => c.ToServiceProvider());
Screenplay = boDiContainer.Resolve<Screenplay>();
initialised = true;
}
}

void OnCustomizeScenarioDependencies(object sender, CustomizeScenarioDependenciesEventArgs args)
{
var container = args.ObjectContainer;
var services = new ServiceProviderAdapter(container);
container.RegisterInstanceAs<IServiceProvider>(services);
var performance = new Performance(services, new [] { GetFeatureIdAndName(container), GetScenarioIdAndName(container) });
var performanceContainer = new PerformanceProvider();
performanceContainer.SetCurrentPerformance(performance);
container.RegisterInstanceAs(performanceContainer);
container.RegisterFactoryAs(c => c.Resolve<PerformanceProvider>().GetCurrentPerformance());

container.RegisterFactoryAs<ICast>(c => new Cast(c.Resolve<IServiceProvider>(), c.Resolve<IPerformance>().PerformanceIdentity));
container.RegisterTypeAs<Stage, IStage>();
}

IdentifierAndName GetFeatureIdAndName(IObjectContainer container)
static void OnCustomizeScenarioDependencies(object sender, CustomizeScenarioDependenciesEventArgs args)
{
var featureInfo = container.Resolve<FeatureInfo>();
return new IdentifierAndName(GetFeatureId(featureInfo).ToString(),
featureInfo.Title,
true);
}
var boDiContainer = args.ObjectContainer;
var services = boDiContainer.ToServiceProvider();
boDiContainer.RegisterInstanceAs(services);

Guid GetFeatureId(FeatureInfo featureContext) => featureContextIds.GetOrAdd(featureContext, _ => Guid.NewGuid());

IdentifierAndName GetScenarioIdAndName(IObjectContainer container)
{
var featureInfo = container.Resolve<FeatureInfo>();
var scenarioInfo = container.Resolve<ScenarioInfo>();
return new IdentifierAndName(GetScenarioId(featureInfo, scenarioInfo).ToString(),
scenarioInfo.Title,
true);
var serviceCollection = boDiContainer.ToServiceCollection();
serviceCollection
.AddSingleton(s => s.GetRequiredService<PerformanceProviderFactory>().GetPerformanceContainer(s))
.AddSingleton(s => s.GetRequiredService<PerformanceProvider>().GetCurrentPerformance())
.AddSingleton<ICast>(s => new Cast(s, s.GetRequiredService<IPerformance>().PerformanceIdentity))
.AddSingleton<IStage, Stage>();
}

Guid GetScenarioId(FeatureInfo featureInfo, ScenarioInfo scenarioInfo)
=> scenarioContextIds.GetOrAdd(new ScenarioAndFeatureInfoKey(scenarioInfo, featureInfo), _ => Guid.NewGuid());
}
}
9 changes: 4 additions & 5 deletions CSF.Screenplay.Reqnroll/ServiceCollectionAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public class ServiceCollectionAdapter : IServiceCollection
// not actually bypassing accessibility.
#pragma warning disable S3011
static readonly MethodInfo
OpenGenericRegisterType = typeof(ServiceCollectionAdapter).GetMethod(nameof(RegisterType), BindingFlags.Instance | BindingFlags.NonPublic),
OpenGenericRegisterInstance = typeof(ServiceCollectionAdapter).GetMethod(nameof(RegisterInstance), BindingFlags.Instance | BindingFlags.NonPublic),
OpenGenericRegisterFactory = typeof(ServiceCollectionAdapter).GetMethod(nameof(RegisterFactory), BindingFlags.Instance | BindingFlags.NonPublic);
#pragma warning restore S3011
Expand Down Expand Up @@ -89,12 +88,12 @@ public void Add(ServiceDescriptor item)
else if (item.ImplementationInstance != null)
OpenGenericRegisterInstance.MakeGenericMethod(item.ServiceType).Invoke(this, new[] { item });
else
OpenGenericRegisterType.MakeGenericMethod(item.ServiceType, item.ImplementationType).Invoke(this, Array.Empty<object>());
RegisterTypeNonGeneric(item.ServiceType, item.ImplementationType);
}

void RegisterType<TSvc,TImpl>() where TImpl : class,TSvc
void RegisterTypeNonGeneric(Type service, Type implementation)
{
wrapped.RegisterTypeAs<TImpl, TSvc>();
((ObjectContainer) wrapped).RegisterTypeAs(implementation, service);
}

void RegisterInstance<T>(ServiceDescriptor item) where T : class
Expand All @@ -104,7 +103,7 @@ void RegisterInstance<T>(ServiceDescriptor item) where T : class

void RegisterFactory<T>(ServiceDescriptor item) where T : class
{
wrapped.RegisterFactoryAs(objectContainer => (T) item.ImplementationFactory(new ServiceProviderAdapter(objectContainer)));
wrapped.RegisterFactoryAs(objectContainer => (T) item.ImplementationFactory(objectContainer.ToServiceProvider()));
}

/// <summary>
Expand Down
66 changes: 66 additions & 0 deletions CSF.Screenplay.Reqnroll/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace CSF.Screenplay
{
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds <see cref="EnumerableResolutionAdapter{T}"/> to the DI container for the service type <typeparamref name="TService"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is required to work around a limitation of the BoDi DI container which ships with Reqnroll/SpecFlow.
/// See <see cref="EnumerableResolutionAdapter{T}"/> for more information.
/// </para>
/// </remarks>
/// <typeparam name="TService">The service type for which to add the adapter</typeparam>
/// <param name="services">A service collection</param>
/// <returns>The same service collection, so calls may be chained</returns>
public static IServiceCollection AddEnumerableAdapter<TService>(this IServiceCollection services) where TService : class
=> services.AddTransient<IEnumerable<TService>, EnumerableResolutionAdapter<TService>>();

/// <summary>
/// Adds enumerable adapters for service types which are required in order to enable
/// <see href="https://learn.microsoft.com/en-us/dotnet/core/extensions/options"> the Microsoft Options Pattern</see> with the specified options type.
/// </summary>
/// <remarks>
/// <para>
/// This is required to work around a limitation of the BoDi DI container which ships with Reqnroll/SpecFlow.
/// See <see cref="EnumerableResolutionAdapter{T}"/> for more information.
/// </para>
/// <para>
/// Use of the options pattern requires the resolution of three enumerable types: <see cref="IConfigureOptions{TOptions}"/>,
/// <see cref="IPostConfigureOptions{TOptions}"/> and <see cref="IValidateOptions{TOptions}"/>. This method uses
/// <see cref="AddEnumerableAdapter{TService}(IServiceCollection)"/> for each of those types.
/// </para>
/// </remarks>
/// <typeparam name="TOptions">The options type</typeparam>
/// <param name="services">A service collection</param>
/// <returns>The same service collection, so calls may be chained</returns>
public static IServiceCollection AddOptionsAdapters<TOptions>(this IServiceCollection services) where TOptions : class
{
return services
.AddEnumerableAdapter<IConfigureOptions<TOptions>>()
.AddEnumerableAdapter<IPostConfigureOptions<TOptions>>()
.AddEnumerableAdapter<IValidateOptions<TOptions>>();
}

/// <summary>
/// Adds the services to DI which are required to use the Reqnroll/SpecFlow plugin.
/// </summary>
/// <param name="services">A service collection</param>
/// <returns>The same service collection, so calls may be chained</returns>
public static IServiceCollection AddScreenplayPlugin(this IServiceCollection services)
{
return services
.AddScreenplay()
.AddOptionsAdapters<ScreenplayOptions>()
.AddSingleton<PerformanceProviderFactory>();
}
}
}
Loading
Loading