Given some class that relies on a constructor parameter that can only be obtained at runtime, how do I ensure instances of that class have their lifetimes still be controlled by the Microsoft DI container if I'm using a custom factory to bypass the runtime parameter limitation?
For example, say we have a interface/class pair such as:
interface IFooService
{
void DoServicyStuff();
}
class MyFooService
{
public MyFooService(string somePeskyRuntimeArgument)
{
this.peskyValue = somePeskyRuntimeArgument;
}
void DoServicyStuff()
{
// do some stuff here with the peskyValue...
}
}
The most common practice to deal with these types of runtime parameters is to build a factory class to basically transfer the parameter from the constructor to a method:
interface IFooServiceFactory
{
IFooService CreateService(string heyItsNowAMethodLevelPeskyParameter);
}
class FooServiceFactory
{
public IFooService CreateService(string heyItsNowAMethodLevelPeskyParameter)
{
return new MyFooService(heyItsNowAMethodLevelPeskyParameter);
}
}
The factory then allows us to provide the value at runtime to create the service and then use the service. There is a problem however: the factory takes full ownership of creating the value, including its lifetime.
Now, what if I want IFooService
instance to be scoped to the current request, the equivalent of the Scoped
lifetime in the container?
If IFooService
is resolved directly from the container, this is straightforward:
services.AddScoped<IFooService, MyFooService>()
However, we cannot resolve it directly from the container because of the runtime argument. .NET Core's container abstraction allows us to build an instance with runtime parameters, but then the container registration lifetime is not honored and it only works on concrete types.
The following doesn't work, because IFooService
is not a concrete type:
class FooServiceFactory
{
private readonly IServiceProvider serviceProvider;
public FooServiceFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider
}
public IFooService CreateService(string heyItsNowAMethodLevelPeskyParameter)
{
return ActivatorUtilities.CreateInstance<IFooService>(
this.serviceProvider,
heyItsNowAMethodLevelPeskyParameter);
}
}
And this also doesn't work, because FooService
is always created regardless of the setup in the container:
class FooServiceFactory
{
private readonly IServiceProvider serviceProvider;
public FooServiceFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider
}
public IFooService CreateService(string heyItsNowAMethodLevelPeskyParameter)
{
return ActivatorUtilities.CreateInstance<MyFooService>(
this.serviceProvider,
heyItsNowAMethodLevelPeskyParameter);
}
}
Even if MyFooService
is directly registered in the container (without its interface), ActivatorUtilities
will still ignore the registration and just create a new one every time it is called.
How can I rely on the container lifetime handling mechanism while still working with a factory like this to resolve a class that requires runtime parameters? If the parameter value doesn't change across multiple calls in the same request, then it should respect whatever is configured in the container, be it Scoped, Singleton, Transient, etc.