Autofac
Autofac copied to clipboard
Merge multiple IContainers together created at different points
Problem Statement
I frequently save my keys and credentials in remote stores (e.g. Azure Key Vault) meaning that I have no synchronous access to them during program startup. Nonetheless, I'd like to be able to register types that are constructed with those secrets, including still other types that I use to retrieve those secrets.
Today, I can create that container at the root, but I can now only instantiate those types that I use to retrieve my secrets, so I either have to pull those immediately so I can construct their various types or build my own wrapper classes that do the same lazily down the road.
Autofac doesn't allow for asynchronous registrations (for good enough reason), so I'm not able to simply resolve my secret retrieval types in the registration method and pull the secret right then as needed.
Desired Solution
I'd rather be able to register these retrieval tools in the root container and go about a normal application launch and at some point down the road, create another container, register the new types in that container and merge this new container into the root so other resources can now utilize these new types as required.
Example 1
I open up my app and have a link a Key Vault secret storing my SendGrid API key. I want to use an HttpClient singleton as I'm going to be accessing other remote content in the same lifetime, so I register that. I build the container so I have access to it.
The application is running and I've injected in my HttpClient. After completing various startup tasks, I'm ready to retrieve my API key, so I use the HttpClient to retrieve the SendGrid API key from the Key Vault using an asynchronous method and create a new instance of the SendGrid client with that value. I'm going to want to use this SendGrid client later on from other classes, but now I'm going to have to just pass it into the methods I call since it's too late to register it in the container.
I'd instead ideally like to be able to create another container builder and register SendGrid within it, then merge this new container (somehow) into the root Autofac container so that both the HttpClient and the SendGrid client types are available to downstream services.
Example 2
I spend a lot of time developing Service Fabric applications. While there's a helpful Autofac package for use with Service Fabric (thanks someone!) it registers the services themselves and not the numerous types created alongside these services. Reading and writing from service state is a regular activity, but I frequently like to put these operations into other dedicated classes to keep the entry class cleaner. This means that I have to inject an IReliableStateManager into each of the dedicated state management classes I create, but the concrete type isn't available until my StatefulService has been created.
As such, while I can use Autofac at the root to create the service and some dependencies, there exist a number of types I have to still manually create and manage instances of myself, which is less than stellar.
Ideally, I'd again be able to use the magic of Autofac.ServiceFabric to create the container at the service entry point and spin up the service, then create another container in that service to register all the instances now available in that class and merge these into the root container for downstream use in other types. Yes, I have to guarantee that I'll provide the type registrations before I use them anywhere, but that's not a largely different problem than my current situation of knowing upfront which types are registered and which I'll be manually passing arguments into the constructors for.
Alternatives You've Considered
Example 1
Instead of registering the types that need these secret values, I've instead largely opted to build static async factories that themselves are registered, but which provide a means to retrieve the secret later on with the injected HttpClient and return an instance of the type requested. This isn't ideal though as it means that I'm effectively getting instanced downstream objects and losing out on the performance gains.
Example 2
Skipping the use of Autofac.ServiceFabric. When the service class is started up, create a new ContainerBuilder in the constructor and register all the various types there, and immediately resolve them (as though the service had them injected) to get the downstream DI benefits without keeping the container around.
Additional Context
I think that about covers it, but happy to think of more examples if the above isn't clear enough.
I will say that there is pretty much no way we'll be able to "merge two containers." I get what you're saying, but we'll need to figure out some alternative solution.
The "two container" situation happens a lot, not just in Service Fabric, but also in things like ASP.NET Core setup, where the application initialization creates a container with some minimal stuff (logging, etc.) and then the application itself has a separate container. It sucks, but it's not unprecedented, so that may just be "how it is."
However, we've also seen things like using a child lifetime scope as the root of the application instead of using the container. You can add registrations to a child lifetime scope on the fly.
var builder = new ContainerBuilder();
builder.RegisterType<AppSetupThing>();
var container = builder.Build();
var resolvedThing = container.Resolve<AppSetupThing>();
var thingTheAppNeeds = resolvedThing.GetWhatTheAppNeeds();
using var appRoot = container.BeginLifetimeScope(b => b.RegisterInstance(thingTheAppNeeds));
app.UseThisAsTheContainer(appRoot);
The other option is to make more use of lambda registrations. That won't help as much in an async situation because DI is about construction (synchronous) not potentially-time-consuming async ops.
var builder = new ContainerBuilder();
builder.RegisterType<AppSetupThing>();
builder.Register(ctx =>
{
var resolvedThing = ctx.Resolve<AppSetupThing>();
var thingTheAppNeeds = resolvedThing.GetWhatTheAppNeeds();
return thingTheAppNeeds;
});
var container = builder.Build();
app.UseThisAsTheContainer(container);
The indirection of the lambda can serve as a way to base one registration off another. It's more work than reflection-based registration, but it gets the job done.
Do either of these open up any possibilities?
If not, can you maybe provide some pseudocode showing something more concrete that isn't working with one of these two things? (And, again, if it's async, I'm not sure there's much we can do, or really want to do, about that. Object construction shouldn't be async.)
For what it's worth, I have to do this (async startup/config fetching), and I do something like the following, using modules:
class MyConfig
{
// Whatever you need in here.
}
interface IConfigRetriever
{
Task<MyConfig> GetConfigAsync();
}
class FoundationModule : Module
{
public override void Load(ContainerBuilder builder)
{
builder.RegisterType<VaultRetriever>().As<IConfigRetriever>();
}
}
class AppModule : FoundationModule
{
private MyConfig _appConfig;
public AppModule(MyConfig appConfig)
{
_appConfig = appConfig;
}
public override void Load(ContainerBuilder builder)
{
// Call the base class to make sure our foundation services are still available.
base.Load(builder);
builder.RegisterInstance(_appConfig);
builder.RegisterType<ServiceThatDependsOnMyConfig>();
// Etc etc.
}
}
// In my Program.cs...
var initBuilder = new ContainerBuilder();
initBuilder.RegisterModule(new FoundationModule());
var initContainer = initBuilder.Build();
var configRetreiver = initContainer.Resolve<IConfigRetriever>();
var config = await configRetreiver.GetConfigAsync();
var appBuilder = new ContainerBuilder();
appBuilder.RegisterModule(new AppModule(config));
var appContainer = appBuilder.Build();
// Use appContainer as per usual.
This obviously has two completely isolated containers, but it's not normally an issue to recreate the foundation services, since most of the underlying IO resources external vault services consume (DbConnection, HttpClient, etc) are connection pooled anyway using static fields, rather than being attached to object lifetimes.
@alistairjevans That's awfully similar to what I do today though I like your use of modules to "hide" the setup bits. All that said though, it delays initial app startup as the types have to be instantiated and available to register to build the appContainer before the rest of the show kicks off.
Ideally, I'd be able to startup the service quickly with the bare minimum of registrations as because each service largely executes in a CQRS-like, external-service-call fashion, ideally it'd be able to pull the credentials and handle the rest of setup after otherwise being in a ready state. In other words, a semi-lazy setup, but in a way that allows for asynchronous activity to occur between the "definition" and whenever it's actually invoked so it can happen when there's downtime in the service. We've got this already with delayed instantiations, but it's the async piece that throws this off.
@tillig Might there be room for an async variation of the delayed instantiatios to handle this? I know in advance everything that I need to create the type, I just don't have the secret value I need to pass into it (yet). Being able to just set this up in one place along with the other registrations and know that when the async operation is complete, the rest of the delayed instantiation would Just Work would pretty much solve what I'm looking for.
What about something along these lines?
//Using Stephen Toub's AsyncLazy implementation https://devblogs.microsoft.com/pfxteam/asynclazyt/
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Factory.StartNew(valueFactory)) { }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
public class SendGridClient
{
public SendGridClient(string apiKey) {
//...
}
}
public class SendGridClientFactory {
private AsyncLazyFunc<SendGridClient> _client;
private readonly KeyVaultFetcher _kvFetcher;
public SendGridClientFactory(AsyncLazyFunc<SendGridClient> client, KeyVaultFetcher keyVaultFetcher) {
_client = client;
_kvFetcher = keyVaultFetcher;
}
public async Task Setup() {
await _client = new AsyncLazy<SendGridClient>(async delegate {
var secret = await _kvFetcher.GetSecretAsync(Environment.GetVariable("SecretUri"));
return new SendGridClient(secret);
}).Value;
return await _client.DoThingAsync();
}
}
public class KeyVaultFetcher
{
private readonly HttpFetcherThing _doodad;
public KeyVaultFetcher(HttpFetcherThing doodad) {
_doodad = doodad;
}
public async string GetSecretAsync(string secretUrl) {
var secret = await _doodad.CallThingAsynchronously(secretUrl);
return secret;
}
}
public static async Task Main(string[] args) {
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<HttpFetcherThing>();
containerBuilder.RegisterType<KeyVaultFetcher>();
containerBuilder.RegisterType<SendGridClient>();
containerBuilder.RegisterType<SendGridClientFactory>();
var container = containerBuilder.Build();
var sendGridFactory = container.Resolve<SendGridClientFactory>();
await sendGridFactory.Setup(); // Optimally, we could skip this altogether and have a conventional async method that's called without explicitly resolving and setting up the factory so we could simply have the following lines
var sendGridClient = container.Resolve<SendGridClient>();
await sendGridClient.SendAnEmail();
}
I completely get the concern about how construction should generally be about synchronous and quick operations, but as this is just a scenario that regularly crops up in my day to day, it feels like it could use another "relationship type" just for this more unusual of purposes to avoid all the workarounds I'm otherwise dealing with.
Thanks for the consideration!
Let's play out the AsyncLazy<T>
thought, maybe that's an interesting thread to pull.
Given:
- Autofac should not be responsible for doing any async-to-sync conversion if at all possible. I recognize for async disposable we have a few
Task.Run(async () => await asyncDisposable.DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
bits strewn about in there, but largely we don't want more of that. - Autofac is not an application startup orchestrator, its primary goal is to construct objects. Having things that do construct-then-initialize-or-orchestrate are nice to have but not at the cost of features/perf/supportability of the object construction functions.
- We do have
AutoActivate
andIStartable
already if you want to do synchronous initialization. (No, we don't wantIStartableAsync
if I can avoid it - see above re:Task.Run
.)
Then:
- How does Autofac know which method the
AsyncLazy<T>
should call when it's resolved? Right now allLazy<T>
does is hold onto the lifetime scope and have a deferredscope.Resolve<T>
call buried in it. If theAsyncLazy<T>
has to callawait yourComponent.Setup()
, what does that registration look like? - How would
AsyncLazy<T>
work with other relationship types? All of the relationships are composable. You can doLazy<IEnumerable<T>>
just as well asIEnumerable<Lazy<T>>
. Are there special considerations aroundAsyncLazy<T>
that we'd need to think about? - If I have a component that does not do async initialization, can I still resolve it using
AsyncLazy<T>
? - Are there components like the one you're talking about that would only support
AsyncLazy<T>
and not plainLazy<T>
? How do we indicate that to folks considering consumers of the dependency aren't supposed to "know" about their dependency chain? (I shouldn't "know" thatIEmailClient
is aSendGridClient
- I'm the consumer, so I also wouldn't know there needs to be any async setup for it.) - There are already challenges with folks injecting
ILifetimeScope
into a constructor and getting into circular dependency issues by adding service location in the middle of the resolution stack. Does this become harder to troubleshoot or provide guidance on if we addAsyncLazy<T>
in the mix where that resolution may happen on a different thread? - If we did need to handle the sync-to-async, it seems like there'd be more desire to also allow configuration of how the awaiter works - it's not just disposal anymore, it'd be part of general application execution on a larger scale. What would that look like?
Honestly not trying to sound nitpicky or anything, these are real things we need to think about if we add a feature like this.
What do some of the answers to those questions look like? As in, pseudocode, and actual drilling down into the details of how things might work? (There are only a couple of unpaid people active on the "Autofac team" so we need help from the community to dig in and actually answer some of the hard questions. Again, sorry, it just kinda is how it is.)
Alternatively, perhaps IStartable
on that factory is enough? You handle the sync-to-async conversion?
public class SendGridFactory : IStartable
{
public async Task Setup()
{
await Task.Delay(1);
}
public void Start()
{
Task.Run(async () => await this.Setup().ConfigureAwait(false)).GetAwaiter().GetResult();
}
}
Great questions and a lot to think over. I'll get back to you as soon as I've mulled this over a bit!
Given we don't have a design for AsyncLazy<T>
and I don't think we'll have one in the foreseeable future, I'm going to close this as "won't fix." If folks want an AsyncLazy<T>
feature, please open a new issue and include details on how it'd be anticipated to work.