3

Perhaps I'm just using the wrong terms while searching, but I haven't found any solid guidance around how to do what I'm seeking to do.

All the guidance around DI registration follows something like the following:

builder.Services.AddSingleton<MyService>(() => new MyService("connectionString"));

But this seems too simple for me to use over here in the real world. I don't store my various credentials in my applications, but rather put them somewhere else like Azure Key Vault or authenticate using a managed identity that itself retrieves connection strings and keys.

This introduces the need then to access the credentials/connection string first, which increasingly is exposed only as an asynchronous operation and introduces the problem I regularly face: 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 need to know about that method in order to utilize it - I can't simply abstract it away in a DI registration.

I could just use .Result or Wait(), but there's plenty of solid 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?

Real-world example

For example, let's say I've got a web app that needs to access Cosmos DB, but via a managed identity, following the instructions here. 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 usages can just 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://learn.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:

builder.Services.Configure<MyCosmosOptions>(builder.Configuration.GetSection("cosmosdb"));
builder.Services.AddSingleton<HttpClient>();

And of course I'm going to need to register this service:

builder.Services.AddSingleton<CosmosKeyService>();

But now I'd also like to register the CosmosClient as created by the method in that service and this is where I start getting confused about the best way forward.

  1. I can't retrieve an instance of the CosmosKeyService from the builder because I haven't yet built it, and after I do, I can't then register new services.

  2. I can't use async methods in the registration itself or I could easily do something like:

builder.Services.AddSingleton<CosmosClient>(async services => {
  var keyService = services.GetService<CosmosKeyService>();
  return await keyService.GetCosmosClient();
});

...and downstream services could simply inject CosmosClient in their various constructors.

  1. Again, any downstream consumer can just inject a CosmosKeyService, but now they're all going to have to "remember" to call the initialization method first so they can retrieve the CosmosClient and utilize it. I'd rather that be handled in registration so that 1) this initialization is hidden and centrally located and 2) the CosmosClient is truly a singleton and not just an artifact of every utilization.

  2. I could create another intermediate service that injects this Key resolver service and retrieve the keys, but it too will need to have this async method that retrieves the keys since I can't just hide that initialization in a registration somewhere (for lack of async support).

For example, I could make another service:

public class CosmosBuilder
{
  private readonly CosmosKeyService _keySvc;
  public CosmosBuilder(CosmosKeyService keySvc)
  {
    _keySvc = keySvc;
  }

  public async Task<CosmosClient> GetCosmosClient() 
  {
    return async _keySvc.GetCosmosClient();
  }
}

But this ultimately still requires a downstream service to inject this service and call that initialization method and if that's necessary, I might as well just stick with injecting the CosmosKeyService and call the method there.

What I'd ideally like to see is some way to hide any async initialization in the registration so that downstream consumers can simply inject CosmosClient and it works, but it's beyond me how that's intended to happen. Can anyone shed some light on this?

Edit to address comment:

I don't want to comment on a 4-year old answer, but the issue I assert with the accepted answer boils down to this part:

Move [initialization] into the Composition Root. At that point, you can create an initialize those classes before registering them in the container and feed those initialized classes into the container as part of registrations.

That's all well and good except:

  1. I only get to "build" my container a single time. I can't build it, then utilize the registrations to accomplish the initialization, then append still more registrations to it for later use.
  2. In my example above, I explicitly utilize elements registered in DI by ASP.NET Core itself (namely IConfiguration), so there's simply no way to even access these except via DI (which, per #1, precludes me from being able to initialize and later supplement my registrations with more implementations).
Whit Waldo
  • 4,806
  • 4
  • 48
  • 70
  • Related: https://stackoverflow.com/questions/45924027/avoiding-all-di-antipatterns-for-types-requiring-asynchronous-initialization (likely even a duplicate) – Steven Nov 03 '21 at 16:22
  • @Steven Addressed your comment (and answer) at the end to circle back to why I'm presently stumped in my example. Your answer was an excellent read otherwise though, thank you. – Whit Waldo Nov 03 '21 at 19:00
  • "then append still more registrations to it for later use." That's indeed not what you should (or can) do. Instead, you can resolve registered components to finish their initialization at the end of the startup process. – Steven Nov 03 '21 at 21:04
  • #1 definitely doesn't seem like the way to go, nor does "build this container, source the services from it, initialize what I need, re-register everything in a second container and use it downstream instead", but that's where I'm at. Namely, there are things already registered in the container that I can't otherwise access (I didn't register them) and I need those things as part of my initialization of other things. – Whit Waldo Nov 03 '21 at 21:08
  • 1
    That's absolutely not what what I'm trying to see. There is no need to rebuild the container nor register everything. As I tried to explain in my old answer, you can either load the configuration before registering your services and supply that config value during registration -or- create one are two components in such way that, after building the container, you can instruct those components to load their configuration, for instance using an Initializate() method. Alternative to that, you can put the loading of the config inside the CR and pass it on to such component using its Init() method. – Steven Nov 03 '21 at 21:14
  • Third option is to let the component load the config internally upon first use. None of this requires rebuilding or registering. – Steven Nov 03 '21 at 21:15
  • 1
    Some of my initializations can be complex and use other components that I'm separately registering. Without async support in the DI registration, am I essentially left to a pre-DI world for all the initialization (e.g. I have to create all these concrete implementations in one place, set everything up, then finally register them all for the DI experience afterwards)? Again, approaching this from a "initialization is abstracted into registration in one place" would grossly simplify the experience. Moving it all on top of registration feels like the only path forward (but not the ideal one). – Whit Waldo Nov 04 '21 at 16:00

0 Answers0