From 01158f825636775c5df75fb918e7c234764117ac Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 12 Feb 2026 17:53:26 +0000 Subject: [PATCH 1/2] WIP docco #269 --- CSF.Screenplay.Abstractions/IPersona.cs | 15 ++-- .../ConstructorInjectIntoThePersona.md | 12 +++ .../ConstructorInjectionForBindings.md | 7 ++ .../dependencyInjection/InjectingServices.md | 73 ++++--------------- .../InjectionForStandaloneScreenplay.md | 17 +++++ .../ParameterInjectionForTests.md | 10 +++ 6 files changed, 72 insertions(+), 62 deletions(-) create mode 100644 CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md create mode 100644 CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md create mode 100644 CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md create mode 100644 CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md diff --git a/CSF.Screenplay.Abstractions/IPersona.cs b/CSF.Screenplay.Abstractions/IPersona.cs index 4d8c1ef6..97beb93b 100644 --- a/CSF.Screenplay.Abstractions/IPersona.cs +++ b/CSF.Screenplay.Abstractions/IPersona.cs @@ -5,13 +5,18 @@ namespace CSF.Screenplay /// A persona is a factory for a commonly-used actor /// /// - /// In Screenplay is is recommended to use memorable actors which are widely understood and recognisable - /// by the team. This is easier if the composition of an actor is the same across every in which + /// In Screenplay is is recommended to use & reuse memorable actors, which are widely understood and recognisable + /// to the development team. This is easier if the composition of an actor is the same across every in which /// they participate. /// /// - /// By using a separate persona implementation for each named actor, the developer can ensure consistent creation - /// for instances of those actors. + /// Use classes which derive from this interface to define those memorable + /// actors. The persona class should configure the actor it creates with a name and + /// a set of abilities appropriate to the actor. + /// + /// + /// Classes which derive from this interface should constructor-inject any dependencies + /// they require, such as those required to grant abilities. /// /// public interface IPersona : IHasName @@ -26,4 +31,4 @@ public interface IPersona : IHasName /// A unique identity for the currently-executing performance Actor GetActor(Guid performanceIdentity); } -} \ No newline at end of file +} diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md new file mode 100644 index 00000000..311bbbf7 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md @@ -0,0 +1,12 @@ +# Injecting services into Personas +Types which derive from [`IPersona`] support constructor-injected dependencies. +Personas are typically used by either [the cast] or [the stage] to get an [Actor]. +The technique in which they are used means that they are resolved, along with their constructor-injected dependencies, from DI. + +Use constructor-injected dependencies in persona classes to provide access to the APIs required to resolve [Abilities] that the actor is to be granted. + +[`IPersona`]: xref:CSF.Screenplay.IPersona +[the cast]: xref:CSF.Screenplay.ICast +[the stage]: xref:CSF.Screenplay.IStage +[Actor]: xref:CSF.Screenplay.Actor +[Abilities]: ../../glossary/Ability.md \ No newline at end of file diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md new file mode 100644 index 00000000..c8cf8e35 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md @@ -0,0 +1,7 @@ +# Constructor injection for bindings +For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes. + +Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic. + +[Reqnroll]: https://reqnroll.net/ +[commonly-used Screenplay services]: InjectableServices.md diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectingServices.md b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectingServices.md index dc46d47a..f04bde79 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectingServices.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectingServices.md @@ -1,65 +1,24 @@ --- uid: InjectingServicesArticle --- - # Injecting services -There are a few techniques in which injecting dependencies is relevant. These are summarised below. -All but one of these techniques provide access to [the services which are added to the container]. - -[the services which are added to the container]: InjectableServices.md - -## Into test logic - -When using automated test logic based upon Screenplay, the use of dependency injection typically takes one of two forms. -Which of these depends upon the nature and paradigm of the test framework. - -For frameworks which are based on **test methods** such as [NUnit], services are typically injected via _method parameter injection_ into the test methods. -[If Screenplay were to be extended] to work with frameworks such as xUnit or MSTest then this is likely to be the technique used. - -For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes. - -Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic. - -[NUnit]: https://nunit.org/ -[If Screenplay were to be extended]: ../extendingScreenplay/TestIntegrations.md -[Reqnroll]: https://reqnroll.net/ -[commonly-used Screenplay services]: InjectableServices.md - -## Into standalone performance logic +The table below summarises how to get services, from various contexts. In the first column are contexts/situations in which a developer could require services from DI. The second column indicates and links to the appropriate technique for that scenario. -If you are [using Screenplay standalone] then the [`Screenplay.ExecuteAsPerformanceAsync`] permits resolution of dependencies via its parameter. -That parameter is a `Func>`. -The service provider may be used to resolve dependency services for the performance's logic. +All but one of these techniques (the last) provides full access to [the services which are added to the container]. -Developers are urged to consider encapsulating their performance logic in implementations of [`IHostsPerformance`]. -Through an overload (extension method) named [`ExecuteAsPerformanceAsync`], developers may specify the concrete implementation of that interface. -This extension method will resolve that implementation type along with any of its constructor-injected dependencies. -This avoids the service locator anti-pattern and provides a convenient pattern by which to write performance logic. +| Context | Technique | +| ---------------------------------------------------- | -------------------------------------------------------- | +| NUnit style test method | [Parameter inject into the test] method | +| Reqnroll style binding class | [Constructor inject into the binding] class | +| Standalone Screenplay: [`ExecuteAsPerformanceAsync`] | [Create a performance host, or use the service provider] | +| Persona class | [Constructor inject into the Persona] class | +| Performable class, such as a Task | [Use the actor's abilities] | -Use services resolved from the service provider, or injected into your [`IHostsPerformance`] implementation, to get access to [commonly-used Screenplay services] and anything else required at the root level of your performance logic. - -[using Screenplay standalone]: ../gettingStarted/nonTesting/index.md -[`Screenplay.ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) -[`IHostsPerformance`]: xref:CSF.Screenplay.IHostsPerformance -[`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) - -## Into personas - -Types which derive from [`IPersona`] support constructor-injected dependencies. -Personas are typically used by either [the cast] or [the stage] to get an [Actor]. -The technique in which they are used means that they are resolved, along with their constructor-injected dependencies, from DI. - -Use constructor-injected dependencies in persona classes to provide access to the APIs required to resolve [Abilities] that the actor is to be granted. - -[`IPersona`]: xref:CSF.Screenplay.IPersona -[the cast]: xref:CSF.Screenplay.ICast -[the stage]: xref:CSF.Screenplay.IStage -[Actor]: xref:CSF.Screenplay.Actor -[Abilities]: ../../glossary/Ability.md - -## Into performables - -_See the article explaining [how performables get their dependencies]_. - -[how performables get their dependencies]: Performables.md +[the services which are added to the container]: InjectableServices.md +[Parameter inject into the test]: ParameterInjectionForTests.md +[Constructor inject into the binding]: ConstructorInjectionForBindings.md +[Create a performance host, or use the service provider]: InjectionForStandaloneScreenplay.md +[Constructor inject into the Persona]: ConstructorInjectIntoThePersona.md +[`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) +[Use the actor's abilities]: Performables.md diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md new file mode 100644 index 00000000..2a36f6fc --- /dev/null +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md @@ -0,0 +1,17 @@ +# Dependency injection for standalone Screenplay + +If you are [using Screenplay standalone] then the [`Screenplay.ExecuteAsPerformanceAsync`] permits resolution of dependencies via its parameter. +That parameter is a `Func>`. +The service provider may be used to resolve dependency services for the performance's logic. + +Developers are urged to consider encapsulating their performance logic in implementations of [`IHostsPerformance`]. +Through an overload (extension method) named [`ExecuteAsPerformanceAsync`], developers may specify the concrete implementation of that interface. +This extension method will resolve that implementation type along with any of its constructor-injected dependencies. +This avoids the service locator anti-pattern and provides a convenient pattern by which to write performance logic. + +Use services resolved from the service provider, or injected into your [`IHostsPerformance`] implementation, to get access to [commonly-used Screenplay services] and anything else required at the root level of your performance logic. + +[using Screenplay standalone]: ../gettingStarted/nonTesting/index.md +[`Screenplay.ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) +[`IHostsPerformance`]: xref:CSF.Screenplay.IHostsPerformance +[`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md new file mode 100644 index 00000000..50cbe7a0 --- /dev/null +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md @@ -0,0 +1,10 @@ +# Parameter injection for tests +For testing frameworks which are based on **test methods** such as [NUnit], services are injected via _method parameter injection_. This is performed directly into each test method which is decorated with the `[Screenplay]` attribute. + +[If Screenplay were to be extended] to work with frameworks such as xUnit or MSTest, then this same technique would be used. + +Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic. + +[NUnit]: https://nunit.org/ +[If Screenplay were to be extended]: ../extendingScreenplay/TestIntegrations.md +[commonly-used Screenplay services]: InjectableServices.md \ No newline at end of file From d1a1abef2b420995aaabf613764d59b3c3dcde37 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Sat, 14 Feb 2026 17:11:11 +0000 Subject: [PATCH 2/2] Resolve #269 - Complete DI docco --- CSF.Screenplay.Abstractions/IPersona.cs | 17 ++--- CSF.Screenplay.Docs/docs/GettingReports.md | 2 +- .../docs/bestPractice/WritingTests.md | 10 +-- .../HowScreenplayAndPerformanceRelate.md | 2 +- .../dependencyInjection/AddingServices.md | 4 +- .../ConstructorInjectIntoThePersona.md | 1 + .../ConstructorInjectionForBindings.md | 24 ++++++- .../DependencyInjectionScope.md | 9 ++- .../InjectionForStandaloneScreenplay.md | 62 ++++++++++++++++--- .../ParameterInjectionForTests.md | 26 ++++++-- .../docs/dependencyInjection/Performables.md | 11 ++-- .../docs/dependencyInjection/index.md | 28 +++------ .../extendingScreenplay/TestIntegrations.md | 4 +- CSF.Screenplay.Docs/docs/toc.yml | 20 ++++++ 14 files changed, 158 insertions(+), 62 deletions(-) diff --git a/CSF.Screenplay.Abstractions/IPersona.cs b/CSF.Screenplay.Abstractions/IPersona.cs index 97beb93b..6ead52bf 100644 --- a/CSF.Screenplay.Abstractions/IPersona.cs +++ b/CSF.Screenplay.Abstractions/IPersona.cs @@ -2,21 +2,22 @@ namespace CSF.Screenplay { - /// A persona is a factory for a commonly-used actor + /// A persona is a factory for a commonly-used /// /// /// In Screenplay is is recommended to use & reuse memorable actors, which are widely understood and recognisable - /// to the development team. This is easier if the composition of an actor is the same across every in which - /// they participate. + /// to the development team. This is easier if the composition of an actor is the same across every + /// in which they participate. Personas facilitate this; the persona class serves + /// as a consistent factory which creates the same named actor in the same manner every time. /// /// - /// Use classes which derive from this interface to define those memorable - /// actors. The persona class should configure the actor it creates with a name and - /// a set of abilities appropriate to the actor. + /// Developers should create an implementation of this interface for each actor which they wish to + /// define, ideally named the same as the name as the Actor. Each persona implementation should + /// name the actor and grant them which are appropriate. /// /// - /// Classes which derive from this interface should constructor-inject any dependencies - /// they require, such as those required to grant abilities. + /// Instance of persona classes are resolved from the Dependency Injection container, so it is appropriate + /// and correct to constructor-inject any services which are required in order to get the ability instances. /// /// public interface IPersona : IHasName diff --git a/CSF.Screenplay.Docs/docs/GettingReports.md b/CSF.Screenplay.Docs/docs/GettingReports.md index d01c425d..8e16a004 100644 --- a/CSF.Screenplay.Docs/docs/GettingReports.md +++ b/CSF.Screenplay.Docs/docs/GettingReports.md @@ -17,7 +17,7 @@ The default filename includes a representation of the current timestamp, so each This path may be altered by specifying a different set of options to the [`AddScreenplay`] service collection extension method. [`ScreenplayOptions.ReportPath`]: xref:CSF.Screenplay.ScreenplayOptions.ReportPath -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) ## Converting JSON to a human-readable report diff --git a/CSF.Screenplay.Docs/docs/bestPractice/WritingTests.md b/CSF.Screenplay.Docs/docs/bestPractice/WritingTests.md index 022f98a8..89b4aa1d 100644 --- a/CSF.Screenplay.Docs/docs/bestPractice/WritingTests.md +++ b/CSF.Screenplay.Docs/docs/bestPractice/WritingTests.md @@ -1,6 +1,6 @@ # Advice for writing tests with Screenplay -Whichever testing integration you choose to use, there are some general pieces of advice and best practice which apply. +Whichever testing integration you choose to use, there are some general pieces of advice and best practice which apply. ## Use high-level tasks @@ -38,16 +38,16 @@ Assertion libraries to consider include [Shouldly] and [Fluent Assertions]. ## Do not include assertions in performables It might be tempting to include assertion syntax within [performables] such as [tasks]. -This is not recommended. +This is not recommended. Where assertions appear within performables: * They reduce the reusability of the performables * They create a dependency between your performables and your testing framework/assertion library * They can make test logic harder to read and understand -It is recommended to keep your performables/tasks free from assertions. -In your test code, use [questions] or question-like tasks to get values/data from the system under test. +It is recommended to keep your performables/tasks free from assertions. +In your test code, use [questions] or question-like tasks to get values/data from the system under test. Write your assertions in your main test logic, asserting that the values retrieved are as-expected. [tasks]: ../../glossary/Task.md -[questions]: ../../glossary/Question.md \ No newline at end of file +[questions]: ../../glossary/Question.md diff --git a/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md b/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md index 35efb41c..ebbdbf52 100644 --- a/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md +++ b/CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md @@ -25,7 +25,7 @@ If Screenplay is not being used for testing, then the consuming logic has the fo [Performance]: xref:CSF.Screenplay.IPerformance [`Screenplay`]: xref:CSF.Screenplay.Screenplay -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) [`BeginScreenplay`]: xref:CSF.Screenplay.Screenplay.BeginScreenplay [`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) [`ExecuteAsPerformance`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformance(CSF.Screenplay.Screenplay,System.Func{System.IServiceProvider,System.Nullable{System.Boolean}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md b/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md index f7a3a025..acc94a5a 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md @@ -3,7 +3,7 @@ There are two techniques to add dependency services to Screenplay. You may either integrate Screenplay into an existing container for your application or tests or you may add additional services via the static [`Create`] factory method. -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) ## Integrating with an existing container @@ -29,7 +29,7 @@ var screenplay = Screenplay.Create(services => { }); ``` -[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) ## A reminder on lifetime scopes diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md index 311bbbf7..a8623969 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectIntoThePersona.md @@ -1,4 +1,5 @@ # Injecting services into Personas + Types which derive from [`IPersona`] support constructor-injected dependencies. Personas are typically used by either [the cast] or [the stage] to get an [Actor]. The technique in which they are used means that they are resolved, along with their constructor-injected dependencies, from DI. diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md index c8cf8e35..bbd6fae3 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ConstructorInjectionForBindings.md @@ -1,7 +1,29 @@ # Constructor injection for bindings -For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes. +For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes. Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic. [Reqnroll]: https://reqnroll.net/ [commonly-used Screenplay services]: InjectableServices.md + +## Example + +This is an example of a Reqnroll-style binding class with a constructor-injected dependency. +This would require [installing the Reqnroll test integration] to run. +Take note of the `stage` parameter in the primary constructor. + +```csharp +[Binding] +public class SampleSteps(IStage stage) +{ + [Given("Simon shows a sample step")] + public async Task GivenSimonShowsASampleStep() + { + var simon = stage.Spotlight(); + + // ... use the actor with some performables, then make assertions etc + } +} +``` + +[installing the Reqnroll test integration]: ../gettingStarted/reqnroll/index.md diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/DependencyInjectionScope.md b/CSF.Screenplay.Docs/docs/dependencyInjection/DependencyInjectionScope.md index 4f3b7a52..a9732386 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/DependencyInjectionScope.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/DependencyInjectionScope.md @@ -5,14 +5,18 @@ uid: DependencyInjectionScopeArticle # Dependency injection scope Developers familiar with dependency injection are likely to be familiar with the concept of [DI Scopes]. -That is - some services which are designated as _scoped_ or _instance per scope_ use a common/shared instance for the lifetime/duration of the scope. +Screenplay uses this concept; a number of its services are added to the container with _a per-scope lifetime_. +Screenplay creates **a new DI scope per [Performance]**. +As you can see on [the diagram of how Actors, Abilities and Performables relate to one another], each [`Screenplay`] contains and executes many performances. +This scope-creation is handled automatically by the Screenplay framework logic. -Within Screenplay logic, a DI scope is automatically created, with a lifetime matching that of the current [Performance]. Within a performance, when any of the scoped services (listed below) are injected, each point of injection will receive the same shared instance of that service. Instances are independent per-performance; each performance gets its own shared instance of each of the listed services. [DI Scopes]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service-lifetimes [Performance]: xref:CSF.Screenplay.Performance +[the diagram of how Actors, Abilities and Performables relate to one another]: ../concepts/MakeupOfAScreenplay.md +[`Screenplay`]: xref:CSF.Screenplay.Screenplay ## List of scoped services @@ -25,6 +29,7 @@ The following services are added to DI "per lifetime scope". ## List of singleton services The following services are added to DI as singletons. +There is only ever a single instance of these services per Screenplay. * [The Screenplay](xref:CSF.Screenplay.Screenplay) * [The event bus](xref:CSF.Screenplay.Performances.PerformanceEventBus) diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md index 2a36f6fc..e2fa4f90 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/InjectionForStandaloneScreenplay.md @@ -1,17 +1,59 @@ # Dependency injection for standalone Screenplay -If you are [using Screenplay standalone] then the [`Screenplay.ExecuteAsPerformanceAsync`] permits resolution of dependencies via its parameter. -That parameter is a `Func>`. -The service provider may be used to resolve dependency services for the performance's logic. - -Developers are urged to consider encapsulating their performance logic in implementations of [`IHostsPerformance`]. -Through an overload (extension method) named [`ExecuteAsPerformanceAsync`], developers may specify the concrete implementation of that interface. -This extension method will resolve that implementation type along with any of its constructor-injected dependencies. -This avoids the service locator anti-pattern and provides a convenient pattern by which to write performance logic. - -Use services resolved from the service provider, or injected into your [`IHostsPerformance`] implementation, to get access to [commonly-used Screenplay services] and anything else required at the root level of your performance logic. +If you are [using Screenplay standalone] then you are likely using the method [`Screenplay.ExecuteAsPerformanceAsync`] or one of its overloads. [using Screenplay standalone]: ../gettingStarted/nonTesting/index.md [`Screenplay.ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) + +## Recommended: Use `IHostsPerformance` + +Developers are urged to consider encapsulating their performance logic in a class which derives from [`IHostsPerformance`]. +Through an overload (extension method) named [`ExecuteAsPerformanceAsync`], developers may specify the concrete implementation type of that class as a generic type parameter. +This overload of `ExecuteAsPerformanceAsync` will resolve your class which derives from `IHostsPerformance` from dependency injection (_remember to add it to the container!_). +This resolution will include all the class' dependencies which are constructor-injected and leads to very clean code. +The service provider is never exposed, thus removing the temptation to use the service locator anti-pattern. + [`IHostsPerformance`]: xref:CSF.Screenplay.IHostsPerformance [`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken) + +### Example of the performance host technique + +This example is functionally identical to the example below. + +```csharp +public class MyPerformance(ICast cast) : IHostsPerformance +{ + public Task ExecutePerformanceAsync(CancellationToken cancellationToken) + { + var aaron = cast.GetActor(); + // Further performance logic ... + } +} + +// To consume the above: +await screenplay.ExecuteAsPerformanceAsync(); +``` + +## Alternative: Use a function + +The original [`Screenplay.ExecuteAsPerformanceAsync`] overload accepts up to three parameters, the first of which is a function. +That function should contain the logic which makes up the performance. +The function (which drives the performance logic) provides two parameters itself: + +* An [`IServiceProvider`](xref:System.IServiceProvider) +* A [`CancellationToken`](xref:System.Threading.CancellationToken) + +The serivce provider may be used to resolve services which may be used by the performance logic. + +### Example of the function-based technique + +This example is functionally identical to the example above. + +```csharp +await screenplay.ExecuteAsPerformanceAsync((services, cancellationToken) => { + var cast = services.GetRequiredService(); + var aaron = cast.GetActor(); + // Further performance logic ... +}); + +``` diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md b/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md index 50cbe7a0..ebdee2fc 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/ParameterInjectionForTests.md @@ -1,10 +1,28 @@ # Parameter injection for tests -For testing frameworks which are based on **test methods** such as [NUnit], services are injected via _method parameter injection_. This is performed directly into each test method which is decorated with the `[Screenplay]` attribute. - -[If Screenplay were to be extended] to work with frameworks such as xUnit or MSTest, then this same technique would be used. +For testing frameworks which are based on **test methods** such as [NUnit], services are injected via _method parameter injection_. This is performed directly into each test method which is decorated with the `[Screenplay]` attribute. Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic. +[If Screenplay were to be extended] to work with frameworks such as xUnit or MSTest, then this same technique would be used. + [NUnit]: https://nunit.org/ [If Screenplay were to be extended]: ../extendingScreenplay/TestIntegrations.md -[commonly-used Screenplay services]: InjectableServices.md \ No newline at end of file +[commonly-used Screenplay services]: InjectableServices.md + +## Example + +This is an example of an NUnit-style test method with a parameter-injected dependency. +This would require [installing the NUnit test integration] to run. +Take note of the Screenplay attribute and the `cast` parameter. + +```csharp +[Test, Screenplay] +public async Task TheSampleTestShouldBeInformative(ICast cast) +{ + var janeSmith = cast.GetActor(); + + // ... use the actor with some performables, then make assertions etc +} +``` + +[installing the NUnit test integration]: ../gettingStarted/nunit3/index.md diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/Performables.md b/CSF.Screenplay.Docs/docs/dependencyInjection/Performables.md index 5b7a69c2..dd62e744 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/Performables.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/Performables.md @@ -6,13 +6,13 @@ This means that dependency services _cannot be constructor-injected_ into perfor ## Performables' state represents parameters Performables are [created using the builder pattern] and do not hold references to any dependency services. -Instead, the state of the performable represents the _'parameters'_ for that performable. -This state is typically set via constructor parameters or public settable properties. +Instead, the state of the performable (and thus their constructor parameters) represent the _parameters for the performable's behaviour_. +This state should be set via constructor parameters and should be immutable. ### An example For example, a [Task] which adds an item to the user's shopping basket might accept a constructor parameter representing the unique ID of the product to be added to the basket. -That unique ID would be stored within the performable instance, in a field or property for example. +That unique ID would be stored within the performable instance, in a `readonly` field. When the relevant `ExecuteAsync` method is executed, the unique ID stored in the performable instance is used to conduct whatever logic is appropriate. ## Use abilities to get dependencies @@ -22,7 +22,7 @@ In this sense, Abilities are a form of dependency injection or service locator ( Whilst service locators are usually considered an anti-pattern, the benefits in this specific case outweigh the disadvantages. The primary benefit is [the ability to create performables from static builders]. -Importantly, _**[Tasks]** should never make use of [Abilities]_. +Generally-speaking, _[**Tasks** should not make use of Abilities]_. Only Actions and Questions should use Abilities. [Performable]: ../../glossary/Performable.md @@ -30,6 +30,7 @@ Only Actions and Questions should use Abilities. [Task]: ../../glossary/Task.md [Actions]: ../../glossary/Action.md [Questions]: ../../glossary/Question.md +[Abilities]: ../../glossary/Ability.md [`GetAbility`]: xref:CSF.Screenplay.ActorExtensions.GetAbility``1(CSF.Screenplay.ICanPerform) [the ability to create performables from static builders]: ../builderPattern/index.md -[Tasks]: ../../glossary/Task.md +[**Tasks** should not make use of Abilities]: ../writingPerformables/TasksDoNotUseAbilities.md diff --git a/CSF.Screenplay.Docs/docs/dependencyInjection/index.md b/CSF.Screenplay.Docs/docs/dependencyInjection/index.md index 81760bae..63fe0551 100644 --- a/CSF.Screenplay.Docs/docs/dependencyInjection/index.md +++ b/CSF.Screenplay.Docs/docs/dependencyInjection/index.md @@ -10,27 +10,13 @@ Alternatively you may create an instance of [`Screenplay`] which uses its own se [dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection [container]: xref:Microsoft.Extensions.DependencyInjection.IServiceCollection -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) [`Screenplay`]: xref:CSF.Screenplay.Screenplay -[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) -## Scopes +## Learn more about DI in Screenplay -Screenplay makes use of [Dependency Injection Scopes]. -As you can see on [the diagram of how Actors, Abilities and Performables relate to one another], each [`Screenplay`] contains and executes many [Performances]. -Each performance is executed within its own DI scope, thus scoped services will have _one shared instance per performance_. - -It is also important to note that within DI, the [`Screenplay`] object _must be added_ as a singleton (the mechanisms described above all do this for you). -As a consequence there may be _at most one_ Screenplay object per dependency injection container. - -[Dependency Injection Scopes]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scoped -[the diagram of how Actors, Abilities and Performables relate to one another]: ../concepts/MakeupOfAScreenplay.md -[Performances]: xref:CSF.Screenplay.IPerformance - -## Using DI in Screenplay - -TODO: Write this section! - -* [Injecting services](InjectingServices.md) -* [Adding services](AddingServices.md) -* [Abilities as a form of DI](../../glossary/Ability.md) +* [How to add services to the container](AddingServices.md) +* [Which services are injectable](InjectableServices.md) +* [Getting/injecting services](InjectingServices.md) +* [DI scopes](DependencyInjectionScope.md) diff --git a/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md b/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md index c04f84d0..47d98d44 100644 --- a/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md +++ b/CSF.Screenplay.Docs/docs/extendingScreenplay/TestIntegrations.md @@ -37,8 +37,8 @@ Terminologies differ between testing frameworks; the word [Scenario] is used to [`Screenplay`]: xref:CSF.Screenplay.Screenplay [Scenarios]: ../../glossary/Scenario.md -[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions}) -[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions}) +[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection) +[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection}) [`IGetsScreenplay`]: xref:CSF.Screenplay.IGetsScreenplay [`BeginScreenplay()`]: xref:CSF.Screenplay.Screenplay.BeginScreenplay [`CompleteScreenplay()`]: xref:CSF.Screenplay.Screenplay.CompleteScreenplay diff --git a/CSF.Screenplay.Docs/docs/toc.yml b/CSF.Screenplay.Docs/docs/toc.yml index ecc58f7a..f0a8aa0b 100644 --- a/CSF.Screenplay.Docs/docs/toc.yml +++ b/CSF.Screenplay.Docs/docs/toc.yml @@ -26,6 +26,26 @@ href: concepts/HowScreenplayAndPerformanceRelate.md - name: Dependency injection href: dependencyInjection/index.md + items: + - name: Adding services + href: dependencyInjection/AddingServices.md + - name: Injectable services + href: dependencyInjection/InjectableServices.md + - name: Injecting services + href: dependencyInjection/InjectingServices.md + items: + - name: Test parameter injection + href: dependencyInjection/ParameterInjectionForTests.md + - name: Test binding injection + href: dependencyInjection/ConstructorInjectionForBindings.md + - name: Standalone Screenplay + href: dependencyInjection/InjectionForStandaloneScreenplay.md + - name: Persona classes + href: dependencyInjection/ConstructorInjectIntoThePersona.md + - name: Into performables + href: dependencyInjection/Performables.md + - name: DI scope + href: dependencyInjection/DependencyInjectionScope.md - name: In the testing stack href: concepts/ScreenplayInTheTestingStack.md - name: Extensions