How to pass a value from an injected service to another one during DI registration
Delegates to the rescue
Assumptions and friction
The Microsoft Dependency Injection (DI) built into modern Dotnet is easy to use and often chained in a builder-like pattern. By shipping software packages with an intuitive setup, a dev can often add functionality by simply adding a Nuget, and then adding it to a service collection during app startup. In most cases this works great and we can be off and running in no time.
Every now and then I come across a Nuget that bakes in an assumption about the setup. The assumption comes in the form of “you’ll add your connection string or whatever it is — by way of the config files”. In the era of best-practices around security, putting secrets into the config is a big no-no. Rather what we often need to do is pull a secret out of a vault like those offered by AWS and Azure instead of committing them to source.
Hmmmmm

To illustrate the issue, consider the following setup extension method for a hypothetical package you just downloaded.
var services = new ServiceCollection();
var configurationManager = new ConfigurationManager();
services.AddCoolThing(new CoolThingOptions
{
SomeSetting = configurationManager.GetSection("CoolThing")["SomeValue"]
});
In the above code it takes a CoolThingOptions
and so long as we provide the values, the package works great. The problem comes in the form of having to grab that setting from another service (like a secrets manager). There’s no reference to the IServiceProvider
in the setup method signature so that we can resolve the secrets service and provide the value to SomeSetting
.
A naive approach
I’ve made the mistake of trying to overcome this problem by trying something like the following:
...
services.AddSecretsManager();
...
var serviceProvider = services.BuildServiceProvider();
services.AddCoolThing(new CoolThingOptions
{
SomeSetting = serviceProvider.GetService<SecretService>().GetSecret("someKey")//
});
...
While the above code actually does work, it’s quite smelly b/c we really should only build the service provider once in our setups. Calling it multiple times during an app setup risks having unexpected behaviors as you are creating more than one root scope provider. I strongly recommend against doing this.
We can do better.
Delegates to the rescue
What we really need to do is create a delegate factory function that will get called in the future instead of immediately. The factory will be provided with the future already-built services provider. From this we can resolve our “other service” (secrets manager), then pass that to the options. For what it’s worth, the pattern you’re about to see is one I learned this pattern from digging thru Entity Framework setup code.
Now you may be wondering, “well I have to use the DI setup extension that ships with the package, I can’t alter the one that ships with it”. Fair, so what we’ll likely have to do is replace the one provided with one that suits our needs. Fortunately most software can be inspected with the built-in decompiler or simply on an open-source repository. For our example, we’ll just use our hypothetical one that we’ll pretend is made by someone else:
public static IServiceCollection AddCoolThing(this IServiceCollection services, CoolThingOptions options)
{
services.AddSingleton(options);
services.AddScoped(serviceProvider => new CoolThing(serviceProvider.GetService<CoolThingOptions>()));
return services;
}
Notice that Microsoft already has the ability to use a service provider with the
AddScoped|Singleton|Transient
methods. The trick we need to do here is hoist up the delegate so that we can use put it in theAddCoolThing()
signature.
So we can simply just do the work of AddCoolThing
but in a way that suits us:
public static IServiceCollection AddCoolThingOptions(this IServiceCollection services, Func<IServiceProvider, CoolThingOptions> func)
{
services.AddScoped(serviceProvider =>
{
return func(serviceProvider);
});
return services;
}
Here we’re just exposing the delegate Microsoft does and then the usage becomes:
services.AddCoolThingOptions(serviceProvider => new CoolThingOptions
{
SomeSetting = "" //now you can resolve your service like a secrets manager properly.
//SomeSetting = serviceProvider.GetService<SecretsManager>().GetSecret("foo")
});
//and then all we have to do is register the cool thing either here or in another setup method with another factory delegate
services.AddScoped(serviceProvider => new CoolThing(serviceProvider.GetService<CoolThingOptions>()));
That’s it, now you can register your downloaded package but also integrate with another service to help construct the options.
Wrapping Up
While it may seem obvious after you break down the problem, it might seem impossible until you realize all the setup extensions are doing is registering dependencies as usual. All we had to do was expose the service provider factory and we easily overcame the problem. There’s a full working project located here for you to review, happy coding!