Problem Statement: I have a custom IConfigurationProvider
that requires a complex service to function properly. This complex service would, naturally be registered to the applications IServiceProvider
. I want to use IServiceCollection
/IServiceProvider
facilities in conjunction with IConfigurationProvider
to avoid manual new of this complex service, and to re-use registration code that would otherwise be written in the normal part of the DI container building portion of the app.
I've found plenty of documentation describing the troubles of needing an IServiceProvider
in an IConfigurationProvider
. This is the closest thing that felt ok to me, and is the inspiration for this post.
Here's my approach at a high level
- Build the configuration up enough to construct the intermediate
IServiceProvider
- Build the intermediate
IServiceProvider
- Build the rest of the configuration via custom
IConfigurationProvider
's that require special services, retrieved viaintermediateServiceProvider.GetRequiredService<T>();
- Transfer the registrations and, specifically, singleton objects, from the intermediate
IServiceCollection
/IServiceProvider
to the finalIServiceCollection
/IServiceProvider
. This will help avoid re-registering things in step #5 and will help avoid second instances of singletons in the finalIServiceProvider
. - Register the final set of services to complete the final
IServiceProvider
, using configuration that was injected in step #4.
#1,#2,#3,#5 are simple enough. #4 is where I'm hitting roadblocks. My first attempt at #4 was the following
foreach (var sd in intermediateServiceCollection)
{
if (sd.Lifetime == ServiceLifetime.Singleton)
{
// Externally owned
if (sd.ImplementationInstance != null)
{
finalServiceCollection.AddSingleton(sd.ServiceType, sd.ImplementationInstance);
}
// Provide a factory function to delegate to intermediate service provider
else
{
finalServiceCollection.AddSingleton(sd.ServiceType,
s => intermediateServiceProvider.GetRequiredService(sd.ServiceType));
}
}
// Transient/scoped service descriptors can be forwarded along without issue
else
{
finalServiceCollection.Add(sd);
}
}
As documented here, registering open-generic types with a factory function is not supported.
After stumbling upon this limitation, my latest approach looks like:
foreach (var sd in intermediateServiceCollection)
{
if (sd.Lifetime == ServiceLifetime.Singleton)
{
// Externally owned
if (sd.ImplementationInstance != null)
{
finalServiceCollection.AddSingleton(sd.ServiceType, sd.ImplementationInstance);
}
// Provide a factory function to delegate to intermediate service provider
else if (!sd.ServiceType.IsGenericType)
{
finalServiceCollection.AddSingleton(sd.ServiceType,
s => intermediateServiceProvider.GetRequiredService(sd.ServiceType));
}
else
{
// Simply adding the service descriptor to the final service collection
// opens the door for singleton instances to be created again
//
// In reality, this may be configurable to raise an exception to signal
// to our developers they need to avoid registering open-generics in the
// bootstrapping portion of the app. But, this may serve it's purpose
// if you can live with multiple instances of a singleton.
finalServiceCollection.Add(sd);
}
}
// Transient/scoped service descriptors can be forwarded along without issue
else
{
finalServiceCollection.Add(sd);
}
}
Obviously, my current implementation is not perfect as it allows for multiple singleton instances if that singleton is registered as an open-generic. But, with an understood limitation of bootstrap registration being non open-generic types, I can "successfully" create an intermediate IServiceProvider
for use within IConfigurationProvider
's and transfer it to the final IServiceProvider
.
- Can anyone provide inspiration that can lead to a complete implementation for #4, specifically around transferring open-generic registrations?
- Is the assumption that this approach is reasonable total nonsense and I should opt for a different pattern to configure my application?
If you use the same configuration provider for intermediate and final service provider and you need the same services in the final service provider as within the intermediate one, why don't you put your whole setup logic of the intermediate provider into a method that gets the target builder as parameter? Then you can first call it to setup your intermediate one and later another time to setup your final provider.
In that case you don't need any kind of reflection and you can use all available extension helper class or own logic to setup the configuration provider.