JasperFx / lamar

Fast Inversion of Control Tool and Successor to StructureMap
https://jasperfx.github.io/lamar
MIT License
563 stars 118 forks source link

Support for asynchronous registrations #299

Closed WhitWaldo closed 2 years ago

WhitWaldo commented 2 years ago

Might there be any interest in supporting asynchronous registrations in Lamar? I typed up a longer ask about this on SO earlier detailing my specific example, but given that Lamar is a relatively new entrant to the IOC space and the broader (and more exclusive) use of async in general in recent years, I wanted to see if there's a chance Lamar might be the first of the IOCs to have this integrated async support (even if presented alongside all the caveats that generally, you'd not want to register things asynchronously).

My own use case boils down to procuring credentials from external resources to initialize third-party clients. 1) I can't just put all my async initialization before IOC registration because my initialization components have already-registered-component dependencies (e.g. by ASP.NET Core itself). To my knowledge, once the container is built, one cannot come back and supplement with additional registrations. 2) Abstracting the initialization into a separate class doesn't resolve the issue since the downstream user of that class still needs to itself call an asynchronous initialization method to kick off the abstracted method. 3) Putting the async logic into a static factory doesn't resolve anything here as I can't use the method asynchronously in the DI registration in the first place. 4) Wrapping the async bit in a Task/Action means that downstream usage of that class might be premature since initialization may not have, in fact, completed yet 5) Depending on .Result or Wait() may be detrimental in UI-based contexts if the code I'm writing today (not in a library) is later used in one of those environments (as a library).

This would all be solved, I think, if I could simply perform registration asynchronously. It happens once, either lazily when the component is initially requested or proactively when the container is built, and would ideally guarantee that it's asynchronously completed before making it available to downstream usages.

Thanks for the consideration!

CodingGorilla commented 2 years ago

@WhitWaldo Can you provide a use case? Not speaking for the project, but I have always considered IoC/DI registration to be a static operation, so I want to understand the need here (for my own edification).

WhitWaldo commented 2 years ago

Sure thing, mostly copied over from my StackOverflow question this morning.

Most guidance, like you indicate, follows something like the following:

For<IMyService>().UseInstance(() => new MyService("connectionString"));

This isn't representative of my real-world applications though. I don't store my various credentials in my application, but rather put them somewhere else like Azure Key Vault or I authenticate via a managed identity that itself asynchronously procures the secrets I use.

This increasingly introduces the need to access the credentials/connection string first (often and increasingly exposed only using an asynchronous route) and introduces the crux of my issue - namely, asynchronous registration isn't a thing.

I could register a service that itself retrieves and exposes the credential in an async method, but now every downstream service is going to be need to know about and invoke that method in order to utilize it - I can't simply abstract it away in the DI registration.

I could just use .Result or Wait(), but there's plenty of guidance that suggests this shouldn't be done for deadlocking reasons. Because this code may be intended for a library that's consumed by an app with a UI, that's a no-go.

So the question is: When I'm unable to synchronously provide my credentials, how do I register my services so that downstream clients need only use the registered client?

Real world use-case

I've got a web app that needs to access CosmosDB, but via a managed identity (since the credentials aren't stored in the service). I need to store some information about the Cosmos DB instance which means a dependency on IConfiguration and I'd like to use a singleton HttpClient to retrieve the necessary keys.

I want to put this into a separate service responsible for setting up the Cosmos DB client so that downstream uses can simply inject the CosmosClient, so my class looks like:

public class CosmosKeyService
{
  private readonly MyCosmosOptions _cosmosOptions;
  private readonly HttpClient _http;

  public CosmosKeyService(IOptions<MyCosmosOptions> options, HttpClient http)
  {
    _cosmosOptions = options.Value;
    _http = http;
  }

  private async Task<string> GetCosmosKey()
  {
    //Follow instructions at https://docs.microsoft.com/en-us/azure/cosmos-db/managed-identity-based-authentication#programmatically-access-the-azure-cosmos-db-keys
    //...
    var keys = await result.Content.ReadFromJsonAsync<CosmosKeys>();
    return keys.PrimaryMasterKey;
  }

  public async Task<CosmosClient> GetCosmosClient()
  {
     var key = await GetCosmosKey();
     return new CosmosClient(_cosmosOptions.CosmosDbEndpoint, key);
  }
}

To support the DI used in this class, my registration then looks like:

//IConfiguration is automatically set up by ASP.NET Core
For<HttpClient>().Use<HttpClient>().Singleton();
For<CosmosKeyService>().Use<CosmosKeyService>().Singleton();

But now I want to register the CosmosClient as created by the method in that CosmosKeyService service and this is where I start running into problems. 1) I cannot retrieve an instance of CosmosKeyService from the container because I haven't yet built it. Once I build it, I cannot then later register additional services. 2) I cannot use async methods in the registration itself or I could easily do something like:

For<CosmosClient>().Use<CosmosClient>(async container => {
  var keyService = container.GetInstance(typeof(CosmosClient));
  return await keyService.GetCosmosClient();
));

...and then downstream services could simply inject CosmosClient in their various constructors without any setup concerns.

Now, I could create a container, source my dependencies from it, initialize the CosmosClient and then register that (and everything a second time) into a second IOC container, but that seems like bad practice.

Again, the root of my issues are: 1) I need to use services that are already registered by something else (e.g. ASP.NET Core) in the DI container and not otherwise available outside of the DI chain 2) I must source something asynchronously in order to use it in a DI registration.

Both would be solved by allowing asynchronous registration. Thanks again for your time.

CodingGorilla commented 2 years ago

Sorry, you did link the SO post, I should have read that, but thanks for reposting. So, my first thought is: How would this even work? IoC/DI is oriented around instance construction and obviously constructors don't allow for async code. You could maybe pull this off with some Lazy<T> magic, but I feel like that would be a crazy nightmare. Do you have any thoughts as to how this might work?

jeremydmiller commented 2 years ago

@WhitWaldo Just catching up. First reaction is to say "use Marten instead of Cosmos" ;)

You're wanting an asynchronous service location more or less, correct? I'm going to say no for Lamar. I don't think what you're doing with the async initialization call should be done inside the IoC container but rather outside in application code. And I totally agree with you not wanting to mix async and sync code. You might go for a higher level abstraction that deals w/ one time initialization inside of an async method on the first usage.

I'm closing this just for the sake of bookkeeping as I don't think I want to incorporate this into Lamar itself.

WhitWaldo commented 2 years ago

Apologies! I didn't see the GitHub notifications on this, but I did want to respond.

@CodingGorilla I've been sitting here for the last 5 days trying to think of how Lamar could even do this and the only solution I could come up with was using IL weaving to dynamically inject static async factories to those objects that have async setups, and that just seems too complicated for the ask.

@jeremydmiller Put simply, I was trying to register ABC and XYZ in Lamar. XYZ had a dependency on ABC, but had to retrieve a value asynchronously from it in order to create the entity to register. The two obvious paths forward were: 1) If asynchronous setups were allowed, I could just have ABC injected into XYZ at setup, pull the value then and now there's my XYZ instance ready to use. 2) Re-build the IoC container after initial registrations. For example, register ABC in the container. Retrieve an instance and pull the data from it for XYZ, create XYZ. Register XYZ in the container and use that one going forward.

The goal was to avoid going through the process of having the setup for ABC in more than one place. If I can register it once for the DI component, I can use it everywhere downstream.

Rather, I ultimately had to settle with creating an instance of ABC outside of Lamar, retrieving the value I needed and then registering both ABC and XYZ in Lamar after the fact. This seems to be the direction most IoC frameworks suggest, so it's my approach going forward, but it does leave me with a lot of initialization logic right before the IoC registration, which doesn't feel like the cleanest approach either, but here we are.

I do thank both you for responding with your thoughts on the matter.