首页 > 解决方案 > 在 IOC 注册之前围绕异步初始化的最佳实践

问题描述

也许我只是在搜索时使用了错误的术语,但我没有找到任何关于如何做我想做的事情的可靠指导。

有关 DI 注册的所有指导都遵循以下内容:

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

但这对我来说似乎太简单了,无法在现实世界中使用。我不会将我的各种凭据存储在我的应用程序中,而是将它们放在 Azure Key Vault 等其他地方,或者使用本身检索连接字符串和密钥的托管标识进行身份验证。

这引入了首先访问凭据/连接字符串的需求,这越来越多地仅作为异步操作公开,并引入了我经常面临的问题:即异步注册不是一回事。

我可以注册一个服务,该服务本身在异步方法中检索和公开凭证,但现在每个下游服务都需要了解该方法才能使用它——我不能简单地在 DI 注册中将其抽象出来。

我可以只使用.Resultor Wait(),但是有很多可靠的指导表明不应该因为死锁的原因而这样做。因为此代码可能适用于由具有 UI 的应用程序使用的库,所以这是不行的。

所以问题是:当我无法同步提供我的凭据时,我该如何注册我的服务?

真实世界的例子

例如,假设我有一个 Web 应用程序需要访问 Cosmos DB,但需要通过托管标识,按照此处的说明进行操作。我需要存储一些有关 Cosmos DB 实例的信息,这意味着对 IConfiguration 的依赖,我想使用单例 HttpClient 来检索必要的密钥。

我想把它放到一个单独的服务中,负责设置 Cosmos DB 客户端,以便下游使用可以注入 CosmosClient,所以我的类看起来像:

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);
  }
}

为了支持此类中使用的 DI,我的注册如下所示:

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

当然我需要注册这个服务:

builder.Services.AddSingleton<CosmosKeyService>();

但是现在我还想注册由该服务中的方法创建的 CosmosClient,这就是我开始对最佳前进方式感到困惑的地方。

  1. 我无法从构建器中检索 CosmosKeyService 的实例,因为我尚未构建它,并且在构建之后,我无法注册新服务。

  2. 我不能在注册本身中使用异步方法,或者我可以轻松地执行以下操作:

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

...并且下游服务可以简单地注入CosmosClient它们的各种构造函数。

  1. 同样,任何下游消费者都可以只注入一个 CosmosKeyService,但现在他们都必须“记住”首先调用初始化方法,以便他们可以检索 CosmosClient 并使用它。我宁愿在注册中进行处理,以便 1)这个初始化被隐藏并位于中心位置,2)CosmosClient 是真正的单例,而不仅仅是每个使用的工件。

  2. 我可以创建另一个中间服务来注入这个密钥解析器服务并检索密钥,但它也需要有这个异步方法来检索密钥,因为我不能只是在某处的注册中隐藏那个初始化(因为缺乏异步支持)。

例如,我可以提供另一项服务:

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

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

但这最终仍然需要下游服务来注入该服务并调用该初始化方法,如果有必要,我还不如坚持注入 CosmosKeyService 并在那里调用该方法。

我最希望看到的是某种方式来隐藏注册中的任何异步初始化,以便下游消费者可以简单地注入 CosmosClient 并且它可以工作,但这超出了我的预期。任何人都可以对此有所了解吗?

编辑地址评论:

我不想评论一个已有 4 年历史的答案,但我用已接受的答案断言的问题归结为这一部分:

将 [initialization] 移动到合成根中。此时,您可以在将这些类注册到容器中之前创建它们的初始化,并将这些初始化的类作为注册的一部分提供给容器。

这一切都很好,除了:

  1. 我只能一次“构建”我的容器。我无法构建它,然后利用注册来完成初始化,然后将更多注册附加到它以供以后使用。
  2. 在上面的示例中,我明确利用了 ASP.NET Core 本身在 DI 中注册的元素(即 IConfiguration),因此除了通过 DI(根据 #1,这使我无法初始化和稍后用更多的实现来补充我的注册)。

标签: c#dependency-injectionasync-await

解决方案


推荐阅读