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

Lao*_*obu 10 c# dotnet-httpclient asp.net-core

我试图弄清楚如何最好地使用ASP.Net Core中的HttpClient类.

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

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

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

"ServicePoints": {
"APIServer": "http://localhost:5001",
"ImageServer": "http://localhost:5002",
}
Run Code Online (Sandbox Code Playgroud)

接下来,我创建了一个HttpClientsFactory,它将实例化我的2个httpclients并将它们保存在静态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];
    }
  } 
Run Code Online (Sandbox Code Playgroud)

然后,我创建了以后在定义我的DI时可以使用的界面.请注意,HttpClientsFactory类继承自此接口.

public interface IHttpClientsFactory
{
    Dictionary<string, HttpClient> Clients();
    HttpClient Client(string key);
}
Run Code Online (Sandbox Code Playgroud)

现在我准备将它注入我的Dependency容器,如下所示,在ConfigureServices方法下的Startup类中.

// Add httpClient service
        services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>();
Run Code Online (Sandbox Code Playgroud)

所有现在都设置为在我的控制器中开始使用它.
首先,我接受了依赖.为此,我创建了一个私有类属性来保存它,然后将它添加到构造函数签名中,并通过将传入对象分配给本地类属性来完成.

private IHttpClientsFactory _httpClientsFactory;
public AppUsersAdminController(IHttpClientsFactory httpClientsFactory)
{
   _httpClientsFactory = httpClientsFactory;
}
Run Code Online (Sandbox Code Playgroud)

最后,我们现在可以使用Factory来请求htppclient并执行调用.下面是我使用httpclientsfactory从imageserver请求图像的示例:

[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");
    }
Run Code Online (Sandbox Code Playgroud)

完成!

我已经测试了它,它在我的开发环境中运行良好.但是,我不确定这是否是实现此目的的最佳方式.我仍然提出以下问题:

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

gar*_*thb 28

如果使用 .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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
                });
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 每次使用“SomeAPIClient”时,这种方法是否都会为“SomeAPIClient”创建一个新的“HttpClient”?我没有在某处看到“Singleton”或“Transient”关键字状态来说明如何配置“IHttpClientFactory”。有一种模式可以将“HttpClient”显式定义为 DI 容器中的单例(尽管这可能会在多租户场景中更改“HttpClients”端点时出现问题,其中 API 的不同使用者需要不同的端点对于“HttpClient”。 (4认同)
  • 文档中的关键摘录表明这不是单例,但确实允许一些重用:“类型化客户端实际上是一个瞬态对象,这意味着每次需要时都会创建一个新实例,并且它将收到一个新的 HttpClient每次构造时都会创建一个实例。但是,池中的 HttpMessageHandler 对象是被多个 HttpClient 实例重用的对象。 (3认同)
  • 有关“HttpClient(Factory)”和相关“HttpMessageHandler”的大量框架信息可以在此处阅读:https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use- httpclientfactory 实现弹性 http-requests#httpclient-lifetimes (2认同)

Lao*_*obu 1

回答 @MuqeetKhan 有关使用 httpclient 请求进行身份验证的问题。

\n\n

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

\n\n

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

\n\n

我\xe2\x80\x99已经实现了Identityserver4作为我的身份验证服务器。这为我提供了基于 ASP Identity 的身份验证。\n为了授权(在本例中使用角色),我在 MVC \xe2\x80\x98and\xe2\x80\x99 API 项目中实现了 IClaimsTransformer(您可以在此处阅读如何将ASP.net Identity Roles 放入Identityserver4 身份令牌)。

\n\n

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

\n\n

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

\n\n

使用此设置,我可以使用具有授权和身份验证功能的 HttpClientsFactory。

\n\n

当然,在重要的安全部分,我缺少的是 HTTPS。我希望我能以某种方式将它添加到我的工厂。一旦实施,我将更新它。

\n\n

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

\n\n

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

\n\n

我的 MVC 控制器调用 \xe2\x80\x98UploadUserPicture\xe2\x80\x99:

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

API处理用户上传

\n\n
    [Authorize(Roles = "Admin")]\n    [HttpPost]\n    public ActionResult UploadUserPicture(String image)\n    {\n     dynamic jsonDe = JsonConvert.DeserializeObject(image);\n\n        if (jsonDe == null)\n        {\n            return new StatusCodeResult((int)HttpStatusCode.NotModified);\n        }\n\n        // create filname for user picture\n        string userId = jsonDe.meta.userid;\n        string userHash = Hashing.GetHashString(userId);\n        string fileName = "User" + userHash + ".jpg";\n\n        // create a new version number\n        string pictureVersion = DateTime.Now.ToString("yyyyMMddHHmmss");\n\n        // get the image bytes and create a memory stream\n        var imagebase64 = jsonDe.output.image;\n        var cleanBase64 = Regex.Replace(imagebase64.ToString(), @"^data:image/\\w+;base64,", "");\n        var bytes = Convert.FromBase64String(cleanBase64);\n        var memoryStream = new MemoryStream(bytes);\n\n        // save the image to the folder\n        var fileSavePath = Path.Combine(_env.WebRootPath + ("/imageuploads"), fileName);\n        FileStream file = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write);\n        try\n        {\n            memoryStream.WriteTo(file);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogDebug(LoggingEvents.UPDATE_ITEM, ex, "Could not write file >{fileSavePath}< to server", fileSavePath);\n            return new StatusCodeResult((int)HttpStatusCode.NotModified);\n        }\n        memoryStream.Dispose();\n        file.Dispose();\n        memoryStream = null;\n        file = null;\n\n        // update database with latest filename and version\n        bool isUpdatedInDatabase = UpdateDatabaseUserPicture(userId, fileName, pictureVersion).Result;\n\n        if (!isUpdatedInDatabase)\n        {\n            return new StatusCodeResult((int)HttpStatusCode.NotModified);\n        }\n\n        return new StatusCodeResult((int)HttpStatusCode.OK);\n    }\n
Run Code Online (Sandbox Code Playgroud)\n