使用Vs 2017社区和azure。
我有一个网络应用程序MVC5,有这个类。
public static class SchedulerHttpClient
{
const string SPNPayload = "resource={0}&client_id={1}&grant_type=client_credentials&client_secret={2}";
private static HttpClient _Client = new HttpClient();
public static HttpClient Client{ get { return _Client; } }//TODO: validate
public static async Task MainAsync()
{
string tenantId = ConfigurationManager.AppSettings["AzureTenantId"];
string clientId = ConfigurationManager.AppSettings["AzureClientId"];
string clientSecret = ConfigurationManager.AppSettings["AzureClientSecret"];
string baseAddress = ConfigurationManager.AppSettings["BaseAddress"];
string token = await AcquireTokenBySPN(tenantId, clientId, clientSecret).ConfigureAwait(false);
_Client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); //TODO ssmith: const or localization
_Client.BaseAddress = new Uri(baseAddress);
}
private static async Task<string> AcquireTokenBySPN(string tenantId, string clientId, string clientSecret)
{
var payload = String.Format(SPNPayload,
WebUtility.UrlEncode(ConfigurationManager.AppSettings["ARMResource"]),
WebUtility.UrlEncode(clientId),
WebUtility.UrlEncode(clientSecret));
var body = await HttpPost(tenantId, payload).ConfigureAwait(false);
return body.access_token;
}
private static async Task<dynamic> HttpPost(string tenantId, string payload)
{
var address = String.Format(ConfigurationManager.AppSettings["TokenEndpoint"], tenantId);
var content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded");
using (var response = await _Client.PostAsync(address, content).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Status: {0}", response.StatusCode);
Console.WriteLine("Content: {0}", await response.Content.ReadAsStringAsync());
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
}
}
}
此类用于启动Httpclient,联系azure获取令牌,并使用它设置客户端,这样我就可以通过授权重新使用。
问题是何时以及如何调用该类,目前我已经尝试过Global.asx,HomeController Constructor方法和Index方法。
public HomeController()
{
//Init();
}
public async void Init()
{
await SchedulerHttpClient.MainAsync().ConfigureAwait(false);
}
public async Task<ActionResult> Index()
{
Init();
try
{
await MakeARMRequests().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine(e.GetBaseException().Message);
}
return View();
}
我得到的错误是
[InvalidOperationException:在异步操作仍处于挂起状态时完成异步模块或处理程序。]
我的静态类是否正确实现?如果是这样,我将如何实例化客户端,然后在我的应用程序中重复使用?
更新@Stephen Cleary:
public async Task<ActionResult> Index()
{
await SchedulerHttpClient.ClientTask.ConfigureAwait(false);
try
{
await MakeARMRequests().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine(e.GetBaseException().Message);
}
return View();
}
static async Task MakeARMRequests()
{
const string ResourceGroup = "fakegrp";
// Create the resource group
// List the Web Apps and their host names
var client = await SchedulerHttpClient.ClientTask;
var response = await client.GetAsync(
$"/subscriptions/{Subscription}/resourceGroups/{ResourceGroup}/providers/Microsoft.Web/sites?api-version=2015-08-01");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
foreach (var app in json.value)
{
Console.WriteLine(app.name);
foreach (var hostname in app.properties.enabledHostNames)
{
Console.WriteLine(" " + hostname);
}
}
}
根据建议,这是重构的类。
public static class SchedulerHttpClient
{
const string SPNPayload = "resource={0}&client_id={1}&grant_type=client_credentials&client_secret={2}";
private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
{
var client = new HttpClient();
await MainAsync(client).ConfigureAwait(false);
return client;
});
public static Task<HttpClient> ClientTask => _Client.Value;
private static async Task MainAsync(HttpClient client)
{
string tenantId = ConfigurationManager.AppSettings["AzureTenantId"];
string clientId = ConfigurationManager.AppSettings["AzureClientId"];
string clientSecret = ConfigurationManager.AppSettings["AzureClientSecret"];
string baseAddress = ConfigurationManager.AppSettings["BaseAddress"];
string token = await AcquireTokenBySPN(client, tenantId, clientId, clientSecret).ConfigureAwait(false);
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); //TODO ssmith: const or localization
client.BaseAddress = new Uri(baseAddress);
}
private static async Task<string> AcquireTokenBySPN(HttpClient client, string tenantId, string clientId, string clientSecret)
{
var payload = String.Format(SPNPayload,
WebUtility.UrlEncode(ConfigurationManager.AppSettings["ARMResource"]),
WebUtility.UrlEncode(clientId),
WebUtility.UrlEncode(clientSecret));
var body = await HttpPost(client, tenantId, payload).ConfigureAwait(false);
return body.access_token;
}
private static async Task<dynamic> HttpPost(HttpClient client, string tenantId, string payload)
{
var address = String.Format(ConfigurationManager.AppSettings["TokenEndpoint"], tenantId);
var content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded");
using (var response = await client.PostAsync(address, content).ConfigureAwait(false))
{
if (!response.IsSuccessStatusCode)
{
Console.WriteLine("Status: {0}", response.StatusCode);
Console.WriteLine("Content: {0}", await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
}
}
}
答案 0 :(得分:2)
您的问题归因于async void
。正如我在intro to async
on ASP.NET文章中所述:
当异步处理程序完成请求,但ASP.NET检测到尚未完成的异步工作时,会收到InvalidOperationException,并显示消息“异步操作仍处于挂起状态时异步模块或处理程序已完成。”这是通常是由于异步代码调用异步void方法......
另请参阅我的article on async
best practices以了解其他原因以避免async void
。
在您的情况下,您有一个需要初始化的单例资源,并且该初始化必须是异步的。您只想启动一次初始化,并且所有调用者都应该共享初始化结果,因此Lazy<T>
似乎是合适的。由于初始化是异步的,因此可以用Task
表示。因此,Lazy<Task>
:
public static class SchedulerHttpClient
{
... // Same as above, but making MainAsync private.
public static readonly Lazy<Task> Initialize = new Lazy<Task>(() => MainAsync());
}
用法:
public async Task<ActionResult> Index()
{
await SchedulerHttpClient.Initialize.Value.ConfigureAwait(false);
try
{
await MakeARMRequests().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine(e.GetBaseException().Message);
}
return View();
}
这足以让您的代码正常工作,但我会更进一步并重构SchedulerHttpClient
,以便它只在HttpClient
初始化之后公开:
public static class SchedulerHttpClient
{
private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
{
var client = new HttpClient();
await MainAsync(client).ConfigureAwait(false);
return client;
});
public static Task<HttpClient> ClientTask => _Client.Value;
private static async Task MainAsync(HttpClient client) { ... }
private static async Task<string> AcquireTokenBySPN(HttpClient client, string tenantId, string clientId, string clientSecret) { ... }
private static async Task<dynamic> HttpPost(HttpClient client, string tenantId, string payload) { ... }
}
这会强制MakeARMRequests
await
SchedulerHttpClient.ClientTask
而非直接访问HttpClient
,因此您无需记住在所有控制器方法中执行此操作
作为最后一点,如果初始化实际失败,您可能想要“重置”Lazy<T>
。这会使这个本土解决方案充分复杂化,我建议改为使用my AsyncLazy<T>
type(available on NuGet)。