-
Notifications
You must be signed in to change notification settings - Fork 108
Introduce ServiceProviderLifetimeType for scoped dependency container #998
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
base: main
Are you sure you want to change the base?
Introduce ServiceProviderLifetimeType for scoped dependency container #998
Conversation
| } | ||
|
|
||
| var assemblies = testRunnerManager.BindingAssemblies; | ||
| var assemblies = _testRunnerManager.BindingAssemblies ?? _bindingRegistryBuilder.GetBindingAssemblies(_testAssemblyProvider.TestAssembly); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure whether we still need the _testRunnerManager.BindingAssemblies here if we've got the other fallback.
|
TODO:
|
… lifetimes and refactor related logic in ServiceCollectionFinder.
…I integration guide.
98ec6a3 to
95086f7
Compare
| ``` | ||
|
|
||
| ## Step by step walkthrough of using Reqnroll.Microsoft.Extensions.DependencyInjection | ||
| ## Install the plugin from NuGet into your Reqnroll project |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I restructured this documentation a bit because it wasn't flowing super well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request introduces the ServiceProviderLifetimeType enum to control when the Microsoft Dependency Injection service provider is created and destroyed, extending beyond the previous global-only lifetime. The implementation adds support for Global (default), TestThread, Feature, and Scenario lifetimes.
Changes:
- Added
ServiceProviderLifetimeTypeenum with four lifetime options - Extended
ScenarioDependenciesAttributewithServiceProviderLifetimeproperty - Modified
ServiceCollectionFinderto cache and expose the configured lifetime - Updated
DependencyInjectionPluginto register service providers at appropriate lifecycle events based on configured lifetime - Enhanced documentation with usage examples and explanations
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/integrations/dependency-injection.md | Added documentation for new ScopeLevel and ServiceProviderLifetime configuration options |
| Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ScenarioDependenciesAttribute.cs | Added ServiceProviderLifetimeType enum and ServiceProviderLifetime property |
| Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/IServiceCollectionFinder.cs | Added GetServiceProviderLifetime method to interface |
| Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs | Implemented lifetime caching and retrieval, refactored cache structure |
| Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionPlugin.cs | Added lifecycle event handlers for TestThread, Feature, and Scenario lifetimes |
| Tests/Reqnroll.PluginTests/Microsoft.Extensions.DependencyInjection/ServiceCollectionFinderTests.cs | Added tests for custom lifetime retrieval and binding assembly fallback |
| CHANGELOG.md | Documented the new feature and added contributor |
Comments suppressed due to low confidence (1)
Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/DependencyInjectionPlugin.cs:140
- There is a potential issue with the scope management behavior when ServiceProviderLifetime is set to Scenario. When a new RootServiceProviderContainer is created for each scenario (lines 121-124), but then the code also checks if spContainer.Scoping == ScopeLevelType.Scenario (line 129) to create additional scopes. This could lead to confusion because:
- If ServiceProviderLifetime is Scenario, the entire provider is rebuilt per scenario
- The ScopeLevel determines if additional scopes are created on top of the provider
However, creating a new scope on top of a per-scenario provider doesn't make semantic sense, as each scenario already has its own provider. Consider either documenting this behavior clearly or adding validation to prevent nonsensical configurations like ServiceProviderLifetime=Scenario with ScopeLevel=Feature.
private static void CustomizeScenarioDependenciesEventHandler(object sender, CustomizeScenarioDependenciesEventArgs args)
{
var serviceCollectionFinder = args.ObjectContainer.Resolve<IServiceCollectionFinder>();
var lifetime = serviceCollectionFinder.GetServiceProviderLifetime();
if (lifetime == ServiceProviderLifetimeType.Scenario)
{
args.ObjectContainer.RegisterFactoryAs<RootServiceProviderContainer>(() => BuildRootServiceProviderContainer(args.ObjectContainer));
}
// 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;
});
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
.../Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs
Outdated
Show resolved
Hide resolved
|
|
||
| ### Configuring the scope and lifetime of the service provider | ||
|
|
||
| For services registered with a scoped lifetime (as opposed to singleton), it might make sense to have a new scope for each scenario rather than each feature (the default). If this is the case, this can be adjusted with the `ScopeLevel` property on the `[ScenarioDependencies]` attribute. For example |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states "it might make sense to have a new scope for each scenario rather than each feature (the default)" but according to the code, ScopeLevelType.Scenario is actually the default (as seen in ScenarioDependenciesAttribute.cs line 48). The documentation incorrectly implies that Feature is the default scope level.
| For services registered with a scoped lifetime (as opposed to singleton), it might make sense to have a new scope for each scenario rather than each feature (the default). If this is the case, this can be adjusted with the `ScopeLevel` property on the `[ScenarioDependencies]` attribute. For example | |
| For services registered with a scoped lifetime (as opposed to singleton), it might make sense to have a new scope for each feature instead of the default per-scenario scope. If this is the case, this can be adjusted with the `ScopeLevel` property on the `[ScenarioDependencies]` attribute. For example |
| ```csharp | ||
| [ScenarioDependencies(ScopeLevel = ScopeLevelType.Scenario)] | ||
| public static IServiceCollection CreateServices() | ||
| ``` |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code example is missing the closing brace and return statement for the CreateServices method. The example should include the complete method body to be valid compilable code.
| ```csharp | ||
| [ScenarioDependencies(ServiceProviderLifetime = ServiceProviderLifetimeType.Feature)] | ||
| public static IServiceCollection CreateServices() | ||
| ``` |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code example is missing the closing brace and return statement for the CreateServices method. The example should include the complete method body to be valid compilable code.
| private void PopulateCache() | ||
| { | ||
| if (_cache != default) | ||
| { | ||
| return _cache; | ||
| return; | ||
| } |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _cache field is accessed and modified without any synchronization mechanism. In concurrent test execution scenarios, multiple threads could potentially call GetServiceProviderLifetime() or GetServiceCollection() simultaneously, leading to race conditions in PopulateCache(). The check at line 47 and the assignment at line 68 are not atomic, which could result in PopulateCache() being executed multiple times or cache being accessed before it's fully populated. Consider adding thread synchronization (e.g., lock statement or Lazy) to ensure thread-safe cache initialization.
| ``` | ||
|
|
||
| ```{note} | ||
| If the `ServiceProviderLifetime` is set to `Scenario` then the `ScopeLevel` is implicitly `Scenario` as well. |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states that "If the ServiceProviderLifetime is set to Scenario then the ScopeLevel is implicitly Scenario as well," but there's no code implementation that enforces this constraint. The ScenarioDependenciesAttribute allows both properties to be set independently without any validation or automatic adjustment. Either the code should be updated to enforce this implicit relationship, or the documentation should be corrected to reflect the actual behavior.
| If the `ServiceProviderLifetime` is set to `Scenario` then the `ScopeLevel` is implicitly `Scenario` as well. | |
| `ServiceProviderLifetime` and `ScopeLevel` are configured independently. If you want both the service provider and the scope to be per-scenario, you must set both `ServiceProviderLifetime = ServiceProviderLifetimeType.Scenario` and `ScopeLevel = ScopeLevelType.Scenario` explicitly. |
| [Fact] | ||
| public void GetServiceProviderLifetime_ReturnsCorrectLifetime() | ||
| { | ||
| // Arrange | ||
| var sut = CreateServiceCollectionFinderWithMocks(typeof(ValidStartWithFeatureLifetime)); | ||
|
|
||
| // Act | ||
| var lifetime = sut.GetServiceProviderLifetime(); | ||
|
|
||
| // Assert | ||
| lifetime.Should().Be(ServiceProviderLifetimeType.Feature); | ||
| } |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new GetServiceProviderLifetime method and the changes to ServiceCollectionFinder lack comprehensive test coverage. Tests only cover the Feature lifetime scenario, but there are no tests for TestThread, Scenario, or the default Global lifetime values. Consider adding test cases for these additional lifetime types to ensure the implementation works correctly for all supported scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expanded tests to cover all states of the service provider lifetime
| private static void CustomizeTestThreadDependenciesEventHandler(object sender, CustomizeTestThreadDependenciesEventArgs args) | ||
| { | ||
| var serviceCollectionFinder = args.ObjectContainer.Resolve<IServiceCollectionFinder>(); | ||
| var lifetime = serviceCollectionFinder.GetServiceProviderLifetime(); | ||
|
|
||
| if (lifetime == ServiceProviderLifetimeType.TestThread) | ||
| { | ||
| args.ObjectContainer.RegisterFactoryAs<RootServiceProviderContainer>(() => BuildRootServiceProviderContainer(args.ObjectContainer)); | ||
| } | ||
| } |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new CustomizeTestThreadDependenciesEventHandler method lacks any test coverage. Since this is a new method that handles the TestThread lifetime configuration, it should have corresponding tests to verify that the RootServiceProviderContainer is registered correctly when ServiceProviderLifetime is set to TestThread.
… `ScopeLevel` with `ServiceProviderLifetime`. Improve test coverage and update documentation.
🤔 What's changed?
The Microsoft Dependency Injection plugin for Reqnroll provides no way of limiting the lifetime of the service provider, with the current implementation being at a global level. Some tests would benefit from this container being rebuilt at a feature or scenario level. This implementation adds that ability with a new property on the ScenarioDependenciesAttribute class, and uses that throughout the rest of the plugin stack.
⚡️ What's your motivation?
See above, since I have projects that would benefit from this :)
🏷️ What kind of change is this?
♻️ Anything particular you want feedback on?
Correctness :)
📋 Checklist: