KeyVault GetSecretAsync永远不会返回

在Web应用程序中使用KeyVault的示例代码中包含以下代码:

public static async Task GetSecret(string secretId) { var secret = await keyVaultClient.GetSecretAsync(secretId); return secret.Value; } 

我已将示例中包含的KeyVaultAccessor对象合并到我的应用程序中以进行测试。 该调用作为对我的一个web api控制器方法的查询的一部分执行:

 var secret = KeyVaultAccessor.GetSecret("https://superSecretUri").Result; 

不幸的是,调用永远不会返回,查询将无限期地挂起……

可能是什么原因,因为坦率地说我不知道​​从哪里开始……?

这是我在博客中完整描述的常见死锁问题。 简而言之, async方法在await完成后尝试返回到ASP.NET请求上下文,但该请求一次只允许一个线程,并且该上下文中已经有一个线程(在调用时被阻塞的线程) Result )。 所以任务是等待上下文自由,并且线程阻止上下文直到任务完成:死锁。

正确的解决方案是使用await而不是Result

 var secret = await KeyVaultAccessor.GetSecret("https://superSecretUri"); 

我使用以下代码覆盖同步上下文:

 var secret = Task.Run(async () => await KeyVaultAccessor.GetSecretAsync("https://superSecretUri")).Result; 

如果您使用的是非异步方法,这仍然允许您使用.Result

不幸的是,调用永远不会返回,查询无限期地挂起……

你有一个经典的僵局。 这就是为什么你不应该阻止异步代码 。 在幕后,编译器生成一个状态机并捕获称为SynchronizationContext东西。 当您同步阻止调用线程时,尝试将延续回发到同一上下文会导致死锁。

而不是使用.Result同步阻塞,使控制器异步并等待从GetSecret返回的Task

 public async IHttpActionResult FooAsync() { var secret = await KeyVaultAccessor.GetSecretAsync("https://superSecretUri"); return Ok(); } 

附注 – 异步方法应遵循命名约定并以Async为后缀。

使用其余的API …

 public class AzureKeyVaultClient { public string GetSecret(string name, string vault) { var client = new RestClient($"https://{vault}.vault.azure.net/"); client.Authenticator = new AzureAuthenticator($"https://vault.azure.net"); var request = new RestRequest($"secrets/{name}?api-version=2016-10-01"); request.Method = Method.GET; var result = client.Execute(request); if (result.StatusCode != HttpStatusCode.OK) { Trace.TraceInformation($"Unable to retrieve {name} from {vault} with response {result.Content}"); var exception = GetKeyVaultErrorFromResponse(result.Content); throw exception; } else { return GetValueFromResponse(result.Content); } } public string GetValueFromResponse(string content) { var result = content.FromJson(); return result.value; } public Exception GetKeyVaultErrorFromResponse(string content) { try { var result = content.FromJson(); var exception = new Exception($"{result.error.code} {result.error.message}"); if(result.error.innererror!=null) { var innerException = new Exception($"{result.error.innererror.code} {result.error.innererror.message}"); } return exception; } catch(Exception e) { return e; } } class keyvaultresponse { public string value { get; set; } public string contentType { get; set; } } class keyvautlerrorresponse { public keyvaulterror error {get;set;} } class keyvaulterror { public string code { get; set; } public string message { get; set; } public keyvaulterror innererror { get; set; } } class AzureAuthenticator : IAuthenticator { private string _authority; private string _clientId; private string _clientSecret; private string _resource; public AzureAuthenticator(string resource) { _authority = WebConfigurationManager.AppSettings["azure:Authority"]; _clientId = WebConfigurationManager.AppSettings["azure:ClientId"]; _clientSecret = WebConfigurationManager.AppSettings["azure:ClientSecret"]; _resource = resource; } public AzureAuthenticator(string resource, string tennant, string clientid, string secret) { //https://login.microsoftonline.com//oauth2/oken _authority = authority; //azure client id (web app or native app _clientId = clientid; //azure client secret _clientSecret = secret; //vault.azure.net _resource = resource; } public void Authenticate(IRestClient client, IRestRequest request) { var token = GetS2SAccessTokenForProdMSA().AccessToken; //Trace.WriteLine(String.Format("obtained bearer token {0} from ADAL and adding to rest request",token)); request.AddHeader("Authorization", String.Format("Bearer {0}", token)); } public AuthenticationResult GetS2SAccessTokenForProdMSA() { return GetS2SAccessToken(_authority, _resource, _clientId, _clientSecret); } private AuthenticationResult GetS2SAccessToken(string authority, string resource, string clientId, string clientSecret) { var clientCredential = new ClientCredential(clientId, clientSecret); AuthenticationContext context = new AuthenticationContext(authority, false); AuthenticationResult authenticationResult = context.AcquireToken( resource, clientCredential); return authenticationResult; } } } 

此通用方法可用于覆盖死锁问题:

 public static T SafeAwaitResult(Func> f) { return Task.Run(async () => await f()).Result; } 

使用:

 SafeAwaitResult(() => foo(param1, param2));