在ASP.Net Core中使用HTTPClient作为DI Singleton的最佳方式



我正在努力找出如何在ASP.Net Core中最好地使用HttpClient类。

根据文档和几篇文章,类最好在应用程序的生命周期内实例化一次,并为多个请求共享。不幸的是,我在Core中找不到如何正确执行此操作的示例,因此我想出了以下解决方案。

我的特殊需求需要使用两个不同的端点(我有一个用于业务逻辑的APIServer和一个API驱动的ImageServer),所以我的想法是有两个可以在应用程序中使用的HttpClient单例。

我在appsettings.json中配置了我的服务点,如下所示:

"ServicePoints": {
"APIServer": "http://localhost:5001",
"ImageServer": "http://localhost:5002",
}

接下来,我创建了一个HttpClientsFactory,它将实例化我的2个httpclient,并将它们保存在一个静态Dictionary中。

public class HttpClientsFactory : IHttpClientsFactory
{
public static Dictionary<string, HttpClient> HttpClients { get; set; }
private readonly ILogger _logger;
private readonly IOptions<ServerOptions> _serverOptionsAccessor;
public HttpClientsFactory(ILoggerFactory loggerFactory, IOptions<ServerOptions> serverOptionsAccessor) {
_logger = loggerFactory.CreateLogger<HttpClientsFactory>();
_serverOptionsAccessor = serverOptionsAccessor;
HttpClients = new Dictionary<string, HttpClient>();
Initialize();
}
private void Initialize()
{
HttpClient client = new HttpClient();
// ADD imageServer
var imageServer = _serverOptionsAccessor.Value.ImageServer;
client.BaseAddress = new Uri(imageServer);
HttpClients.Add("imageServer", client);
// ADD apiServer
var apiServer = _serverOptionsAccessor.Value.APIServer;
client.BaseAddress = new Uri(apiServer);
HttpClients.Add("apiServer", client);
}
public Dictionary<string, HttpClient> Clients()
{
return HttpClients;
}
public HttpClient Client(string key)
{
return Clients()[key];
}
} 

然后,我创建了一个接口,可以在以后定义DI时使用。请注意,HttpClientsFactory类继承了这个接口。

public interface IHttpClientsFactory
{
Dictionary<string, HttpClient> Clients();
HttpClient Client(string key);
}

现在,我准备在ConfigureServices方法下的Startup类中将其注入到依赖关系容器中,如下所示。

// Add httpClient service
services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>();

现在一切都准备好了,开始在我的控制器中使用它
首先,我接受依赖关系。为此,我创建了一个私有类属性来保存它,然后将它添加到构造函数签名中,最后将传入对象分配给本地类属性。

private IHttpClientsFactory _httpClientsFactory;
public AppUsersAdminController(IHttpClientsFactory httpClientsFactory)
{
_httpClientsFactory = httpClientsFactory;
}

最后,我们现在可以使用Factory来请求htppclient并执行调用。下面是一个示例,我使用httpclientsfactory:从图像服务器请求图像

[HttpGet]
public async Task<ActionResult> GetUserPicture(string imgName)
{
// get imageserver uri
var imageServer = _optionsAccessor.Value.ImageServer;
// create path to requested image
var path = imageServer + "/imageuploads/" + imgName;
var client = _httpClientsFactory.Client("imageServer");
byte[] image = await client.GetByteArrayAsync(path);
return base.File(image, "image/jpeg");
}

完成!

我已经测试过了,它在我的开发环境中非常有效。然而,我不确定这是否是实现这一目标的最佳方式。我仍然有以下问题:

  1. 这个解决方案线程安全吗?(根据MS文档:"此类型的任何公共静态(在Visual Basic中共享)成员都是线程安全的。")
  2. 这种设置能够在不打开许多单独连接的情况下处理重负载吗
  3. 在ASP.Net核心中如何处理"Singleton HttpClient"中描述的DNS问题?小心这种严重的行为以及如何解决位于http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. 还有其他改进或建议吗

如果使用.net core 2.1或更高版本,最好的方法是使用新的HttpClientFactory。我想微软意识到了人们面临的所有问题,所以他们为我们做了艰苦的工作。请参阅下面的设置方法。

注:添加对Microsoft.Extensions.Http的引用。

1-添加一个使用HttpClient 的类

public interface ISomeApiClient
{
Task<HttpResponseMessage> GetSomethingAsync(string query);
}
public class SomeApiClient : ISomeApiClient
{
private readonly HttpClient _client;
public SomeApiClient (HttpClient client)
{
_client = client;
}
public async Task<SomeModel> GetSomethingAsync(string query)
{
var response = await _client.GetAsync($"?querystring={query}");
if (response.IsSuccessStatusCode)
{
var model = await response.Content.ReadAsJsonAsync<SomeModel>();
return model;
}
// Handle Error
}
}

2-在Startup.cs 的ConfigureServices(IServiceCollection services)中注册您的客户

var someApiSettings = Configuration.GetSection("SomeApiSettings").Get<SomeApiSettings>(); //Settings stored in app.config (base url, api key to add to header for all requests)
services.AddHttpClient<ISomeApiClient, SomeApiClient>("SomeApi",
client =>
{
client.BaseAddress = new Uri(someApiSettings.BaseAddress);
client.DefaultRequestHeaders.Add("api-key", someApiSettings.ApiKey);
});

3-在您的代码中使用客户端

public class MyController
{
private readonly ISomeApiClient _client;
public MyController(ISomeApiClient client)
{
_client = client;
}
[HttpGet]
public async Task<IActionResult> GetAsync(string query)
{
var response = await _client.GetSomethingAsync(query);
// Do something with response
return Ok();
}
}

在使用services.AddHttpClient启动时,您可以添加任意数量的客户端并根据需要注册任意数量的客户

感谢Steve Gordon和他的帖子帮助我在代码中使用它!

回答@MuqeetKhan关于在httpclient请求中使用身份验证的问题。

首先,我使用DI和工厂的动机是让我能够轻松地将我的应用程序扩展到不同和多个API,并在整个代码中轻松地访问这些应用程序。这是一个我希望能够多次重复使用的模板。

在上面原始问题中描述的"GetUserPicture"控制器的情况下,出于简单的原因,我确实删除了身份验证。然而,老实说,我仍然怀疑我是否需要它来简单地从图像服务器检索图像。无论如何,在其他控制器中,我肯定需要它,所以…

我已经实现了Identityserver4作为我的身份验证服务器。这为我提供了ASP身份验证之上的身份验证。对于授权(在这种情况下使用角色),我在我的MVC和API项目中实现了IClaimsTransformer(您可以在如何将ASP.net标识角色放入Identityserver4标识令牌中了解更多信息)。

现在,当我进入控制器时,我有一个经过身份验证和授权的用户,我可以为其检索访问令牌。我使用这个令牌来调用我的api,当然,它调用identityserver的同一个实例来验证用户是否经过了身份验证。

最后一步是允许我的API验证用户是否有权调用请求的API控制器。如前所述,在使用IClaimsTransformer的API请求管道中,我检索调用用户的授权,并将其添加到传入声明中。注意,在MVC调用和API的情况下,我因此检索授权2次;一次在MVC请求流水线中,一次在API请求流水线中。

使用此设置,我可以使用我的HttpClientsFactory进行授权和身份验证。

在安全方面,我缺少的当然是HTTPS。我希望我能以某种方式把它添加到我的工厂里。一旦我实现了它,我会更新它。

一如既往,欢迎提出任何建议。

下面是一个示例,我使用身份验证将图像上传到Imageserver(用户必须登录并具有角色管理员)。

我的MVC控制器调用"UploadUserPicture":

[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<ActionResult> UploadUserPicture()
{
// collect name image server
var imageServer = _optionsAccessor.Value.ImageServer;
// collect image in Request Form from Slim Image Cropper plugin
var json = _httpContextAccessor.HttpContext.Request.Form["slim[]"];
// Collect access token to be able to call API
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
// prepare api call to update image on imageserver and update database
var client = _httpClientsFactory.Client("imageServer");
client.DefaultRequestHeaders.Accept.Clear();
client.SetBearerToken(accessToken);
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("image", json[0])
});
HttpResponseMessage response = await client.PostAsync("api/UserPicture/UploadUserPicture", content);
if (response.StatusCode != HttpStatusCode.OK)
{
return StatusCode((int)HttpStatusCode.InternalServerError);
}
return StatusCode((int)HttpStatusCode.OK);
}

API处理用户上传

[Authorize(Roles = "Admin")]
[HttpPost]
public ActionResult UploadUserPicture(String image)
{
dynamic jsonDe = JsonConvert.DeserializeObject(image);
if (jsonDe == null)
{
return new StatusCodeResult((int)HttpStatusCode.NotModified);
}
// create filname for user picture
string userId = jsonDe.meta.userid;
string userHash = Hashing.GetHashString(userId);
string fileName = "User" + userHash + ".jpg";
// create a new version number
string pictureVersion = DateTime.Now.ToString("yyyyMMddHHmmss");
// get the image bytes and create a memory stream
var imagebase64 = jsonDe.output.image;
var cleanBase64 = Regex.Replace(imagebase64.ToString(), @"^data:image/w+;base64,", "");
var bytes = Convert.FromBase64String(cleanBase64);
var memoryStream = new MemoryStream(bytes);
// save the image to the folder
var fileSavePath = Path.Combine(_env.WebRootPath + ("/imageuploads"), fileName);
FileStream file = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write);
try
{
memoryStream.WriteTo(file);
}
catch (Exception ex)
{
_logger.LogDebug(LoggingEvents.UPDATE_ITEM, ex, "Could not write file >{fileSavePath}< to server", fileSavePath);
return new StatusCodeResult((int)HttpStatusCode.NotModified);
}
memoryStream.Dispose();
file.Dispose();
memoryStream = null;
file = null;
// update database with latest filename and version
bool isUpdatedInDatabase = UpdateDatabaseUserPicture(userId, fileName, pictureVersion).Result;
if (!isUpdatedInDatabase)
{
return new StatusCodeResult((int)HttpStatusCode.NotModified);
}
return new StatusCodeResult((int)HttpStatusCode.OK);
}
对于不能使用di的情况:
using System.Net.Http;
public class SomeClass
{
private static readonly HttpClient Client;
static SomeClass()
{
var handler = new SocketsHttpHandler
{
// Sets how long a connection can be in the pool to be considered reusable (by default - infinite)
PooledConnectionLifetime = TimeSpan.FromMinutes(1),
};
Client = new HttpClient(handler, disposeHandler: false);
}

...
}

参考https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-ihttpclientfactory 的5.0#替代方案

相关内容

最新更新