依赖注入IOC容器注册前异步初始化的最佳实践问题求教
我搜索时使用的关键词可能有误,一直未找到匹配我需求的可靠指导方案。目前所有关于DI注册的指导都采用类似如下写法:
builder.Services.AddSingleton<MyService>(() => new MyService("connectionString"));
但这种方式过于简单,不符合实际生产场景需求。我不会将各类凭据存储在应用代码中,而是存放在Azure Key Vault这类服务中,或者使用托管身份认证,由托管身份自行获取连接字符串和密钥。
这就产生了优先获取凭据/连接字符串的需求,而这类操作现在基本仅提供异步接口,由此带来我经常遇到的问题:目前没有异步注册服务的机制。
我可以先注册一个服务,由它通过异步方法获取并返回凭据,但这样所有下游服务都需要知道要调用这个异步方法才能使用凭据,我无法在DI注册环节将这个逻辑抽象隐藏。
我也可以用.Result或者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() { // 实现逻辑省略 //... 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注册到容器中,这就是我找不到最优方案的点:
- 我无法从builder中获取CosmosKeyService的实例,因为此时容器还未构建,而容器构建完成后就不能再注册新服务了。
- 注册逻辑本身不支持异步方法,否则我可以直接这么写:
builder.Services.AddSingleton<CosmosClient>(async services => { var keyService = services.GetService<CosmosKeyService>(); return await keyService.GetCosmosClient(); });
这样下游服务就可以直接在构造函数中注入CosmosClient使用。
- 下游服务可以直接注入CosmosKeyService,但这样所有使用者都需要「记住」先调用初始化方法来获取CosmosClient才能使用。我更希望这个逻辑在注册环节处理,一是可以将初始化逻辑隐藏并集中管理,二是保证CosmosClient是真正的单例,而非每次使用都生成新实例。
- 我可以再创建一个中间服务注入这个密钥解析服务来获取密钥,但它同样需要提供异步方法来获取密钥,因为我没法在注册环节隐藏这个初始化逻辑(因为不支持异步注册)。
比如我可以创建如下服务:
public class CosmosBuilder { private readonly CosmosKeyService _keySvc; public CosmosBuilder(CosmosKeyService keySvc) { _keySvc = keySvc; } public async Task<CosmosClient> GetCosmosClient() { return await _keySvc.GetCosmosClient(); } }
但这种方式最终还是需要下游服务注入这个服务并调用初始化方法,如果必须这么做,我还不如直接让下游注入CosmosKeyService然后调用对应方法。
我理想的方案是可以在注册环节隐藏所有异步初始化逻辑,下游使用者直接注入CosmosClient就可以正常使用,但我不知道该如何实现。
针对评论的补充说明
我不想在一个4年前的回答下评论,但是我认为那个被采纳的回答存在以下问题:
将[初始化]逻辑移到组合根(Composition Root)中,这样你就可以在注册服务之前创建并初始化这些类,然后把初始化完成的实例注册到容器中。
这个方案听起来没问题,但存在两个限制:
- 容器只能「构建」一次,我不能先构建容器,用已经注册的服务完成初始化,再追加注册更多服务供后续使用。
- 在我上面的示例中,我明确用到了ASP.NET Core本身注册到DI中的服务(即IConfiguration),所以我除了通过DI之外没有其他方式可以访问这些服务,结合第一条限制,我没法先完成初始化再补充注册其他实现。
内容的提问来源于stack exchange,提问作者Whit Waldo

