在Web App中实现Static HttpClient

时间:2017-03-22 14:38:35

标签: c# azure

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

1 个答案:

答案 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> typeavailable on NuGet)。