【问题标题】:Creating HttpClient in older ASP.NET MVC app without Startup在没有启动的旧 ASP.NET MVC 应用程序中创建 HttpClient
【发布时间】:2018-04-03 03:25:27
【问题描述】:

我有一个旧版本的 ASP.NET MVC 应用程序,它没有 Startup.cs。我想实现一种简洁的方式来拥有一个HttpClient,我将用于我对第三方的 API 调用。

这是我迄今为止根据我收到的关于这个问题的一些想法/建议所做的。问题是当我进行 API 调用时,它无处可去。我把它放在try catch 中,但我什至没有例外。 API 提供者告诉我他们没有看到搜索参数。

首先,我创建了这个HttpClientAccessor 用于延迟加载。

public static class HttpClientAccessor
{
   public static Func<HttpClient> ValueFactory = () =>
   {
      var client = new HttpClient();

      client.BaseAddress = new Uri("https://apiUrl.com");
      client.DefaultRequestHeaders.Accept.Clear();
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      client.DefaultRequestHeaders.TryAddWithoutValidation("APIAccessToken", "token1");
      client.DefaultRequestHeaders.TryAddWithoutValidation("UserToken", "token2");

       return client;
   };

   private static Lazy<HttpClient> client = new Lazy<HttpClient>(ValueFactory);

   public static HttpClient HttpClient
   {
      get { return client.Value; }
   }
}

然后我创建了自己的 API 客户端,这样我就可以在一个地方拥有 API 调用函数,如下所示:

public class MyApiClient
{

   public async Task GetSomeData()
   {
       var client = HttpClientAccessor.HttpClient;
       try
       {
           var result = await client.GetStringAsync("somedata/search?text=test");
           var output = JObject.Parse(result);
       }
       catch(Exception e)
       {
           var error = e.Message;
       }
    }
}

然后在我的 ASP.NET 控制器操作中,我这样做:

public class MyController : Controller
{
   private static readonly MyApiClient _apiClient = new MyApiClient ();

   public ActionResult ApiTest()
   {
       var data = _apiClient.GetSomeData().Wait();
   }
}

知道我的错误在哪里吗?

更新: 这种简单的方法效果很好:

public class MyController : Controller
{
   private static readonly HttpClient _client = new HttpClient();

   public ActionResult ApiTest()
   {
       _client.BaseAddress = new Uri("https://apiUrl.com");
       _client.DefaultRequestHeaders.Accept.Clear();
       _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
       _client.DefaultRequestHeaders.TryAddWithoutValidation("APIAccessToken", "token1");
       _client.DefaultRequestHeaders.TryAddWithoutValidation("UserToken", "token2");

       var response = _client.GetStringAsync("somedata/search?text=test").Result;
   }
}

【问题讨论】:

  • 没有理由拥有单例。

标签: c# asp.net asp.net-mvc asp.net-mvc-4


【解决方案1】:

如前所述,依赖注入没有被使用,所以从技术上讲,不需要一个组合根来初始化这些东西。

如果不需要在启动时实际初始化客户端,您可以考虑使用惰性单例方法。

一个例子

public static class HttpClientAccessor {
   public static Func<HttpClient> ValueFactory = () => {
      var client = new HttpClient();

      client.BaseAddress = new Uri("https://apiUrl.com");
      client.DefaultRequestHeaders.Accept.Clear();
      client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      client.DefaultRequestHeaders.TryAddWithoutValidation("APIAccessToken", "token1");
      client.DefaultRequestHeaders.TryAddWithoutValidation("UserToken", "token2");

       return client;
   };

   private static Lazy<HttpClient> client = new Lazy<HttpClient>(ValueFactory);

   public static HttpClient HttpClient {
      get { return client.Value; }
   }
}

如果客户端需要额外设置,Lazy&lt;HttpClient&gt; 的工厂委托可以变得更复杂。

无论何时需要客户,您都可以调用服务

var client = HttpClientAccessor.HttpClient;

var response = await client.GetStringAsync("{url}");

客户端将在首次使用时初始化,您将在后续调用该实例时获得相同的实例。

在您的控制器中使用时,您将异步调用与阻塞调用行 .Wait().Result 混合在一起。这会导致死锁,应该避免。

public class MyController : Controller {
    private static readonly MyApiClient _apiClient = new MyApiClient ();

    public async Task<ActionResult> ApiTest() {
        var data = await _apiClient.GetSomeData();

        //...
    }
}

代码应该一直是异步的。

参考Async/Await - Best Practices in Asynchronous Programming

【讨论】:

  • 谢谢。我只是在阅读有关使单例线程安全的信息。 lazy 方法只会在必要时创建实例,对吗?让它线程安全怎么样?我在某处读到使用单个实例实际上会产生性能问题。建议的正确方法是每个线程有一个实例。
  • @Sam 建议HttpClient 在应用程序的生命周期中使用一个实例。拥有多个实例并处理它们会产生性能问题。
  • 我实际上计划创建一个ApiClient,它将创建HttpClient,并使用您的方法创建我的ApiClient。还是创建一个可以从我的ApiClient 访问的“通用”HttpClient 是一个更好的主意?
  • 好的。你搞定了。当我进行控制器操作async 时,代码开始工作。感谢您坚持这一点并帮助我。我真的很感激。
  • BaseAddress 中不需要斜杠,因为它的路径是空的。请参阅 StackOverflow 上的 Why is HttpClient BaseAddress not working? 讨论。
【解决方案2】:

Application_Start() 方法是正确的地方。但我不得不问:为什么必须在“应用程序启动”时创建 HttpClient 实例?通常,HttpClient 是一些“资源”,您可以在需要时创建它。而且也没有必要将其设置为“Singleton”。只需将其包装在 using 块中即可。 (也许您想将 API 包装器设为 Singleton?)

public class APICaller
{

    //make the APICaller singleton in some way here
    //...


    // the api calling method:
    public string CallAPI(string someParameter)
    {
        var response = "";
        using (var client = new HttpClient())
        {
            //calling the API
        }
        return response;
    }
}

【讨论】:

  • HttpClient 是一个静态资源,可在应用程序期间重复使用。
  • @ATerry 包含一个链接,该链接指向支持您的主张的内容。请
  • @Seabizkit 阅读备注,msdn.microsoft.com/en-us/library/…
  • @ATerry 在该链接中没有任何地方可以支持您所说的HttpClient is meant to be a static resource 我不认为这种说法是正确的。欢迎大家指正。即使在他们自己的示例中,他们每次都覆盖静态实例。我相信 HttpClient 应该有一个生命周期。
  • @Seabizkit 这直接来自HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors. Below is an example using HttpClient correctly. 的备注 该示例显示了一个静态 HttpClient,而不是每个实例。如果您不相信我或 MSDN,只需 google “HttpClient Static”;有数百篇关于这个问题的文章。
【解决方案3】:

主要问题是不正确的异步代码。

您正在使用 Task.Wait(),它与异步 MyApiClient.GetSomeData() 一起导致 ASP.NET 请求上下文出现死锁。这是一个非常常见的问题,请参阅 StackOverflow 上的 An async/await example that causes a deadlock。带有Task.Result 属性调用的代码正在工作,因为HttpClient.GetStringAsync() 可能会采取预防措施来防止死锁。请参阅 MSDN 上的 Task.ConfigureAwait() 页面和 StackOverflow 上的 Best practice to call ConfigureAwait for all server-side code 讨论。

使用 C# 编写单例有多种选择。有关详细概述,请参阅 Jon Skeet 的 Implementing the Singleton Pattern in C# 文章。

【讨论】:

    【解决方案4】:

    正如您所提到的,您可以在控制器上使用静态类成员。 HttpClient 只需设置一次;所以在控制器的静态构造函数中这样做。此外,请确保将 async/await 用于异步方法,尤其是对于长时间运行的 http 请求。根据您的需要,IOC 和抽象层会有意义。

    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace TestApi
    {
        public class MyController : Controller
        {
            private const string ApiUrlString = "https://apiUrl.com";
            private static readonly Uri ApiUri = new Uri(ApiUrlString);
            private static readonly HttpClient RestClient;
    
            static MyController()
            {
                this.RestClient = new HttpClient{
                    BaseAddress = ApiUri
                }
                this.RestClient.DefaultRequestHeaders.Accept.Clear();
                this.RestClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                RestClient.DefaultRequestHeaders.TryAddWithoutValidation("APIAccessToken", "token1");
                RestClient.DefaultRequestHeaders.TryAddWithoutValidation("UserToken", "token2");
            }
    
            public async Task<IActionResult> ApiTest()
            {
                return this.Ok(await this.RestClient.GetStringAsync("somedata/search?text=test"));
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2012-11-24
      • 1970-01-01
      • 1970-01-01
      • 2020-08-09
      • 1970-01-01
      • 2010-10-27
      • 1970-01-01
      • 1970-01-01
      • 2011-05-06
      相关资源
      最近更新 更多