Open 13dante04 opened 1 week ago
@13dante04 Thank you for the repro project. It helped a lot to analyze the issue. Pretty complex.
In order to understand the problem, please let me summarize first how the Reqnroll MSDI plugin works (this is not the full behavior, but the core concept):
[ScenarioDependencies]
method that creates an IServiceCollection
. This is not a service provider yet, just a recipe to create a service provider.CreateScope
method on the root service provider to create a sub-service provider for the execution of the scenario. So there is another service provider created.[ScenarioDependencies]
it is normally resolved from the sub-service provider. Also if you register things with services.AddTransient<SomeType>(sp => do something with sp)
, the provided sp
is the sub-service provider.Your problem seems to be rooted in how the AddHttpClient
extension method and the returned IHttpClientBuilder
works. Due to the internal behavior of this, it always operates on the root service provider and ignores any sub-service providers created by CreateScope
. There is a related issue in the related repo: https://github.com/dotnet/extensions/issues/521
Basically the suggestion there is not to use a message handler that has a reference to any context-specific state, but pass any state required through the Properties
collection of the http request. Something similar is mentioned in the MSDI docs as well.
For me the fundamental question is whether the sub-scoping our plugin does is really a good idea or we should rather build a service provider for each scenario execution based on the service collection. (Although with this registering shared, singleton objects would not be possible easily.) Your input is appreciated on this, because it seems we need to re-design the MSDI plugin anyway.
For you (I guess) the question is if we can have a workaround. I played with it and found two worarounds:
In your example, there is a SomeHttpClient
class that wraps the functions of the HttpClient. This class can have a dependency to the ScenarioContext
and in the methods (like in the PostAsync
method in your example), it could build the HttpRequestMessage
and set the ScenarioContext
to the Properties
(or Options
).
The message handler can then get the ScenarioContext
from the request and work with it.
Since you cannot use the AddHttpClient
method on the root service collection, alternatively you can create a brand new service collection for each scenario and build an additional service provider to resolve the HttpClient needs. The only trick is that in the normal service provider that is built based on the [ScenarioDependencies]
method, you need to "forward" these requests to the new service provider. Here is a code file (a replacement of the DependencyInjection.cs
in your sample) that does this:
using System;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Reqnroll.BoDi;
using Reqnroll.Infrastructure;
using Reqnroll.Microsoft.Extensions.DependencyInjection;
using Reqnroll.MSDI.Clients;
namespace Reqnroll.MSDI;
[Binding]
public class DependencyInjection
{
[ScenarioDependencies]
public static IServiceCollection CreateContainer()
{
var services = new ServiceCollection();
// this two registration is not needed
//services.AddSingleton<ReqnrollOutputHelper>();
//services.AddTransient<CustomHttpMessageHandler>();
// moved to InitScenarioServiceProviderForHttpClient
// services.AddHttpClient<SomeHttpClient>().ConfigurePrimaryHttpMessageHandler(sp => new CustomHttpMessageHandler(sp));
// resolve SomeHttpClient from the scenarios specific service provider
services.AddTransient<SomeHttpClient>(sp => sp.GetRequiredService<IContextManager>().ScenarioContext.ScenarioContainer.Resolve<SomeHttpClient>());
return services;
}
[BeforeScenario(Order = -1)]
public void InitScenarioServiceProviderForHttpClient(IObjectContainer objectContainer)
{
var services = new ServiceCollection();
// initialize Http Client handling
services.AddHttpClient<SomeHttpClient>().ConfigurePrimaryHttpMessageHandler(sp => new CustomHttpMessageHandler(sp));
// "forward" ScenarioContext request to the Reqnroll container
services.AddTransient(_ => objectContainer.Resolve<ScenarioContext>());
// make a service provider to handle the HttpClients within this scenario execution
var serviceProvider = services.BuildServiceProvider();
// allow resolving SomeHttpClient via the specific service provider
objectContainer.RegisterFactoryAs(() => serviceProvider.GetRequiredService<SomeHttpClient>());
}
}
Reqnroll Version
2.2.1
Which test runner are you using?
xUnit
Test Runner Version Number
2.8.2
.NET Implementation
.NET 8.0
Test Execution Method
ReSharper Test Runner
Content of reqnroll.json configuration file
No response
Issue Description
When trying to inject ScenarioContext in an HttpClient handler, when using Microsoft DI. This error occurs
Use case is, setting data in the scenario context, which is later on set as a request header.
In this specific scenario, I'm trying to get the scenario context when overriding the SendAsync method.
This happens during test execution, when sending an http request, not before test execution as the message says
There is a repro project but here's also the block where scenario context is being accessed.
Entire stack trace attached as a file. trace.txt
Steps to Reproduce
1) Use Reqnroll.Microsoft.Extensions.DependencyInjection; 2) Add an HttpClient with a custom HttpMessageHandler 3) Inject ScenarioContext in the HttpMessageHandler
4) Get error message
Link to Repro Project
https://github.com/13dante04/Reqnroll.MSDI