在没有启动的情况下在较旧的 ASP.NET MVC 应用程序中创建 HttpClient
Creating HttpClient in older ASP.NET MVC app without Startup
我有一个旧版本的 ASP.NET MVC 应用程序,它没有 Startup.cs
。我想实现一种干净的方式来获得一个 HttpClient
,我将用于我对第三方的 API 调用。
这是我到目前为止所做的,基于我收到的关于这个问题的一些 ideas/recommendations。问题是,当我拨打 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;
}
}
如前所述,没有使用依赖注入,因此从技术上讲,不需要在这些东西已经初始化的地方使用组合根。
如果不需要在启动时实际初始化客户端,您可以考虑使用惰性单例方法。
一个例子
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<HttpClient>
的工厂委托可以变得更复杂。
在需要客户的地方调用服务
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();
//...
}
}
代码应该始终是异步的。
Application_Start() 方法是正确的地方。但我不得不问:为什么你必须在 "application starts" 时创建 HttpClient 实例?一般来说,HttpClient是一些"resource",你想使用它的时候创建它就可以了。而且也不需要设置成"Singleton"。只需将其包装在 using 块中即可。 (也许您想将 API 包装器设为单例?)
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;
}
}
主要问题是不正确的异步代码。
您正在使用 Task.Wait()
,它与异步 MyApiClient.GetSomeData()
一起导致 ASP.NET 请求上下文出现死锁。这是一个非常常见的问题,请参阅 Whosebug 上的 An async/await example that causes a deadlock on Whosebug. Code with Task.Result
property call is working because HttpClient.GetStringAsync()
probably takes preventative measures against deadlocks. See Task.ConfigureAwait()
page on MSDN and Best practice to call ConfigureAwait for all server-side code 讨论。
使用 C# 编写单例有多种选择。有关详细概述,请参阅 Jon Skeet 的 Implementing the Singleton Pattern in C# 文章。
正如您所提到的,您可以只在控制器上使用静态 class 成员。 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"));
}
}
}
我有一个旧版本的 ASP.NET MVC 应用程序,它没有 Startup.cs
。我想实现一种干净的方式来获得一个 HttpClient
,我将用于我对第三方的 API 调用。
这是我到目前为止所做的,基于我收到的关于这个问题的一些 ideas/recommendations。问题是,当我拨打 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;
}
}
如前所述,没有使用依赖注入,因此从技术上讲,不需要在这些东西已经初始化的地方使用组合根。
如果不需要在启动时实际初始化客户端,您可以考虑使用惰性单例方法。
一个例子
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<HttpClient>
的工厂委托可以变得更复杂。
在需要客户的地方调用服务
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();
//...
}
}
代码应该始终是异步的。
Application_Start() 方法是正确的地方。但我不得不问:为什么你必须在 "application starts" 时创建 HttpClient 实例?一般来说,HttpClient是一些"resource",你想使用它的时候创建它就可以了。而且也不需要设置成"Singleton"。只需将其包装在 using 块中即可。 (也许您想将 API 包装器设为单例?)
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;
}
}
主要问题是不正确的异步代码。
您正在使用 Task.Wait()
,它与异步 MyApiClient.GetSomeData()
一起导致 ASP.NET 请求上下文出现死锁。这是一个非常常见的问题,请参阅 Whosebug 上的 An async/await example that causes a deadlock on Whosebug. Code with Task.Result
property call is working because HttpClient.GetStringAsync()
probably takes preventative measures against deadlocks. See Task.ConfigureAwait()
page on MSDN and Best practice to call ConfigureAwait for all server-side code 讨论。
使用 C# 编写单例有多种选择。有关详细概述,请参阅 Jon Skeet 的 Implementing the Singleton Pattern in C# 文章。
正如您所提到的,您可以只在控制器上使用静态 class 成员。 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"));
}
}
}